From 68fcf9d2dfd78e6e684bc2f044166b0f9c14de57 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 5 Jun 2024 16:39:22 +1000 Subject: [PATCH 001/678] refactor(ui): revise types for line and rect objects - Create separate object types for brush and eraser lines, instead of a single type that has a `tool` field. - Create new object type for rect shapes. - Add logic to schemas to migrate old object types to new. - Update renderers & reducers. --- .../features/controlLayers/konva/renderers.ts | 56 ++++++++--- .../controlLayers/store/controlLayersSlice.ts | 41 +++++--- .../src/features/controlLayers/store/types.ts | 99 ++++++++++++++++--- 3 files changed, 155 insertions(+), 41 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers.ts index f521c77ed45..fd95d2409a4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers.ts @@ -34,13 +34,14 @@ import { isRenderableLayer, } from 'features/controlLayers/store/controlLayersSlice'; import type { + BrushLine, ControlAdapterLayer, + EraserLine, InitialImageLayer, Layer, + RectShape, RegionalGuidanceLayer, Tool, - VectorMaskLine, - VectorMaskRect, } from 'features/controlLayers/store/types'; import { t } from 'i18next'; import Konva from 'konva'; @@ -274,33 +275,64 @@ const createRGLayer = ( }; /** - * Creates a konva line from a vector mask line. - * @param vectorMaskLine The vector mask line state + * Creates a konva vector mask brush line from a vector mask line. + * @param brushLine The vector mask line state * @param layerObjectGroup The konva layer's object group to add the line to */ -const createVectorMaskLine = (vectorMaskLine: VectorMaskLine, layerObjectGroup: Konva.Group): Konva.Line => { +const createVectorMaskBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Group): Konva.Line => { const konvaLine = new Konva.Line({ - id: vectorMaskLine.id, - key: vectorMaskLine.id, + id: brushLine.id, + key: brushLine.id, name: RG_LAYER_LINE_NAME, - strokeWidth: vectorMaskLine.strokeWidth, + strokeWidth: brushLine.strokeWidth, tension: 0, lineCap: 'round', lineJoin: 'round', shadowForStrokeEnabled: false, - globalCompositeOperation: vectorMaskLine.tool === 'brush' ? 'source-over' : 'destination-out', + globalCompositeOperation: 'source-over', listening: false, }); layerObjectGroup.add(konvaLine); return konvaLine; }; +/** + * Creates a konva vector mask eraser line from a vector mask line. + * @param eraserLine The vector mask line state + * @param layerObjectGroup The konva layer's object group to add the line to + */ +const createVectorMaskEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva.Group): Konva.Line => { + const konvaLine = new Konva.Line({ + id: eraserLine.id, + key: eraserLine.id, + name: RG_LAYER_LINE_NAME, + strokeWidth: eraserLine.strokeWidth, + tension: 0, + lineCap: 'round', + lineJoin: 'round', + shadowForStrokeEnabled: false, + globalCompositeOperation: 'destination-out', + listening: false, + }); + layerObjectGroup.add(konvaLine); + return konvaLine; +}; + +const createVectorMaskLine = (maskObject: BrushLine | EraserLine, layerObjectGroup: Konva.Group): Konva.Line => { + if (maskObject.type === 'brush_line') { + return createVectorMaskBrushLine(maskObject, layerObjectGroup); + } else { + // maskObject.type === 'eraser_line' + return createVectorMaskEraserLine(maskObject, layerObjectGroup); + } +}; + /** * Creates a konva rect from a vector mask rect. * @param vectorMaskRect The vector mask rect state * @param layerObjectGroup The konva layer's object group to add the line to */ -const createVectorMaskRect = (vectorMaskRect: VectorMaskRect, layerObjectGroup: Konva.Group): Konva.Rect => { +const createVectorMaskRect = (vectorMaskRect: RectShape, layerObjectGroup: Konva.Group): Konva.Rect => { const konvaRect = new Konva.Rect({ id: vectorMaskRect.id, key: vectorMaskRect.id, @@ -369,7 +401,7 @@ const renderRGLayer = ( } for (const maskObject of layerState.maskObjects) { - if (maskObject.type === 'vector_mask_line') { + if (maskObject.type === 'brush_line' || maskObject.type === 'eraser_line') { const vectorMaskLine = stage.findOne(`#${maskObject.id}`) ?? createVectorMaskLine(maskObject, konvaObjectGroup); @@ -384,7 +416,7 @@ const renderRGLayer = ( vectorMaskLine.stroke(rgbColor); groupNeedsCache = true; } - } else if (maskObject.type === 'vector_mask_rect') { + } else if (maskObject.type === 'rect_shape') { const konvaObject = stage.findOne(`#${maskObject.id}`) ?? createVectorMaskRect(maskObject, konvaObjectGroup); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 8d6a6ecfd94..e9374017e16 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -47,17 +47,19 @@ import type { AddLineArg, AddPointToLineArg, AddRectArg, + BrushLine, ControlAdapterLayer, ControlLayersState, DrawingTool, + EraserLine, InitialImageLayer, IPAdapterLayer, Layer, + RectShape, RegionalGuidanceLayer, Tool, - VectorMaskLine, - VectorMaskRect, } from './types'; +import { DEFAULT_RGBA_COLOR } from './types'; export const initialControlLayersState: ControlLayersState = { _version: 3, @@ -77,7 +79,8 @@ export const initialControlLayersState: ControlLayersState = { }, }; -const isLine = (obj: VectorMaskLine | VectorMaskRect): obj is VectorMaskLine => obj.type === 'vector_mask_line'; +const isLine = (obj: BrushLine | EraserLine | RectShape): obj is BrushLine | EraserLine => + obj.type === 'brush_line' || obj.type === 'eraser_line'; export const isRegionalGuidanceLayer = (layer?: Layer): layer is RegionalGuidanceLayer => layer?.type === 'regional_guidance_layer'; export const isControlAdapterLayer = (layer?: Layer): layer is ControlAdapterLayer => @@ -491,15 +494,26 @@ export const controlLayersSlice = createSlice({ const { layerId, points, tool, lineUuid } = action.payload; const layer = selectRGLayerOrThrow(state, layerId); const lineId = getRGLayerLineId(layer.id, lineUuid); - layer.maskObjects.push({ - type: 'vector_mask_line', - tool: tool, - id: lineId, - // Points must be offset by the layer's x and y coordinates - // TODO: Handle this in the event listener? - points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y], - strokeWidth: state.brushSize, - }); + if (tool === 'brush') { + layer.maskObjects.push({ + id: lineId, + type: 'brush_line', + // Points must be offset by the layer's x and y coordinates + // TODO: Handle this in the event listener? + points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y], + strokeWidth: state.brushSize, + color: DEFAULT_RGBA_COLOR, + }); + } else { + layer.maskObjects.push({ + id: lineId, + type: 'eraser_line', + // Points must be offset by the layer's x and y coordinates + // TODO: Handle this in the event listener? + points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y], + strokeWidth: state.brushSize, + }); + } layer.bboxNeedsUpdate = true; layer.uploadedMaskImage = null; }, @@ -530,12 +544,13 @@ export const controlLayersSlice = createSlice({ const layer = selectRGLayerOrThrow(state, layerId); const id = getRGLayerRectId(layer.id, rectUuid); layer.maskObjects.push({ - type: 'vector_mask_rect', + type: 'rect_shape', id, x: rect.x - layer.x, y: rect.y - layer.y, width: rect.width, height: rect.height, + color: DEFAULT_RGBA_COLOR, }); layer.bboxNeedsUpdate = true; layer.uploadedMaskImage = null; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index bd86a8aa206..ff9f8e160d3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -5,13 +5,15 @@ import { zT2IAdapterConfigV2, } from 'features/controlLayers/util/controlAdapters'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; +import type { + ParameterHeight, + ParameterNegativePrompt, + ParameterNegativeStylePromptSDXL, + ParameterPositivePrompt, + ParameterPositiveStylePromptSDXL, + ParameterWidth, +} from 'features/parameters/types/parameterSchemas'; import { - type ParameterHeight, - type ParameterNegativePrompt, - type ParameterNegativeStylePromptSDXL, - type ParameterPositivePrompt, - type ParameterPositiveStylePromptSDXL, - type ParameterWidth, zAutoNegative, zParameterNegativePrompt, zParameterPositivePrompt, @@ -28,16 +30,15 @@ export type DrawingTool = z.infer; const zPoints = z.array(z.number()).refine((points) => points.length % 2 === 0, { message: 'Must have an even number of points', }); -const zVectorMaskLine = z.object({ +const zOLD_VectorMaskLine = z.object({ id: z.string(), type: z.literal('vector_mask_line'), tool: zDrawingTool, strokeWidth: z.number().min(1), points: zPoints, }); -export type VectorMaskLine = z.infer; -const zVectorMaskRect = z.object({ +const zOLD_VectorMaskRect = z.object({ id: z.string(), type: z.literal('vector_mask_rect'), x: z.number(), @@ -45,7 +46,45 @@ const zVectorMaskRect = z.object({ width: z.number().min(1), height: z.number().min(1), }); -export type VectorMaskRect = z.infer; + +const zRgbColor = z.object({ + r: z.number().int().min(0).max(255), + g: z.number().int().min(0).max(255), + b: z.number().int().min(0).max(255), +}); +const zRgbaColor = zRgbColor.extend({ + a: z.number().min(0).max(1), +}); +type RgbaColor = z.infer; +export const DEFAULT_RGBA_COLOR: RgbaColor = { r: 255, g: 255, b: 255, a: 1 }; + +const zBrushLine = z.object({ + id: z.string(), + type: z.literal('brush_line'), + strokeWidth: z.number().min(1), + points: zPoints, + color: zRgbaColor, +}); +export type BrushLine = z.infer; + +const zEraserline = z.object({ + id: z.string(), + type: z.literal('eraser_line'), + strokeWidth: z.number().min(1), + points: zPoints, +}); +export type EraserLine = z.infer; + +const zRectShape = z.object({ + id: z.string(), + type: z.literal('rect_shape'), + x: z.number(), + y: z.number(), + width: z.number().min(1), + height: z.number().min(1), + color: zRgbaColor, +}); +export type RectShape = z.infer; const zLayerBase = z.object({ id: z.string(), @@ -80,14 +119,42 @@ const zIPAdapterLayer = zLayerBase.extend({ }); export type IPAdapterLayer = z.infer; -const zRgbColor = z.object({ - r: z.number().int().min(0).max(255), - g: z.number().int().min(0).max(255), - b: z.number().int().min(0).max(255), -}); +const zMaskObject = z + .discriminatedUnion('type', [zOLD_VectorMaskLine, zOLD_VectorMaskRect, zBrushLine, zEraserline, zRectShape]) + .transform((val) => { + // Migrate old vector mask objects to new format + if (val.type === 'vector_mask_line') { + const { tool, ...rest } = val; + if (tool === 'brush') { + const asBrushline: BrushLine = { + ...rest, + type: 'brush_line', + color: { r: 255, g: 255, b: 255, a: 1 }, + }; + return asBrushline; + } else if (tool === 'eraser') { + const asEraserLine: EraserLine = { + ...rest, + type: 'eraser_line', + }; + return asEraserLine; + } + } else if (val.type === 'vector_mask_rect') { + const asRectShape: RectShape = { + ...val, + type: 'rect_shape', + color: { r: 255, g: 255, b: 255, a: 1 }, + }; + return asRectShape; + } else { + return val; + } + }) + .pipe(z.discriminatedUnion('type', [zBrushLine, zEraserline, zRectShape])); + const zRegionalGuidanceLayer = zRenderableLayerBase.extend({ type: z.literal('regional_guidance_layer'), - maskObjects: z.array(z.discriminatedUnion('type', [zVectorMaskLine, zVectorMaskRect])), + maskObjects: z.array(zMaskObject), positivePrompt: zParameterPositivePrompt.nullable(), negativePrompt: zParameterNegativePrompt.nullable(), ipAdapters: z.array(zIPAdapterConfigV2), From b590c73c083db7a04c7f6456c12e40bf35816329 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 5 Jun 2024 17:23:02 +1000 Subject: [PATCH 002/678] feat(ui): scaffold out raster layers Raster layers may have images, lines and shapes. These will replace initial image layers and provide sketching functionality like we have on canvas. --- invokeai/frontend/web/public/locales/en.json | 2 + .../src/common/hooks/useIsReadyToEnqueue.ts | 1 + .../components/AddLayerButton.tsx | 8 +- .../components/CALayer/CALayerOpacity.tsx | 4 +- .../components/ControlLayersPanelContent.tsx | 4 + .../components/LayerCommon/LayerMenu.tsx | 15 +++- .../components/LayerCommon/LayerTitle.tsx | 2 + .../components/RasterLayer/RasterLayer.tsx | 44 ++++++++++ .../RasterLayer/RasterLayerOpacity.tsx | 84 +++++++++++++++++++ .../controlLayers/hooks/layerStateHooks.ts | 17 +++- .../features/controlLayers/konva/naming.ts | 2 + .../controlLayers/store/controlLayersSlice.ts | 42 +++++++++- .../src/features/controlLayers/store/types.ts | 46 +++++++++- 13 files changed, 260 insertions(+), 11 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerOpacity.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index dd747e72f39..ebb313cd702 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1665,6 +1665,8 @@ "addIPAdapter": "Add $t(common.ipAdapter)", "regionalGuidance": "Regional Guidance", "regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)", + "raster": "Raster", + "rasterLayer": "$t(controlLayers.raster) $t(unifiedCanvas.layer)", "opacity": "Opacity", "globalControlAdapter": "Global $t(controlnet.controlAdapter_one)", "globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)", diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index 7ea53115858..77fe9be9b21 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -29,6 +29,7 @@ const LAYER_TYPE_TO_TKEY: Record = { control_adapter_layer: 'controlLayers.globalControlAdapter', ip_adapter_layer: 'controlLayers.globalIPAdapter', regional_guidance_layer: 'controlLayers.regionalGuidance', + raster_layer: 'controlLayers.raster', }; const createSelector = (templates: Templates) => diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx index c7a49da8c7e..fdfa70ae2cd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx @@ -1,7 +1,7 @@ import { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { useAddCALayer, useAddIILayer, useAddIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; -import { rgLayerAdded } from 'features/controlLayers/store/controlLayersSlice'; +import { rasterLayerAdded, rgLayerAdded } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPlusBold } from 'react-icons/pi'; @@ -15,6 +15,9 @@ export const AddLayerButton = memo(() => { const addRGLayer = useCallback(() => { dispatch(rgLayerAdded()); }, [dispatch]); + const addRasterLayer = useCallback(() => { + dispatch(rasterLayerAdded()); + }, [dispatch]); return ( @@ -30,6 +33,9 @@ export const AddLayerButton = memo(() => { } onClick={addRGLayer}> {t('controlLayers.regionalGuidanceLayer')} + } onClick={addRasterLayer}> + {t('controlLayers.rasterLayer')} + } onClick={addCALayer} isDisabled={isAddCALayerDisabled}> {t('controlLayers.globalControlAdapterLayer')} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx index 353f8e03072..e272282ea8d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx @@ -14,7 +14,7 @@ import { } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; -import { useLayerOpacity } from 'features/controlLayers/hooks/layerStateHooks'; +import { useCALayerOpacity } from 'features/controlLayers/hooks/layerStateHooks'; import { caLayerIsFilterEnabledChanged, caLayerOpacityChanged } from 'features/controlLayers/store/controlLayersSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; @@ -31,7 +31,7 @@ const formatPct = (v: number | string) => `${v} %`; const CALayerOpacity = ({ layerId }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const { opacity, isFilterEnabled } = useLayerOpacity(layerId); + const { opacity, isFilterEnabled } = useCALayerOpacity(layerId); const onChangeOpacity = useCallback( (v: number) => { dispatch(caLayerOpacityChanged({ layerId, opacity: v / 100 })); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index d3ddc071395..4f17870e68b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -9,6 +9,7 @@ import { CALayer } from 'features/controlLayers/components/CALayer/CALayer'; import { DeleteAllLayersButton } from 'features/controlLayers/components/DeleteAllLayersButton'; import { IILayer } from 'features/controlLayers/components/IILayer/IILayer'; import { IPALayer } from 'features/controlLayers/components/IPALayer/IPALayer'; +import { RasterLayer } from 'features/controlLayers/components/RasterLayer/RasterLayer'; import { RGLayer } from 'features/controlLayers/components/RGLayer/RGLayer'; import { isRenderableLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; import type { Layer } from 'features/controlLayers/store/types'; @@ -64,6 +65,9 @@ const LayerWrapper = memo(({ id, type }: LayerWrapperProps) => { if (type === 'initial_image_layer') { return ; } + if (type === 'raster_layer') { + return ; + } }); LayerWrapper.displayName = 'LayerWrapper'; 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 12074d12b80..0a3b52a3ff7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenu.tsx @@ -5,7 +5,7 @@ import { LayerMenuArrangeActions } from 'features/controlLayers/components/Layer import { LayerMenuRGActions } from 'features/controlLayers/components/LayerCommon/LayerMenuRGActions'; import { useLayerType } from 'features/controlLayers/hooks/layerStateHooks'; import { layerDeleted, layerReset } from 'features/controlLayers/store/controlLayersSlice'; -import { memo, useCallback } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowCounterClockwiseBold, PiDotsThreeVerticalBold, PiTrashSimpleBold } from 'react-icons/pi'; @@ -21,6 +21,15 @@ export const LayerMenu = memo(({ layerId }: Props) => { const deleteLayer = useCallback(() => { dispatch(layerDeleted(layerId)); }, [dispatch, layerId]); + const shouldShowArrangeActions = useMemo(() => { + return ( + layerType === 'regional_guidance_layer' || + layerType === 'control_adapter_layer' || + layerType === 'initial_image_layer' || + layerType === 'raster_layer' + ); + }, [layerType]); + return ( { )} - {(layerType === 'regional_guidance_layer' || - layerType === 'control_adapter_layer' || - layerType === 'initial_image_layer') && ( + {shouldShowArrangeActions && ( <> diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx index b29c3753fc8..a74729d91bb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx @@ -18,6 +18,8 @@ export const LayerTitle = memo(({ type }: Props) => { return t('controlLayers.globalIPAdapter'); } else if (type === 'initial_image_layer') { return t('controlLayers.globalInitialImage'); + } else if (type === 'raster_layer') { + return t('controlLayers.rasterLayer'); } }, [t, type]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx new file mode 100644 index 00000000000..80a32509b41 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx @@ -0,0 +1,44 @@ +import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; +import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; +import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; +import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; +import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; +import { layerSelected, selectRasterLayerOrThrow } from 'features/controlLayers/store/controlLayersSlice'; +import { memo, useCallback } from 'react'; + +import { RasterLayerOpacity } from './RasterLayerOpacity'; + +type Props = { + layerId: string; +}; + +export const RasterLayer = memo(({ layerId }: Props) => { + const dispatch = useAppDispatch(); + const isSelected = useAppSelector((s) => selectRasterLayerOrThrow(s.controlLayers.present, layerId).isSelected); + const onClick = useCallback(() => { + dispatch(layerSelected(layerId)); + }, [dispatch, layerId]); + const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); + + return ( + + + + + + + + + + {isOpen && ( + + PLACEHOLDER + + )} + + ); +}); + +RasterLayer.displayName = 'RasterLayer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerOpacity.tsx new file mode 100644 index 00000000000..05e4acd8490 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerOpacity.tsx @@ -0,0 +1,84 @@ +import { + CompositeNumberInput, + CompositeSlider, + Flex, + FormControl, + FormLabel, + IconButton, + Popover, + PopoverArrow, + PopoverBody, + PopoverContent, + PopoverTrigger, +} from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { stopPropagation } from 'common/util/stopPropagation'; +import { useRasterLayerOpacity } from 'features/controlLayers/hooks/layerStateHooks'; +import { rasterLayerOpacityChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiDropHalfFill } from 'react-icons/pi'; + +type Props = { + layerId: string; +}; + +const marks = [0, 25, 50, 75, 100]; +const formatPct = (v: number | string) => `${v} %`; + +export const RasterLayerOpacity = memo(({ layerId }: Props) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const opacity = useRasterLayerOpacity(layerId); + const onChangeOpacity = useCallback( + (v: number) => { + dispatch(rasterLayerOpacityChanged({ layerId, opacity: v / 100 })); + }, + [dispatch, layerId] + ); + return ( + + + } + variant="ghost" + onDoubleClick={stopPropagation} + /> + + + + + + + {t('controlLayers.opacity')} + + + + + + + + ); +}); + +RasterLayerOpacity.displayName = 'RasterLayerOpacity'; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts index 21e49ba15e7..b036b257422 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts @@ -3,6 +3,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { isControlAdapterLayer, + isRasterLayer, isRegionalGuidanceLayer, selectControlLayersSlice, } from 'features/controlLayers/store/controlLayersSlice'; @@ -67,7 +68,7 @@ export const useLayerType = (layerId: string) => { return type; }; -export const useLayerOpacity = (layerId: string) => { +export const useCALayerOpacity = (layerId: string) => { const selectLayer = useMemo( () => createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { @@ -80,3 +81,17 @@ export const useLayerOpacity = (layerId: string) => { const opacity = useAppSelector(selectLayer); return opacity; }; + +export const useRasterLayerOpacity = (layerId: string) => { + const selectLayer = useMemo( + () => + createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { + const layer = controlLayers.present.layers.filter(isRasterLayer).find((l) => l.id === layerId); + assert(layer, `Layer ${layerId} not found`); + return Math.round(layer.opacity * 100); + }), + [layerId] + ); + const opacity = useAppSelector(selectLayer); + return opacity; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts index 354719c8366..3a338b41a06 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts @@ -25,9 +25,11 @@ 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 RASTER_LAYER_NAME = 'raster_layer'; // 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 getRGLayerLineId = (layerId: string, lineId: string) => `${layerId}.line_${lineId}`; export const getRGLayerRectId = (layerId: string, lineId: string) => `${layerId}.rect_${lineId}`; export const getRGLayerObjectGroupId = (layerId: string, groupId: string) => `${layerId}.objectGroup_${groupId}`; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index e9374017e16..b0cf1707f0b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -7,6 +7,7 @@ import { roundDownToMultiple } from 'common/util/roundDownToMultiple'; import { getCALayerId, getIPALayerId, + getRasterLayerId, getRGLayerId, getRGLayerLineId, getRGLayerRectId, @@ -55,6 +56,7 @@ import type { InitialImageLayer, IPAdapterLayer, Layer, + RasterLayer, RectShape, RegionalGuidanceLayer, Tool, @@ -87,12 +89,14 @@ export const isControlAdapterLayer = (layer?: Layer): layer is ControlAdapterLay layer?.type === 'control_adapter_layer'; export const isIPAdapterLayer = (layer?: Layer): layer is IPAdapterLayer => layer?.type === 'ip_adapter_layer'; export const isInitialImageLayer = (layer?: Layer): layer is InitialImageLayer => layer?.type === 'initial_image_layer'; +export const isRasterLayer = (layer?: Layer): layer is RasterLayer => layer?.type === 'raster_layer'; export const isRenderableLayer = ( layer?: Layer ): layer is RegionalGuidanceLayer | ControlAdapterLayer | InitialImageLayer => layer?.type === 'regional_guidance_layer' || layer?.type === 'control_adapter_layer' || - layer?.type === 'initial_image_layer'; + layer?.type === 'initial_image_layer' || + layer?.type === 'raster_layer'; export const selectCALayerOrThrow = (state: ControlLayersState, layerId: string): ControlAdapterLayer => { const layer = state.layers.find((l) => l.id === layerId); @@ -109,6 +113,11 @@ export const selectIILayerOrThrow = (state: ControlLayersState, layerId: string) assert(isInitialImageLayer(layer)); return layer; }; +export const selectRasterLayerOrThrow = (state: ControlLayersState, layerId: string): RasterLayer => { + const layer = state.layers.find((l) => l.id === layerId); + assert(isRasterLayer(layer)); + return layer; +}; const selectCAOrIPALayerOrThrow = ( state: ControlLayersState, layerId: string @@ -699,6 +708,34 @@ export const controlLayersSlice = createSlice({ }, //#endregion + //#region Raster Layers + rasterLayerAdded: { + reducer: (state, action: PayloadAction<{ layerId: string }>) => { + const { layerId } = action.payload; + const layer: RasterLayer = { + id: getRasterLayerId(layerId), + type: 'raster_layer', + isEnabled: true, + bbox: null, + bboxNeedsUpdate: false, + objects: [], + opacity: 1, + x: 0, + y: 0, + isSelected: true, + }; + state.layers.push(layer); + exclusivelySelectLayer(state, layer.id); + }, + prepare: () => ({ payload: { layerId: uuidv4() } }), + }, + rasterLayerOpacityChanged: (state, action: PayloadAction<{ layerId: string; opacity: number }>) => { + const { layerId, opacity } = action.payload; + const layer = selectRasterLayerOrThrow(state, layerId); + layer.opacity = opacity; + }, + //#endregion + //#region Globals positivePromptChanged: (state, action: PayloadAction) => { state.positivePrompt = action.payload; @@ -874,6 +911,9 @@ export const { iiLayerImageChanged, iiLayerOpacityChanged, iiLayerDenoisingStrengthChanged, + // Raster layers + rasterLayerAdded, + rasterLayerOpacityChanged, // Globals positivePromptChanged, negativePromptChanged, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index ff9f8e160d3..03c47da357e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -58,6 +58,8 @@ const zRgbaColor = zRgbColor.extend({ type RgbaColor = z.infer; export const DEFAULT_RGBA_COLOR: RgbaColor = { r: 255, g: 255, b: 255, a: 1 }; +const zOpacity = z.number().gte(0).lte(1); + const zBrushLine = z.object({ id: z.string(), type: z.literal('brush_line'), @@ -86,6 +88,36 @@ const zRectShape = z.object({ }); export type RectShape = z.infer; +const zEllipseShape = z.object({ + id: z.string(), + type: z.literal('ellipse_shape'), + x: z.number(), + y: z.number(), + width: z.number().min(1), + height: z.number().min(1), + color: zRgbaColor, +}); +export type EllipseShape = z.infer; + +const zPolygonShape = z.object({ + id: z.string(), + type: z.literal('polygon_shape'), + points: zPoints, + color: zRgbaColor, +}); +export type PolygonShape = z.infer; + +const zImageObject = z.object({ + id: z.string(), + type: z.literal('image'), + image: zImageWithDims, + x: z.number(), + y: z.number(), + width: z.number().min(1), + height: z.number().min(1), +}); +export type ImageObject = z.infer; + const zLayerBase = z.object({ id: z.string(), isEnabled: z.boolean().default(true), @@ -105,9 +137,18 @@ const zRenderableLayerBase = zLayerBase.extend({ bboxNeedsUpdate: z.boolean(), }); +const zRasterLayer = zRenderableLayerBase.extend({ + type: z.literal('raster_layer'), + opacity: zOpacity, + objects: z.array( + z.discriminatedUnion('type', [zImageObject, zBrushLine, zEraserline, zRectShape, zEllipseShape, zPolygonShape]) + ), +}); +export type RasterLayer = z.infer; + const zControlAdapterLayer = zRenderableLayerBase.extend({ type: z.literal('control_adapter_layer'), - opacity: z.number().gte(0).lte(1), + opacity: zOpacity, isFilterEnabled: z.boolean(), controlAdapter: z.discriminatedUnion('type', [zControlNetConfigV2, zT2IAdapterConfigV2]), }); @@ -166,7 +207,7 @@ export type RegionalGuidanceLayer = z.infer; const zInitialImageLayer = zRenderableLayerBase.extend({ type: z.literal('initial_image_layer'), - opacity: z.number().gte(0).lte(1), + opacity: zOpacity, image: zImageWithDims.nullable(), denoisingStrength: zParameterStrength, }); @@ -177,6 +218,7 @@ export const zLayer = z.discriminatedUnion('type', [ zControlAdapterLayer, zIPAdapterLayer, zInitialImageLayer, + zRasterLayer, ]); export type Layer = z.infer; From 5c38241e33100622c93d4508b9bcd96fed378954 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 5 Jun 2024 21:24:26 +1000 Subject: [PATCH 003/678] feat(ui): add raster layer rendering and interaction (WIP) --- .../src/common/hooks/useIsReadyToEnqueue.ts | 2 +- .../components/BrushColorPicker.tsx | 48 ++++ .../components/ControlLayersToolbar.tsx | 2 + .../components/StageComponent.tsx | 88 +++--- .../controlLayers/components/ToolChooser.tsx | 2 +- .../features/controlLayers/konva/events.ts | 118 +++++--- .../features/controlLayers/konva/naming.ts | 10 +- .../features/controlLayers/konva/renderers.ts | 266 +++++++++++++----- .../controlLayers/store/controlLayersSlice.ts | 176 +++++++----- .../src/features/controlLayers/store/types.ts | 33 ++- 10 files changed, 528 insertions(+), 217 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/BrushColorPicker.tsx diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index 77fe9be9b21..1d610d32c22 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -182,7 +182,7 @@ const createSelector = (templates: Templates) => if (l.type === 'regional_guidance_layer') { // Must have a region - if (l.maskObjects.length === 0) { + if (l.objects.length === 0) { problems.push(i18n.t('parameters.invoke.layer.rgNoRegion')); } // Must have at least 1 prompt or IP Adapter diff --git a/invokeai/frontend/web/src/features/controlLayers/components/BrushColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/BrushColorPicker.tsx new file mode 100644 index 00000000000..517385f0d34 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/BrushColorPicker.tsx @@ -0,0 +1,48 @@ +import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger, Tooltip } 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'; +import { brushColorChanged } from 'features/controlLayers/store/controlLayersSlice'; +import type { RgbaColor } from 'features/controlLayers/store/types'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const BrushColorPicker = memo(() => { + const { t } = useTranslation(); + const brushColor = useAppSelector((s) => s.controlLayers.present.brushColor); + const dispatch = useAppDispatch(); + const onChange = useCallback( + (color: RgbaColor) => { + dispatch(brushColorChanged(color)); + }, + [dispatch] + ); + return ( + + + + + + + + + + + + + + + ); +}); + +BrushColorPicker.displayName = 'BrushColorPicker'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index 8cc3aa93fe4..55025d40f22 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -1,5 +1,6 @@ /* eslint-disable i18next/no-literal-string */ import { Flex } from '@invoke-ai/ui-library'; +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'; @@ -18,6 +19,7 @@ export const ControlLayersToolbar = memo(() => { + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index 9226abf2078..a4dc52751e1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -8,26 +8,32 @@ import { BRUSH_SPACING_PCT, MAX_BRUSH_SPACING_PX, MIN_BRUSH_SPACING_PX } from 'f import { setStageEventHandlers } from 'features/controlLayers/konva/events'; import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers'; import { + $brushColor, $brushSize, $brushSpacingPx, $isDrawing, $lastAddedPoint, $lastCursorPos, $lastMouseDownPos, - $selectedLayerId, - $selectedLayerType, + $selectedLayer, $shouldInvertBrushSizeScrollDirection, $tool, + brushLineAdded, brushSizeChanged, + eraserLineAdded, isRegionalGuidanceLayer, layerBboxChanged, layerTranslated, - rgLayerLineAdded, - rgLayerPointsAdded, - rgLayerRectAdded, + linePointsAdded, + rectAdded, selectControlLayersSlice, } from 'features/controlLayers/store/controlLayersSlice'; -import type { AddLineArg, AddPointToLineArg, AddRectArg } from 'features/controlLayers/store/types'; +import type { + AddBrushLineArg, + AddEraserLineArg, + AddPointToLineArg, + AddRectShapeArg, +} from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; import { clamp } from 'lodash-es'; @@ -41,16 +47,20 @@ Konva.showWarnings = false; const log = logger('controlLayers'); -const selectSelectedLayerColor = createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { +const selectBrushColor = createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { const layer = controlLayers.present.layers .filter(isRegionalGuidanceLayer) .find((l) => l.id === controlLayers.present.selectedLayerId); - return layer?.previewColor ?? null; + + if (layer) { + return { ...layer.previewColor, a: controlLayers.present.globalMaskLayerOpacity }; + } + + return controlLayers.present.brushColor; }); -const selectSelectedLayerType = createSelector(selectControlLayersSlice, (controlLayers) => { - const selectedLayer = controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId); - return selectedLayer?.type ?? null; +const selectSelectedLayer = createSelector(selectControlLayersSlice, (controlLayers) => { + return controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId) ?? null; }); const useStageRenderer = ( @@ -64,8 +74,8 @@ const useStageRenderer = ( const tool = useStore($tool); const lastCursorPos = useStore($lastCursorPos); const lastMouseDownPos = useStore($lastMouseDownPos); - const selectedLayerIdColor = useAppSelector(selectSelectedLayerColor); - const selectedLayerType = useAppSelector(selectSelectedLayerType); + const brushColor = useAppSelector(selectBrushColor); + const selectedLayer = useAppSelector(selectSelectedLayer); const layerIds = useMemo(() => state.layers.map((l) => l.id), [state.layers]); const layerCount = useMemo(() => state.layers.length, [state.layers]); const renderers = useMemo(() => (asPreview ? debouncedRenderers : normalRenderers), [asPreview]); @@ -77,18 +87,19 @@ const useStageRenderer = ( ); useLayoutEffect(() => { + $brushColor.set(brushColor); $brushSize.set(state.brushSize); $brushSpacingPx.set(brushSpacingPx); - $selectedLayerId.set(state.selectedLayerId); - $selectedLayerType.set(selectedLayerType); + $selectedLayer.set(selectedLayer); $shouldInvertBrushSizeScrollDirection.set(shouldInvertBrushSizeScrollDirection); }, [ brushSpacingPx, - selectedLayerIdColor, - selectedLayerType, + brushColor, + selectedLayer, shouldInvertBrushSizeScrollDirection, state.brushSize, state.selectedLayerId, + state.brushColor, ]); const onLayerPosChanged = useCallback( @@ -105,21 +116,27 @@ const useStageRenderer = ( [dispatch] ); - const onRGLayerLineAdded = useCallback( - (arg: AddLineArg) => { - dispatch(rgLayerLineAdded(arg)); + const onBrushLineAdded = useCallback( + (arg: AddBrushLineArg) => { + dispatch(brushLineAdded(arg)); + }, + [dispatch] + ); + const onEraserLineAdded = useCallback( + (arg: AddEraserLineArg) => { + dispatch(eraserLineAdded(arg)); }, [dispatch] ); - const onRGLayerPointAddedToLine = useCallback( + const onPointAddedToLine = useCallback( (arg: AddPointToLineArg) => { - dispatch(rgLayerPointsAdded(arg)); + dispatch(linePointsAdded(arg)); }, [dispatch] ); - const onRGLayerRectAdded = useCallback( - (arg: AddRectArg) => { - dispatch(rgLayerRectAdded(arg)); + const onRectShapeAdded = useCallback( + (arg: AddRectShapeArg) => { + dispatch(rectAdded(arg)); }, [dispatch] ); @@ -155,21 +172,22 @@ const useStageRenderer = ( $lastCursorPos, $lastAddedPoint, $brushSize, + $brushColor, $brushSpacingPx, - $selectedLayerId, - $selectedLayerType, + $selectedLayer, $shouldInvertBrushSizeScrollDirection, - onRGLayerLineAdded, - onRGLayerPointAddedToLine, - onRGLayerRectAdded, onBrushSizeChanged, + onBrushLineAdded, + onEraserLineAdded, + onPointAddedToLine, + onRectShapeAdded, }); return () => { log.trace('Removing stage listeners'); cleanup(); }; - }, [asPreview, onBrushSizeChanged, onRGLayerLineAdded, onRGLayerPointAddedToLine, onRGLayerRectAdded, stage]); + }, [asPreview, onBrushLineAdded, onBrushSizeChanged, onEraserLineAdded, onPointAddedToLine, onRectShapeAdded, stage]); useLayoutEffect(() => { log.trace('Updating stage dimensions'); @@ -205,8 +223,8 @@ const useStageRenderer = ( renderers.renderToolPreview( stage, tool, - selectedLayerIdColor, - selectedLayerType, + brushColor, + selectedLayer?.type ?? null, state.globalMaskLayerOpacity, lastCursorPos, lastMouseDownPos, @@ -216,8 +234,8 @@ const useStageRenderer = ( asPreview, stage, tool, - selectedLayerIdColor, - selectedLayerType, + brushColor, + selectedLayer, state.globalMaskLayerOpacity, lastCursorPos, lastMouseDownPos, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx index f97a0f35e52..b9ea0af4597 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx @@ -15,7 +15,7 @@ import { PiArrowsOutCardinalBold, PiEraserBold, PiPaintBrushBold, PiRectangleBol const selectIsDisabled = createSelector(selectControlLayersSlice, (controlLayers) => { const selectedLayer = controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId); - return selectedLayer?.type !== 'regional_guidance_layer'; + return selectedLayer?.type !== 'regional_guidance_layer' && selectedLayer?.type !== 'raster_layer'; }); export const ToolChooser: React.FC = () => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 8b130e940f5..0a26dba92d2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -5,10 +5,19 @@ import { getScaledFlooredCursorPosition, snapPosToStage, } from 'features/controlLayers/konva/util'; -import type { AddLineArg, AddPointToLineArg, AddRectArg, Layer, Tool } from 'features/controlLayers/store/types'; +import { + type AddBrushLineArg, + type AddEraserLineArg, + type AddPointToLineArg, + type AddRectShapeArg, + DEFAULT_RGBA_COLOR, + type Layer, + type Tool, +} from 'features/controlLayers/store/types'; import type Konva from 'konva'; import type { Vector2d } from 'konva/lib/types'; import type { WritableAtom } from 'nanostores'; +import type { RgbaColor } from 'react-colorful'; import { TOOL_PREVIEW_LAYER_ID } from './naming'; @@ -19,14 +28,15 @@ type SetStageEventHandlersArg = { $lastMouseDownPos: WritableAtom; $lastCursorPos: WritableAtom; $lastAddedPoint: WritableAtom; + $brushColor: WritableAtom; $brushSize: WritableAtom; $brushSpacingPx: WritableAtom; - $selectedLayerId: WritableAtom; - $selectedLayerType: WritableAtom; + $selectedLayer: WritableAtom; $shouldInvertBrushSizeScrollDirection: WritableAtom; - onRGLayerLineAdded: (arg: AddLineArg) => void; - onRGLayerPointAddedToLine: (arg: AddPointToLineArg) => void; - onRGLayerRectAdded: (arg: AddRectArg) => void; + onBrushLineAdded: (arg: AddBrushLineArg) => void; + onEraserLineAdded: (arg: AddEraserLineArg) => void; + onPointAddedToLine: (arg: AddPointToLineArg) => void; + onRectShapeAdded: (arg: AddRectShapeArg) => void; onBrushSizeChanged: (size: number) => void; }; @@ -46,14 +56,15 @@ export const setStageEventHandlers = ({ $lastMouseDownPos, $lastCursorPos, $lastAddedPoint, + $brushColor, $brushSize, $brushSpacingPx, - $selectedLayerId, - $selectedLayerType, + $selectedLayer, $shouldInvertBrushSizeScrollDirection, - onRGLayerLineAdded, - onRGLayerPointAddedToLine, - onRGLayerRectAdded, + onBrushLineAdded, + onEraserLineAdded, + onPointAddedToLine, + onRectShapeAdded, onBrushSizeChanged, }: SetStageEventHandlersArg): (() => void) => { stage.on('mouseenter', (e) => { @@ -72,16 +83,25 @@ export const setStageEventHandlers = ({ } const tool = $tool.get(); const pos = syncCursorPos(stage, $lastCursorPos); - const selectedLayerId = $selectedLayerId.get(); - const selectedLayerType = $selectedLayerType.get(); - if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') { + const selectedLayer = $selectedLayer.get(); + if (!pos || !selectedLayer) { + return; + } + if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { return; } - if (tool === 'brush' || tool === 'eraser') { - onRGLayerLineAdded({ - layerId: selectedLayerId, + if (tool === 'brush') { + onBrushLineAdded({ + layerId: selectedLayer.id, + points: [pos.x, pos.y, pos.x, pos.y], + color: selectedLayer.type === 'raster_layer' ? $brushColor.get() : DEFAULT_RGBA_COLOR, + }); + $isDrawing.set(true); + $lastMouseDownPos.set(pos); + } else if (tool === 'eraser') { + onEraserLineAdded({ + layerId: selectedLayer.id, points: [pos.x, pos.y, pos.x, pos.y], - tool, }); $isDrawing.set(true); $lastMouseDownPos.set(pos); @@ -96,24 +116,27 @@ export const setStageEventHandlers = ({ return; } const pos = $lastCursorPos.get(); - const selectedLayerId = $selectedLayerId.get(); - const selectedLayerType = $selectedLayerType.get(); + const selectedLayer = $selectedLayer.get(); - if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') { + if (!pos || !selectedLayer) { + return; + } + if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { return; } const lastPos = $lastMouseDownPos.get(); const tool = $tool.get(); - if (lastPos && selectedLayerId && tool === 'rect') { + if (lastPos && selectedLayer.id && tool === 'rect') { const snappedPos = snapPosToStage(pos, stage); - onRGLayerRectAdded({ - layerId: selectedLayerId, + onRectShapeAdded({ + layerId: selectedLayer.id, rect: { x: Math.min(snappedPos.x, lastPos.x), y: Math.min(snappedPos.y, lastPos.y), width: Math.abs(snappedPos.x - lastPos.x), height: Math.abs(snappedPos.y - lastPos.y), }, + color: selectedLayer.type === 'raster_layer' ? $brushColor.get() : DEFAULT_RGBA_COLOR, }); } $isDrawing.set(false); @@ -127,12 +150,14 @@ export const setStageEventHandlers = ({ } const tool = $tool.get(); const pos = syncCursorPos(stage, $lastCursorPos); - const selectedLayerId = $selectedLayerId.get(); - const selectedLayerType = $selectedLayerType.get(); + const selectedLayer = $selectedLayer.get(); stage.findOne(`#${TOOL_PREVIEW_LAYER_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); - if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') { + if (!pos || !selectedLayer) { + return; + } + if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { return; } if (getIsFocused(stage) && getIsMouseDown(e) && (tool === 'brush' || tool === 'eraser')) { @@ -146,10 +171,21 @@ export const setStageEventHandlers = ({ } } $lastAddedPoint.set({ x: pos.x, y: pos.y }); - onRGLayerPointAddedToLine({ layerId: selectedLayerId, point: [pos.x, pos.y] }); + onPointAddedToLine({ layerId: selectedLayer.id, point: [pos.x, pos.y] }); } else { - // Start a new line - onRGLayerLineAdded({ layerId: selectedLayerId, points: [pos.x, pos.y, pos.x, pos.y], tool }); + if (tool === 'brush') { + // Start a new line + onBrushLineAdded({ + layerId: selectedLayer.id, + points: [pos.x, pos.y, pos.x, pos.y], + color: selectedLayer.type === 'raster_layer' ? $brushColor.get() : DEFAULT_RGBA_COLOR, + }); + } else if (tool === 'eraser') { + onEraserLineAdded({ + layerId: selectedLayer.id, + points: [pos.x, pos.y, pos.x, pos.y], + }); + } } $isDrawing.set(true); } @@ -164,28 +200,36 @@ export const setStageEventHandlers = ({ $isDrawing.set(false); $lastCursorPos.set(null); $lastMouseDownPos.set(null); - const selectedLayerId = $selectedLayerId.get(); - const selectedLayerType = $selectedLayerType.get(); + const selectedLayer = $selectedLayer.get(); const tool = $tool.get(); stage.findOne(`#${TOOL_PREVIEW_LAYER_ID}`)?.visible(false); - if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') { + if (!pos || !selectedLayer) { + return; + } + if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { return; } if (getIsFocused(stage) && getIsMouseDown(e) && (tool === 'brush' || tool === 'eraser')) { - onRGLayerPointAddedToLine({ layerId: selectedLayerId, point: [pos.x, pos.y] }); + onPointAddedToLine({ layerId: selectedLayer.id, point: [pos.x, pos.y] }); } }); stage.on('wheel', (e) => { e.evt.preventDefault(); - const selectedLayerType = $selectedLayerType.get(); const tool = $tool.get(); - if (selectedLayerType !== 'regional_guidance_layer' || (tool !== 'brush' && tool !== 'eraser')) { + const selectedLayer = $selectedLayer.get(); + + if (tool !== 'brush' && tool !== 'eraser') { + return; + } + if (!selectedLayer) { + return; + } + if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { return; } - // Invert the delta if the property is set to true let delta = e.evt.deltaY; if ($shouldInvertBrushSizeScrollDirection.get()) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts index 3a338b41a06..f8175c96552 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts @@ -26,13 +26,17 @@ 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 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'; // 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 getRGLayerLineId = (layerId: string, lineId: string) => `${layerId}.line_${lineId}`; -export const getRGLayerRectId = (layerId: string, lineId: string) => `${layerId}.rect_${lineId}`; -export const getRGLayerObjectGroupId = (layerId: string, groupId: string) => `${layerId}.objectGroup_${groupId}`; +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 getObjectGroupId = (layerId: string, groupId: string) => `${layerId}.objectGroup_${groupId}`; export const getLayerBboxId = (layerId: string) => `${layerId}.bbox`; export const getCALayerId = (layerId: string) => `control_adapter_layer_${layerId}`; export const getCALayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers.ts index fd95d2409a4..d69c14afa37 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers.ts @@ -10,11 +10,13 @@ import { getCALayerImageId, getIILayerImageId, getLayerBboxId, - getRGLayerObjectGroupId, + getObjectGroupId, INITIAL_IMAGE_LAYER_IMAGE_NAME, INITIAL_IMAGE_LAYER_NAME, LAYER_BBOX_NAME, NO_LAYERS_MESSAGE_LAYER_ID, + RASTER_LAYER_NAME, + RASTER_LAYER_OBJECT_GROUP_NAME, RG_LAYER_LINE_NAME, RG_LAYER_NAME, RG_LAYER_OBJECT_GROUP_NAME, @@ -30,6 +32,7 @@ import { getScaledFlooredCursorPosition, snapPosToStage } from 'features/control import { isControlAdapterLayer, isInitialImageLayer, + isRasterLayer, isRegionalGuidanceLayer, isRenderableLayer, } from 'features/controlLayers/store/controlLayersSlice'; @@ -39,15 +42,17 @@ import type { EraserLine, InitialImageLayer, Layer, + RasterLayer, RectShape, RegionalGuidanceLayer, + RgbaColor, Tool, } from 'features/controlLayers/store/types'; +import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types'; import { t } from 'i18next'; import Konva from 'konva'; import type { IRect, Vector2d } from 'konva/lib/types'; import { debounce } from 'lodash-es'; -import type { RgbColor } from 'react-colorful'; import type { ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; @@ -59,21 +64,6 @@ import { TRANSPARENCY_CHECKER_PATTERN, } from './constants'; -const mapId = (object: { id: string }): string => object.id; - -/** - * 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; - -/** - * 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; -}; - /** * Creates the singleton tool preview layer and all its objects. * @param stage The konva stage @@ -130,7 +120,7 @@ const createToolPreviewLayer = (stage: Konva.Stage): Konva.Layer => { const renderToolPreview = ( stage: Konva.Stage, tool: Tool, - color: RgbColor | null, + brushColor: RgbaColor, selectedLayerType: Layer['type'] | null, globalMaskLayerOpacity: number, cursorPos: Vector2d | null, @@ -142,7 +132,7 @@ const renderToolPreview = ( if (layerCount === 0) { // We have no layers, so we should not render any tool stage.container().style.cursor = 'default'; - } else if (selectedLayerType !== 'regional_guidance_layer') { + } else if (selectedLayerType !== 'regional_guidance_layer' && selectedLayerType !== 'raster_layer') { // Non-mask-guidance layers don't have tools stage.container().style.cursor = 'not-allowed'; } else if (tool === 'move') { @@ -173,14 +163,14 @@ const renderToolPreview = ( assert(rectPreview, 'Rect preview not found'); // No need to render the brush preview if the cursor position or color is missing - if (cursorPos && color && (tool === 'brush' || tool === 'eraser')) { + if (cursorPos && (tool === 'brush' || tool === 'eraser')) { // Update the fill circle const brushPreviewFill = brushPreviewGroup.findOne(`#${TOOL_PREVIEW_BRUSH_FILL_ID}`); brushPreviewFill?.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: brushSize / 2, - fill: rgbaColorToString({ ...color, a: globalMaskLayerOpacity }), + fill: rgbaColorToString(brushColor), globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out', }); @@ -263,7 +253,7 @@ const createRGLayer = ( // The object group holds all of the layer's objects (e.g. lines and rects) const konvaObjectGroup = new Konva.Group({ - id: getRGLayerObjectGroupId(layerState.id, uuidv4()), + id: getObjectGroupId(layerState.id, uuidv4()), name: RG_LAYER_OBJECT_GROUP_NAME, listening: false, }); @@ -273,13 +263,14 @@ const createRGLayer = ( return konvaLayer; }; +//#endregion /** - * Creates a konva vector mask brush line from a vector mask line. - * @param brushLine The vector mask line state + * 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 */ -const createVectorMaskBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Group): Konva.Line => { +const createBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Group): Konva.Line => { const konvaLine = new Konva.Line({ id: brushLine.id, key: brushLine.id, @@ -291,17 +282,18 @@ const createVectorMaskBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva shadowForStrokeEnabled: false, globalCompositeOperation: 'source-over', listening: false, + stroke: rgbaColorToString(brushLine.color), }); layerObjectGroup.add(konvaLine); return konvaLine; }; /** - * Creates a konva vector mask eraser line from a vector mask line. - * @param eraserLine The vector mask line state + * 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 */ -const createVectorMaskEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva.Group): Konva.Line => { +const createEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva.Group): Konva.Line => { const konvaLine = new Konva.Line({ id: eraserLine.id, key: eraserLine.id, @@ -313,42 +305,35 @@ const createVectorMaskEraserLine = (eraserLine: EraserLine, layerObjectGroup: Ko shadowForStrokeEnabled: false, globalCompositeOperation: 'destination-out', listening: false, + stroke: rgbaColorToString(DEFAULT_RGBA_COLOR), }); layerObjectGroup.add(konvaLine); return konvaLine; }; -const createVectorMaskLine = (maskObject: BrushLine | EraserLine, layerObjectGroup: Konva.Group): Konva.Line => { - if (maskObject.type === 'brush_line') { - return createVectorMaskBrushLine(maskObject, layerObjectGroup); - } else { - // maskObject.type === 'eraser_line' - return createVectorMaskEraserLine(maskObject, layerObjectGroup); - } -}; - /** - * Creates a konva rect from a vector mask rect. - * @param vectorMaskRect The vector mask rect state + * 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 */ -const createVectorMaskRect = (vectorMaskRect: RectShape, layerObjectGroup: Konva.Group): Konva.Rect => { +const createRectShape = (rectShape: RectShape, layerObjectGroup: Konva.Group): Konva.Rect => { const konvaRect = new Konva.Rect({ - id: vectorMaskRect.id, - key: vectorMaskRect.id, + id: rectShape.id, + key: rectShape.id, name: RG_LAYER_RECT_NAME, - x: vectorMaskRect.x, - y: vectorMaskRect.y, - width: vectorMaskRect.width, - height: vectorMaskRect.height, + x: rectShape.x, + y: rectShape.y, + width: rectShape.width, + height: rectShape.height, listening: false, + fill: rgbaColorToString(rectShape.color), }); layerObjectGroup.add(konvaRect); return konvaRect; }; /** - * Creates the "compositing rect" for a layer. + * Creates the "compositing rect" for a regional guidance layer. * @param konvaLayer The konva layer */ const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => { @@ -358,7 +343,7 @@ const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => { }; /** - * Renders a regional guidance layer. + * Renders a raster layer. * @param stage The konva stage * @param layerState The regional guidance layer state * @param globalMaskLayerOpacity The global mask layer opacity @@ -391,7 +376,7 @@ const renderRGLayer = ( // We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required. let groupNeedsCache = false; - const objectIds = layerState.maskObjects.map(mapId); + const objectIds = layerState.objects.map(mapId); // Destroy any objects that are no longer in the redux state for (const objectNode of konvaObjectGroup.find(selectVectorMaskObjects)) { if (!objectIds.includes(objectNode.id())) { @@ -400,29 +385,41 @@ const renderRGLayer = ( } } - for (const maskObject of layerState.maskObjects) { - if (maskObject.type === 'brush_line' || maskObject.type === 'eraser_line') { - const vectorMaskLine = - stage.findOne(`#${maskObject.id}`) ?? createVectorMaskLine(maskObject, konvaObjectGroup); + for (const obj of layerState.objects) { + if (obj.type === 'brush_line') { + const konvaBrushLine = stage.findOne(`#${obj.id}`) ?? createBrushLine(obj, konvaObjectGroup); // Only update the points if they have changed. The point values are never mutated, they are only added to the // array, so checking the length is sufficient to determine if we need to re-cache. - if (vectorMaskLine.points().length !== maskObject.points.length) { - vectorMaskLine.points(maskObject.points); + if (konvaBrushLine.points().length !== obj.points.length) { + konvaBrushLine.points(obj.points); groupNeedsCache = true; } // Only update the color if it has changed. - if (vectorMaskLine.stroke() !== rgbColor) { - vectorMaskLine.stroke(rgbColor); + if (konvaBrushLine.stroke() !== rgbColor) { + konvaBrushLine.stroke(rgbColor); groupNeedsCache = true; } - } else if (maskObject.type === 'rect_shape') { - const konvaObject = - stage.findOne(`#${maskObject.id}`) ?? createVectorMaskRect(maskObject, konvaObjectGroup); + } else if (obj.type === 'eraser_line') { + const konvaEraserLine = stage.findOne(`#${obj.id}`) ?? createEraserLine(obj, konvaObjectGroup); + // Only update the points if they have changed. The point values are never mutated, they are only added to the + // array, so checking the length is sufficient to determine if we need to re-cache. + if (konvaEraserLine.points().length !== obj.points.length) { + konvaEraserLine.points(obj.points); + groupNeedsCache = true; + } // Only update the color if it has changed. - if (konvaObject.fill() !== rgbColor) { - konvaObject.fill(rgbColor); + if (konvaEraserLine.stroke() !== rgbColor) { + konvaEraserLine.stroke(rgbColor); + groupNeedsCache = true; + } + } else if (obj.type === 'rect_shape') { + const konvaRectShape = stage.findOne(`#${obj.id}`) ?? createRectShape(obj, konvaObjectGroup); + + // Only update the color if it has changed. + if (konvaRectShape.fill() !== rgbColor) { + konvaRectShape.fill(rgbColor); groupNeedsCache = true; } } @@ -485,6 +482,126 @@ const renderRGLayer = ( } }; +/** + * Creates a raster layer. + * @param stage The konva stage + * @param layerState The raster layer state + * @param onLayerPosChanged Callback for when the layer's position changes + */ +const createRasterLayer = ( + stage: Konva.Stage, + layerState: RasterLayer, + onLayerPosChanged?: (layerId: string, x: number, y: number) => void +): Konva.Layer => { + // This layer hasn't been added to the konva state yet + const konvaLayer = new Konva.Layer({ + id: layerState.id, + name: RASTER_LAYER_NAME, + draggable: true, + dragDistance: 0, + }); + + // 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) { + konvaLayer.on('dragend', function (e) { + onLayerPosChanged(layerState.id, Math.floor(e.target.x()), Math.floor(e.target.y())); + }); + } + + // The dragBoundFunc limits how far the layer can be dragged + konvaLayer.dragBoundFunc(function (pos) { + const cursorPos = getScaledFlooredCursorPosition(stage); + if (!cursorPos) { + return this.getAbsolutePosition(); + } + // Prevent the user from dragging the layer out of the stage bounds by constaining the cursor position to the stage bounds + if ( + cursorPos.x < 0 || + cursorPos.x > stage.width() / stage.scaleX() || + cursorPos.y < 0 || + cursorPos.y > stage.height() / stage.scaleY() + ) { + return this.getAbsolutePosition(); + } + 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; +}; + +/** + * Renders a regional guidance layer. + * @param stage The konva stage + * @param layerState The regional guidance layer state + * @param tool The current tool + * @param onLayerPosChanged Callback for when the layer's position changes + */ +const renderRasterLayer = ( + 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); + + // Update the layer's position and listening state + konvaLayer.setAttrs({ + listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events + x: Math.floor(layerState.x), + 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 objectIds = layerState.objects.map(mapId); + // Destroy any objects that are no longer in the redux state + for (const objectNode of konvaObjectGroup.getChildren()) { + if (!objectIds.includes(objectNode.id())) { + objectNode.destroy(); + } + } + + for (const obj of layerState.objects) { + if (obj.type === 'brush_line') { + const konvaBrushLine = stage.findOne(`#${obj.id}`) ?? createBrushLine(obj, konvaObjectGroup); + // 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); + // 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); + } + } + } + + // Only update layer visibility if it has changed. + if (konvaLayer.visible() !== layerState.isEnabled) { + konvaLayer.visible(layerState.isEnabled); + } + + konvaObjectGroup.opacity(layerState.opacity); +}; + /** * Creates an initial image konva layer. * @param stage The konva stage @@ -805,6 +922,9 @@ const renderLayers = ( if (isInitialImageLayer(layer)) { renderIILayer(stage, layer, getImageDTO); } + if (isRasterLayer(layer)) { + renderRasterLayer(stage, layer, tool, onLayerPosChanged); + } // IP Adapter layers are not rendered } }; @@ -886,7 +1006,7 @@ const updateBboxes = ( const visible = bboxRect.visible(); bboxRect.visible(false); - if (rgLayer.maskObjects.length === 0) { + if (rgLayer.objects.length === 0) { // No objects - no bbox to calculate onBboxChanged(rgLayer.id, null); } else { @@ -1041,3 +1161,23 @@ export const debouncedRenderers = { arrangeLayers: debounce(arrangeLayers, DEBOUNCE_MS), updateBboxes: debounce(updateBboxes, DEBOUNCE_MS), }; + +//#region util +const mapId = (object: { id: string }): string => object.id; + +/** + * 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() === RASTER_LAYER_NAME; + +/** + * 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; +}; +//#endregion diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index b0cf1707f0b..16069daecb2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -5,12 +5,13 @@ import { moveBackward, moveForward, moveToBack, moveToFront } from 'common/util/ import { deepClone } from 'common/util/deepClone'; import { roundDownToMultiple } from 'common/util/roundDownToMultiple'; import { + getBrushLineId, getCALayerId, + getEraserLineId, getIPALayerId, getRasterLayerId, + getRectId, getRGLayerId, - getRGLayerLineId, - getRGLayerRectId, INITIAL_IMAGE_LAYER_ID, } from 'features/controlLayers/konva/naming'; import type { @@ -45,20 +46,24 @@ import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; import type { - AddLineArg, + AddBrushLineArg, + AddEraserLineArg, AddPointToLineArg, - AddRectArg, + AddRectShapeArg, BrushLine, ControlAdapterLayer, ControlLayersState, - DrawingTool, + EllipseShape, EraserLine, + ImageObject, InitialImageLayer, IPAdapterLayer, Layer, + PolygonShape, RasterLayer, RectShape, RegionalGuidanceLayer, + RgbaColor, Tool, } from './types'; import { DEFAULT_RGBA_COLOR } from './types'; @@ -67,6 +72,7 @@ export const initialControlLayersState: ControlLayersState = { _version: 3, selectedLayerId: null, brushSize: 100, + brushColor: DEFAULT_RGBA_COLOR, layers: [], globalMaskLayerOpacity: 0.3, // this globally changes all mask layers' opacity positivePrompt: '', @@ -81,8 +87,9 @@ export const initialControlLayersState: ControlLayersState = { }, }; -const isLine = (obj: BrushLine | EraserLine | RectShape): obj is BrushLine | EraserLine => - obj.type === 'brush_line' || obj.type === 'eraser_line'; +const isLine = ( + obj: BrushLine | EraserLine | RectShape | EllipseShape | PolygonShape | ImageObject +): obj is BrushLine => obj.type === 'brush_line' || obj.type === 'eraser_line'; export const isRegionalGuidanceLayer = (layer?: Layer): layer is RegionalGuidanceLayer => layer?.type === 'regional_guidance_layer'; export const isControlAdapterLayer = (layer?: Layer): layer is ControlAdapterLayer => @@ -131,6 +138,14 @@ const selectRGLayerOrThrow = (state: ControlLayersState, layerId: string): Regio assert(isRegionalGuidanceLayer(layer)); return layer; }; +const selectRGOrRasterLayerOrThrow = ( + state: ControlLayersState, + layerId: string +): RegionalGuidanceLayer | RasterLayer => { + const layer = state.layers.find((l) => l.id === layerId); + assert(isRegionalGuidanceLayer(layer) || isRasterLayer(layer)); + return layer; +}; export const selectRGLayerIPAdapterOrThrow = ( state: ControlLayersState, layerId: string, @@ -187,7 +202,7 @@ export const controlLayersSlice = createSlice({ layer.bboxNeedsUpdate = false; if (bbox === null && layer.type === 'regional_guidance_layer') { // The layer was fully erased, empty its objects to prevent accumulation of invisible objects - layer.maskObjects = []; + layer.objects = []; layer.uploadedMaskImage = null; } } @@ -196,7 +211,7 @@ export const controlLayersSlice = createSlice({ const layer = state.layers.find((l) => l.id === action.payload); // TODO(psyche): Should other layer types also have reset functionality? if (isRegionalGuidanceLayer(layer)) { - layer.maskObjects = []; + layer.objects = []; layer.bbox = null; layer.isEnabled = true; layer.bboxNeedsUpdate = false; @@ -455,7 +470,7 @@ export const controlLayersSlice = createSlice({ isEnabled: true, bbox: null, bboxNeedsUpdate: false, - maskObjects: [], + objects: [], previewColor: getVectorMaskPreviewColor(state), x: 0, y: 0, @@ -490,81 +505,102 @@ export const controlLayersSlice = createSlice({ const layer = selectRGLayerOrThrow(state, layerId); layer.previewColor = color; }, - rgLayerLineAdded: { + brushLineAdded: { reducer: ( state, - action: PayloadAction<{ - layerId: string; - points: [number, number, number, number]; - tool: DrawingTool; - lineUuid: string; - }> + action: PayloadAction< + AddBrushLineArg & { + lineUuid: string; + } + > ) => { - const { layerId, points, tool, lineUuid } = action.payload; - const layer = selectRGLayerOrThrow(state, layerId); - const lineId = getRGLayerLineId(layer.id, lineUuid); - if (tool === 'brush') { - layer.maskObjects.push({ - id: lineId, - type: 'brush_line', - // Points must be offset by the layer's x and y coordinates - // TODO: Handle this in the event listener? - points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y], - strokeWidth: state.brushSize, - color: DEFAULT_RGBA_COLOR, - }); - } else { - layer.maskObjects.push({ - id: lineId, - type: 'eraser_line', - // Points must be offset by the layer's x and y coordinates - // TODO: Handle this in the event listener? - points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y], - strokeWidth: state.brushSize, - }); + const { layerId, points, lineUuid, color } = action.payload; + const layer = selectRGOrRasterLayerOrThrow(state, layerId); + layer.objects.push({ + id: getBrushLineId(layer.id, lineUuid), + type: 'brush_line', + // Points must be offset by the layer's x and y coordinates + // TODO: Handle this in the event listener? + points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y], + strokeWidth: state.brushSize, + color, + }); + layer.bboxNeedsUpdate = true; + if (layer.type === 'regional_guidance_layer') { + layer.uploadedMaskImage = null; } + }, + prepare: (payload: AddBrushLineArg) => ({ + payload: { ...payload, lineUuid: uuidv4() }, + }), + }, + eraserLineAdded: { + reducer: ( + state, + action: PayloadAction< + AddEraserLineArg & { + lineUuid: string; + } + > + ) => { + const { layerId, points, lineUuid } = action.payload; + const layer = selectRGOrRasterLayerOrThrow(state, layerId); + layer.objects.push({ + id: getEraserLineId(layer.id, lineUuid), + type: 'eraser_line', + // Points must be offset by the layer's x and y coordinates + // TODO: Handle this in the event listener? + points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y], + strokeWidth: state.brushSize, + }); layer.bboxNeedsUpdate = true; - layer.uploadedMaskImage = null; + if (isRegionalGuidanceLayer(layer)) { + layer.uploadedMaskImage = null; + } }, - prepare: (payload: AddLineArg) => ({ + prepare: (payload: AddEraserLineArg) => ({ payload: { ...payload, lineUuid: uuidv4() }, }), }, - rgLayerPointsAdded: (state, action: PayloadAction) => { + linePointsAdded: (state, action: PayloadAction) => { const { layerId, point } = action.payload; - const layer = selectRGLayerOrThrow(state, layerId); - const lastLine = layer.maskObjects.findLast(isLine); - if (!lastLine) { + const layer = selectRGOrRasterLayerOrThrow(state, layerId); + const lastLine = layer.objects.findLast(isLine); + if (!lastLine || !isLine(lastLine)) { return; } // Points must be offset by the layer's x and y coordinates // TODO: Handle this in the event listener lastLine.points.push(point[0] - layer.x, point[1] - layer.y); layer.bboxNeedsUpdate = true; - layer.uploadedMaskImage = null; + if (isRegionalGuidanceLayer(layer)) { + layer.uploadedMaskImage = null; + } }, - rgLayerRectAdded: { - reducer: (state, action: PayloadAction<{ layerId: string; rect: IRect; rectUuid: string }>) => { - const { layerId, rect, rectUuid } = action.payload; + rectAdded: { + reducer: (state, action: PayloadAction) => { + const { layerId, rect, rectUuid, color } = action.payload; if (rect.height === 0 || rect.width === 0) { // Ignore zero-area rectangles return; } - const layer = selectRGLayerOrThrow(state, layerId); - const id = getRGLayerRectId(layer.id, rectUuid); - layer.maskObjects.push({ + const layer = selectRGOrRasterLayerOrThrow(state, layerId); + const id = getRectId(layer.id, rectUuid); + layer.objects.push({ type: 'rect_shape', id, x: rect.x - layer.x, y: rect.y - layer.y, width: rect.width, height: rect.height, - color: DEFAULT_RGBA_COLOR, + color, }); layer.bboxNeedsUpdate = true; - layer.uploadedMaskImage = null; + if (isRegionalGuidanceLayer(layer)) { + layer.uploadedMaskImage = null; + } }, - prepare: (payload: AddRectArg) => ({ payload: { ...payload, rectUuid: uuidv4() } }), + prepare: (payload: AddRectShapeArg) => ({ payload: { ...payload, rectUuid: uuidv4() } }), }, rgLayerMaskImageUploaded: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO }>) => { const { layerId, imageDTO } = action.payload; @@ -776,6 +812,9 @@ export const controlLayersSlice = createSlice({ brushSizeChanged: (state, action: PayloadAction) => { state.brushSize = Math.round(action.payload); }, + brushColorChanged: (state, action: PayloadAction) => { + state.brushColor = action.payload; + }, globalMaskLayerOpacityChanged: (state, action: PayloadAction) => { state.globalMaskLayerOpacity = action.payload; }, @@ -892,9 +931,10 @@ export const { rgLayerPositivePromptChanged, rgLayerNegativePromptChanged, rgLayerPreviewColorChanged, - rgLayerLineAdded, - rgLayerPointsAdded, - rgLayerRectAdded, + brushLineAdded, + eraserLineAdded, + linePointsAdded, + rectAdded, rgLayerMaskImageUploaded, rgLayerAutoNegativeChanged, rgLayerIPAdapterAdded, @@ -924,6 +964,7 @@ export const { heightChanged, aspectRatioChanged, brushSizeChanged, + brushColorChanged, globalMaskLayerOpacityChanged, undo, redo, @@ -960,9 +1001,9 @@ export const $lastAddedPoint = atom(null); // Some nanostores that are manually synced to redux state to provide imperative access // TODO(psyche): This is a hack, figure out another way to handle this... export const $brushSize = atom(0); +export const $brushColor = atom(DEFAULT_RGBA_COLOR); export const $brushSpacingPx = atom(0); -export const $selectedLayerId = atom(null); -export const $selectedLayerType = atom(null); +export const $selectedLayer = atom(null); export const $shouldInvertBrushSizeScrollDirection = atom(false); export const controlLayersPersistConfig: PersistConfig = { @@ -998,10 +1039,10 @@ export const controlLayersUndoableConfig: UndoableOptions { - // Ignore all actions from other slices - if (!action.type.startsWith(controlLayersSlice.name)) { - return false; - } - // This action is triggered on state changes, including when we undo. If we do not ignore this action, when we - // undo, this action triggers and empties the future states array. Therefore, we must ignore this action. - if (layerBboxChanged.match(action)) { - return false; - } - return true; + return false; }, }; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 03c47da357e..ab40c258249 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -55,7 +55,7 @@ const zRgbColor = z.object({ const zRgbaColor = zRgbColor.extend({ a: z.number().min(0).max(1), }); -type RgbaColor = z.infer; +export type RgbaColor = z.infer; export const DEFAULT_RGBA_COLOR: RgbaColor = { r: 255, g: 255, b: 255, a: 1 }; const zOpacity = z.number().gte(0).lte(1); @@ -193,7 +193,7 @@ const zMaskObject = z }) .pipe(z.discriminatedUnion('type', [zBrushLine, zEraserline, zRectShape])); -const zRegionalGuidanceLayer = zRenderableLayerBase.extend({ +const zOLD_RegionalGuidanceLayer = zRenderableLayerBase.extend({ type: z.literal('regional_guidance_layer'), maskObjects: z.array(zMaskObject), positivePrompt: zParameterPositivePrompt.nullable(), @@ -203,7 +203,28 @@ const zRegionalGuidanceLayer = zRenderableLayerBase.extend({ autoNegative: zAutoNegative, uploadedMaskImage: zImageWithDims.nullable(), }); -export type RegionalGuidanceLayer = z.infer; +const zRegionalGuidanceLayer = zRenderableLayerBase.extend({ + type: z.literal('regional_guidance_layer'), + objects: z.array(zMaskObject), + positivePrompt: zParameterPositivePrompt.nullable(), + negativePrompt: zParameterNegativePrompt.nullable(), + ipAdapters: z.array(zIPAdapterConfigV2), + previewColor: zRgbColor, + autoNegative: zAutoNegative, + uploadedMaskImage: zImageWithDims.nullable(), +}); +const zRGLayer = z + .union([zOLD_RegionalGuidanceLayer, zRegionalGuidanceLayer]) + .transform((val) => { + if ('maskObjects' in val) { + const { maskObjects, ...rest } = val; + return { ...rest, objects: maskObjects }; + } else { + return val; + } + }) + .pipe(zRegionalGuidanceLayer); +export type RegionalGuidanceLayer = z.infer; const zInitialImageLayer = zRenderableLayerBase.extend({ type: z.literal('initial_image_layer'), @@ -227,6 +248,7 @@ export type ControlLayersState = { selectedLayerId: string | null; layers: Layer[]; brushSize: number; + brushColor: RgbaColor; globalMaskLayerOpacity: number; positivePrompt: ParameterPositivePrompt; negativePrompt: ParameterNegativePrompt; @@ -240,6 +262,7 @@ export type ControlLayersState = { }; }; -export type AddLineArg = { layerId: string; points: [number, number, number, number]; tool: DrawingTool }; +export type AddEraserLineArg = { layerId: string; points: [number, number, number, number] }; +export type AddBrushLineArg = AddEraserLineArg & { color: RgbaColor }; export type AddPointToLineArg = { layerId: string; point: [number, number] }; -export type AddRectArg = { layerId: string; rect: IRect }; +export type AddRectShapeArg = { layerId: string; rect: IRect; color: RgbaColor }; From 84b8096cc9378465bfce2079465689a40f6c49ca Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 5 Jun 2024 23:01:57 +1000 Subject: [PATCH 004/678] feat(ui): raster layer logic - Deduplicate shared logic - Split up giant renderers file into separate cohesive files - Tons of cleanup - Progress on raster layer functionality --- .../listeners/controlAdapterPreprocessor.ts | 2 +- .../listeners/imageDeletionListeners.ts | 4 +- .../components/AddPromptButtons.tsx | 2 +- .../components/CALayer/CALayer.tsx | 7 +- .../CALayer/CALayerControlAdapterWrapper.tsx | 7 +- .../components/CALayer/CALayerOpacity.tsx | 4 +- .../components/ControlLayersPanelContent.tsx | 3 +- .../components/IILayer/IILayer.tsx | 9 +- .../components/IPALayer/IPALayer.tsx | 7 +- .../IPALayer/IPALayerIPAdapterWrapper.tsx | 7 +- .../LayerCommon/LayerMenuArrangeActions.tsx | 2 +- .../LayerCommon/LayerMenuRGActions.tsx | 2 +- .../LayerOpacity.tsx} | 17 +- .../components/RGLayer/RGLayer.tsx | 7 +- .../RGLayer/RGLayerAutoNegativeCheckbox.tsx | 7 +- .../components/RGLayer/RGLayerColorPicker.tsx | 7 +- .../RGLayer/RGLayerIPAdapterList.tsx | 3 +- .../components/RasterLayer/RasterLayer.tsx | 12 +- .../RasterLayer/RasterLayerOpacity.tsx | 84 -- .../components/StageComponent.tsx | 4 +- .../controlLayers/hooks/addLayerHooks.ts | 2 +- .../controlLayers/hooks/layerStateHooks.ts | 22 +- .../features/controlLayers/konva/constants.ts | 5 + .../features/controlLayers/konva/events.ts | 5 + .../features/controlLayers/konva/renderers.ts | 1183 ----------------- .../konva/renderers/background.ts | 67 + .../konva/{ => renderers}/bbox.ts | 110 +- .../controlLayers/konva/renderers/caLayer.ts | 162 +++ .../controlLayers/konva/renderers/iiLayer.ts | 149 +++ .../controlLayers/konva/renderers/layers.ts | 118 ++ .../konva/renderers/noLayersMessage.ts | 53 + .../controlLayers/konva/renderers/objects.ts | 77 ++ .../konva/renderers/rasterLayer.ts | 135 ++ .../controlLayers/konva/renderers/rgLayer.ts | 229 ++++ .../konva/renderers/toolPreview.ts | 161 +++ .../src/features/controlLayers/konva/util.ts | 38 + .../controlLayers/store/controlLayersSlice.ts | 365 +++-- .../src/features/controlLayers/store/types.ts | 60 +- .../deleteImageModal/store/selectors.ts | 6 +- .../util/graph/generation/addControlLayers.ts | 8 +- .../generation/buildGenerationTabGraph.ts | 2 +- 41 files changed, 1589 insertions(+), 1565 deletions(-) rename invokeai/frontend/web/src/features/controlLayers/components/{IILayer/IILayerOpacity.tsx => LayerCommon/LayerOpacity.tsx} (85%) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerOpacity.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/renderers.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts rename invokeai/frontend/web/src/features/controlLayers/konva/{ => renderers}/bbox.ts (58%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/renderers/iiLayer.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/renderers/noLayersMessage.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/renderers/toolPreview.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts index a1eb917ebb3..cd8fb69ca08 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts @@ -10,8 +10,8 @@ import { caLayerProcessorConfigChanged, caLayerProcessorPendingBatchIdChanged, caLayerRecalled, - isControlAdapterLayer, } from 'features/controlLayers/store/controlLayersSlice'; +import { isControlAdapterLayer } from 'features/controlLayers/store/types'; import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts index 489adb74769..61df8846f04 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts @@ -8,13 +8,13 @@ import { selectControlAdapterAll, } from 'features/controlAdapters/store/controlAdaptersSlice'; import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; +import { layerDeleted } from 'features/controlLayers/store/controlLayersSlice'; import { isControlAdapterLayer, isInitialImageLayer, isIPAdapterLayer, isRegionalGuidanceLayer, - layerDeleted, -} from 'features/controlLayers/store/controlLayersSlice'; +} from 'features/controlLayers/store/types'; import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions'; import { isModalOpenChanged } from 'features/deleteImageModal/store/slice'; import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx index 26d9c8ce69f..e339d8315e0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx @@ -3,11 +3,11 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAddIPAdapterToIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; import { - isRegionalGuidanceLayer, rgLayerNegativePromptChanged, rgLayerPositivePromptChanged, selectControlLayersSlice, } from 'features/controlLayers/store/controlLayersSlice'; +import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPlusBold } from 'react-icons/pi'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx index 9e71ad943c6..868693e58ce 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx @@ -6,7 +6,8 @@ import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMe import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; -import { layerSelected, selectCALayerOrThrow } from 'features/controlLayers/store/controlLayersSlice'; +import { layerSelected, selectLayerOrThrow } from 'features/controlLayers/store/controlLayersSlice'; +import { isControlAdapterLayer } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import CALayerOpacity from './CALayerOpacity'; @@ -17,7 +18,9 @@ type Props = { export const CALayer = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); - const isSelected = useAppSelector((s) => selectCALayerOrThrow(s.controlLayers.present, layerId).isSelected); + const isSelected = useAppSelector( + (s) => selectLayerOrThrow(s.controlLayers.present, layerId, isControlAdapterLayer).isSelected + ); const onClick = useCallback(() => { dispatch(layerSelected(layerId)); }, [dispatch, layerId]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx index a44ae32c137..6c498fe1aa5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx @@ -8,8 +8,9 @@ import { caLayerProcessorConfigChanged, caOrIPALayerBeginEndStepPctChanged, caOrIPALayerWeightChanged, - selectCALayerOrThrow, + selectLayerOrThrow, } from 'features/controlLayers/store/controlLayersSlice'; +import { isControlAdapterLayer } from 'features/controlLayers/store/types'; import type { ControlModeV2, ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; import type { CALayerImageDropData } from 'features/dnd/types'; import { memo, useCallback, useMemo } from 'react'; @@ -26,7 +27,9 @@ type Props = { export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); - const controlAdapter = useAppSelector((s) => selectCALayerOrThrow(s.controlLayers.present, layerId).controlAdapter); + const controlAdapter = useAppSelector( + (s) => selectLayerOrThrow(s.controlLayers.present, layerId, isControlAdapterLayer).controlAdapter + ); const onChangeBeginEndStepPct = useCallback( (beginEndStepPct: [number, number]) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx index e272282ea8d..94f7cdf5fe1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx @@ -15,7 +15,7 @@ import { import { useAppDispatch } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; import { useCALayerOpacity } from 'features/controlLayers/hooks/layerStateHooks'; -import { caLayerIsFilterEnabledChanged, caLayerOpacityChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { caLayerIsFilterEnabledChanged, layerOpacityChanged } from 'features/controlLayers/store/controlLayersSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -34,7 +34,7 @@ const CALayerOpacity = ({ layerId }: Props) => { const { opacity, isFilterEnabled } = useCALayerOpacity(layerId); const onChangeOpacity = useCallback( (v: number) => { - dispatch(caLayerOpacityChanged({ layerId, opacity: v / 100 })); + dispatch(layerOpacityChanged({ layerId, opacity: v / 100 })); }, [dispatch, layerId] ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index 4f17870e68b..d4baabab8b6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -11,8 +11,9 @@ import { IILayer } from 'features/controlLayers/components/IILayer/IILayer'; import { IPALayer } from 'features/controlLayers/components/IPALayer/IPALayer'; import { RasterLayer } from 'features/controlLayers/components/RasterLayer/RasterLayer'; import { RGLayer } from 'features/controlLayers/components/RGLayer/RGLayer'; -import { isRenderableLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; import type { Layer } from 'features/controlLayers/store/types'; +import { isRenderableLayer } from 'features/controlLayers/store/types'; import { partition } from 'lodash-es'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx index c53c4c76319..43857b6fc3e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx @@ -1,9 +1,9 @@ import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IILayerOpacity from 'features/controlLayers/components/IILayer/IILayerOpacity'; import { InitialImagePreview } from 'features/controlLayers/components/IILayer/InitialImagePreview'; import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; +import { LayerOpacity } from 'features/controlLayers/components/LayerCommon/LayerOpacity'; import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; @@ -11,8 +11,9 @@ import { iiLayerDenoisingStrengthChanged, iiLayerImageChanged, layerSelected, - selectIILayerOrThrow, + selectLayerOrThrow, } from 'features/controlLayers/store/controlLayersSlice'; +import { isInitialImageLayer } from 'features/controlLayers/store/types'; import type { IILayerImageDropData } from 'features/dnd/types'; import ImageToImageStrength from 'features/parameters/components/ImageToImage/ImageToImageStrength'; import { memo, useCallback, useMemo } from 'react'; @@ -24,7 +25,7 @@ type Props = { export const IILayer = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); - const layer = useAppSelector((s) => selectIILayerOrThrow(s.controlLayers.present, layerId)); + const layer = useAppSelector((s) => selectLayerOrThrow(s.controlLayers.present, layerId, isInitialImageLayer)); const onClick = useCallback(() => { dispatch(layerSelected(layerId)); }, [dispatch, layerId]); @@ -69,7 +70,7 @@ export const IILayer = memo(({ layerId }: Props) => { - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx index e8f60c8d074..e4d3dd9e4ff 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx @@ -5,7 +5,8 @@ import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; -import { layerSelected, selectIPALayerOrThrow } from 'features/controlLayers/store/controlLayersSlice'; +import { layerSelected, selectLayerOrThrow } from 'features/controlLayers/store/controlLayersSlice'; +import { isIPAdapterLayer } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; type Props = { @@ -14,7 +15,9 @@ type Props = { export const IPALayer = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); - const isSelected = useAppSelector((s) => selectIPALayerOrThrow(s.controlLayers.present, layerId).isSelected); + const isSelected = useAppSelector( + (s) => selectLayerOrThrow(s.controlLayers.present, layerId, isIPAdapterLayer).isSelected + ); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); const onClick = useCallback(() => { dispatch(layerSelected(layerId)); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx index 9f99710dac5..6492e3cf32f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx @@ -7,8 +7,9 @@ import { ipaLayerImageChanged, ipaLayerMethodChanged, ipaLayerModelChanged, - selectIPALayerOrThrow, + selectLayerOrThrow, } from 'features/controlLayers/store/controlLayersSlice'; +import { isIPAdapterLayer } from 'features/controlLayers/store/types'; import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; import type { IPALayerImageDropData } from 'features/dnd/types'; import { memo, useCallback, useMemo } from 'react'; @@ -20,7 +21,9 @@ type Props = { export const IPALayerIPAdapterWrapper = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); - const ipAdapter = useAppSelector((s) => selectIPALayerOrThrow(s.controlLayers.present, layerId).ipAdapter); + const ipAdapter = useAppSelector( + (s) => selectLayerOrThrow(s.controlLayers.present, layerId, isIPAdapterLayer).ipAdapter + ); const onChangeBeginEndStepPct = useCallback( (beginEndStepPct: [number, number]) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuArrangeActions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuArrangeActions.tsx index 9c51671a392..3e65eda783a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuArrangeActions.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuArrangeActions.tsx @@ -2,13 +2,13 @@ import { MenuItem } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { - isRenderableLayer, layerMovedBackward, layerMovedForward, layerMovedToBack, layerMovedToFront, selectControlLayersSlice, } from 'features/controlLayers/store/controlLayersSlice'; +import { isRenderableLayer } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowDownBold, PiArrowLineDownBold, PiArrowLineUpBold, PiArrowUpBold } from 'react-icons/pi'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx index 172709ec146..905abfd00df 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx @@ -3,11 +3,11 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAddIPAdapterToIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; import { - isRegionalGuidanceLayer, rgLayerNegativePromptChanged, rgLayerPositivePromptChanged, selectControlLayersSlice, } from 'features/controlLayers/store/controlLayersSlice'; +import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPlusBold } from 'react-icons/pi'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerOpacity.tsx similarity index 85% rename from invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayerOpacity.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerOpacity.tsx index 9918dda5b80..f488d9600a4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayerOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerOpacity.tsx @@ -15,14 +15,14 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; import { - iiLayerOpacityChanged, - isInitialImageLayer, + layerOpacityChanged, selectControlLayersSlice, + selectLayerOrThrow, } from 'features/controlLayers/store/controlLayersSlice'; +import { isLayerWithOpacity } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiDropHalfFill } from 'react-icons/pi'; -import { assert } from 'tsafe'; type Props = { layerId: string; @@ -31,14 +31,13 @@ type Props = { const marks = [0, 25, 50, 75, 100]; const formatPct = (v: number | string) => `${v} %`; -const IILayerOpacity = ({ layerId }: Props) => { +export const LayerOpacity = memo(({ layerId }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const selectOpacity = useMemo( () => createSelector(selectControlLayersSlice, (controlLayers) => { - const layer = controlLayers.present.layers.filter(isInitialImageLayer).find((l) => l.id === layerId); - assert(layer, `Layer ${layerId} not found`); + const layer = selectLayerOrThrow(controlLayers.present, layerId, isLayerWithOpacity); return Math.round(layer.opacity * 100); }), [layerId] @@ -46,7 +45,7 @@ const IILayerOpacity = ({ layerId }: Props) => { const opacity = useAppSelector(selectOpacity); const onChangeOpacity = useCallback( (v: number) => { - dispatch(iiLayerOpacityChanged({ layerId, opacity: v / 100 })); + dispatch(layerOpacityChanged({ layerId, opacity: v / 100 })); }, [dispatch, layerId] ); @@ -93,6 +92,6 @@ const IILayerOpacity = ({ layerId }: Props) => { ); -}; +}); -export default memo(IILayerOpacity); +LayerOpacity.displayName = 'LayerOpacity'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx index cc331017d3e..fa552dd4cf2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx @@ -8,11 +8,8 @@ import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMe import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; -import { - isRegionalGuidanceLayer, - layerSelected, - selectControlLayersSlice, -} from 'features/controlLayers/store/controlLayersSlice'; +import { layerSelected, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { assert } from 'tsafe'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx index 89edb58d2f4..c5a7be1c3e6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx @@ -1,11 +1,8 @@ import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { - isRegionalGuidanceLayer, - rgLayerAutoNegativeChanged, - selectControlLayersSlice, -} from 'features/controlLayers/store/controlLayersSlice'; +import { rgLayerAutoNegativeChanged, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import type { ChangeEvent } from 'react'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx index 624047caf3a..78c16a773b4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx @@ -4,11 +4,8 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import RgbColorPicker from 'common/components/RgbColorPicker'; import { stopPropagation } from 'common/util/stopPropagation'; import { rgbColorToString } from 'features/canvas/util/colorToString'; -import { - isRegionalGuidanceLayer, - rgLayerPreviewColorChanged, - selectControlLayersSlice, -} from 'features/controlLayers/store/controlLayersSlice'; +import { rgLayerPreviewColorChanged, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; import type { RgbColor } from 'react-colorful'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx index 578d3789bfd..1d5698ce031 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx @@ -2,7 +2,8 @@ import { Divider, Flex } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { RGLayerIPAdapterWrapper } from 'features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper'; -import { isRegionalGuidanceLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; import { assert } from 'tsafe'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx index 80a32509b41..b2f54c83026 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx @@ -2,21 +2,23 @@ import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; +import { LayerOpacity } from 'features/controlLayers/components/LayerCommon/LayerOpacity'; import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; -import { layerSelected, selectRasterLayerOrThrow } from 'features/controlLayers/store/controlLayersSlice'; +import { layerSelected, selectLayerOrThrow } from 'features/controlLayers/store/controlLayersSlice'; +import { isRasterLayer } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; -import { RasterLayerOpacity } from './RasterLayerOpacity'; - type Props = { layerId: string; }; export const RasterLayer = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); - const isSelected = useAppSelector((s) => selectRasterLayerOrThrow(s.controlLayers.present, layerId).isSelected); + const isSelected = useAppSelector( + (s) => selectLayerOrThrow(s.controlLayers.present, layerId, isRasterLayer).isSelected + ); const onClick = useCallback(() => { dispatch(layerSelected(layerId)); }, [dispatch, layerId]); @@ -28,7 +30,7 @@ export const RasterLayer = memo(({ layerId }: Props) => { - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerOpacity.tsx deleted file mode 100644 index 05e4acd8490..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerOpacity.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { - CompositeNumberInput, - CompositeSlider, - Flex, - FormControl, - FormLabel, - IconButton, - Popover, - PopoverArrow, - PopoverBody, - PopoverContent, - PopoverTrigger, -} from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { stopPropagation } from 'common/util/stopPropagation'; -import { useRasterLayerOpacity } from 'features/controlLayers/hooks/layerStateHooks'; -import { rasterLayerOpacityChanged } from 'features/controlLayers/store/controlLayersSlice'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiDropHalfFill } from 'react-icons/pi'; - -type Props = { - layerId: string; -}; - -const marks = [0, 25, 50, 75, 100]; -const formatPct = (v: number | string) => `${v} %`; - -export const RasterLayerOpacity = memo(({ layerId }: Props) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const opacity = useRasterLayerOpacity(layerId); - const onChangeOpacity = useCallback( - (v: number) => { - dispatch(rasterLayerOpacityChanged({ layerId, opacity: v / 100 })); - }, - [dispatch, layerId] - ); - return ( - - - } - variant="ghost" - onDoubleClick={stopPropagation} - /> - - - - - - - {t('controlLayers.opacity')} - - - - - - - - ); -}); - -RasterLayerOpacity.displayName = 'RasterLayerOpacity'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index a4dc52751e1..dc82e307166 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -6,7 +6,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { BRUSH_SPACING_PCT, MAX_BRUSH_SPACING_PX, MIN_BRUSH_SPACING_PX } from 'features/controlLayers/konva/constants'; import { setStageEventHandlers } from 'features/controlLayers/konva/events'; -import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers'; +import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers/layers'; import { $brushColor, $brushSize, @@ -21,7 +21,6 @@ import { brushLineAdded, brushSizeChanged, eraserLineAdded, - isRegionalGuidanceLayer, layerBboxChanged, layerTranslated, linePointsAdded, @@ -34,6 +33,7 @@ import type { AddPointToLineArg, AddRectShapeArg, } from 'features/controlLayers/store/types'; +import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; import { clamp } from 'lodash-es'; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts index dcbbeb8db5f..244e57c6558 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts @@ -3,9 +3,9 @@ import { caLayerAdded, iiLayerAdded, ipaLayerAdded, - isInitialImageLayer, rgLayerIPAdapterAdded, } from 'features/controlLayers/store/controlLayersSlice'; +import { isInitialImageLayer } from 'features/controlLayers/store/types'; import { buildControlNet, buildIPAdapter, diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts index b036b257422..c643b863fd6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts @@ -1,12 +1,8 @@ import { createSelector } from '@reduxjs/toolkit'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { - isControlAdapterLayer, - isRasterLayer, - isRegionalGuidanceLayer, - selectControlLayersSlice, -} from 'features/controlLayers/store/controlLayersSlice'; +import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import { isControlAdapterLayer, isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; import { assert } from 'tsafe'; @@ -81,17 +77,3 @@ export const useCALayerOpacity = (layerId: string) => { const opacity = useAppSelector(selectLayer); return opacity; }; - -export const useRasterLayerOpacity = (layerId: string) => { - const selectLayer = useMemo( - () => - createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { - const layer = controlLayers.present.layers.filter(isRasterLayer).find((l) => l.id === layerId); - assert(layer, `Layer ${layerId} not found`); - return Math.round(layer.opacity * 100); - }), - [layerId] - ); - const opacity = useAppSelector(selectLayer); - return opacity; -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts b/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts index 27bfc8b7310..638b6da7486 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts @@ -34,3 +34,8 @@ export const MIN_BRUSH_SPACING_PX = 5; * The maximum brush spacing in pixels. */ export const MAX_BRUSH_SPACING_PX = 15; + +/** + * The debounce time in milliseconds for debounced renderers. + */ +export const DEBOUNCE_MS = 300; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 0a26dba92d2..e07753127dd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -67,6 +67,7 @@ export const setStageEventHandlers = ({ onRectShapeAdded, onBrushSizeChanged, }: SetStageEventHandlersArg): (() => void) => { + //#region mouseenter stage.on('mouseenter', (e) => { const stage = e.target.getStage(); if (!stage) { @@ -76,6 +77,7 @@ export const setStageEventHandlers = ({ stage.findOne(`#${TOOL_PREVIEW_LAYER_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); }); + //#region mousedown stage.on('mousedown', (e) => { const stage = e.target.getStage(); if (!stage) { @@ -110,6 +112,7 @@ export const setStageEventHandlers = ({ } }); + //#region mouseup stage.on('mouseup', (e) => { const stage = e.target.getStage(); if (!stage) { @@ -143,6 +146,7 @@ export const setStageEventHandlers = ({ $lastMouseDownPos.set(null); }); + //#region mousemove stage.on('mousemove', (e) => { const stage = e.target.getStage(); if (!stage) { @@ -191,6 +195,7 @@ export const setStageEventHandlers = ({ } }); + //#region mouseleave stage.on('mouseleave', (e) => { const stage = e.target.getStage(); if (!stage) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers.ts deleted file mode 100644 index d69c14afa37..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers.ts +++ /dev/null @@ -1,1183 +0,0 @@ -import { rgbaColorToString, rgbColorToString } from 'features/canvas/util/colorToString'; -import { getLayerBboxFast, getLayerBboxPixels } from 'features/controlLayers/konva/bbox'; -import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; -import { - BACKGROUND_LAYER_ID, - BACKGROUND_RECT_ID, - CA_LAYER_IMAGE_NAME, - CA_LAYER_NAME, - COMPOSITING_RECT_NAME, - getCALayerImageId, - getIILayerImageId, - getLayerBboxId, - getObjectGroupId, - INITIAL_IMAGE_LAYER_IMAGE_NAME, - INITIAL_IMAGE_LAYER_NAME, - LAYER_BBOX_NAME, - NO_LAYERS_MESSAGE_LAYER_ID, - RASTER_LAYER_NAME, - RASTER_LAYER_OBJECT_GROUP_NAME, - RG_LAYER_LINE_NAME, - RG_LAYER_NAME, - RG_LAYER_OBJECT_GROUP_NAME, - RG_LAYER_RECT_NAME, - TOOL_PREVIEW_BRUSH_BORDER_INNER_ID, - TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID, - TOOL_PREVIEW_BRUSH_FILL_ID, - TOOL_PREVIEW_BRUSH_GROUP_ID, - TOOL_PREVIEW_LAYER_ID, - TOOL_PREVIEW_RECT_ID, -} from 'features/controlLayers/konva/naming'; -import { getScaledFlooredCursorPosition, snapPosToStage } from 'features/controlLayers/konva/util'; -import { - isControlAdapterLayer, - isInitialImageLayer, - isRasterLayer, - isRegionalGuidanceLayer, - isRenderableLayer, -} from 'features/controlLayers/store/controlLayersSlice'; -import type { - BrushLine, - ControlAdapterLayer, - EraserLine, - InitialImageLayer, - Layer, - RasterLayer, - RectShape, - RegionalGuidanceLayer, - RgbaColor, - Tool, -} from 'features/controlLayers/store/types'; -import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types'; -import { t } from 'i18next'; -import Konva from 'konva'; -import type { IRect, Vector2d } from 'konva/lib/types'; -import { debounce } from 'lodash-es'; -import type { ImageDTO } from 'services/api/types'; -import { assert } from 'tsafe'; -import { v4 as uuidv4 } from 'uuid'; - -import { - BBOX_SELECTED_STROKE, - BRUSH_BORDER_INNER_COLOR, - BRUSH_BORDER_OUTER_COLOR, - TRANSPARENCY_CHECKER_PATTERN, -} from './constants'; - -/** - * Creates the singleton tool preview layer and all its objects. - * @param stage The konva stage - */ -const createToolPreviewLayer = (stage: Konva.Stage): Konva.Layer => { - // Initialize the brush preview layer & add to the stage - const toolPreviewLayer = new Konva.Layer({ id: TOOL_PREVIEW_LAYER_ID, visible: false, listening: false }); - stage.add(toolPreviewLayer); - - // Create the brush preview group & circles - const brushPreviewGroup = new Konva.Group({ id: TOOL_PREVIEW_BRUSH_GROUP_ID }); - const brushPreviewFill = new Konva.Circle({ - id: TOOL_PREVIEW_BRUSH_FILL_ID, - listening: false, - strokeEnabled: false, - }); - brushPreviewGroup.add(brushPreviewFill); - const brushPreviewBorderInner = new Konva.Circle({ - id: TOOL_PREVIEW_BRUSH_BORDER_INNER_ID, - listening: false, - stroke: BRUSH_BORDER_INNER_COLOR, - strokeWidth: 1, - strokeEnabled: true, - }); - brushPreviewGroup.add(brushPreviewBorderInner); - const brushPreviewBorderOuter = new Konva.Circle({ - id: TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID, - listening: false, - stroke: BRUSH_BORDER_OUTER_COLOR, - strokeWidth: 1, - strokeEnabled: true, - }); - brushPreviewGroup.add(brushPreviewBorderOuter); - toolPreviewLayer.add(brushPreviewGroup); - - // 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 }); - toolPreviewLayer.add(rectPreview); - - return toolPreviewLayer; -}; - -/** - * Renders the brush preview for the selected tool. - * @param stage The konva stage - * @param tool The selected tool - * @param color The selected layer's color - * @param selectedLayerType The selected layer's type - * @param globalMaskLayerOpacity The global mask layer opacity - * @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 = ( - stage: Konva.Stage, - tool: Tool, - brushColor: RgbaColor, - selectedLayerType: Layer['type'] | null, - globalMaskLayerOpacity: number, - cursorPos: Vector2d | null, - lastMouseDownPos: Vector2d | null, - brushSize: number -): void => { - const layerCount = stage.find(selectRenderableLayers).length; - // Update the stage's pointer style - if (layerCount === 0) { - // We have no layers, so we should not render any tool - stage.container().style.cursor = 'default'; - } else if (selectedLayerType !== 'regional_guidance_layer' && selectedLayerType !== 'raster_layer') { - // Non-mask-guidance layers don't have tools - stage.container().style.cursor = 'not-allowed'; - } else if (tool === 'move') { - // Move tool gets a pointer - stage.container().style.cursor = 'default'; - } else if (tool === 'rect') { - // Move rect gets a crosshair - stage.container().style.cursor = 'crosshair'; - } else { - // Else we hide the native cursor and use the konva-rendered brush preview - stage.container().style.cursor = 'none'; - } - - const toolPreviewLayer = stage.findOne(`#${TOOL_PREVIEW_LAYER_ID}`) ?? createToolPreviewLayer(stage); - - if (!cursorPos || layerCount === 0) { - // We can bail early if the mouse isn't over the stage or there are no layers - toolPreviewLayer.visible(false); - return; - } - - toolPreviewLayer.visible(true); - - const brushPreviewGroup = stage.findOne(`#${TOOL_PREVIEW_BRUSH_GROUP_ID}`); - assert(brushPreviewGroup, 'Brush preview group not found'); - - const rectPreview = stage.findOne(`#${TOOL_PREVIEW_RECT_ID}`); - assert(rectPreview, 'Rect preview not found'); - - // No need to render the brush preview if the cursor position or color is missing - if (cursorPos && (tool === 'brush' || tool === 'eraser')) { - // Update the fill circle - const brushPreviewFill = brushPreviewGroup.findOne(`#${TOOL_PREVIEW_BRUSH_FILL_ID}`); - brushPreviewFill?.setAttrs({ - x: cursorPos.x, - y: cursorPos.y, - radius: brushSize / 2, - fill: rgbaColorToString(brushColor), - globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out', - }); - - // Update the inner border of the brush preview - const brushPreviewInner = toolPreviewLayer.findOne(`#${TOOL_PREVIEW_BRUSH_BORDER_INNER_ID}`); - brushPreviewInner?.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: brushSize / 2 }); - - // Update the outer border of the brush preview - const brushPreviewOuter = toolPreviewLayer.findOne(`#${TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID}`); - brushPreviewOuter?.setAttrs({ - x: cursorPos.x, - y: cursorPos.y, - radius: brushSize / 2 + 1, - }); - - brushPreviewGroup.visible(true); - } else { - brushPreviewGroup.visible(false); - } - - if (cursorPos && lastMouseDownPos && tool === 'rect') { - const snappedPos = snapPosToStage(cursorPos, stage); - const rectPreview = toolPreviewLayer.findOne(`#${TOOL_PREVIEW_RECT_ID}`); - rectPreview?.setAttrs({ - x: Math.min(snappedPos.x, lastMouseDownPos.x), - y: Math.min(snappedPos.y, lastMouseDownPos.y), - width: Math.abs(snappedPos.x - lastMouseDownPos.x), - height: Math.abs(snappedPos.y - lastMouseDownPos.y), - }); - rectPreview?.visible(true); - } else { - rectPreview?.visible(false); - } -}; - -/** - * Creates a regional guidance layer. - * @param stage The konva stage - * @param layerState The regional guidance layer state - * @param onLayerPosChanged Callback for when the layer's position changes - */ -const createRGLayer = ( - stage: Konva.Stage, - layerState: RegionalGuidanceLayer, - onLayerPosChanged?: (layerId: string, x: number, y: number) => void -): Konva.Layer => { - // This layer hasn't been added to the konva state yet - const konvaLayer = new Konva.Layer({ - id: layerState.id, - name: RG_LAYER_NAME, - draggable: true, - dragDistance: 0, - }); - - // 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) { - konvaLayer.on('dragend', function (e) { - onLayerPosChanged(layerState.id, Math.floor(e.target.x()), Math.floor(e.target.y())); - }); - } - - // The dragBoundFunc limits how far the layer can be dragged - konvaLayer.dragBoundFunc(function (pos) { - const cursorPos = getScaledFlooredCursorPosition(stage); - if (!cursorPos) { - return this.getAbsolutePosition(); - } - // Prevent the user from dragging the layer out of the stage bounds by constaining the cursor position to the stage bounds - if ( - cursorPos.x < 0 || - cursorPos.x > stage.width() / stage.scaleX() || - cursorPos.y < 0 || - cursorPos.y > stage.height() / stage.scaleY() - ) { - return this.getAbsolutePosition(); - } - 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; -}; -//#endregion - -/** - * 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 - */ -const createBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Group): Konva.Line => { - const konvaLine = new Konva.Line({ - id: brushLine.id, - key: brushLine.id, - name: RG_LAYER_LINE_NAME, - strokeWidth: brushLine.strokeWidth, - tension: 0, - lineCap: 'round', - lineJoin: 'round', - shadowForStrokeEnabled: false, - globalCompositeOperation: 'source-over', - listening: false, - stroke: rgbaColorToString(brushLine.color), - }); - layerObjectGroup.add(konvaLine); - return konvaLine; -}; - -/** - * 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 - */ -const createEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva.Group): Konva.Line => { - const konvaLine = new Konva.Line({ - id: eraserLine.id, - key: eraserLine.id, - name: RG_LAYER_LINE_NAME, - strokeWidth: eraserLine.strokeWidth, - tension: 0, - lineCap: 'round', - lineJoin: 'round', - shadowForStrokeEnabled: false, - globalCompositeOperation: 'destination-out', - listening: false, - stroke: rgbaColorToString(DEFAULT_RGBA_COLOR), - }); - layerObjectGroup.add(konvaLine); - return konvaLine; -}; - -/** - * 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 - */ -const createRectShape = (rectShape: RectShape, layerObjectGroup: Konva.Group): Konva.Rect => { - const konvaRect = new Konva.Rect({ - id: rectShape.id, - key: rectShape.id, - name: RG_LAYER_RECT_NAME, - x: rectShape.x, - y: rectShape.y, - width: rectShape.width, - height: rectShape.height, - listening: false, - fill: rgbaColorToString(rectShape.color), - }); - layerObjectGroup.add(konvaRect); - return konvaRect; -}; - -/** - * Creates the "compositing rect" for a regional guidance layer. - * @param konvaLayer The konva layer - */ -const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => { - const compositingRect = new Konva.Rect({ name: COMPOSITING_RECT_NAME, listening: false }); - konvaLayer.add(compositingRect); - return compositingRect; -}; - -/** - * Renders a raster layer. - * @param stage The konva stage - * @param layerState The regional guidance layer state - * @param globalMaskLayerOpacity The global mask layer opacity - * @param tool The current tool - * @param onLayerPosChanged Callback for when the layer's position changes - */ -const renderRGLayer = ( - stage: Konva.Stage, - layerState: RegionalGuidanceLayer, - globalMaskLayerOpacity: number, - tool: Tool, - onLayerPosChanged?: (layerId: string, x: number, y: number) => void -): void => { - const konvaLayer = - stage.findOne(`#${layerState.id}`) ?? createRGLayer(stage, layerState, onLayerPosChanged); - - // Update the layer's position and listening state - konvaLayer.setAttrs({ - listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events - x: Math.floor(layerState.x), - y: Math.floor(layerState.y), - }); - - // 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}`); - - // We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required. - let groupNeedsCache = false; - - const objectIds = layerState.objects.map(mapId); - // Destroy any objects that are no longer in the redux state - for (const objectNode of konvaObjectGroup.find(selectVectorMaskObjects)) { - if (!objectIds.includes(objectNode.id())) { - objectNode.destroy(); - groupNeedsCache = true; - } - } - - for (const obj of layerState.objects) { - if (obj.type === 'brush_line') { - const konvaBrushLine = stage.findOne(`#${obj.id}`) ?? createBrushLine(obj, konvaObjectGroup); - - // Only update the points if they have changed. The point values are never mutated, they are only added to the - // array, so checking the length is sufficient to determine if we need to re-cache. - if (konvaBrushLine.points().length !== obj.points.length) { - konvaBrushLine.points(obj.points); - groupNeedsCache = true; - } - // Only update the color if it has changed. - if (konvaBrushLine.stroke() !== rgbColor) { - konvaBrushLine.stroke(rgbColor); - groupNeedsCache = true; - } - } else if (obj.type === 'eraser_line') { - const konvaEraserLine = stage.findOne(`#${obj.id}`) ?? createEraserLine(obj, konvaObjectGroup); - - // Only update the points if they have changed. The point values are never mutated, they are only added to the - // array, so checking the length is sufficient to determine if we need to re-cache. - if (konvaEraserLine.points().length !== obj.points.length) { - konvaEraserLine.points(obj.points); - groupNeedsCache = true; - } - // Only update the color if it has changed. - if (konvaEraserLine.stroke() !== rgbColor) { - konvaEraserLine.stroke(rgbColor); - groupNeedsCache = true; - } - } else if (obj.type === 'rect_shape') { - const konvaRectShape = stage.findOne(`#${obj.id}`) ?? createRectShape(obj, konvaObjectGroup); - - // Only update the color if it has changed. - if (konvaRectShape.fill() !== rgbColor) { - konvaRectShape.fill(rgbColor); - groupNeedsCache = true; - } - } - } - - // Only update layer visibility if it has changed. - if (konvaLayer.visible() !== layerState.isEnabled) { - konvaLayer.visible(layerState.isEnabled); - groupNeedsCache = true; - } - - if (konvaObjectGroup.getChildren().length === 0) { - // No objects - clear the cache to reset the previous pixel data - konvaObjectGroup.clearCache(); - return; - } - - const compositingRect = - konvaLayer.findOne(`.${COMPOSITING_RECT_NAME}`) ?? createCompositingRect(konvaLayer); - - /** - * When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows - * shapes to render as a "raster" layer with all pixels drawn at the same color and opacity. - * - * Without this special handling, each shape is drawn individually with the given opacity, atop the other shapes. The - * effect is like if you have a Photoshop Group consisting of many shapes, each of which has the given opacity. - * Overlapping shapes will have their colors blended together, and the final color is the result of all the shapes. - * - * 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. - */ - if (layerState.isSelected && tool !== 'move') { - // We must clear the cache first so Konva will re-draw the group with the new compositing rect - if (konvaObjectGroup.isCached()) { - konvaObjectGroup.clearCache(); - } - // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work - konvaObjectGroup.opacity(1); - - compositingRect.setAttrs({ - // The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already - ...(!layerState.bboxNeedsUpdate && layerState.bbox ? layerState.bbox : getLayerBboxFast(konvaLayer)), - fill: rgbColor, - opacity: globalMaskLayerOpacity, - // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) - globalCompositeOperation: 'source-in', - visible: true, - // This rect must always be on top of all other shapes - zIndex: konvaObjectGroup.getChildren().length, - }); - } else { - // The compositing rect should only be shown when the layer is selected. - compositingRect.visible(false); - // Cache only if needed - or if we are on this code path and _don't_ have a cache - if (groupNeedsCache || !konvaObjectGroup.isCached()) { - konvaObjectGroup.cache(); - } - // Updating group opacity does not require re-caching - konvaObjectGroup.opacity(globalMaskLayerOpacity); - } -}; - -/** - * Creates a raster layer. - * @param stage The konva stage - * @param layerState The raster layer state - * @param onLayerPosChanged Callback for when the layer's position changes - */ -const createRasterLayer = ( - stage: Konva.Stage, - layerState: RasterLayer, - onLayerPosChanged?: (layerId: string, x: number, y: number) => void -): Konva.Layer => { - // This layer hasn't been added to the konva state yet - const konvaLayer = new Konva.Layer({ - id: layerState.id, - name: RASTER_LAYER_NAME, - draggable: true, - dragDistance: 0, - }); - - // 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) { - konvaLayer.on('dragend', function (e) { - onLayerPosChanged(layerState.id, Math.floor(e.target.x()), Math.floor(e.target.y())); - }); - } - - // The dragBoundFunc limits how far the layer can be dragged - konvaLayer.dragBoundFunc(function (pos) { - const cursorPos = getScaledFlooredCursorPosition(stage); - if (!cursorPos) { - return this.getAbsolutePosition(); - } - // Prevent the user from dragging the layer out of the stage bounds by constaining the cursor position to the stage bounds - if ( - cursorPos.x < 0 || - cursorPos.x > stage.width() / stage.scaleX() || - cursorPos.y < 0 || - cursorPos.y > stage.height() / stage.scaleY() - ) { - return this.getAbsolutePosition(); - } - 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; -}; - -/** - * Renders a regional guidance layer. - * @param stage The konva stage - * @param layerState The regional guidance layer state - * @param tool The current tool - * @param onLayerPosChanged Callback for when the layer's position changes - */ -const renderRasterLayer = ( - 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); - - // Update the layer's position and listening state - konvaLayer.setAttrs({ - listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events - x: Math.floor(layerState.x), - 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 objectIds = layerState.objects.map(mapId); - // Destroy any objects that are no longer in the redux state - for (const objectNode of konvaObjectGroup.getChildren()) { - if (!objectIds.includes(objectNode.id())) { - objectNode.destroy(); - } - } - - for (const obj of layerState.objects) { - if (obj.type === 'brush_line') { - const konvaBrushLine = stage.findOne(`#${obj.id}`) ?? createBrushLine(obj, konvaObjectGroup); - // 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); - // 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); - } - } - } - - // Only update layer visibility if it has changed. - if (konvaLayer.visible() !== layerState.isEnabled) { - konvaLayer.visible(layerState.isEnabled); - } - - konvaObjectGroup.opacity(layerState.opacity); -}; - -/** - * 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({ - id: layerState.id, - name: INITIAL_IMAGE_LAYER_NAME, - imageSmoothingEnabled: true, - listening: false, - }); - stage.add(konvaLayer); - return konvaLayer; -}; - -/** - * 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({ - name: INITIAL_IMAGE_LAYER_IMAGE_NAME, - image: imageEl, - }); - konvaLayer.add(konvaImage); - return konvaImage; -}; - -/** - * Updates an initial image layer's attributes (width, height, opacity, visibility). - * @param stage The konva stage - * @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, - // but it doesn't seem to break anything. - // TODO(psyche): Investigate and report upstream. - const newWidth = stage.width() / stage.scaleX(); - const newHeight = stage.height() / stage.scaleY(); - if ( - konvaImage.width() !== newWidth || - konvaImage.height() !== newHeight || - konvaImage.visible() !== layerState.isEnabled - ) { - konvaImage.setAttrs({ - opacity: layerState.opacity, - scaleX: 1, - scaleY: 1, - width: stage.width() / stage.scaleX(), - height: stage.height() / stage.scaleY(), - visible: layerState.isEnabled, - }); - } - if (konvaImage.opacity() !== layerState.opacity) { - konvaImage.opacity(layerState.opacity); - } -}; - -/** - * 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, - konvaLayer: Konva.Layer, - layerState: InitialImageLayer, - getImageDTO: (imageName: string) => Promise -): Promise => { - if (layerState.image) { - const imageName = layerState.image.name; - const imageDTO = await getImageDTO(imageName); - if (!imageDTO) { - return; - } - const imageEl = new Image(); - const imageId = getIILayerImageId(layerState.id, imageName); - imageEl.onload = () => { - // Find the existing image or create a new one - must find using the name, bc the id may have just changed - const konvaImage = - konvaLayer.findOne(`.${INITIAL_IMAGE_LAYER_IMAGE_NAME}`) ?? - createIILayerImage(konvaLayer, imageEl); - - // Update the image's attributes - konvaImage.setAttrs({ - id: imageId, - image: imageEl, - }); - updateIILayerImageAttrs(stage, konvaImage, layerState); - imageEl.id = imageId; - }; - imageEl.src = imageDTO.image_url; - } else { - konvaLayer.findOne(`.${INITIAL_IMAGE_LAYER_IMAGE_NAME}`)?.destroy(); - } -}; - -/** - * 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 -): void => { - const konvaLayer = stage.findOne(`#${layerState.id}`) ?? createIILayer(stage, layerState); - const konvaImage = konvaLayer.findOne(`.${INITIAL_IMAGE_LAYER_IMAGE_NAME}`); - const canvasImageSource = konvaImage?.image(); - let imageSourceNeedsUpdate = false; - if (canvasImageSource instanceof HTMLImageElement) { - const image = layerState.image; - if (image && canvasImageSource.id !== getCALayerImageId(layerState.id, image.name)) { - imageSourceNeedsUpdate = true; - } else if (!image) { - imageSourceNeedsUpdate = true; - } - } else if (!canvasImageSource) { - imageSourceNeedsUpdate = true; - } - - if (imageSourceNeedsUpdate) { - updateIILayerImageSource(stage, konvaLayer, layerState, getImageDTO); - } else if (konvaImage) { - updateIILayerImageAttrs(stage, konvaImage, layerState); - } -}; - -/** - * 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({ - id: layerState.id, - name: CA_LAYER_NAME, - imageSmoothingEnabled: true, - listening: false, - }); - stage.add(konvaLayer); - return konvaLayer; -}; - -/** - * 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({ - name: CA_LAYER_IMAGE_NAME, - image: imageEl, - }); - konvaLayer.add(konvaImage); - return konvaImage; -}; - -/** - * 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, - konvaLayer: Konva.Layer, - layerState: ControlAdapterLayer, - getImageDTO: (imageName: string) => Promise -): Promise => { - const image = layerState.controlAdapter.processedImage ?? layerState.controlAdapter.image; - if (image) { - const imageName = image.name; - const imageDTO = await getImageDTO(imageName); - if (!imageDTO) { - return; - } - const imageEl = new Image(); - const imageId = getCALayerImageId(layerState.id, imageName); - imageEl.onload = () => { - // Find the existing image or create a new one - must find using the name, bc the id may have just changed - const konvaImage = - konvaLayer.findOne(`.${CA_LAYER_IMAGE_NAME}`) ?? createCALayerImage(konvaLayer, imageEl); - - // Update the image's attributes - konvaImage.setAttrs({ - id: imageId, - image: imageEl, - }); - updateCALayerImageAttrs(stage, konvaImage, layerState); - // Must cache after this to apply the filters - konvaImage.cache(); - imageEl.id = imageId; - }; - imageEl.src = imageDTO.image_url; - } else { - konvaLayer.findOne(`.${CA_LAYER_IMAGE_NAME}`)?.destroy(); - } -}; - -/** - * 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, - konvaImage: Konva.Image, - layerState: ControlAdapterLayer -): void => { - let needsCache = false; - // 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. - // TODO(psyche): Investigate and report upstream. - const newWidth = stage.width() / stage.scaleX(); - const newHeight = stage.height() / stage.scaleY(); - const hasFilter = konvaImage.filters() !== null && konvaImage.filters().length > 0; - if ( - konvaImage.width() !== newWidth || - konvaImage.height() !== newHeight || - konvaImage.visible() !== layerState.isEnabled || - hasFilter !== layerState.isFilterEnabled - ) { - konvaImage.setAttrs({ - opacity: layerState.opacity, - scaleX: 1, - scaleY: 1, - width: stage.width() / stage.scaleX(), - height: stage.height() / stage.scaleY(), - visible: layerState.isEnabled, - filters: layerState.isFilterEnabled ? [LightnessToAlphaFilter] : [], - }); - needsCache = true; - } - if (konvaImage.opacity() !== layerState.opacity) { - konvaImage.opacity(layerState.opacity); - } - if (needsCache) { - konvaImage.cache(); - } -}; - -/** - * 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 -): void => { - const konvaLayer = stage.findOne(`#${layerState.id}`) ?? createCALayer(stage, layerState); - const konvaImage = konvaLayer.findOne(`.${CA_LAYER_IMAGE_NAME}`); - const canvasImageSource = konvaImage?.image(); - let imageSourceNeedsUpdate = false; - if (canvasImageSource instanceof HTMLImageElement) { - const image = layerState.controlAdapter.processedImage ?? layerState.controlAdapter.image; - if (image && canvasImageSource.id !== getCALayerImageId(layerState.id, image.name)) { - imageSourceNeedsUpdate = true; - } else if (!image) { - imageSourceNeedsUpdate = true; - } - } else if (!canvasImageSource) { - imageSourceNeedsUpdate = true; - } - - if (imageSourceNeedsUpdate) { - updateCALayerImageSource(stage, konvaLayer, layerState, getImageDTO); - } else if (konvaImage) { - updateCALayerImageAttrs(stage, konvaImage, layerState); - } -}; - -/** - * Renders the layers on the stage. - * @param stage The konva stage - * @param layerStates Array of all layer states - * @param globalMaskLayerOpacity The global mask layer opacity - * @param tool The current tool - * @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 = ( - stage: Konva.Stage, - layerStates: Layer[], - globalMaskLayerOpacity: number, - tool: Tool, - getImageDTO: (imageName: string) => Promise, - onLayerPosChanged?: (layerId: string, x: number, y: number) => void -): void => { - const layerIds = layerStates.filter(isRenderableLayer).map(mapId); - // Remove un-rendered layers - for (const konvaLayer of stage.find(selectRenderableLayers)) { - if (!layerIds.includes(konvaLayer.id())) { - konvaLayer.destroy(); - } - } - - for (const layer of layerStates) { - if (isRegionalGuidanceLayer(layer)) { - renderRGLayer(stage, layer, globalMaskLayerOpacity, tool, onLayerPosChanged); - } - if (isControlAdapterLayer(layer)) { - renderCALayer(stage, layer, getImageDTO); - } - if (isInitialImageLayer(layer)) { - renderIILayer(stage, layer, getImageDTO); - } - if (isRasterLayer(layer)) { - renderRasterLayer(stage, layer, tool, onLayerPosChanged); - } - // IP Adapter layers are not rendered - } -}; - -/** - * Creates a bounding box rect for a layer. - * @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 - */ -const createBboxRect = (layerState: Layer, konvaLayer: Konva.Layer): Konva.Rect => { - const rect = new Konva.Rect({ - id: getLayerBboxId(layerState.id), - name: LAYER_BBOX_NAME, - strokeWidth: 1, - visible: false, - }); - konvaLayer.add(rect); - return rect; -}; - -/** - * Renders the bounding boxes for the layers. - * @param stage The konva stage - * @param layerStates An array of layers to draw bboxes for - * @param tool The current tool - * @returns - */ -const renderBboxes = (stage: Konva.Stage, layerStates: Layer[], tool: Tool): void => { - // Hide all bboxes so they don't interfere with getClientRect - for (const bboxRect of stage.find(`.${LAYER_BBOX_NAME}`)) { - bboxRect.visible(false); - bboxRect.listening(false); - } - // No selected layer or not using the move tool - nothing more to do here - if (tool !== 'move') { - return; - } - - for (const layer of layerStates.filter(isRegionalGuidanceLayer)) { - if (!layer.bbox) { - continue; - } - const konvaLayer = stage.findOne(`#${layer.id}`); - assert(konvaLayer, `Layer ${layer.id} not found in stage`); - - const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layer, konvaLayer); - - bboxRect.setAttrs({ - visible: !layer.bboxNeedsUpdate, - listening: layer.isSelected, - x: layer.bbox.x, - y: layer.bbox.y, - width: layer.bbox.width, - height: layer.bbox.height, - stroke: layer.isSelected ? BBOX_SELECTED_STROKE : '', - }); - } -}; - -/** - * Calculates the bbox of each regional guidance layer. Only calculates if the mask has changed. - * @param stage The konva stage - * @param layerStates An array of layers to calculate bboxes for - * @param onBboxChanged Callback for when the bounding box changes - */ -const updateBboxes = ( - stage: Konva.Stage, - layerStates: Layer[], - onBboxChanged: (layerId: string, bbox: IRect | null) => void -): void => { - for (const rgLayer of layerStates.filter(isRegionalGuidanceLayer)) { - const konvaLayer = stage.findOne(`#${rgLayer.id}`); - assert(konvaLayer, `Layer ${rgLayer.id} not found in stage`); - // We only need to recalculate the bbox if the layer has changed - if (rgLayer.bboxNeedsUpdate) { - const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rgLayer, konvaLayer); - - // Hide the bbox while we calculate the new bbox, else the bbox will be included in the calculation - const visible = bboxRect.visible(); - bboxRect.visible(false); - - if (rgLayer.objects.length === 0) { - // No objects - no bbox to calculate - onBboxChanged(rgLayer.id, null); - } else { - // Calculate the bbox by rendering the layer and checking its pixels - onBboxChanged(rgLayer.id, getLayerBboxPixels(konvaLayer)); - } - - // Restore the visibility of the bbox - bboxRect.visible(visible); - } - } -}; - -/** - * Creates the background layer for the stage. - * @param stage The konva stage - */ -const createBackgroundLayer = (stage: Konva.Stage): Konva.Layer => { - const layer = new Konva.Layer({ - id: BACKGROUND_LAYER_ID, - }); - const background = new Konva.Rect({ - id: BACKGROUND_RECT_ID, - x: stage.x(), - y: 0, - width: stage.width() / stage.scaleX(), - height: stage.height() / stage.scaleY(), - listening: false, - opacity: 0.2, - }); - layer.add(background); - stage.add(layer); - const image = new Image(); - image.onload = () => { - background.fillPatternImage(image); - }; - image.src = TRANSPARENCY_CHECKER_PATTERN; - return layer; -}; - -/** - * Renders the background layer for the stage. - * @param stage The konva stage - * @param width The unscaled width of the canvas - * @param height The unscaled height of the canvas - */ -const renderBackground = (stage: Konva.Stage, width: number, height: number): void => { - const layer = stage.findOne(`#${BACKGROUND_LAYER_ID}`) ?? createBackgroundLayer(stage); - - const background = layer.findOne(`#${BACKGROUND_RECT_ID}`); - assert(background, 'Background rect not found'); - // ensure background rect is in the top-left of the canvas - background.absolutePosition({ x: 0, y: 0 }); - - // set the dimensions of the background rect to match the canvas - not the stage!!! - background.size({ - width: width / stage.scaleX(), - height: height / stage.scaleY(), - }); - - // Calculate the amount the stage is moved - including the effect of scaling - const stagePos = { - x: -stage.x() / stage.scaleX(), - y: -stage.y() / stage.scaleY(), - }; - - // Apply that movement to the fill pattern - background.fillPatternOffset(stagePos); -}; - -/** - * Arranges all layers in the z-axis by updating their z-indices. - * @param stage The konva stage - * @param layerIds An array of redux layer ids, in their z-index order - */ -const arrangeLayers = (stage: Konva.Stage, layerIds: string[]): void => { - let nextZIndex = 0; - // Background is the first layer - stage.findOne(`#${BACKGROUND_LAYER_ID}`)?.zIndex(nextZIndex++); - // Then arrange the redux layers in order - for (const layerId of layerIds) { - stage.findOne(`#${layerId}`)?.zIndex(nextZIndex++); - } - // Finally, the tool preview layer is always on top - stage.findOne(`#${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 noLayersMessageLayer = new Konva.Layer({ - id: NO_LAYERS_MESSAGE_LAYER_ID, - opacity: 0.7, - listening: false, - }); - const text = new Konva.Text({ - x: 0, - y: 0, - align: 'center', - verticalAlign: 'middle', - text: t('controlLayers.noLayersAdded', 'No Layers Added'), - fontFamily: '"Inter Variable", sans-serif', - fontStyle: '600', - fill: 'white', - }); - noLayersMessageLayer.add(text); - stage.add(noLayersMessageLayer); - return noLayersMessageLayer; -}; - -/** - * 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 = - stage.findOne(`#${NO_LAYERS_MESSAGE_LAYER_ID}`) ?? createNoLayersMessageLayer(stage); - if (layerCount === 0) { - noLayersMessageLayer.findOne('Text')?.setAttrs({ - width, - height, - fontSize: 32 / stage.scaleX(), - }); - } else { - noLayersMessageLayer?.destroy(); - } -}; - -export const renderers = { - renderToolPreview, - renderLayers, - renderBboxes, - renderBackground, - renderNoLayersMessage, - arrangeLayers, - updateBboxes, -}; - -const DEBOUNCE_MS = 300; - -export const debouncedRenderers = { - renderToolPreview: debounce(renderToolPreview, DEBOUNCE_MS), - renderLayers: debounce(renderLayers, DEBOUNCE_MS), - renderBboxes: debounce(renderBboxes, DEBOUNCE_MS), - renderBackground: debounce(renderBackground, DEBOUNCE_MS), - renderNoLayersMessage: debounce(renderNoLayersMessage, DEBOUNCE_MS), - arrangeLayers: debounce(arrangeLayers, DEBOUNCE_MS), - updateBboxes: debounce(updateBboxes, DEBOUNCE_MS), -}; - -//#region util -const mapId = (object: { id: string }): string => object.id; - -/** - * 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() === RASTER_LAYER_NAME; - -/** - * 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; -}; -//#endregion diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts new file mode 100644 index 00000000000..d5dcfddcda1 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts @@ -0,0 +1,67 @@ +import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants'; +import { BACKGROUND_LAYER_ID, BACKGROUND_RECT_ID } from 'features/controlLayers/konva/naming'; +import Konva from 'konva'; +import { assert } from 'tsafe'; + +/** + * The stage background is a semi-transparent checkerboard pattern. We use konva's `fillPatternImage` to apply the + * a data URL of the pattern image to the background rect. Some scaling and positioning is required to ensure the + * everything lines up correctly. + */ + +/** + * Creates the background layer for the stage. + * @param stage The konva stage + */ +const createBackgroundLayer = (stage: Konva.Stage): Konva.Layer => { + const layer = new Konva.Layer({ + id: BACKGROUND_LAYER_ID, + }); + const background = new Konva.Rect({ + id: BACKGROUND_RECT_ID, + x: stage.x(), + y: 0, + width: stage.width() / stage.scaleX(), + height: stage.height() / stage.scaleY(), + listening: false, + opacity: 0.2, + }); + layer.add(background); + stage.add(layer); + const image = new Image(); + image.onload = () => { + background.fillPatternImage(image); + }; + image.src = TRANSPARENCY_CHECKER_PATTERN; + return layer; +}; + +/** + * Renders the background layer for the stage. + * @param stage The konva stage + * @param width The unscaled width of the canvas + * @param height The unscaled height of the canvas + */ +export const renderBackground = (stage: Konva.Stage, width: number, height: number): void => { + const layer = stage.findOne(`#${BACKGROUND_LAYER_ID}`) ?? createBackgroundLayer(stage); + + const background = layer.findOne(`#${BACKGROUND_RECT_ID}`); + assert(background, 'Background rect not found'); + // ensure background rect is in the top-left of the canvas + background.absolutePosition({ x: 0, y: 0 }); + + // set the dimensions of the background rect to match the canvas - not the stage!!! + background.size({ + width: width / stage.scaleX(), + height: height / stage.scaleY(), + }); + + // Calculate the amount the stage is moved - including the effect of scaling + const stagePos = { + x: -stage.x() / stage.scaleX(), + y: -stage.y() / stage.scaleY(), + }; + + // Apply that movement to the fill pattern + background.fillPatternOffset(stagePos); +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/bbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts similarity index 58% rename from invokeai/frontend/web/src/features/controlLayers/konva/bbox.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts index 505998cb394..869fe847d13 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/bbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts @@ -1,10 +1,17 @@ import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; import { imageDataToDataURL } from 'features/canvas/util/blobToDataURL'; +import { BBOX_SELECTED_STROKE } from 'features/controlLayers/konva/constants'; +import { getLayerBboxId, LAYER_BBOX_NAME, RG_LAYER_OBJECT_GROUP_NAME } from 'features/controlLayers/konva/naming'; +import type { Layer, Tool } from 'features/controlLayers/store/types'; +import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; import { assert } from 'tsafe'; -import { RG_LAYER_OBJECT_GROUP_NAME } from './naming'; +/** + * Logic to create and render bounding boxes for layers. + * Some utils are included for calculating bounding boxes. + */ type Extents = { minX: number; @@ -15,7 +22,6 @@ type Extents = { const GET_CLIENT_RECT_CONFIG = { skipTransform: true }; -//#region getImageDataBbox /** * Get the bounding box of an image. * @param imageData The ImageData object to get the bounding box of. @@ -53,9 +59,7 @@ const getImageDataBbox = (imageData: ImageData): Extents | null => { return isEmpty ? null : { minX, minY, maxX, maxY }; }; -//#endregion -//#region getIsolatedRGLayerClone /** * Clones a regional guidance konva layer onto an offscreen stage/canvas. This allows the pixel data for a given layer * to be captured, manipulated or analyzed without interference from other layers. @@ -92,15 +96,13 @@ const getIsolatedRGLayerClone = (layer: Konva.Layer): { stageClone: Konva.Stage; return { stageClone, layerClone }; }; -//#endregion -//#region getLayerBboxPixels /** * Get the bounding box of a regional prompt konva layer. This function has special handling for regional prompt layers. * @param layer The konva layer to get the bounding box of. * @param preview Whether to open a new tab displaying the rendered layer, which is used to calculate the bbox. */ -export const getLayerBboxPixels = (layer: Konva.Layer, preview: boolean = false): IRect | null => { +const getLayerBboxPixels = (layer: Konva.Layer, preview: boolean = false): IRect | null => { // To calculate the layer's bounding box, we must first export it to a pixel array, then do some math. // // Though it is relatively fast, we can't use Konva's `getClientRect`. It programmatically determines the rect @@ -143,9 +145,7 @@ export const getLayerBboxPixels = (layer: Konva.Layer, preview: boolean = false) return correctedLayerBbox; }; -//#endregion -//#region getLayerBboxFast /** * Get the bounding box of a konva layer. This function is faster than `getLayerBboxPixels` but less accurate. It * should only be used when there are no eraser strokes or shapes in the layer. @@ -161,4 +161,94 @@ export const getLayerBboxFast = (layer: Konva.Layer): IRect => { height: Math.floor(bbox.height), }; }; -//#endregion + +/** + * Creates a bounding box rect for a layer. + * @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 + */ +const createBboxRect = (layerState: Layer, konvaLayer: Konva.Layer): Konva.Rect => { + const rect = new Konva.Rect({ + id: getLayerBboxId(layerState.id), + name: LAYER_BBOX_NAME, + strokeWidth: 1, + visible: false, + }); + konvaLayer.add(rect); + return rect; +}; + +/** + * Calculates the bbox of each regional guidance layer. Only calculates if the mask has changed. + * @param stage The konva stage + * @param layerStates An array of layers to calculate bboxes for + * @param onBboxChanged Callback for when the bounding box changes + */ +export const updateBboxes = ( + stage: Konva.Stage, + layerStates: Layer[], + onBboxChanged: (layerId: string, bbox: IRect | null) => void +): void => { + for (const rgLayer of layerStates.filter(isRegionalGuidanceLayer)) { + const konvaLayer = stage.findOne(`#${rgLayer.id}`); + assert(konvaLayer, `Layer ${rgLayer.id} not found in stage`); + // We only need to recalculate the bbox if the layer has changed + if (rgLayer.bboxNeedsUpdate) { + const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rgLayer, konvaLayer); + + // Hide the bbox while we calculate the new bbox, else the bbox will be included in the calculation + const visible = bboxRect.visible(); + bboxRect.visible(false); + + if (rgLayer.objects.length === 0) { + // No objects - no bbox to calculate + onBboxChanged(rgLayer.id, null); + } else { + // Calculate the bbox by rendering the layer and checking its pixels + onBboxChanged(rgLayer.id, getLayerBboxPixels(konvaLayer)); + } + + // Restore the visibility of the bbox + bboxRect.visible(visible); + } + } +}; + +/** + * Renders the bounding boxes for the layers. + * @param stage The konva stage + * @param layerStates An array of layers to draw bboxes for + * @param tool The current tool + * @returns + */ +export const renderBboxes = (stage: Konva.Stage, layerStates: Layer[], tool: Tool): void => { + // Hide all bboxes so they don't interfere with getClientRect + for (const bboxRect of stage.find(`.${LAYER_BBOX_NAME}`)) { + bboxRect.visible(false); + bboxRect.listening(false); + } + // No selected layer or not using the move tool - nothing more to do here + if (tool !== 'move') { + return; + } + + for (const layer of layerStates.filter(isRegionalGuidanceLayer)) { + if (!layer.bbox) { + continue; + } + const konvaLayer = stage.findOne(`#${layer.id}`); + assert(konvaLayer, `Layer ${layer.id} not found in stage`); + + const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layer, konvaLayer); + + bboxRect.setAttrs({ + visible: !layer.bboxNeedsUpdate, + listening: layer.isSelected, + x: layer.bbox.x, + y: layer.bbox.y, + width: layer.bbox.width, + height: layer.bbox.height, + stroke: layer.isSelected ? BBOX_SELECTED_STROKE : '', + }); + } +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts new file mode 100644 index 00000000000..d08d0bd60ef --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts @@ -0,0 +1,162 @@ +import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; +import { CA_LAYER_IMAGE_NAME, CA_LAYER_NAME, getCALayerImageId } from 'features/controlLayers/konva/naming'; +import type { ControlAdapterLayer } from 'features/controlLayers/store/types'; +import Konva from 'konva'; +import type { ImageDTO } from 'services/api/types'; + +/** + * Logic for creating and rendering control adapter (control net & t2i adapter) layers. These layers have image objects + * and require some special handling to update the source and attributes as control images are swapped or processed. + */ + +/** + * 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({ + id: layerState.id, + name: CA_LAYER_NAME, + imageSmoothingEnabled: true, + listening: false, + }); + stage.add(konvaLayer); + return konvaLayer; +}; + +/** + * 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({ + name: CA_LAYER_IMAGE_NAME, + image: imageEl, + }); + konvaLayer.add(konvaImage); + return konvaImage; +}; + +/** + * 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, + konvaLayer: Konva.Layer, + layerState: ControlAdapterLayer, + getImageDTO: (imageName: string) => Promise +): Promise => { + const image = layerState.controlAdapter.processedImage ?? layerState.controlAdapter.image; + if (image) { + const imageName = image.name; + const imageDTO = await getImageDTO(imageName); + if (!imageDTO) { + return; + } + const imageEl = new Image(); + const imageId = getCALayerImageId(layerState.id, imageName); + imageEl.onload = () => { + // Find the existing image or create a new one - must find using the name, bc the id may have just changed + const konvaImage = + konvaLayer.findOne(`.${CA_LAYER_IMAGE_NAME}`) ?? createCALayerImage(konvaLayer, imageEl); + + // Update the image's attributes + konvaImage.setAttrs({ + id: imageId, + image: imageEl, + }); + updateCALayerImageAttrs(stage, konvaImage, layerState); + // Must cache after this to apply the filters + konvaImage.cache(); + imageEl.id = imageId; + }; + imageEl.src = imageDTO.image_url; + } else { + konvaLayer.findOne(`.${CA_LAYER_IMAGE_NAME}`)?.destroy(); + } +}; + +/** + * 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, + konvaImage: Konva.Image, + layerState: ControlAdapterLayer +): void => { + let needsCache = false; + // 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. + // TODO(psyche): Investigate and report upstream. + const newWidth = stage.width() / stage.scaleX(); + const newHeight = stage.height() / stage.scaleY(); + const hasFilter = konvaImage.filters() !== null && konvaImage.filters().length > 0; + if ( + konvaImage.width() !== newWidth || + konvaImage.height() !== newHeight || + konvaImage.visible() !== layerState.isEnabled || + hasFilter !== layerState.isFilterEnabled + ) { + konvaImage.setAttrs({ + opacity: layerState.opacity, + scaleX: 1, + scaleY: 1, + width: stage.width() / stage.scaleX(), + height: stage.height() / stage.scaleY(), + visible: layerState.isEnabled, + filters: layerState.isFilterEnabled ? [LightnessToAlphaFilter] : [], + }); + needsCache = true; + } + if (konvaImage.opacity() !== layerState.opacity) { + konvaImage.opacity(layerState.opacity); + } + if (needsCache) { + konvaImage.cache(); + } +}; + +/** + * 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 + */ +export const renderCALayer = ( + stage: Konva.Stage, + layerState: ControlAdapterLayer, + getImageDTO: (imageName: string) => Promise +): void => { + const konvaLayer = stage.findOne(`#${layerState.id}`) ?? createCALayer(stage, layerState); + const konvaImage = konvaLayer.findOne(`.${CA_LAYER_IMAGE_NAME}`); + const canvasImageSource = konvaImage?.image(); + let imageSourceNeedsUpdate = false; + if (canvasImageSource instanceof HTMLImageElement) { + const image = layerState.controlAdapter.processedImage ?? layerState.controlAdapter.image; + if (image && canvasImageSource.id !== getCALayerImageId(layerState.id, image.name)) { + imageSourceNeedsUpdate = true; + } else if (!image) { + imageSourceNeedsUpdate = true; + } + } else if (!canvasImageSource) { + imageSourceNeedsUpdate = true; + } + + if (imageSourceNeedsUpdate) { + updateCALayerImageSource(stage, konvaLayer, layerState, getImageDTO); + } else if (konvaImage) { + updateCALayerImageAttrs(stage, konvaImage, layerState); + } +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/iiLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/iiLayer.ts new file mode 100644 index 00000000000..cf1b69d6668 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/iiLayer.ts @@ -0,0 +1,149 @@ +import { + getCALayerImageId, + getIILayerImageId, + INITIAL_IMAGE_LAYER_IMAGE_NAME, + INITIAL_IMAGE_LAYER_NAME, +} from 'features/controlLayers/konva/naming'; +import type { InitialImageLayer } from 'features/controlLayers/store/types'; +import Konva from 'konva'; +import type { ImageDTO } from 'services/api/types'; + +/** + * Logic for creating and rendering initial image layers. Well, just the one, actually, because it's a singleton. + * TODO(psyche): Raster layers effectively supersede the initial image layer type. + */ + +/** + * 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({ + id: layerState.id, + name: INITIAL_IMAGE_LAYER_NAME, + imageSmoothingEnabled: true, + listening: false, + }); + stage.add(konvaLayer); + return konvaLayer; +}; + +/** + * 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({ + name: INITIAL_IMAGE_LAYER_IMAGE_NAME, + image: imageEl, + }); + konvaLayer.add(konvaImage); + return konvaImage; +}; + +/** + * Updates an initial image layer's attributes (width, height, opacity, visibility). + * @param stage The konva stage + * @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, + // but it doesn't seem to break anything. + // TODO(psyche): Investigate and report upstream. + const newWidth = stage.width() / stage.scaleX(); + const newHeight = stage.height() / stage.scaleY(); + if ( + konvaImage.width() !== newWidth || + konvaImage.height() !== newHeight || + konvaImage.visible() !== layerState.isEnabled + ) { + konvaImage.setAttrs({ + opacity: layerState.opacity, + scaleX: 1, + scaleY: 1, + width: stage.width() / stage.scaleX(), + height: stage.height() / stage.scaleY(), + visible: layerState.isEnabled, + }); + } + if (konvaImage.opacity() !== layerState.opacity) { + konvaImage.opacity(layerState.opacity); + } +}; + +/** + * 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, + konvaLayer: Konva.Layer, + layerState: InitialImageLayer, + getImageDTO: (imageName: string) => Promise +): Promise => { + if (layerState.image) { + const imageName = layerState.image.name; + const imageDTO = await getImageDTO(imageName); + if (!imageDTO) { + return; + } + const imageEl = new Image(); + const imageId = getIILayerImageId(layerState.id, imageName); + imageEl.onload = () => { + // Find the existing image or create a new one - must find using the name, bc the id may have just changed + const konvaImage = + konvaLayer.findOne(`.${INITIAL_IMAGE_LAYER_IMAGE_NAME}`) ?? + createIILayerImage(konvaLayer, imageEl); + + // Update the image's attributes + konvaImage.setAttrs({ + id: imageId, + image: imageEl, + }); + updateIILayerImageAttrs(stage, konvaImage, layerState); + imageEl.id = imageId; + }; + imageEl.src = imageDTO.image_url; + } else { + konvaLayer.findOne(`.${INITIAL_IMAGE_LAYER_IMAGE_NAME}`)?.destroy(); + } +}; + +/** + * 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 + */ +export const renderIILayer = ( + stage: Konva.Stage, + layerState: InitialImageLayer, + getImageDTO: (imageName: string) => Promise +): void => { + const konvaLayer = stage.findOne(`#${layerState.id}`) ?? createIILayer(stage, layerState); + const konvaImage = konvaLayer.findOne(`.${INITIAL_IMAGE_LAYER_IMAGE_NAME}`); + const canvasImageSource = konvaImage?.image(); + let imageSourceNeedsUpdate = false; + if (canvasImageSource instanceof HTMLImageElement) { + const image = layerState.image; + if (image && canvasImageSource.id !== getCALayerImageId(layerState.id, image.name)) { + imageSourceNeedsUpdate = true; + } else if (!image) { + imageSourceNeedsUpdate = true; + } + } else if (!canvasImageSource) { + imageSourceNeedsUpdate = true; + } + + if (imageSourceNeedsUpdate) { + updateIILayerImageSource(stage, konvaLayer, layerState, getImageDTO); + } else if (konvaImage) { + updateIILayerImageAttrs(stage, konvaImage, layerState); + } +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts new file mode 100644 index 00000000000..8243b81504c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -0,0 +1,118 @@ +import { DEBOUNCE_MS } from 'features/controlLayers/konva/constants'; +import { BACKGROUND_LAYER_ID, TOOL_PREVIEW_LAYER_ID } from 'features/controlLayers/konva/naming'; +import { renderBackground } from 'features/controlLayers/konva/renderers/background'; +import { renderBboxes, updateBboxes } from 'features/controlLayers/konva/renderers/bbox'; +import { renderCALayer } from 'features/controlLayers/konva/renderers/caLayer'; +import { renderIILayer } from 'features/controlLayers/konva/renderers/iiLayer'; +import { renderNoLayersMessage } from 'features/controlLayers/konva/renderers/noLayersMessage'; +import { renderRasterLayer } from 'features/controlLayers/konva/renderers/rasterLayer'; +import { renderRGLayer } from 'features/controlLayers/konva/renderers/rgLayer'; +import { renderToolPreview } from 'features/controlLayers/konva/renderers/toolPreview'; +import { mapId, selectRenderableLayers } from 'features/controlLayers/konva/util'; +import type { Layer, Tool } from 'features/controlLayers/store/types'; +import { + isControlAdapterLayer, + isInitialImageLayer, + isRasterLayer, + isRegionalGuidanceLayer, + isRenderableLayer, +} from 'features/controlLayers/store/types'; +import type Konva from 'konva'; +import { debounce } from 'lodash-es'; +import type { ImageDTO } from 'services/api/types'; + +/** + * Logic for rendering arranging and rendering all layers. + */ + +/** + * Arranges all layers in the z-axis by updating their z-indices. + * @param stage The konva stage + * @param layerIds An array of redux layer ids, in their z-index order + */ +const arrangeLayers = (stage: Konva.Stage, layerIds: string[]): void => { + let nextZIndex = 0; + // Background is the first layer + stage.findOne(`#${BACKGROUND_LAYER_ID}`)?.zIndex(nextZIndex++); + // Then arrange the redux layers in order + for (const layerId of layerIds) { + stage.findOne(`#${layerId}`)?.zIndex(nextZIndex++); + } + // Finally, the tool preview layer is always on top + stage.findOne(`#${TOOL_PREVIEW_LAYER_ID}`)?.zIndex(nextZIndex++); +}; + +/** + * Renders the layers on the stage. + * @param stage The konva stage + * @param layerStates Array of all layer states + * @param globalMaskLayerOpacity The global mask layer opacity + * @param tool The current tool + * @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 = ( + stage: Konva.Stage, + layerStates: Layer[], + globalMaskLayerOpacity: number, + tool: Tool, + getImageDTO: (imageName: string) => Promise, + onLayerPosChanged?: (layerId: string, x: number, y: number) => void +): void => { + const layerIds = layerStates.filter(isRenderableLayer).map(mapId); + // Remove un-rendered layers + for (const konvaLayer of stage.find(selectRenderableLayers)) { + if (!layerIds.includes(konvaLayer.id())) { + konvaLayer.destroy(); + } + } + + for (const layer of layerStates) { + if (isRegionalGuidanceLayer(layer)) { + renderRGLayer(stage, layer, globalMaskLayerOpacity, tool, onLayerPosChanged); + } + if (isControlAdapterLayer(layer)) { + renderCALayer(stage, layer, getImageDTO); + } + if (isInitialImageLayer(layer)) { + renderIILayer(stage, layer, getImageDTO); + } + if (isRasterLayer(layer)) { + renderRasterLayer(stage, layer, tool, onLayerPosChanged); + } + // IP Adapter layers are not rendered + } +}; + +/** + * All the renderers for the Konva stage. + */ +export const renderers = { + renderToolPreview, + renderLayers, + renderBboxes, + renderBackground, + renderNoLayersMessage, + arrangeLayers, + updateBboxes, +}; + +/** + * Gets the renderers with debouncing applied. + * @param ms The debounce time in milliseconds + * @returns The renderers with debouncing applied + */ +const getDebouncedRenderers = (ms = DEBOUNCE_MS): typeof renderers => ({ + renderToolPreview: debounce(renderToolPreview, ms), + renderLayers: debounce(renderLayers, ms), + renderBboxes: debounce(renderBboxes, ms), + renderBackground: debounce(renderBackground, ms), + renderNoLayersMessage: debounce(renderNoLayersMessage, ms), + arrangeLayers: debounce(arrangeLayers, ms), + updateBboxes: debounce(updateBboxes, ms), +}); + +/** + * All the renderers for the Konva stage, debounced. + */ +export const debouncedRenderers: typeof renderers = getDebouncedRenderers(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/noLayersMessage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/noLayersMessage.ts new file mode 100644 index 00000000000..eae41d70d8d --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/noLayersMessage.ts @@ -0,0 +1,53 @@ +import { NO_LAYERS_MESSAGE_LAYER_ID } from 'features/controlLayers/konva/naming'; +import { t } from 'i18next'; +import Konva from 'konva'; + +/** + * Logic for creating and rendering a fallback message when there are no layers to render. + */ + +/** + * Creates the "no layers" fallback layer + * @param stage The konva stage + */ +const createNoLayersMessageLayer = (stage: Konva.Stage): Konva.Layer => { + const noLayersMessageLayer = new Konva.Layer({ + id: NO_LAYERS_MESSAGE_LAYER_ID, + opacity: 0.7, + listening: false, + }); + const text = new Konva.Text({ + x: 0, + y: 0, + align: 'center', + verticalAlign: 'middle', + text: t('controlLayers.noLayersAdded', 'No Layers Added'), + fontFamily: '"Inter Variable", sans-serif', + fontStyle: '600', + fill: 'white', + }); + noLayersMessageLayer.add(text); + stage.add(noLayersMessageLayer); + return noLayersMessageLayer; +}; + +/** + * 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 + */ +export const renderNoLayersMessage = (stage: Konva.Stage, layerCount: number, width: number, height: number): void => { + const noLayersMessageLayer = + stage.findOne(`#${NO_LAYERS_MESSAGE_LAYER_ID}`) ?? createNoLayersMessageLayer(stage); + if (layerCount === 0) { + noLayersMessageLayer.findOne('Text')?.setAttrs({ + width, + height, + fontSize: 32 / stage.scaleX(), + }); + } else { + noLayersMessageLayer?.destroy(); + } +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts new file mode 100644 index 00000000000..50d23bd63c0 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -0,0 +1,77 @@ +import { rgbaColorToString } from 'features/canvas/util/colorToString'; +import { RG_LAYER_LINE_NAME, RG_LAYER_RECT_NAME } 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'; + +/** + * Utilities to create various konva objects from layer state. These are used by both the raster and regional guidance + * layers types. + */ + +/** + * 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 + */ +export const createBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Group): Konva.Line => { + const konvaLine = new Konva.Line({ + id: brushLine.id, + key: brushLine.id, + name: RG_LAYER_LINE_NAME, + strokeWidth: brushLine.strokeWidth, + tension: 0, + lineCap: 'round', + lineJoin: 'round', + shadowForStrokeEnabled: false, + globalCompositeOperation: 'source-over', + listening: false, + stroke: rgbaColorToString(brushLine.color), + }); + layerObjectGroup.add(konvaLine); + return konvaLine; +}; + +/** + * 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 + */ +export const createEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva.Group): Konva.Line => { + const konvaLine = new Konva.Line({ + id: eraserLine.id, + key: eraserLine.id, + name: RG_LAYER_LINE_NAME, + strokeWidth: eraserLine.strokeWidth, + tension: 0, + lineCap: 'round', + lineJoin: 'round', + shadowForStrokeEnabled: false, + globalCompositeOperation: 'destination-out', + listening: false, + stroke: rgbaColorToString(DEFAULT_RGBA_COLOR), + }); + layerObjectGroup.add(konvaLine); + return konvaLine; +}; + +/** + * 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 + */ +export const createRectShape = (rectShape: RectShape, layerObjectGroup: Konva.Group): Konva.Rect => { + const konvaRect = new Konva.Rect({ + id: rectShape.id, + key: rectShape.id, + name: RG_LAYER_RECT_NAME, + x: rectShape.x, + y: rectShape.y, + width: rectShape.width, + height: rectShape.height, + listening: false, + fill: rgbaColorToString(rectShape.color), + }); + layerObjectGroup.add(konvaRect); + return konvaRect; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts new file mode 100644 index 00000000000..81251b5f2bc --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts @@ -0,0 +1,135 @@ +import { + getObjectGroupId, + RASTER_LAYER_NAME, + RASTER_LAYER_OBJECT_GROUP_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 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. + */ + +/** + * Creates a raster layer. + * @param stage The konva stage + * @param layerState The raster layer state + * @param onLayerPosChanged Callback for when the layer's position changes + */ +const createRasterLayer = ( + stage: Konva.Stage, + layerState: RasterLayer, + onLayerPosChanged?: (layerId: string, x: number, y: number) => void +): Konva.Layer => { + // This layer hasn't been added to the konva state yet + const konvaLayer = new Konva.Layer({ + id: layerState.id, + name: RASTER_LAYER_NAME, + draggable: true, + dragDistance: 0, + }); + + // 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) { + konvaLayer.on('dragend', function (e) { + onLayerPosChanged(layerState.id, Math.floor(e.target.x()), Math.floor(e.target.y())); + }); + } + + // The dragBoundFunc limits how far the layer can be dragged + konvaLayer.dragBoundFunc(function (pos) { + const cursorPos = getScaledFlooredCursorPosition(stage); + if (!cursorPos) { + return this.getAbsolutePosition(); + } + // Prevent the user from dragging the layer out of the stage bounds by constaining the cursor position to the stage bounds + if ( + cursorPos.x < 0 || + cursorPos.x > stage.width() / stage.scaleX() || + cursorPos.y < 0 || + cursorPos.y > stage.height() / stage.scaleY() + ) { + return this.getAbsolutePosition(); + } + 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; +}; + +/** + * Renders a regional guidance layer. + * @param stage The konva stage + * @param layerState The regional guidance layer state + * @param tool The current tool + * @param onLayerPosChanged Callback for when the layer's position changes + */ +export const renderRasterLayer = ( + 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); + + // Update the layer's position and listening state + konvaLayer.setAttrs({ + listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events + x: Math.floor(layerState.x), + 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 objectIds = layerState.objects.map(mapId); + // Destroy any objects that are no longer in the redux state + for (const objectNode of konvaObjectGroup.getChildren()) { + if (!objectIds.includes(objectNode.id())) { + objectNode.destroy(); + } + } + + for (const obj of layerState.objects) { + if (obj.type === 'brush_line') { + const konvaBrushLine = stage.findOne(`#${obj.id}`) ?? createBrushLine(obj, konvaObjectGroup); + // 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); + // 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); + } + } + } + + // Only update layer visibility if it has changed. + if (konvaLayer.visible() !== layerState.isEnabled) { + konvaLayer.visible(layerState.isEnabled); + } + + konvaObjectGroup.opacity(layerState.opacity); +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts new file mode 100644 index 00000000000..471f23ac5a1 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts @@ -0,0 +1,229 @@ +import { rgbColorToString } from 'features/canvas/util/colorToString'; +import { + COMPOSITING_RECT_NAME, + getObjectGroupId, + RG_LAYER_NAME, + RG_LAYER_OBJECT_GROUP_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 { 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. + * + * Some special handling is needed to render layer opacity correctly using a "compositing rect". See the comments + * in `renderRGLayer`. + */ + +/** + * Creates the "compositing rect" for a regional guidance layer. + * @param konvaLayer The konva layer + */ +const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => { + const compositingRect = new Konva.Rect({ name: COMPOSITING_RECT_NAME, listening: false }); + konvaLayer.add(compositingRect); + return compositingRect; +}; + +/** + * Creates a regional guidance layer. + * @param stage The konva stage + * @param layerState The regional guidance layer state + * @param onLayerPosChanged Callback for when the layer's position changes + */ +const createRGLayer = ( + stage: Konva.Stage, + layerState: RegionalGuidanceLayer, + onLayerPosChanged?: (layerId: string, x: number, y: number) => void +): Konva.Layer => { + // This layer hasn't been added to the konva state yet + const konvaLayer = new Konva.Layer({ + id: layerState.id, + name: RG_LAYER_NAME, + draggable: true, + dragDistance: 0, + }); + + // 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) { + konvaLayer.on('dragend', function (e) { + onLayerPosChanged(layerState.id, Math.floor(e.target.x()), Math.floor(e.target.y())); + }); + } + + // The dragBoundFunc limits how far the layer can be dragged + konvaLayer.dragBoundFunc(function (pos) { + const cursorPos = getScaledFlooredCursorPosition(stage); + if (!cursorPos) { + return this.getAbsolutePosition(); + } + // Prevent the user from dragging the layer out of the stage bounds by constaining the cursor position to the stage bounds + if ( + cursorPos.x < 0 || + cursorPos.x > stage.width() / stage.scaleX() || + cursorPos.y < 0 || + cursorPos.y > stage.height() / stage.scaleY() + ) { + return this.getAbsolutePosition(); + } + 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; +}; + +/** + * Renders a raster layer. + * @param stage The konva stage + * @param layerState The regional guidance layer state + * @param globalMaskLayerOpacity The global mask layer opacity + * @param tool The current tool + * @param onLayerPosChanged Callback for when the layer's position changes + */ +export const renderRGLayer = ( + stage: Konva.Stage, + layerState: RegionalGuidanceLayer, + globalMaskLayerOpacity: number, + tool: Tool, + onLayerPosChanged?: (layerId: string, x: number, y: number) => void +): void => { + const konvaLayer = + stage.findOne(`#${layerState.id}`) ?? createRGLayer(stage, layerState, onLayerPosChanged); + + // Update the layer's position and listening state + konvaLayer.setAttrs({ + listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events + x: Math.floor(layerState.x), + y: Math.floor(layerState.y), + }); + + // 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}`); + + // We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required. + let groupNeedsCache = false; + + const objectIds = layerState.objects.map(mapId); + // Destroy any objects that are no longer in the redux state + for (const objectNode of konvaObjectGroup.find(selectVectorMaskObjects)) { + if (!objectIds.includes(objectNode.id())) { + objectNode.destroy(); + groupNeedsCache = true; + } + } + + for (const obj of layerState.objects) { + if (obj.type === 'brush_line') { + const konvaBrushLine = stage.findOne(`#${obj.id}`) ?? createBrushLine(obj, konvaObjectGroup); + + // Only update the points if they have changed. The point values are never mutated, they are only added to the + // array, so checking the length is sufficient to determine if we need to re-cache. + if (konvaBrushLine.points().length !== obj.points.length) { + konvaBrushLine.points(obj.points); + groupNeedsCache = true; + } + // Only update the color if it has changed. + if (konvaBrushLine.stroke() !== rgbColor) { + konvaBrushLine.stroke(rgbColor); + groupNeedsCache = true; + } + } else if (obj.type === 'eraser_line') { + const konvaEraserLine = stage.findOne(`#${obj.id}`) ?? createEraserLine(obj, konvaObjectGroup); + + // Only update the points if they have changed. The point values are never mutated, they are only added to the + // array, so checking the length is sufficient to determine if we need to re-cache. + if (konvaEraserLine.points().length !== obj.points.length) { + konvaEraserLine.points(obj.points); + groupNeedsCache = true; + } + // Only update the color if it has changed. + if (konvaEraserLine.stroke() !== rgbColor) { + konvaEraserLine.stroke(rgbColor); + groupNeedsCache = true; + } + } else if (obj.type === 'rect_shape') { + const konvaRectShape = stage.findOne(`#${obj.id}`) ?? createRectShape(obj, konvaObjectGroup); + + // Only update the color if it has changed. + if (konvaRectShape.fill() !== rgbColor) { + konvaRectShape.fill(rgbColor); + groupNeedsCache = true; + } + } + } + + // Only update layer visibility if it has changed. + if (konvaLayer.visible() !== layerState.isEnabled) { + konvaLayer.visible(layerState.isEnabled); + groupNeedsCache = true; + } + + if (konvaObjectGroup.getChildren().length === 0) { + // No objects - clear the cache to reset the previous pixel data + konvaObjectGroup.clearCache(); + return; + } + + const compositingRect = + konvaLayer.findOne(`.${COMPOSITING_RECT_NAME}`) ?? createCompositingRect(konvaLayer); + + /** + * When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows + * shapes to render as a "raster" layer with all pixels drawn at the same color and opacity. + * + * Without this special handling, each shape is drawn individually with the given opacity, atop the other shapes. The + * effect is like if you have a Photoshop Group consisting of many shapes, each of which has the given opacity. + * Overlapping shapes will have their colors blended together, and the final color is the result of all the shapes. + * + * 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. + */ + if (layerState.isSelected && tool !== 'move') { + // We must clear the cache first so Konva will re-draw the group with the new compositing rect + if (konvaObjectGroup.isCached()) { + konvaObjectGroup.clearCache(); + } + // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work + konvaObjectGroup.opacity(1); + + compositingRect.setAttrs({ + // The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already + ...(!layerState.bboxNeedsUpdate && layerState.bbox ? layerState.bbox : getLayerBboxFast(konvaLayer)), + fill: rgbColor, + opacity: globalMaskLayerOpacity, + // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) + globalCompositeOperation: 'source-in', + visible: true, + // This rect must always be on top of all other shapes + zIndex: konvaObjectGroup.getChildren().length, + }); + } else { + // The compositing rect should only be shown when the layer is selected. + compositingRect.visible(false); + // Cache only if needed - or if we are on this code path and _don't_ have a cache + if (groupNeedsCache || !konvaObjectGroup.isCached()) { + konvaObjectGroup.cache(); + } + // Updating group opacity does not require re-caching + konvaObjectGroup.opacity(globalMaskLayerOpacity); + } +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/toolPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/toolPreview.ts new file mode 100644 index 00000000000..ae085f8ad8d --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/toolPreview.ts @@ -0,0 +1,161 @@ +import { rgbaColorToString } from 'features/canvas/util/colorToString'; +import { BRUSH_BORDER_INNER_COLOR, BRUSH_BORDER_OUTER_COLOR } from 'features/controlLayers/konva/constants'; +import { + TOOL_PREVIEW_BRUSH_BORDER_INNER_ID, + TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID, + TOOL_PREVIEW_BRUSH_FILL_ID, + TOOL_PREVIEW_BRUSH_GROUP_ID, + TOOL_PREVIEW_LAYER_ID, + TOOL_PREVIEW_RECT_ID, +} from 'features/controlLayers/konva/naming'; +import { selectRenderableLayers, snapPosToStage } from 'features/controlLayers/konva/util'; +import type { Layer, RgbaColor, Tool } from 'features/controlLayers/store/types'; +import Konva from 'konva'; +import type { Vector2d } from 'konva/lib/types'; +import { assert } from 'tsafe'; + +/** + * Logic to create and render the singleton tool preview layer. + */ + +/** + * Creates the singleton tool preview layer and all its objects. + * @param stage The konva stage + */ +const createToolPreviewLayer = (stage: Konva.Stage): Konva.Layer => { + // Initialize the brush preview layer & add to the stage + const toolPreviewLayer = new Konva.Layer({ id: TOOL_PREVIEW_LAYER_ID, visible: false, listening: false }); + stage.add(toolPreviewLayer); + + // Create the brush preview group & circles + const brushPreviewGroup = new Konva.Group({ id: TOOL_PREVIEW_BRUSH_GROUP_ID }); + const brushPreviewFill = new Konva.Circle({ + id: TOOL_PREVIEW_BRUSH_FILL_ID, + listening: false, + strokeEnabled: false, + }); + brushPreviewGroup.add(brushPreviewFill); + const brushPreviewBorderInner = new Konva.Circle({ + id: TOOL_PREVIEW_BRUSH_BORDER_INNER_ID, + listening: false, + stroke: BRUSH_BORDER_INNER_COLOR, + strokeWidth: 1, + strokeEnabled: true, + }); + brushPreviewGroup.add(brushPreviewBorderInner); + const brushPreviewBorderOuter = new Konva.Circle({ + id: TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID, + listening: false, + stroke: BRUSH_BORDER_OUTER_COLOR, + strokeWidth: 1, + strokeEnabled: true, + }); + brushPreviewGroup.add(brushPreviewBorderOuter); + toolPreviewLayer.add(brushPreviewGroup); + + // 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 }); + toolPreviewLayer.add(rectPreview); + + return toolPreviewLayer; +}; + +/** + * Renders the brush preview for the selected tool. + * @param stage The konva stage + * @param tool The selected tool + * @param color The selected layer's color + * @param selectedLayerType The selected layer's type + * @param globalMaskLayerOpacity The global mask layer opacity + * @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 + */ +export const renderToolPreview = ( + stage: Konva.Stage, + tool: Tool, + brushColor: RgbaColor, + selectedLayerType: Layer['type'] | null, + globalMaskLayerOpacity: number, + cursorPos: Vector2d | null, + lastMouseDownPos: Vector2d | null, + brushSize: number +): void => { + const layerCount = stage.find(selectRenderableLayers).length; + // Update the stage's pointer style + if (layerCount === 0) { + // We have no layers, so we should not render any tool + stage.container().style.cursor = 'default'; + } else if (selectedLayerType !== 'regional_guidance_layer' && selectedLayerType !== 'raster_layer') { + // Non-mask-guidance layers don't have tools + stage.container().style.cursor = 'not-allowed'; + } else if (tool === 'move') { + // Move tool gets a pointer + stage.container().style.cursor = 'default'; + } else if (tool === 'rect') { + // Move rect gets a crosshair + stage.container().style.cursor = 'crosshair'; + } else { + // Else we hide the native cursor and use the konva-rendered brush preview + stage.container().style.cursor = 'none'; + } + + const toolPreviewLayer = stage.findOne(`#${TOOL_PREVIEW_LAYER_ID}`) ?? createToolPreviewLayer(stage); + + if (!cursorPos || layerCount === 0) { + // We can bail early if the mouse isn't over the stage or there are no layers + toolPreviewLayer.visible(false); + return; + } + + toolPreviewLayer.visible(true); + + const brushPreviewGroup = stage.findOne(`#${TOOL_PREVIEW_BRUSH_GROUP_ID}`); + assert(brushPreviewGroup, 'Brush preview group not found'); + + const rectPreview = stage.findOne(`#${TOOL_PREVIEW_RECT_ID}`); + assert(rectPreview, 'Rect preview not found'); + + // No need to render the brush preview if the cursor position or color is missing + if (cursorPos && (tool === 'brush' || tool === 'eraser')) { + // Update the fill circle + const brushPreviewFill = brushPreviewGroup.findOne(`#${TOOL_PREVIEW_BRUSH_FILL_ID}`); + brushPreviewFill?.setAttrs({ + x: cursorPos.x, + y: cursorPos.y, + radius: brushSize / 2, + fill: rgbaColorToString(brushColor), + globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out', + }); + + // Update the inner border of the brush preview + const brushPreviewInner = toolPreviewLayer.findOne(`#${TOOL_PREVIEW_BRUSH_BORDER_INNER_ID}`); + brushPreviewInner?.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: brushSize / 2 }); + + // Update the outer border of the brush preview + const brushPreviewOuter = toolPreviewLayer.findOne(`#${TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID}`); + brushPreviewOuter?.setAttrs({ + x: cursorPos.x, + y: cursorPos.y, + radius: brushSize / 2 + 1, + }); + + brushPreviewGroup.visible(true); + } else { + brushPreviewGroup.visible(false); + } + + if (cursorPos && lastMouseDownPos && tool === 'rect') { + const snappedPos = snapPosToStage(cursorPos, stage); + const rectPreview = toolPreviewLayer.findOne(`#${TOOL_PREVIEW_RECT_ID}`); + rectPreview?.setAttrs({ + x: Math.min(snappedPos.x, lastMouseDownPos.x), + y: Math.min(snappedPos.y, lastMouseDownPos.y), + width: Math.abs(snappedPos.x - lastMouseDownPos.x), + height: Math.abs(snappedPos.y - lastMouseDownPos.y), + }); + rectPreview?.visible(true); + } else { + rectPreview?.visible(false); + } +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index 29f81fb7990..2eed6a663ba 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -1,3 +1,11 @@ +import { + CA_LAYER_NAME, + INITIAL_IMAGE_LAYER_NAME, + RASTER_LAYER_NAME, + RG_LAYER_LINE_NAME, + RG_LAYER_NAME, + RG_LAYER_RECT_NAME, +} from 'features/controlLayers/konva/naming'; import type Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; import type { Vector2d } from 'konva/lib/types'; @@ -65,3 +73,33 @@ export const getIsMouseDown = (e: KonvaEventObject): boolean => e.ev */ export const getIsFocused = (stage: Konva.Stage): boolean => stage.container().contains(document.activeElement); //#endregion + +//#region mapId +/** + * Simple util to map an object to its id property. Serves as a minor optimization to avoid recreating a map callback + * every time we need to map an object to its id, which happens very often. + * @param object The object with an `id` property + * @returns The object's id property + */ +export const mapId = (object: { id: string }): string => object.id; +//#endregion + +//#region konva selector callbacks +/** + * 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; + +/** + * 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; +}; +//#endregion diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 16069daecb2..c9fe1509698 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -50,23 +50,28 @@ import type { AddEraserLineArg, AddPointToLineArg, AddRectShapeArg, - BrushLine, ControlAdapterLayer, ControlLayersState, - EllipseShape, - EraserLine, - ImageObject, InitialImageLayer, IPAdapterLayer, Layer, - PolygonShape, RasterLayer, - RectShape, RegionalGuidanceLayer, RgbaColor, Tool, } from './types'; -import { DEFAULT_RGBA_COLOR } from './types'; +import { + DEFAULT_RGBA_COLOR, + isCAOrIPALayer, + isControlAdapterLayer, + isInitialImageLayer, + isIPAdapterLayer, + isLine, + isRasterLayer, + isRegionalGuidanceLayer, + isRenderableLayer, + isRGOrRasterlayer, +} from './types'; export const initialControlLayersState: ControlLayersState = { _version: 3, @@ -87,76 +92,31 @@ export const initialControlLayersState: ControlLayersState = { }, }; -const isLine = ( - obj: BrushLine | EraserLine | RectShape | EllipseShape | PolygonShape | ImageObject -): obj is BrushLine => obj.type === 'brush_line' || obj.type === 'eraser_line'; -export const isRegionalGuidanceLayer = (layer?: Layer): layer is RegionalGuidanceLayer => - layer?.type === 'regional_guidance_layer'; -export const isControlAdapterLayer = (layer?: Layer): layer is ControlAdapterLayer => - layer?.type === 'control_adapter_layer'; -export const isIPAdapterLayer = (layer?: Layer): layer is IPAdapterLayer => layer?.type === 'ip_adapter_layer'; -export const isInitialImageLayer = (layer?: Layer): layer is InitialImageLayer => layer?.type === 'initial_image_layer'; -export const isRasterLayer = (layer?: Layer): layer is RasterLayer => layer?.type === 'raster_layer'; -export const isRenderableLayer = ( - layer?: Layer -): layer is RegionalGuidanceLayer | ControlAdapterLayer | InitialImageLayer => - layer?.type === 'regional_guidance_layer' || - layer?.type === 'control_adapter_layer' || - layer?.type === 'initial_image_layer' || - layer?.type === 'raster_layer'; - -export const selectCALayerOrThrow = (state: ControlLayersState, layerId: string): ControlAdapterLayer => { - const layer = state.layers.find((l) => l.id === layerId); - assert(isControlAdapterLayer(layer)); - return layer; -}; -export const selectIPALayerOrThrow = (state: ControlLayersState, layerId: string): IPAdapterLayer => { - const layer = state.layers.find((l) => l.id === layerId); - assert(isIPAdapterLayer(layer)); - return layer; -}; -export const selectIILayerOrThrow = (state: ControlLayersState, layerId: string): InitialImageLayer => { - const layer = state.layers.find((l) => l.id === layerId); - assert(isInitialImageLayer(layer)); - return layer; -}; -export const selectRasterLayerOrThrow = (state: ControlLayersState, layerId: string): RasterLayer => { - const layer = state.layers.find((l) => l.id === layerId); - assert(isRasterLayer(layer)); - return layer; -}; -const selectCAOrIPALayerOrThrow = ( - state: ControlLayersState, - layerId: string -): ControlAdapterLayer | IPAdapterLayer => { - const layer = state.layers.find((l) => l.id === layerId); - assert(isControlAdapterLayer(layer) || isIPAdapterLayer(layer)); - return layer; -}; -const selectRGLayerOrThrow = (state: ControlLayersState, layerId: string): RegionalGuidanceLayer => { - const layer = state.layers.find((l) => l.id === layerId); - assert(isRegionalGuidanceLayer(layer)); - return layer; -}; -const selectRGOrRasterLayerOrThrow = ( +/** + * A selector that accepts a type guard and returns the first layer that matches the guard. + * Throws if the layer is not found or does not match the guard. + */ +export const selectLayerOrThrow = ( state: ControlLayersState, - layerId: string -): RegionalGuidanceLayer | RasterLayer => { + layerId: string, + predicate: (layer: Layer) => layer is T +): T => { const layer = state.layers.find((l) => l.id === layerId); - assert(isRegionalGuidanceLayer(layer) || isRasterLayer(layer)); + assert(layer && predicate(layer)); return layer; }; + export const selectRGLayerIPAdapterOrThrow = ( state: ControlLayersState, layerId: string, ipAdapterId: string ): IPAdapterConfigV2 => { - const layer = state.layers.find((l) => l.id === layerId); - assert(isRegionalGuidanceLayer(layer)); + const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer); const ipAdapter = layer.ipAdapters.find((ipAdapter) => ipAdapter.id === ipAdapterId); assert(ipAdapter); return ipAdapter; }; + const getVectorMaskPreviewColor = (state: ControlLayersState): RgbColor => { const rgLayers = state.layers.filter(isRegionalGuidanceLayer); const lastColor = rgLayers[rgLayers.length - 1]?.previewColor; @@ -222,6 +182,13 @@ export const controlLayersSlice = createSlice({ state.layers = state.layers.filter((l) => l.id !== action.payload); state.selectedLayerId = state.layers[0]?.id ?? null; }, + layerOpacityChanged: (state, action: PayloadAction<{ layerId: string; opacity: number }>) => { + const { layerId, opacity } = action.payload; + const layer = state.layers.find((l) => l.id === layerId); + if (isControlAdapterLayer(layer) || isInitialImageLayer(layer) || isRasterLayer(layer)) { + layer.opacity = opacity; + } + }, layerMovedForward: (state, action: PayloadAction) => { const cb = (l: Layer) => l.id === action.payload; const [renderableLayers, ipAdapterLayers] = partition(state.layers, isRenderableLayer); @@ -291,7 +258,7 @@ export const controlLayersSlice = createSlice({ }, caLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { const { layerId, imageDTO } = action.payload; - const layer = selectCALayerOrThrow(state, layerId); + const layer = selectLayerOrThrow(state, layerId, isControlAdapterLayer); layer.bbox = null; layer.bboxNeedsUpdate = true; layer.isEnabled = true; @@ -309,7 +276,7 @@ export const controlLayersSlice = createSlice({ }, caLayerProcessedImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { const { layerId, imageDTO } = action.payload; - const layer = selectCALayerOrThrow(state, layerId); + const layer = selectLayerOrThrow(state, layerId, isControlAdapterLayer); layer.bbox = null; layer.bboxNeedsUpdate = true; layer.isEnabled = true; @@ -323,7 +290,7 @@ export const controlLayersSlice = createSlice({ }> ) => { const { layerId, modelConfig } = action.payload; - const layer = selectCALayerOrThrow(state, layerId); + const layer = selectLayerOrThrow(state, layerId, isControlAdapterLayer); if (!modelConfig) { layer.controlAdapter.model = null; return; @@ -347,7 +314,7 @@ export const controlLayersSlice = createSlice({ }, caLayerControlModeChanged: (state, action: PayloadAction<{ layerId: string; controlMode: ControlModeV2 }>) => { const { layerId, controlMode } = action.payload; - const layer = selectCALayerOrThrow(state, layerId); + const layer = selectLayerOrThrow(state, layerId, isControlAdapterLayer); assert(layer.controlAdapter.type === 'controlnet'); layer.controlAdapter.controlMode = controlMode; }, @@ -356,7 +323,7 @@ export const controlLayersSlice = createSlice({ action: PayloadAction<{ layerId: string; processorConfig: ProcessorConfig | null }> ) => { const { layerId, processorConfig } = action.payload; - const layer = selectCALayerOrThrow(state, layerId); + const layer = selectLayerOrThrow(state, layerId, isControlAdapterLayer); layer.controlAdapter.processorConfig = processorConfig; if (!processorConfig) { layer.controlAdapter.processedImage = null; @@ -364,20 +331,15 @@ export const controlLayersSlice = createSlice({ }, caLayerIsFilterEnabledChanged: (state, action: PayloadAction<{ layerId: string; isFilterEnabled: boolean }>) => { const { layerId, isFilterEnabled } = action.payload; - const layer = selectCALayerOrThrow(state, layerId); + const layer = selectLayerOrThrow(state, layerId, isControlAdapterLayer); layer.isFilterEnabled = isFilterEnabled; }, - caLayerOpacityChanged: (state, action: PayloadAction<{ layerId: string; opacity: number }>) => { - const { layerId, opacity } = action.payload; - const layer = selectCALayerOrThrow(state, layerId); - layer.opacity = opacity; - }, caLayerProcessorPendingBatchIdChanged: ( state, action: PayloadAction<{ layerId: string; batchId: string | null }> ) => { const { layerId, batchId } = action.payload; - const layer = selectCALayerOrThrow(state, layerId); + const layer = selectLayerOrThrow(state, layerId, isControlAdapterLayer); layer.controlAdapter.processorPendingBatchId = batchId; }, //#endregion @@ -403,12 +365,12 @@ export const controlLayersSlice = createSlice({ }, ipaLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { const { layerId, imageDTO } = action.payload; - const layer = selectIPALayerOrThrow(state, layerId); + const layer = selectLayerOrThrow(state, layerId, isIPAdapterLayer); layer.ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; }, ipaLayerMethodChanged: (state, action: PayloadAction<{ layerId: string; method: IPMethodV2 }>) => { const { layerId, method } = action.payload; - const layer = selectIPALayerOrThrow(state, layerId); + const layer = selectLayerOrThrow(state, layerId, isIPAdapterLayer); layer.ipAdapter.method = method; }, ipaLayerModelChanged: ( @@ -419,7 +381,7 @@ export const controlLayersSlice = createSlice({ }> ) => { const { layerId, modelConfig } = action.payload; - const layer = selectIPALayerOrThrow(state, layerId); + const layer = selectLayerOrThrow(state, layerId, isIPAdapterLayer); if (!modelConfig) { layer.ipAdapter.model = null; return; @@ -431,7 +393,7 @@ export const controlLayersSlice = createSlice({ action: PayloadAction<{ layerId: string; clipVisionModel: CLIPVisionModelV2 }> ) => { const { layerId, clipVisionModel } = action.payload; - const layer = selectIPALayerOrThrow(state, layerId); + const layer = selectLayerOrThrow(state, layerId, isIPAdapterLayer); layer.ipAdapter.clipVisionModel = clipVisionModel; }, //#endregion @@ -439,7 +401,7 @@ export const controlLayersSlice = createSlice({ //#region CA or IPA Layers caOrIPALayerWeightChanged: (state, action: PayloadAction<{ layerId: string; weight: number }>) => { const { layerId, weight } = action.payload; - const layer = selectCAOrIPALayerOrThrow(state, layerId); + const layer = selectLayerOrThrow(state, layerId, isCAOrIPALayer); if (layer.type === 'control_adapter_layer') { layer.controlAdapter.weight = weight; } else { @@ -451,7 +413,7 @@ export const controlLayersSlice = createSlice({ action: PayloadAction<{ layerId: string; beginEndStepPct: [number, number] }> ) => { const { layerId, beginEndStepPct } = action.payload; - const layer = selectCAOrIPALayerOrThrow(state, layerId); + const layer = selectLayerOrThrow(state, layerId, isCAOrIPALayer); if (layer.type === 'control_adapter_layer') { layer.controlAdapter.beginEndStepPct = beginEndStepPct; } else { @@ -492,119 +454,23 @@ export const controlLayersSlice = createSlice({ }, rgLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => { const { layerId, prompt } = action.payload; - const layer = selectRGLayerOrThrow(state, layerId); + const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer); layer.positivePrompt = prompt; }, rgLayerNegativePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => { const { layerId, prompt } = action.payload; - const layer = selectRGLayerOrThrow(state, layerId); + const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer); layer.negativePrompt = prompt; }, rgLayerPreviewColorChanged: (state, action: PayloadAction<{ layerId: string; color: RgbColor }>) => { const { layerId, color } = action.payload; - const layer = selectRGLayerOrThrow(state, layerId); + const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer); layer.previewColor = color; }, - brushLineAdded: { - reducer: ( - state, - action: PayloadAction< - AddBrushLineArg & { - lineUuid: string; - } - > - ) => { - const { layerId, points, lineUuid, color } = action.payload; - const layer = selectRGOrRasterLayerOrThrow(state, layerId); - layer.objects.push({ - id: getBrushLineId(layer.id, lineUuid), - type: 'brush_line', - // Points must be offset by the layer's x and y coordinates - // TODO: Handle this in the event listener? - points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y], - strokeWidth: state.brushSize, - color, - }); - layer.bboxNeedsUpdate = true; - if (layer.type === 'regional_guidance_layer') { - layer.uploadedMaskImage = null; - } - }, - prepare: (payload: AddBrushLineArg) => ({ - payload: { ...payload, lineUuid: uuidv4() }, - }), - }, - eraserLineAdded: { - reducer: ( - state, - action: PayloadAction< - AddEraserLineArg & { - lineUuid: string; - } - > - ) => { - const { layerId, points, lineUuid } = action.payload; - const layer = selectRGOrRasterLayerOrThrow(state, layerId); - layer.objects.push({ - id: getEraserLineId(layer.id, lineUuid), - type: 'eraser_line', - // Points must be offset by the layer's x and y coordinates - // TODO: Handle this in the event listener? - points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y], - strokeWidth: state.brushSize, - }); - layer.bboxNeedsUpdate = true; - if (isRegionalGuidanceLayer(layer)) { - layer.uploadedMaskImage = null; - } - }, - prepare: (payload: AddEraserLineArg) => ({ - payload: { ...payload, lineUuid: uuidv4() }, - }), - }, - linePointsAdded: (state, action: PayloadAction) => { - const { layerId, point } = action.payload; - const layer = selectRGOrRasterLayerOrThrow(state, layerId); - const lastLine = layer.objects.findLast(isLine); - if (!lastLine || !isLine(lastLine)) { - return; - } - // Points must be offset by the layer's x and y coordinates - // TODO: Handle this in the event listener - lastLine.points.push(point[0] - layer.x, point[1] - layer.y); - layer.bboxNeedsUpdate = true; - if (isRegionalGuidanceLayer(layer)) { - layer.uploadedMaskImage = null; - } - }, - rectAdded: { - reducer: (state, action: PayloadAction) => { - const { layerId, rect, rectUuid, color } = action.payload; - if (rect.height === 0 || rect.width === 0) { - // Ignore zero-area rectangles - return; - } - const layer = selectRGOrRasterLayerOrThrow(state, layerId); - const id = getRectId(layer.id, rectUuid); - layer.objects.push({ - type: 'rect_shape', - id, - x: rect.x - layer.x, - y: rect.y - layer.y, - width: rect.width, - height: rect.height, - color, - }); - layer.bboxNeedsUpdate = true; - if (isRegionalGuidanceLayer(layer)) { - layer.uploadedMaskImage = null; - } - }, - prepare: (payload: AddRectShapeArg) => ({ payload: { ...payload, rectUuid: uuidv4() } }), - }, + rgLayerMaskImageUploaded: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO }>) => { const { layerId, imageDTO } = action.payload; - const layer = selectRGLayerOrThrow(state, layerId); + const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer); layer.uploadedMaskImage = imageDTOToImageWithDims(imageDTO); }, rgLayerAutoNegativeChanged: ( @@ -612,17 +478,17 @@ export const controlLayersSlice = createSlice({ action: PayloadAction<{ layerId: string; autoNegative: ParameterAutoNegative }> ) => { const { layerId, autoNegative } = action.payload; - const layer = selectRGLayerOrThrow(state, layerId); + const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer); layer.autoNegative = autoNegative; }, rgLayerIPAdapterAdded: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfigV2 }>) => { const { layerId, ipAdapter } = action.payload; - const layer = selectRGLayerOrThrow(state, layerId); + const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer); layer.ipAdapters.push(ipAdapter); }, rgLayerIPAdapterDeleted: (state, action: PayloadAction<{ layerId: string; ipAdapterId: string }>) => { const { layerId, ipAdapterId } = action.payload; - const layer = selectRGLayerOrThrow(state, layerId); + const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer); layer.ipAdapters = layer.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId); }, rgLayerIPAdapterImageChanged: ( @@ -726,20 +592,15 @@ export const controlLayersSlice = createSlice({ }, iiLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { const { layerId, imageDTO } = action.payload; - const layer = selectIILayerOrThrow(state, layerId); + const layer = selectLayerOrThrow(state, layerId, isInitialImageLayer); layer.bbox = null; layer.bboxNeedsUpdate = true; layer.isEnabled = true; layer.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; }, - iiLayerOpacityChanged: (state, action: PayloadAction<{ layerId: string; opacity: number }>) => { - const { layerId, opacity } = action.payload; - const layer = selectIILayerOrThrow(state, layerId); - layer.opacity = opacity; - }, iiLayerDenoisingStrengthChanged: (state, action: PayloadAction<{ layerId: string; denoisingStrength: number }>) => { const { layerId, denoisingStrength } = action.payload; - const layer = selectIILayerOrThrow(state, layerId); + const layer = selectLayerOrThrow(state, layerId, isInitialImageLayer); layer.denoisingStrength = denoisingStrength; }, //#endregion @@ -765,10 +626,105 @@ export const controlLayersSlice = createSlice({ }, prepare: () => ({ payload: { layerId: uuidv4() } }), }, - rasterLayerOpacityChanged: (state, action: PayloadAction<{ layerId: string; opacity: number }>) => { - const { layerId, opacity } = action.payload; - const layer = selectRasterLayerOrThrow(state, layerId); - layer.opacity = opacity; + //#endregion + + //#region Objects + brushLineAdded: { + reducer: ( + state, + action: PayloadAction< + AddBrushLineArg & { + lineUuid: string; + } + > + ) => { + const { layerId, points, lineUuid, color } = action.payload; + const layer = selectLayerOrThrow(state, layerId, isRGOrRasterlayer); + layer.objects.push({ + id: getBrushLineId(layer.id, lineUuid), + type: 'brush_line', + // Points must be offset by the layer's x and y coordinates + // TODO: Handle this in the event listener? + points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y], + strokeWidth: state.brushSize, + color, + }); + layer.bboxNeedsUpdate = true; + if (layer.type === 'regional_guidance_layer') { + layer.uploadedMaskImage = null; + } + }, + prepare: (payload: AddBrushLineArg) => ({ + payload: { ...payload, lineUuid: uuidv4() }, + }), + }, + eraserLineAdded: { + reducer: ( + state, + action: PayloadAction< + AddEraserLineArg & { + lineUuid: string; + } + > + ) => { + const { layerId, points, lineUuid } = action.payload; + const layer = selectLayerOrThrow(state, layerId, isRGOrRasterlayer); + layer.objects.push({ + id: getEraserLineId(layer.id, lineUuid), + type: 'eraser_line', + // Points must be offset by the layer's x and y coordinates + // TODO: Handle this in the event listener? + points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y], + strokeWidth: state.brushSize, + }); + layer.bboxNeedsUpdate = true; + if (isRegionalGuidanceLayer(layer)) { + layer.uploadedMaskImage = null; + } + }, + prepare: (payload: AddEraserLineArg) => ({ + payload: { ...payload, lineUuid: uuidv4() }, + }), + }, + linePointsAdded: (state, action: PayloadAction) => { + const { layerId, point } = action.payload; + const layer = selectLayerOrThrow(state, layerId, isRGOrRasterlayer); + const lastLine = layer.objects.findLast(isLine); + if (!lastLine || !isLine(lastLine)) { + return; + } + // Points must be offset by the layer's x and y coordinates + // TODO: Handle this in the event listener + lastLine.points.push(point[0] - layer.x, point[1] - layer.y); + layer.bboxNeedsUpdate = true; + if (isRegionalGuidanceLayer(layer)) { + layer.uploadedMaskImage = null; + } + }, + rectAdded: { + reducer: (state, action: PayloadAction) => { + const { layerId, rect, rectUuid, color } = action.payload; + if (rect.height === 0 || rect.width === 0) { + // Ignore zero-area rectangles + return; + } + const layer = selectLayerOrThrow(state, layerId, isRGOrRasterlayer); + const id = getRectId(layer.id, rectUuid); + layer.objects.push({ + type: 'rect_shape', + id, + x: rect.x - layer.x, + y: rect.y - layer.y, + width: rect.width, + height: rect.height, + color, + }); + layer.bboxNeedsUpdate = true; + if (isRegionalGuidanceLayer(layer)) { + layer.uploadedMaskImage = null; + } + }, + prepare: (payload: AddRectShapeArg) => ({ payload: { ...payload, rectUuid: uuidv4() } }), }, //#endregion @@ -898,6 +854,7 @@ export const { layerBboxChanged, layerReset, layerDeleted, + layerOpacityChanged, layerMovedForward, layerMovedToFront, layerMovedBackward, @@ -913,7 +870,6 @@ export const { caLayerControlModeChanged, caLayerProcessorConfigChanged, caLayerIsFilterEnabledChanged, - caLayerOpacityChanged, caLayerProcessorPendingBatchIdChanged, // IPA Layers ipaLayerAdded, @@ -949,11 +905,9 @@ export const { iiLayerAdded, iiLayerRecalled, iiLayerImageChanged, - iiLayerOpacityChanged, iiLayerDenoisingStrengthChanged, // Raster layers rasterLayerAdded, - rasterLayerOpacityChanged, // Globals positivePromptChanged, negativePromptChanged, @@ -1053,6 +1007,15 @@ export const controlLayersUndoableConfig: UndoableOptions { - return false; + // // Ignore all actions from other slices + // if (!action.type.startsWith(controlLayersSlice.name)) { + // return false; + // } + // // This action is triggered on state changes, including when we undo. If we do not ignore this action, when we + // // undo, this action triggers and empties the future states array. Therefore, we must ignore this action. + // if (layerBboxChanged.match(action)) { + // return false; + // } + return true; }, }; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index ab40c258249..32ed9a674b5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -25,7 +25,6 @@ import { z } from 'zod'; const zTool = z.enum(['brush', 'eraser', 'move', 'rect']); export type Tool = z.infer; const zDrawingTool = zTool.extract(['brush', 'eraser']); -export type DrawingTool = z.infer; const zPoints = z.array(z.number()).refine((points) => points.length % 2 === 0, { message: 'Must have an even number of points', @@ -118,6 +117,16 @@ const zImageObject = z.object({ }); export type ImageObject = z.infer; +const zAnyLayerObject = z.discriminatedUnion('type', [ + zImageObject, + zBrushLine, + zEraserline, + zRectShape, + zEllipseShape, + zPolygonShape, +]); +export type AnyLayerObject = z.infer; + const zLayerBase = z.object({ id: z.string(), isEnabled: z.boolean().default(true), @@ -140,9 +149,7 @@ const zRenderableLayerBase = zLayerBase.extend({ const zRasterLayer = zRenderableLayerBase.extend({ type: z.literal('raster_layer'), opacity: zOpacity, - objects: z.array( - z.discriminatedUnion('type', [zImageObject, zBrushLine, zEraserline, zRectShape, zEllipseShape, zPolygonShape]) - ), + objects: z.array(zAnyLayerObject), }); export type RasterLayer = z.infer; @@ -213,6 +220,7 @@ const zRegionalGuidanceLayer = zRenderableLayerBase.extend({ autoNegative: zAutoNegative, uploadedMaskImage: zImageWithDims.nullable(), }); +// TODO(psyche): This doesn't migrate correctly! const zRGLayer = z .union([zOLD_RegionalGuidanceLayer, zRegionalGuidanceLayer]) .transform((val) => { @@ -265,4 +273,46 @@ export type ControlLayersState = { export type AddEraserLineArg = { layerId: string; points: [number, number, number, number] }; export type AddBrushLineArg = AddEraserLineArg & { color: RgbaColor }; export type AddPointToLineArg = { layerId: string; point: [number, number] }; -export type AddRectShapeArg = { layerId: string; rect: IRect; color: RgbaColor }; +export type AddRectShapeArg = { layerId: string; rect: IRect; color: RgbaColor }; //#region Type guards + +//#region Type guards +export const isLine = (obj: AnyLayerObject): obj is BrushLine | EraserLine => { + return obj.type === 'brush_line' || obj.type === 'eraser_line'; +}; +export const isRegionalGuidanceLayer = (layer?: Layer): layer is RegionalGuidanceLayer => { + return layer?.type === 'regional_guidance_layer'; +}; +export const isControlAdapterLayer = (layer?: Layer): layer is ControlAdapterLayer => { + return layer?.type === 'control_adapter_layer'; +}; +export const isIPAdapterLayer = (layer?: Layer): layer is IPAdapterLayer => { + return layer?.type === 'ip_adapter_layer'; +}; +export const isInitialImageLayer = (layer?: Layer): layer is InitialImageLayer => { + return layer?.type === 'initial_image_layer'; +}; +export const isRasterLayer = (layer?: Layer): layer is RasterLayer => { + return layer?.type === 'raster_layer'; +}; +export const isRenderableLayer = ( + layer?: Layer +): layer is RegionalGuidanceLayer | ControlAdapterLayer | InitialImageLayer => { + return ( + layer?.type === 'regional_guidance_layer' || + layer?.type === 'control_adapter_layer' || + layer?.type === 'initial_image_layer' || + layer?.type === 'raster_layer' + ); +}; +export const isLayerWithOpacity = (layer?: Layer): layer is ControlAdapterLayer | InitialImageLayer | RasterLayer => { + return ( + layer?.type === 'control_adapter_layer' || layer?.type === 'initial_image_layer' || layer?.type === 'raster_layer' + ); +}; +export const isCAOrIPALayer = (layer?: Layer): layer is ControlAdapterLayer | IPAdapterLayer => { + return isControlAdapterLayer(layer) || isIPAdapterLayer(layer); +}; +export const isRGOrRasterlayer = (layer?: Layer): layer is RegionalGuidanceLayer | RasterLayer => { + return isRegionalGuidanceLayer(layer) || isRasterLayer(layer); +}; +//#endregion diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts index a7934f72d29..aeb41c402c7 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts @@ -7,14 +7,14 @@ import { } from 'features/controlAdapters/store/controlAdaptersSlice'; import type { ControlAdaptersState } from 'features/controlAdapters/store/types'; import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; +import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import type { ControlLayersState } from 'features/controlLayers/store/types'; import { isControlAdapterLayer, isInitialImageLayer, isIPAdapterLayer, isRegionalGuidanceLayer, - selectControlLayersSlice, -} from 'features/controlLayers/store/controlLayersSlice'; -import type { ControlLayersState } from 'features/controlLayers/store/types'; +} from 'features/controlLayers/store/types'; import { selectDeleteImageModalSlice } from 'features/deleteImageModal/store/slice'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; import type { NodesState } from 'features/nodes/store/types'; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts index 4261318479d..6adee170649 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts @@ -4,15 +4,15 @@ import { deepClone } from 'common/util/deepClone'; import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; import { blobToDataURL } from 'features/canvas/util/blobToDataURL'; import { RG_LAYER_NAME } from 'features/controlLayers/konva/naming'; -import { renderers } from 'features/controlLayers/konva/renderers'; +import { renderers } from 'features/controlLayers/konva/renderers/layers'; +import { rgLayerMaskImageUploaded } from 'features/controlLayers/store/controlLayersSlice'; +import type { InitialImageLayer, Layer, RegionalGuidanceLayer } from 'features/controlLayers/store/types'; import { isControlAdapterLayer, isInitialImageLayer, isIPAdapterLayer, isRegionalGuidanceLayer, - rgLayerMaskImageUploaded, -} from 'features/controlLayers/store/controlLayersSlice'; -import type { InitialImageLayer, Layer, RegionalGuidanceLayer } from 'features/controlLayers/store/types'; +} from 'features/controlLayers/store/types'; import type { ControlNetConfigV2, ImageWithDims, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts index 9f03f58a696..288f0a944fd 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts @@ -1,5 +1,5 @@ import type { RootState } from 'app/store/store'; -import { isInitialImageLayer, isRegionalGuidanceLayer } from 'features/controlLayers/store/controlLayersSlice'; +import { isInitialImageLayer, isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { CLIP_SKIP, From 24f17479dec12ae1d32828805d09b96739dc6f08 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 6 Jun 2024 08:53:38 +1000 Subject: [PATCH 005/678] feat(ui): temp disable history on CL --- .../src/features/controlLayers/store/controlLayersSlice.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index c9fe1509698..b2fc8b07542 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -1007,6 +1007,8 @@ export const controlLayersUndoableConfig: UndoableOptions { + // TODO(psyche): TEMP OVERRIDE + return false; // // Ignore all actions from other slices // if (!action.type.startsWith(controlLayersSlice.name)) { // return false; @@ -1016,6 +1018,6 @@ export const controlLayersUndoableConfig: UndoableOptions Date: Thu, 6 Jun 2024 08:53:53 +1000 Subject: [PATCH 006/678] feat(ui): cancel shape drawing on esc --- .../components/StageComponent.tsx | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index dc82e307166..ab9adfc0c5f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -161,9 +161,20 @@ const useStageRenderer = ( useLayoutEffect(() => { log.trace('Adding stage listeners'); - if (asPreview) { + if (asPreview || !container) { return; } + + const cancelShape = (e: KeyboardEvent) => { + // Cancel shape drawing on escape + if (e.key === 'Escape') { + $isDrawing.set(false); + $lastMouseDownPos.set(null); + } + }; + + container.addEventListener('keydown', cancelShape); + const cleanup = setStageEventHandlers({ stage, $tool, @@ -186,8 +197,18 @@ const useStageRenderer = ( return () => { log.trace('Removing stage listeners'); cleanup(); + container.removeEventListener('keydown', cancelShape); }; - }, [asPreview, onBrushLineAdded, onBrushSizeChanged, onEraserLineAdded, onPointAddedToLine, onRectShapeAdded, stage]); + }, [ + asPreview, + onBrushLineAdded, + onBrushSizeChanged, + onEraserLineAdded, + onPointAddedToLine, + onRectShapeAdded, + stage, + container, + ]); useLayoutEffect(() => { log.trace('Updating stage dimensions'); From d340038f486160443ed11e4f1fac5f5c8fa9eeb8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 6 Jun 2024 09:23:56 +1000 Subject: [PATCH 007/678] feat(ui): rect shape preview now has fill --- .../controlLayers/konva/renderers/toolPreview.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/toolPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/toolPreview.ts index ae085f8ad8d..1acef926661 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/toolPreview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/toolPreview.ts @@ -1,5 +1,9 @@ import { rgbaColorToString } from 'features/canvas/util/colorToString'; -import { BRUSH_BORDER_INNER_COLOR, BRUSH_BORDER_OUTER_COLOR } from 'features/controlLayers/konva/constants'; +import { + BBOX_SELECTED_STROKE, + BRUSH_BORDER_INNER_COLOR, + BRUSH_BORDER_OUTER_COLOR, +} from 'features/controlLayers/konva/constants'; import { TOOL_PREVIEW_BRUSH_BORDER_INNER_ID, TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID, @@ -54,7 +58,12 @@ const createToolPreviewLayer = (stage: Konva.Stage): Konva.Layer => { toolPreviewLayer.add(brushPreviewGroup); // 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: BBOX_SELECTED_STROKE, + strokeWidth: 1, + }); toolPreviewLayer.add(rectPreview); return toolPreviewLayer; @@ -153,6 +162,7 @@ export const renderToolPreview = ( y: Math.min(snappedPos.y, lastMouseDownPos.y), width: Math.abs(snappedPos.x - lastMouseDownPos.x), height: Math.abs(snappedPos.y - lastMouseDownPos.y), + fill: rgbaColorToString(brushColor), }); rectPreview?.visible(true); } else { From 0356f970f31000151723340f65eb2e48895bde6f 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 008/678] 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 0a3b52a3ff7..aabad5ed63b 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 f8175c96552..19c8a3332b0 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 50d23bd63c0..b5a1f516ed2 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 81251b5f2bc..1c23d480365 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 471f23ac5a1..4321a85c01d 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 2eed6a663ba..0061889a5ea 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 b2fc8b07542..a1709cac6da 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); From 86e7f242381d7472eae0e6e1dfbf314ca315e321 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 6 Jun 2024 09:54:24 +1000 Subject: [PATCH 009/678] tidy(ui): clean up event handlers Separate logic for each tool in preparation for ellipse and polygon tools. --- .../features/controlLayers/konva/events.ts | 137 ++++++++++++------ 1 file changed, 92 insertions(+), 45 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index e07753127dd..1da8b6e8cf1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -40,7 +40,13 @@ type SetStageEventHandlersArg = { onBrushSizeChanged: (size: number) => void; }; -const syncCursorPos = (stage: Konva.Stage, $lastCursorPos: WritableAtom) => { +/** + * Updates the last cursor position atom with the current cursor position, returning the new position or `null` if the + * cursor is not over the stage. + * @param stage The konva stage + * @param $lastCursorPos The last cursor pos as a nanostores atom + */ +const updateLastCursorPos = (stage: Konva.Stage, $lastCursorPos: WritableAtom) => { const pos = getScaledFlooredCursorPosition(stage); if (!pos) { return null; @@ -49,6 +55,32 @@ const syncCursorPos = (stage: Konva.Stage, $lastCursorPos: WritableAtom, + $brushSpacingPx: WritableAtom, + onPointAddedToLine: (arg: AddPointToLineArg) => void +) => { + // Continue the last line + const lastAddedPoint = $lastAddedPoint.get(); + if (lastAddedPoint) { + // Dispatching redux events impacts perf substantially - using brush spacing keeps dispatches to a reasonable number + if (Math.hypot(lastAddedPoint.x - currentPos.x, lastAddedPoint.y - currentPos.y) < $brushSpacingPx.get()) { + return null; + } + } + onPointAddedToLine({ layerId, point: [currentPos.x, currentPos.y] }); +}; + export const setStageEventHandlers = ({ stage, $tool, @@ -84,7 +116,7 @@ export const setStageEventHandlers = ({ return; } const tool = $tool.get(); - const pos = syncCursorPos(stage, $lastCursorPos); + const pos = updateLastCursorPos(stage, $lastCursorPos); const selectedLayer = $selectedLayer.get(); if (!pos || !selectedLayer) { return; @@ -100,14 +132,19 @@ export const setStageEventHandlers = ({ }); $isDrawing.set(true); $lastMouseDownPos.set(pos); - } else if (tool === 'eraser') { + } + + if (tool === 'eraser') { onEraserLineAdded({ layerId: selectedLayer.id, points: [pos.x, pos.y, pos.x, pos.y], }); $isDrawing.set(true); $lastMouseDownPos.set(pos); - } else if (tool === 'rect') { + } + + if (tool === 'rect') { + $isDrawing.set(true); $lastMouseDownPos.set(snapPosToStage(pos, stage)); } }); @@ -127,21 +164,25 @@ export const setStageEventHandlers = ({ if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { return; } - const lastPos = $lastMouseDownPos.get(); const tool = $tool.get(); - if (lastPos && selectedLayer.id && tool === 'rect') { - const snappedPos = snapPosToStage(pos, stage); - onRectShapeAdded({ - layerId: selectedLayer.id, - rect: { - x: Math.min(snappedPos.x, lastPos.x), - y: Math.min(snappedPos.y, lastPos.y), - width: Math.abs(snappedPos.x - lastPos.x), - height: Math.abs(snappedPos.y - lastPos.y), - }, - color: selectedLayer.type === 'raster_layer' ? $brushColor.get() : DEFAULT_RGBA_COLOR, - }); + + if (tool === 'rect') { + const lastMouseDownPos = $lastMouseDownPos.get(); + if (lastMouseDownPos) { + const snappedPos = snapPosToStage(pos, stage); + onRectShapeAdded({ + layerId: selectedLayer.id, + rect: { + x: Math.min(snappedPos.x, lastMouseDownPos.x), + y: Math.min(snappedPos.y, lastMouseDownPos.y), + width: Math.abs(snappedPos.x - lastMouseDownPos.x), + height: Math.abs(snappedPos.y - lastMouseDownPos.y), + }, + color: selectedLayer.type === 'raster_layer' ? $brushColor.get() : DEFAULT_RGBA_COLOR, + }); + } } + $isDrawing.set(false); $lastMouseDownPos.set(null); }); @@ -153,7 +194,7 @@ export const setStageEventHandlers = ({ return; } const tool = $tool.get(); - const pos = syncCursorPos(stage, $lastCursorPos); + const pos = updateLastCursorPos(stage, $lastCursorPos); const selectedLayer = $selectedLayer.get(); stage.findOne(`#${TOOL_PREVIEW_LAYER_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); @@ -164,34 +205,34 @@ export const setStageEventHandlers = ({ if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { return; } - if (getIsFocused(stage) && getIsMouseDown(e) && (tool === 'brush' || tool === 'eraser')) { + if (!getIsFocused(stage) || !getIsMouseDown(e)) { + return; + } + + if (tool === 'brush') { if ($isDrawing.get()) { // Continue the last line - const lastAddedPoint = $lastAddedPoint.get(); - if (lastAddedPoint) { - // Dispatching redux events impacts perf substantially - using brush spacing keeps dispatches to a reasonable number - if (Math.hypot(lastAddedPoint.x - pos.x, lastAddedPoint.y - pos.y) < $brushSpacingPx.get()) { - return; - } - } - $lastAddedPoint.set({ x: pos.x, y: pos.y }); - onPointAddedToLine({ layerId: selectedLayer.id, point: [pos.x, pos.y] }); + maybeAddNextPoint(selectedLayer.id, pos, $lastAddedPoint, $brushSpacingPx, onPointAddedToLine); } else { - if (tool === 'brush') { - // Start a new line - onBrushLineAdded({ - layerId: selectedLayer.id, - points: [pos.x, pos.y, pos.x, pos.y], - color: selectedLayer.type === 'raster_layer' ? $brushColor.get() : DEFAULT_RGBA_COLOR, - }); - } else if (tool === 'eraser') { - onEraserLineAdded({ - layerId: selectedLayer.id, - points: [pos.x, pos.y, pos.x, pos.y], - }); - } + // Start a new line + onBrushLineAdded({ + layerId: selectedLayer.id, + points: [pos.x, pos.y, pos.x, pos.y], + color: selectedLayer.type === 'raster_layer' ? $brushColor.get() : DEFAULT_RGBA_COLOR, + }); + $isDrawing.set(true); + } + } + + if (tool === 'eraser') { + if ($isDrawing.get()) { + // Continue the last line + maybeAddNextPoint(selectedLayer.id, pos, $lastAddedPoint, $brushSpacingPx, onPointAddedToLine); + } else { + // Start a new line + onEraserLineAdded({ layerId: selectedLayer.id, points: [pos.x, pos.y, pos.x, pos.y] }); + $isDrawing.set(true); } - $isDrawing.set(true); } }); @@ -201,7 +242,7 @@ export const setStageEventHandlers = ({ if (!stage) { return; } - const pos = syncCursorPos(stage, $lastCursorPos); + const pos = updateLastCursorPos(stage, $lastCursorPos); $isDrawing.set(false); $lastCursorPos.set(null); $lastMouseDownPos.set(null); @@ -216,8 +257,14 @@ export const setStageEventHandlers = ({ if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { return; } - if (getIsFocused(stage) && getIsMouseDown(e) && (tool === 'brush' || tool === 'eraser')) { - onPointAddedToLine({ layerId: selectedLayer.id, point: [pos.x, pos.y] }); + if (getIsFocused(stage) && getIsMouseDown(e)) { + if (tool === 'brush') { + onPointAddedToLine({ layerId: selectedLayer.id, point: [pos.x, pos.y] }); + } + + if (tool === 'eraser') { + onPointAddedToLine({ layerId: selectedLayer.id, point: [pos.x, pos.y] }); + } } }); From 1d24cb94b45af9027d678d9f1427586fe4bc1b8d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 6 Jun 2024 11:06:49 +1000 Subject: [PATCH 010/678] feat(ui): support image objects on raster layers Just the UI and internal state, not rendering yet. --- .../listeners/imageDropped.ts | 19 +++++++++++++++ .../components/LayerCommon/LayerWrapper.tsx | 1 + .../components/RasterLayer/RasterLayer.tsx | 14 ++++++++++- .../controlLayers/store/controlLayersSlice.ts | 24 ++++++++++++++++++- .../src/features/controlLayers/store/types.ts | 4 +++- .../web/src/features/dnd/types/index.ts | 7 ++++++ .../web/src/features/dnd/util/isValidDrop.ts | 2 ++ 7 files changed, 68 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index a65c31b7cdc..b5431508cf7 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -10,6 +10,7 @@ import { import { caLayerImageChanged, iiLayerImageChanged, + imageAdded, ipaLayerImageChanged, rgLayerIPAdapterImageChanged, } from 'features/controlLayers/store/controlLayersSlice'; @@ -161,6 +162,24 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => return; } + /** + * Image dropped on Raster layer + */ + if ( + overData.actionType === 'ADD_RASTER_LAYER_IMAGE' && + activeData.payloadType === 'IMAGE_DTO' && + activeData.payload.imageDTO + ) { + const { layerId } = overData.context; + dispatch( + imageAdded({ + layerId, + imageDTO: activeData.payload.imageDTO, + }) + ); + return; + } + /** * Image dropped on Canvas */ diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerWrapper.tsx index 9757cf39729..804ae40070f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerWrapper.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerWrapper.tsx @@ -11,6 +11,7 @@ type Props = PropsWithChildren<{ export const LayerWrapper = memo(({ onClick, borderColor, children }: Props) => { return ( { }, [dispatch, layerId]); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); + const droppableData = useMemo(() => { + const _droppableData: RasterLayerImageDropData = { + id: layerId, + actionType: 'ADD_RASTER_LAYER_IMAGE', + context: { layerId }, + }; + return _droppableData; + }, [layerId]); + return ( @@ -39,6 +50,7 @@ export const RasterLayer = memo(({ layerId }: Props) => { PLACEHOLDER )} + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index a1709cac6da..4c2c98fe40d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -8,6 +8,7 @@ import { getBrushLineId, getCALayerId, getEraserLineId, + getImageObjectId, getIPALayerId, getRasterLayerId, getRectId, @@ -48,6 +49,7 @@ import { v4 as uuidv4 } from 'uuid'; import type { AddBrushLineArg, AddEraserLineArg, + AddImageObjectArg, AddPointToLineArg, AddRectShapeArg, ControlAdapterLayer, @@ -715,7 +717,7 @@ export const controlLayersSlice = createSlice({ return; } const layer = selectLayerOrThrow(state, layerId, isRGOrRasterlayer); - const id = getRectId(layer.id, rectUuid); + const id = getRectShapeId(layer.id, rectUuid); layer.objects.push({ type: 'rect_shape', id, @@ -732,6 +734,25 @@ export const controlLayersSlice = createSlice({ }, prepare: (payload: AddRectShapeArg) => ({ payload: { ...payload, rectUuid: uuidv4() } }), }, + imageAdded: { + reducer: (state, action: PayloadAction) => { + const { layerId, imageUuid, imageDTO } = action.payload; + const layer = selectLayerOrThrow(state, layerId, isRasterLayer); + const id = getImageObjectId(layer.id, imageUuid); + const { width, height, image_name: name } = imageDTO; + layer.objects.push({ + type: 'image', + id, + x: 0, + y: 0, + width, + height, + image: { width, height, name }, + }); + layer.bboxNeedsUpdate = true; + }, + prepare: (payload: AddImageObjectArg) => ({ payload: { ...payload, imageUuid: uuidv4() } }), + }, //#endregion //#region Globals @@ -897,6 +918,7 @@ export const { eraserLineAdded, linePointsAdded, rectAdded, + imageAdded, rgLayerMaskImageUploaded, rgLayerAutoNegativeChanged, rgLayerIPAdapterAdded, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 32ed9a674b5..8fea17406c6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -20,6 +20,7 @@ import { zParameterStrength, } from 'features/parameters/types/parameterSchemas'; import type { IRect } from 'konva/lib/types'; +import type { ImageDTO } from 'services/api/types'; import { z } from 'zod'; const zTool = z.enum(['brush', 'eraser', 'move', 'rect']); @@ -273,7 +274,8 @@ export type ControlLayersState = { export type AddEraserLineArg = { layerId: string; points: [number, number, number, number] }; export type AddBrushLineArg = AddEraserLineArg & { color: RgbaColor }; export type AddPointToLineArg = { layerId: string; point: [number, number] }; -export type AddRectShapeArg = { layerId: string; rect: IRect; color: RgbaColor }; //#region Type guards +export type AddRectShapeArg = { layerId: string; rect: IRect; color: RgbaColor }; +export type AddImageObjectArg = { layerId: string; imageDTO: ImageDTO }; //#region Type guards export const isLine = (obj: AnyLayerObject): obj is BrushLine | EraserLine => { diff --git a/invokeai/frontend/web/src/features/dnd/types/index.ts b/invokeai/frontend/web/src/features/dnd/types/index.ts index 93bde117a11..1d99f4a4156 100644 --- a/invokeai/frontend/web/src/features/dnd/types/index.ts +++ b/invokeai/frontend/web/src/features/dnd/types/index.ts @@ -58,6 +58,13 @@ export type IILayerImageDropData = BaseDropData & { }; }; +export type RasterLayerImageDropData = BaseDropData & { + actionType: 'ADD_RASTER_LAYER_IMAGE'; + context: { + layerId: string; + }; +}; + export type CanvasInitialImageDropData = BaseDropData & { actionType: 'SET_CANVAS_INITIAL_IMAGE'; }; diff --git a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts index 3f8fe5ab734..4f0a31d387c 100644 --- a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts +++ b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts @@ -33,6 +33,8 @@ export const isValidDrop = (overData?: TypesafeDroppableData | null, activeData? return payloadType === 'IMAGE_DTO'; case 'SELECT_FOR_COMPARE': return payloadType === 'IMAGE_DTO'; + case 'ADD_RASTER_LAYER_IMAGE': + return payloadType === 'IMAGE_DTO'; case 'ADD_TO_BOARD': { // If the board is the same, don't allow the drop From d5abdfa3b02a516b2fd7a82ffe0d5badb05c43f2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 6 Jun 2024 13:05:07 +1000 Subject: [PATCH 011/678] feat(ui): wip raster layers I meant to split this up into smaller commits and undo some of it, but I committed afterwards and it's tedious to undo. --- .../components/BrushColorPicker.tsx | 28 +++++++---------- .../controlLayers/components/BrushSize.tsx | 2 +- .../components/ControlLayersToolbar.tsx | 23 +++++++++----- .../features/controlLayers/konva/naming.ts | 4 ++- .../controlLayers/konva/renderers/objects.ts | 31 ++++++++++++++++++- .../konva/renderers/rasterLayer.ts | 29 ++++++++++++----- .../src/features/controlLayers/konva/util.ts | 4 ++- .../controlLayers/store/controlLayersSlice.ts | 2 +- 8 files changed, 88 insertions(+), 35 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/BrushColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/BrushColorPicker.tsx index 517385f0d34..4c218a87fd0 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 a34250c29f5..731f9a1c779 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 55025d40f22..37f46b51cdf 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 19c8a3332b0..b5ceefbf14b 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 b5a1f516ed2..e5e54c95934 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 1c23d480365..ca74fee7185 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 0061889a5ea..1143940bfea 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 4c2c98fe40d..345752c93a3 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'; From 6a118f172e34505cd2e8f0e6ef732cfd22e40034 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 6 Jun 2024 16:18:45 +1000 Subject: [PATCH 012/678] fix(ui): jank when starting a shape when not already focused on stage --- .../frontend/web/src/features/controlLayers/konva/events.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 1da8b6e8cf1..1f66d208cec 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -1,6 +1,5 @@ import { calculateNewBrushSize } from 'features/canvas/hooks/useCanvasZoom'; import { - getIsFocused, getIsMouseDown, getScaledFlooredCursorPosition, snapPosToStage, @@ -205,7 +204,8 @@ export const setStageEventHandlers = ({ if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { return; } - if (!getIsFocused(stage) || !getIsMouseDown(e)) { + + if (!getIsMouseDown(e)) { return; } @@ -257,7 +257,7 @@ export const setStageEventHandlers = ({ if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { return; } - if (getIsFocused(stage) && getIsMouseDown(e)) { + if (getIsMouseDown(e)) { if (tool === 'brush') { onPointAddedToLine({ layerId: selectedLayer.id, point: [pos.x, pos.y] }); } From 4788172206c3692f2c58f844647c1e4ab6fcab25 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 6 Jun 2024 17:01:41 +1000 Subject: [PATCH 013/678] fix(ui): brush spacing handling --- .../web/src/features/controlLayers/konva/events.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 1f66d208cec..6ee781edaf7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -1,9 +1,5 @@ import { calculateNewBrushSize } from 'features/canvas/hooks/useCanvasZoom'; -import { - getIsMouseDown, - getScaledFlooredCursorPosition, - snapPosToStage, -} from 'features/controlLayers/konva/util'; +import { getIsMouseDown, getScaledFlooredCursorPosition, snapPosToStage } from 'features/controlLayers/konva/util'; import { type AddBrushLineArg, type AddEraserLineArg, @@ -74,9 +70,10 @@ const maybeAddNextPoint = ( if (lastAddedPoint) { // Dispatching redux events impacts perf substantially - using brush spacing keeps dispatches to a reasonable number if (Math.hypot(lastAddedPoint.x - currentPos.x, lastAddedPoint.y - currentPos.y) < $brushSpacingPx.get()) { - return null; + return; } } + $lastAddedPoint.set(currentPos); onPointAddedToLine({ layerId, point: [currentPos.x, currentPos.y] }); }; From b46ca55c7d0a13e5998e010a0fccc296f2cb7fb7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 6 Jun 2024 17:03:28 +1000 Subject: [PATCH 014/678] feat(ui): do not fill brush preview when drawing --- .../src/features/controlLayers/components/StageComponent.tsx | 5 ++++- .../features/controlLayers/konva/renderers/toolPreview.ts | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index ab9adfc0c5f..dcead425f63 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -74,6 +74,7 @@ const useStageRenderer = ( const tool = useStore($tool); const lastCursorPos = useStore($lastCursorPos); const lastMouseDownPos = useStore($lastMouseDownPos); + const isDrawing = useStore($isDrawing); const brushColor = useAppSelector(selectBrushColor); const selectedLayer = useAppSelector(selectSelectedLayer); const layerIds = useMemo(() => state.layers.map((l) => l.id), [state.layers]); @@ -249,7 +250,8 @@ const useStageRenderer = ( state.globalMaskLayerOpacity, lastCursorPos, lastMouseDownPos, - state.brushSize + state.brushSize, + isDrawing ); }, [ asPreview, @@ -262,6 +264,7 @@ const useStageRenderer = ( lastMouseDownPos, state.brushSize, renderers, + isDrawing, ]); useLayoutEffect(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/toolPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/toolPreview.ts index 1acef926661..5cf963334ff 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/toolPreview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/toolPreview.ts @@ -88,7 +88,8 @@ export const renderToolPreview = ( globalMaskLayerOpacity: number, cursorPos: Vector2d | null, lastMouseDownPos: Vector2d | null, - brushSize: number + brushSize: number, + isDrawing: boolean ): void => { const layerCount = stage.find(selectRenderableLayers).length; // Update the stage's pointer style @@ -133,7 +134,7 @@ export const renderToolPreview = ( x: cursorPos.x, y: cursorPos.y, radius: brushSize / 2, - fill: rgbaColorToString(brushColor), + fill: isDrawing ? '' : rgbaColorToString(brushColor), globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out', }); From 557603d2ae5158e0d345abdab711a3879cc1c740 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 6 Jun 2024 17:14:29 +1000 Subject: [PATCH 015/678] feat(ui): bbox calc for raster layers --- .../controlLayers/konva/renderers/bbox.ts | 47 +++++++++++++------ .../controlLayers/store/controlLayersSlice.ts | 11 +++-- .../src/features/controlLayers/store/types.ts | 11 ++--- 3 files changed, 43 insertions(+), 26 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts index 869fe847d13..cd052b5eb3f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts @@ -1,9 +1,14 @@ import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; import { imageDataToDataURL } from 'features/canvas/util/blobToDataURL'; import { BBOX_SELECTED_STROKE } from 'features/controlLayers/konva/constants'; -import { getLayerBboxId, LAYER_BBOX_NAME, RG_LAYER_OBJECT_GROUP_NAME } from 'features/controlLayers/konva/naming'; +import { + getLayerBboxId, + LAYER_BBOX_NAME, + RASTER_LAYER_OBJECT_GROUP_NAME, + RG_LAYER_OBJECT_GROUP_NAME, +} from 'features/controlLayers/konva/naming'; import type { Layer, Tool } from 'features/controlLayers/store/types'; -import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; +import { isRegionalGuidanceLayer, isRGOrRasterlayer } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; import { assert } from 'tsafe'; @@ -64,9 +69,13 @@ const getImageDataBbox = (imageData: ImageData): Extents | null => { * Clones a regional guidance konva layer onto an offscreen stage/canvas. This allows the pixel data for a given layer * to be captured, manipulated or analyzed without interference from other layers. * @param layer The konva layer to clone. + * @param filterChildren A callback to filter out unwanted children * @returns The cloned stage and layer. */ -const getIsolatedRGLayerClone = (layer: Konva.Layer): { stageClone: Konva.Stage; layerClone: Konva.Layer } => { +const getIsolatedLayerClone = ( + layer: Konva.Layer, + filterChildren: (node: Konva.Node) => boolean +): { stageClone: Konva.Stage; layerClone: Konva.Layer } => { const stage = layer.getStage(); // Construct an offscreen canvas with the same dimensions as the layer's stage. @@ -84,7 +93,7 @@ const getIsolatedRGLayerClone = (layer: Konva.Layer): { stageClone: Konva.Stage; stageClone.add(layerClone); for (const child of layerClone.getChildren()) { - if (child.name() === RG_LAYER_OBJECT_GROUP_NAME && child.hasChildren()) { + if (filterChildren(child) && child.hasChildren()) { // We need to cache the group to ensure it composites out eraser strokes correctly child.opacity(1); child.cache(); @@ -102,7 +111,11 @@ const getIsolatedRGLayerClone = (layer: Konva.Layer): { stageClone: Konva.Stage; * @param layer The konva layer to get the bounding box of. * @param preview Whether to open a new tab displaying the rendered layer, which is used to calculate the bbox. */ -const getLayerBboxPixels = (layer: Konva.Layer, preview: boolean = false): IRect | null => { +const getLayerBboxPixels = ( + layer: Konva.Layer, + filterChildren: (node: Konva.Node) => boolean, + preview: boolean = false +): IRect | null => { // To calculate the layer's bounding box, we must first export it to a pixel array, then do some math. // // Though it is relatively fast, we can't use Konva's `getClientRect`. It programmatically determines the rect @@ -110,7 +123,7 @@ const getLayerBboxPixels = (layer: Konva.Layer, preview: boolean = false): IRect // // This doesn't work when some shapes are drawn with composite operations that "erase" pixels, like eraser lines. // These shapes' extents are still calculated as if they were solid, leading to a bounding box that is too large. - const { stageClone, layerClone } = getIsolatedRGLayerClone(layer); + const { stageClone, layerClone } = getIsolatedLayerClone(layer, filterChildren); // Get a worst-case rect using the relatively fast `getClientRect`. const layerRect = layerClone.getClientRect(); @@ -178,6 +191,9 @@ const createBboxRect = (layerState: Layer, konvaLayer: Konva.Layer): Konva.Rect return rect; }; +const filterRGChildren = (node: Konva.Node): boolean => node.name() === RG_LAYER_OBJECT_GROUP_NAME; +const filterRasterChildren = (node: Konva.Node): boolean => node.name() === RASTER_LAYER_OBJECT_GROUP_NAME; + /** * Calculates the bbox of each regional guidance layer. Only calculates if the mask has changed. * @param stage The konva stage @@ -189,23 +205,24 @@ export const updateBboxes = ( layerStates: Layer[], onBboxChanged: (layerId: string, bbox: IRect | null) => void ): void => { - for (const rgLayer of layerStates.filter(isRegionalGuidanceLayer)) { - const konvaLayer = stage.findOne(`#${rgLayer.id}`); - assert(konvaLayer, `Layer ${rgLayer.id} not found in stage`); + for (const layerState of layerStates.filter(isRGOrRasterlayer)) { + const konvaLayer = stage.findOne(`#${layerState.id}`); + assert(konvaLayer, `Layer ${layerState.id} not found in stage`); // We only need to recalculate the bbox if the layer has changed - if (rgLayer.bboxNeedsUpdate) { - const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rgLayer, konvaLayer); + if (layerState.bboxNeedsUpdate) { + const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer); // Hide the bbox while we calculate the new bbox, else the bbox will be included in the calculation const visible = bboxRect.visible(); bboxRect.visible(false); - if (rgLayer.objects.length === 0) { + if (layerState.objects.length === 0) { // No objects - no bbox to calculate - onBboxChanged(rgLayer.id, null); + onBboxChanged(layerState.id, null); } else { // Calculate the bbox by rendering the layer and checking its pixels - onBboxChanged(rgLayer.id, getLayerBboxPixels(konvaLayer)); + const filterChildren = isRegionalGuidanceLayer(layerState) ? filterRGChildren : filterRasterChildren; + onBboxChanged(layerState.id, getLayerBboxPixels(konvaLayer, filterChildren)); } // Restore the visibility of the bbox @@ -232,7 +249,7 @@ export const renderBboxes = (stage: Konva.Stage, layerStates: Layer[], tool: Too return; } - for (const layer of layerStates.filter(isRegionalGuidanceLayer)) { + for (const layer of layerStates.filter(isRGOrRasterlayer)) { if (!layer.bbox) { continue; } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 345752c93a3..25be668117c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -162,10 +162,15 @@ export const controlLayersSlice = createSlice({ if (isRenderableLayer(layer)) { layer.bbox = bbox; layer.bboxNeedsUpdate = false; - if (bbox === null && layer.type === 'regional_guidance_layer') { + if (bbox === null) { // The layer was fully erased, empty its objects to prevent accumulation of invisible objects - layer.objects = []; - layer.uploadedMaskImage = null; + if (isRegionalGuidanceLayer(layer)) { + layer.objects = []; + layer.uploadedMaskImage = null; + } + if (isRasterLayer(layer)) { + layer.objects = []; + } } } }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 8fea17406c6..4e161069d9a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -298,18 +298,13 @@ export const isRasterLayer = (layer?: Layer): layer is RasterLayer => { }; export const isRenderableLayer = ( layer?: Layer -): layer is RegionalGuidanceLayer | ControlAdapterLayer | InitialImageLayer => { +): layer is RegionalGuidanceLayer | ControlAdapterLayer | InitialImageLayer | RasterLayer => { return ( - layer?.type === 'regional_guidance_layer' || - layer?.type === 'control_adapter_layer' || - layer?.type === 'initial_image_layer' || - layer?.type === 'raster_layer' + isRegionalGuidanceLayer(layer) || isControlAdapterLayer(layer) || isInitialImageLayer(layer) || isRasterLayer(layer) ); }; export const isLayerWithOpacity = (layer?: Layer): layer is ControlAdapterLayer | InitialImageLayer | RasterLayer => { - return ( - layer?.type === 'control_adapter_layer' || layer?.type === 'initial_image_layer' || layer?.type === 'raster_layer' - ); + return isControlAdapterLayer(layer) || isInitialImageLayer(layer) || isRasterLayer(layer); }; export const isCAOrIPALayer = (layer?: Layer): layer is ControlAdapterLayer | IPAdapterLayer => { return isControlAdapterLayer(layer) || isIPAdapterLayer(layer); From db766bd9ae3fe7b5d5c7518d727b2835700f9233 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 6 Jun 2024 17:14:42 +1000 Subject: [PATCH 016/678] feat(ui): image loading fallback for raster layers --- invokeai/frontend/web/public/locales/en.json | 1 + .../controlLayers/konva/renderers/objects.ts | 64 ++++++++++++++++--- .../konva/renderers/rasterLayer.ts | 22 ++----- 3 files changed, 62 insertions(+), 25 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index ebb313cd702..d755b915afc 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -115,6 +115,7 @@ "githubLabel": "Github", "goTo": "Go to", "hotkeysLabel": "Hotkeys", + "loadingImage": "Loading Image", "imageFailedToLoad": "Unable to Load Image", "img2img": "Image To Image", "inpaint": "inpaint", 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 e5e54c95934..9c3ab76cad9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -2,6 +2,7 @@ import { rgbaColorToString } from 'features/canvas/util/colorToString'; import { getObjectGroupId } from 'features/controlLayers/konva/naming'; import type { BrushLine, EraserLine, ImageObject, RectShape } from 'features/controlLayers/store/types'; import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types'; +import { t } from 'i18next'; import Konva from 'konva'; import { getImageDTO } from 'services/api/endpoints/images'; import { v4 as uuidv4 } from 'uuid'; @@ -81,16 +82,58 @@ export const createRectShape = (rectShape: RectShape, layerObjectGroup: Konva.Gr return konvaRect; }; -export const createImageObject = async ( +const createImagePlaceholderGroup = ( + imageObject: ImageObject +): { konvaPlaceholderGroup: Konva.Group; onError: () => void; onLoading: () => void; onLoaded: () => void } => { + const { width, height } = imageObject.image; + const konvaPlaceholderGroup = new Konva.Group({ name: 'image-placeholder', listening: false }); + const konvaPlaceholderRect = new Konva.Rect({ + fill: 'hsl(220 12% 45% / 1)', // 'base.500' + width, + height, + }); + const konvaPlaceholderText = new Konva.Text({ + name: 'image-placeholder-text', + fill: 'hsl(220 12% 10% / 1)', // 'base.900' + width, + height, + align: 'center', + verticalAlign: 'middle', + fontFamily: '"Inter Variable", sans-serif', + fontSize: width / 16, + fontStyle: '600', + text: 'Loading Image', + listening: false, + }); + konvaPlaceholderGroup.add(konvaPlaceholderRect); + konvaPlaceholderGroup.add(konvaPlaceholderText); + + const onError = () => { + konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load')); + }; + const onLoading = () => { + konvaPlaceholderText.text(t('common.loadingImage', 'Loading Image')); + }; + const onLoaded = () => { + konvaPlaceholderGroup.destroy(); + }; + return { konvaPlaceholderGroup, onError, onLoading, onLoaded }; +}; + +export const createImageObjectGroup = async ( imageObject: ImageObject, layerObjectGroup: Konva.Group, name: string -): Promise => { - const imageDTO = await getImageDTO(imageObject.image.name); - if (!imageDTO) { - return null; - } - return new Promise((resolve) => { +): Promise => { + const konvaImageGroup = new Konva.Group({ id: imageObject.id, name, listening: false }); + const placeholder = createImagePlaceholderGroup(imageObject); + konvaImageGroup.add(placeholder.konvaPlaceholderGroup); + layerObjectGroup.add(konvaImageGroup); + getImageDTO(imageObject.image.name).then((imageDTO) => { + if (!imageDTO) { + placeholder.onError(); + return; + } const imageEl = new Image(); imageEl.onload = () => { const konvaImage = new Konva.Image({ @@ -99,15 +142,16 @@ export const createImageObject = async ( listening: false, image: imageEl, }); - layerObjectGroup.add(konvaImage); - resolve(konvaImage); + placeholder.onLoaded(); + konvaImageGroup.add(konvaImage); }; imageEl.onerror = () => { - resolve(null); + placeholder.onError(); }; imageEl.id = imageObject.id; imageEl.src = imageDTO.image_url; }); + return konvaImageGroup; }; /** * Creates a konva group for a layer's objects. 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 ca74fee7185..84fad00cb9b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts @@ -9,14 +9,13 @@ import { import { createBrushLine, createEraserLine, - createImageObject, + createImageObjectGroup, 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. @@ -109,10 +108,7 @@ export const renderRasterLayer = async ( } } - for (let i = 0; i < layerState.objects.length; i++) { - const obj = layerState.objects[i]; - assert(obj); - const zIndex = layerState.objects.length - i; + for (const obj of layerState.objects) { if (obj.type === 'brush_line') { const konvaBrushLine = konvaObjectGroup.findOne(`#${obj.id}`) ?? @@ -121,7 +117,6 @@ export const renderRasterLayer = async ( if (konvaBrushLine.points().length !== obj.points.length) { konvaBrushLine.points(obj.points); } - konvaBrushLine.zIndex(zIndex); } else if (obj.type === 'eraser_line') { const konvaEraserLine = konvaObjectGroup.findOne(`#${obj.id}`) ?? @@ -130,17 +125,14 @@ export const renderRasterLayer = async ( if (konvaEraserLine.points().length !== obj.points.length) { konvaEraserLine.points(obj.points); } - konvaEraserLine.zIndex(zIndex); } else if (obj.type === 'rect_shape') { - const konvaRect = - konvaObjectGroup.findOne(`#${obj.id}`) ?? + if (!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); + if (!konvaObjectGroup.findOne(`#${obj.id}`)) { + createImageObjectGroup(obj, konvaObjectGroup, RASTER_LAYER_IMAGE_NAME); + } } } From 4f05a7b8d095e2dacf69c7d6941bc685aced72f6 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 6 Jun 2024 18:17:49 +1000 Subject: [PATCH 017/678] fix(ui): show color picker when using rect tool --- .../features/controlLayers/components/ControlLayersToolbar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index 37f46b51cdf..7f140e2be63 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -17,7 +17,7 @@ export const ControlLayersToolbar = memo(() => { return tool === 'brush' || tool === 'eraser'; }, [tool]); const withBrushColor = useMemo(() => { - return tool === 'brush'; + return tool === 'brush' || tool === 'rect'; }, [tool]); return ( From 9124604dc461bfa2c1209709f272f15b8e8e7ff4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 6 Jun 2024 18:19:39 +1000 Subject: [PATCH 018/678] feat(ui): add x,y,scaleX,scaleY,rotation to objects --- .../controlLayers/konva/renderers/objects.ts | 55 ++++++++++++++----- .../controlLayers/store/controlLayersSlice.ts | 16 ++++++ .../src/features/controlLayers/store/types.ts | 46 ++++++++++------ 3 files changed, 86 insertions(+), 31 deletions(-) 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 9c3ab76cad9..5563b7e4d22 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -19,18 +19,23 @@ import { v4 as uuidv4 } from 'uuid'; * @param name The konva name for the line */ export const createBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Group, name: string): Konva.Line => { + const { id, strokeWidth, color, x, y, scaleX, scaleY, rotation } = brushLine; const konvaLine = new Konva.Line({ - id: brushLine.id, - key: brushLine.id, + id, name, - strokeWidth: brushLine.strokeWidth, + strokeWidth, tension: 0, lineCap: 'round', lineJoin: 'round', shadowForStrokeEnabled: false, globalCompositeOperation: 'source-over', listening: false, - stroke: rgbaColorToString(brushLine.color), + stroke: rgbaColorToString(color), + x, + y, + scaleX, + scaleY, + rotation, }); layerObjectGroup.add(konvaLine); return konvaLine; @@ -43,11 +48,11 @@ export const createBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Gr * @param name The konva name for the line */ export const createEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva.Group, name: string): Konva.Line => { + const { id, strokeWidth, x, y, scaleX, scaleY, rotation } = eraserLine; const konvaLine = new Konva.Line({ - id: eraserLine.id, - key: eraserLine.id, + id, name, - strokeWidth: eraserLine.strokeWidth, + strokeWidth, tension: 0, lineCap: 'round', lineJoin: 'round', @@ -55,6 +60,11 @@ export const createEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva globalCompositeOperation: 'destination-out', listening: false, stroke: rgbaColorToString(DEFAULT_RGBA_COLOR), + x, + y, + scaleX, + scaleY, + rotation, }); layerObjectGroup.add(konvaLine); return konvaLine; @@ -67,14 +77,18 @@ export const createEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva * @param name The konva name for the rect */ export const createRectShape = (rectShape: RectShape, layerObjectGroup: Konva.Group, name: string): Konva.Rect => { + const { id, x, y, width, height, scaleX, scaleY, rotation } = rectShape; + const konvaRect = new Konva.Rect({ - id: rectShape.id, - key: rectShape.id, + id, name, - x: rectShape.x, - y: rectShape.y, - width: rectShape.width, - height: rectShape.height, + x, + y, + width, + height, + scaleX, + scaleY, + rotation, listening: false, fill: rgbaColorToString(rectShape.color), }); @@ -125,7 +139,20 @@ export const createImageObjectGroup = async ( layerObjectGroup: Konva.Group, name: string ): Promise => { - const konvaImageGroup = new Konva.Group({ id: imageObject.id, name, listening: false }); + const { id, x, y, width, height, scaleX, scaleY, rotation } = imageObject; + + const konvaImageGroup = new Konva.Group({ + id, + x, + y, + width, + height, + scaleX, + scaleY, + rotation, + name, + listening: false, + }); const placeholder = createImagePlaceholderGroup(imageObject); konvaImageGroup.add(placeholder.konvaPlaceholderGroup); layerObjectGroup.add(konvaImageGroup); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 25be668117c..ec214fd5ce5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -656,6 +656,11 @@ export const controlLayersSlice = createSlice({ layer.objects.push({ id: getBrushLineId(layer.id, lineUuid), type: 'brush_line', + x: 0, + y: 0, + scaleX: 1, + scaleY: 1, + rotation: 0, // Points must be offset by the layer's x and y coordinates // TODO: Handle this in the event listener? points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y], @@ -685,6 +690,11 @@ export const controlLayersSlice = createSlice({ layer.objects.push({ id: getEraserLineId(layer.id, lineUuid), type: 'eraser_line', + x: 0, + y: 0, + scaleX: 1, + scaleY: 1, + rotation: 0, // Points must be offset by the layer's x and y coordinates // TODO: Handle this in the event listener? points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y], @@ -728,6 +738,9 @@ export const controlLayersSlice = createSlice({ id, x: rect.x - layer.x, y: rect.y - layer.y, + scaleX: 1, + scaleY: 1, + rotation: 0, width: rect.width, height: rect.height, color, @@ -750,6 +763,9 @@ export const controlLayersSlice = createSlice({ id, x: 0, y: 0, + scaleX: 1, + scaleY: 1, + rotation: 0, width, height, image: { width, height, name }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 4e161069d9a..73654288da2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -60,8 +60,16 @@ export const DEFAULT_RGBA_COLOR: RgbaColor = { r: 255, g: 255, b: 255, a: 1 }; const zOpacity = z.number().gte(0).lte(1); -const zBrushLine = z.object({ +const zObjectBase = z.object({ id: z.string(), + x: z.number().catch(0), + y: z.number().catch(0), + scaleX: z.number().catch(1), + scaleY: z.number().catch(1), + rotation: z.number().catch(0), +}); + +const zBrushLine = zObjectBase.extend({ type: z.literal('brush_line'), strokeWidth: z.number().min(1), points: zPoints, @@ -69,50 +77,39 @@ const zBrushLine = z.object({ }); export type BrushLine = z.infer; -const zEraserline = z.object({ - id: z.string(), +const zEraserline = zObjectBase.extend({ type: z.literal('eraser_line'), strokeWidth: z.number().min(1), points: zPoints, }); export type EraserLine = z.infer; -const zRectShape = z.object({ - id: z.string(), +const zRectShape = zObjectBase.extend({ type: z.literal('rect_shape'), - x: z.number(), - y: z.number(), width: z.number().min(1), height: z.number().min(1), color: zRgbaColor, }); export type RectShape = z.infer; -const zEllipseShape = z.object({ - id: z.string(), +const zEllipseShape = zObjectBase.extend({ type: z.literal('ellipse_shape'), - x: z.number(), - y: z.number(), width: z.number().min(1), height: z.number().min(1), color: zRgbaColor, }); export type EllipseShape = z.infer; -const zPolygonShape = z.object({ - id: z.string(), +const zPolygonShape = zObjectBase.extend({ type: z.literal('polygon_shape'), points: zPoints, color: zRgbaColor, }); export type PolygonShape = z.infer; -const zImageObject = z.object({ - id: z.string(), +const zImageObject = zObjectBase.extend({ type: z.literal('image'), image: zImageWithDims, - x: z.number(), - y: z.number(), width: z.number().min(1), height: z.number().min(1), }); @@ -179,12 +176,22 @@ const zMaskObject = z ...rest, type: 'brush_line', color: { r: 255, g: 255, b: 255, a: 1 }, + x: 0, + y: 0, + scaleX: 1, + scaleY: 1, + rotation: 0, }; return asBrushline; } else if (tool === 'eraser') { const asEraserLine: EraserLine = { ...rest, type: 'eraser_line', + x: 0, + y: 0, + scaleX: 1, + scaleY: 1, + rotation: 0, }; return asEraserLine; } @@ -193,6 +200,11 @@ const zMaskObject = z ...val, type: 'rect_shape', color: { r: 255, g: 255, b: 255, a: 1 }, + x: 0, + y: 0, + scaleX: 1, + scaleY: 1, + rotation: 0, }; return asRectShape; } else { From 0d0004018bf27f038ba1e2ef7780a89b0eb17442 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 6 Jun 2024 18:43:08 +1000 Subject: [PATCH 019/678] docs(ui): konva image object docstrings --- .../controlLayers/konva/renderers/objects.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 5563b7e4d22..b6ca47650c1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -96,6 +96,11 @@ export const createRectShape = (rectShape: RectShape, layerObjectGroup: Konva.Gr return konvaRect; }; +/** + * Creates an image placeholder group for an image object. + * @param imageObject The image object state + * @returns The konva group for the image placeholder, and callbacks to handle loading and error states + */ const createImagePlaceholderGroup = ( imageObject: ImageObject ): { konvaPlaceholderGroup: Konva.Group; onError: () => void; onLoading: () => void; onLoaded: () => void } => { @@ -134,6 +139,14 @@ const createImagePlaceholderGroup = ( return { konvaPlaceholderGroup, onError, onLoading, onLoaded }; }; +/** + * Creates an image object group. Because images are loaded asynchronously, and we need to handle loading an error state, + * the image is rendered in a group, which includes a placeholder. + * @param imageObject The image object state + * @param layerObjectGroup The konva layer's object group to add the image to + * @param name The konva name for the image + * @returns A promise that resolves to the konva group for the image object + */ export const createImageObjectGroup = async ( imageObject: ImageObject, layerObjectGroup: Konva.Group, From 8ec08063f47c2d6f255f9efe6b79434b818d1ed1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 6 Jun 2024 18:44:16 +1000 Subject: [PATCH 020/678] feat(ui): layers manage their own bbox --- .../components/StageComponent.tsx | 9 --- .../controlLayers/konva/renderers/bbox.ts | 60 +------------------ .../controlLayers/konva/renderers/layers.ts | 4 +- .../controlLayers/konva/renderers/objects.ts | 21 ++++++- .../konva/renderers/rasterLayer.ts | 20 +++++++ .../controlLayers/konva/renderers/rgLayer.ts | 20 +++++++ 6 files changed, 62 insertions(+), 72 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index dcead425f63..a98cff2b6b5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -281,15 +281,6 @@ const useStageRenderer = ( state.size.height, ]); - useLayoutEffect(() => { - log.trace('Rendering bbox'); - if (asPreview) { - // Preview should not display bboxes - return; - } - renderers.renderBboxes(stage, state.layers, tool); - }, [stage, asPreview, state.layers, tool, onBboxChanged, renderers]); - useLayoutEffect(() => { if (asPreview) { // Preview should not check for transparency diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts index cd052b5eb3f..316ef85110c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts @@ -1,13 +1,12 @@ import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; import { imageDataToDataURL } from 'features/canvas/util/blobToDataURL'; -import { BBOX_SELECTED_STROKE } from 'features/controlLayers/konva/constants'; import { - getLayerBboxId, LAYER_BBOX_NAME, RASTER_LAYER_OBJECT_GROUP_NAME, RG_LAYER_OBJECT_GROUP_NAME, } from 'features/controlLayers/konva/naming'; -import type { Layer, Tool } from 'features/controlLayers/store/types'; +import { createBboxRect } from 'features/controlLayers/konva/renderers/objects'; +import type { Layer } from 'features/controlLayers/store/types'; import { isRegionalGuidanceLayer, isRGOrRasterlayer } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; @@ -175,22 +174,6 @@ export const getLayerBboxFast = (layer: Konva.Layer): IRect => { }; }; -/** - * Creates a bounding box rect for a layer. - * @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 - */ -const createBboxRect = (layerState: Layer, konvaLayer: Konva.Layer): Konva.Rect => { - const rect = new Konva.Rect({ - id: getLayerBboxId(layerState.id), - name: LAYER_BBOX_NAME, - strokeWidth: 1, - visible: false, - }); - konvaLayer.add(rect); - return rect; -}; - const filterRGChildren = (node: Konva.Node): boolean => node.name() === RG_LAYER_OBJECT_GROUP_NAME; const filterRasterChildren = (node: Konva.Node): boolean => node.name() === RASTER_LAYER_OBJECT_GROUP_NAME; @@ -230,42 +213,3 @@ export const updateBboxes = ( } } }; - -/** - * Renders the bounding boxes for the layers. - * @param stage The konva stage - * @param layerStates An array of layers to draw bboxes for - * @param tool The current tool - * @returns - */ -export const renderBboxes = (stage: Konva.Stage, layerStates: Layer[], tool: Tool): void => { - // Hide all bboxes so they don't interfere with getClientRect - for (const bboxRect of stage.find(`.${LAYER_BBOX_NAME}`)) { - bboxRect.visible(false); - bboxRect.listening(false); - } - // No selected layer or not using the move tool - nothing more to do here - if (tool !== 'move') { - return; - } - - for (const layer of layerStates.filter(isRGOrRasterlayer)) { - if (!layer.bbox) { - continue; - } - const konvaLayer = stage.findOne(`#${layer.id}`); - assert(konvaLayer, `Layer ${layer.id} not found in stage`); - - const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layer, konvaLayer); - - bboxRect.setAttrs({ - visible: !layer.bboxNeedsUpdate, - listening: layer.isSelected, - x: layer.bbox.x, - y: layer.bbox.y, - width: layer.bbox.width, - height: layer.bbox.height, - stroke: layer.isSelected ? BBOX_SELECTED_STROKE : '', - }); - } -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts index 8243b81504c..2b5fea493f4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -1,7 +1,7 @@ import { DEBOUNCE_MS } from 'features/controlLayers/konva/constants'; import { BACKGROUND_LAYER_ID, TOOL_PREVIEW_LAYER_ID } from 'features/controlLayers/konva/naming'; import { renderBackground } from 'features/controlLayers/konva/renderers/background'; -import { renderBboxes, updateBboxes } from 'features/controlLayers/konva/renderers/bbox'; +import { updateBboxes } from 'features/controlLayers/konva/renderers/bbox'; import { renderCALayer } from 'features/controlLayers/konva/renderers/caLayer'; import { renderIILayer } from 'features/controlLayers/konva/renderers/iiLayer'; import { renderNoLayersMessage } from 'features/controlLayers/konva/renderers/noLayersMessage'; @@ -90,7 +90,6 @@ const renderLayers = ( export const renderers = { renderToolPreview, renderLayers, - renderBboxes, renderBackground, renderNoLayersMessage, arrangeLayers, @@ -105,7 +104,6 @@ export const renderers = { const getDebouncedRenderers = (ms = DEBOUNCE_MS): typeof renderers => ({ renderToolPreview: debounce(renderToolPreview, ms), renderLayers: debounce(renderLayers, ms), - renderBboxes: debounce(renderBboxes, ms), renderBackground: debounce(renderBackground, ms), renderNoLayersMessage: debounce(renderNoLayersMessage, ms), arrangeLayers: debounce(arrangeLayers, ms), 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 b6ca47650c1..2d0747effd3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -1,6 +1,6 @@ import { rgbaColorToString } from 'features/canvas/util/colorToString'; -import { getObjectGroupId } from 'features/controlLayers/konva/naming'; -import type { BrushLine, EraserLine, ImageObject, RectShape } from 'features/controlLayers/store/types'; +import { getLayerBboxId, getObjectGroupId, LAYER_BBOX_NAME } from 'features/controlLayers/konva/naming'; +import type { BrushLine, EraserLine, ImageObject, Layer, RectShape } from 'features/controlLayers/store/types'; import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types'; import { t } from 'i18next'; import Konva from 'konva'; @@ -193,6 +193,23 @@ export const createImageObjectGroup = async ( }); return konvaImageGroup; }; + +/** + * Creates a bounding box rect for a layer. + * @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 + */ +export const createBboxRect = (layerState: Layer, konvaLayer: Konva.Layer): Konva.Rect => { + const rect = new Konva.Rect({ + id: getLayerBboxId(layerState.id), + name: LAYER_BBOX_NAME, + strokeWidth: 1, + visible: false, + }); + konvaLayer.add(rect); + return rect; +}; + /** * 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 84fad00cb9b..af55f7f1f2f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts @@ -1,4 +1,6 @@ +import { BBOX_SELECTED_STROKE } from 'features/controlLayers/konva/constants'; import { + LAYER_BBOX_NAME, RASTER_LAYER_BRUSH_LINE_NAME, RASTER_LAYER_ERASER_LINE_NAME, RASTER_LAYER_IMAGE_NAME, @@ -7,6 +9,7 @@ import { RASTER_LAYER_RECT_SHAPE_NAME, } from 'features/controlLayers/konva/naming'; import { + createBboxRect, createBrushLine, createEraserLine, createImageObjectGroup, @@ -141,5 +144,22 @@ export const renderRasterLayer = async ( konvaLayer.visible(layerState.isEnabled); } + const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer); + + if (layerState.bbox) { + const active = !layerState.bboxNeedsUpdate && layerState.isSelected && tool === 'move'; + bboxRect.setAttrs({ + visible: active, + listening: active, + x: layerState.bbox.x, + y: layerState.bbox.y, + width: layerState.bbox.width, + height: layerState.bbox.height, + stroke: layerState.isSelected ? BBOX_SELECTED_STROKE : '', + }); + } else { + bboxRect.visible(false); + } + konvaObjectGroup.opacity(layerState.opacity); }; 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 4321a85c01d..32a3f0e3bbc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts @@ -1,6 +1,8 @@ import { rgbColorToString } from 'features/canvas/util/colorToString'; +import { BBOX_SELECTED_STROKE } from 'features/controlLayers/konva/constants'; import { COMPOSITING_RECT_NAME, + LAYER_BBOX_NAME, RG_LAYER_BRUSH_LINE_NAME, RG_LAYER_ERASER_LINE_NAME, RG_LAYER_NAME, @@ -9,6 +11,7 @@ import { } from 'features/controlLayers/konva/naming'; import { getLayerBboxFast } from 'features/controlLayers/konva/renderers/bbox'; import { + createBboxRect, createBrushLine, createEraserLine, createObjectGroup, @@ -227,4 +230,21 @@ export const renderRGLayer = ( // Updating group opacity does not require re-caching konvaObjectGroup.opacity(globalMaskLayerOpacity); } + + const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer); + + if (layerState.bbox) { + const active = !layerState.bboxNeedsUpdate && layerState.isSelected && tool === 'move'; + bboxRect.setAttrs({ + visible: active, + listening: active, + x: layerState.bbox.x, + y: layerState.bbox.y, + width: layerState.bbox.width, + height: layerState.bbox.height, + stroke: layerState.isSelected ? BBOX_SELECTED_STROKE : '', + }); + } else { + bboxRect.visible(false); + } }; From 1237e839cafbc077a8a2d3cce20d3f85fc59d665 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 7 Jun 2024 15:31:59 +1000 Subject: [PATCH 021/678] Revert "feat(ui): add x,y,scaleX,scaleY,rotation to objects" This reverts commit 53318b396c967c72326a7e4dea09667b2ab20bdd. --- .../controlLayers/konva/renderers/objects.ts | 55 +++++-------------- .../controlLayers/store/controlLayersSlice.ts | 16 ------ .../src/features/controlLayers/store/types.ts | 46 ++++++---------- 3 files changed, 31 insertions(+), 86 deletions(-) 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 2d0747effd3..7f09e87f85c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -19,23 +19,18 @@ import { v4 as uuidv4 } from 'uuid'; * @param name The konva name for the line */ export const createBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Group, name: string): Konva.Line => { - const { id, strokeWidth, color, x, y, scaleX, scaleY, rotation } = brushLine; const konvaLine = new Konva.Line({ - id, + id: brushLine.id, + key: brushLine.id, name, - strokeWidth, + strokeWidth: brushLine.strokeWidth, tension: 0, lineCap: 'round', lineJoin: 'round', shadowForStrokeEnabled: false, globalCompositeOperation: 'source-over', listening: false, - stroke: rgbaColorToString(color), - x, - y, - scaleX, - scaleY, - rotation, + stroke: rgbaColorToString(brushLine.color), }); layerObjectGroup.add(konvaLine); return konvaLine; @@ -48,11 +43,11 @@ export const createBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Gr * @param name The konva name for the line */ export const createEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva.Group, name: string): Konva.Line => { - const { id, strokeWidth, x, y, scaleX, scaleY, rotation } = eraserLine; const konvaLine = new Konva.Line({ - id, + id: eraserLine.id, + key: eraserLine.id, name, - strokeWidth, + strokeWidth: eraserLine.strokeWidth, tension: 0, lineCap: 'round', lineJoin: 'round', @@ -60,11 +55,6 @@ export const createEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva globalCompositeOperation: 'destination-out', listening: false, stroke: rgbaColorToString(DEFAULT_RGBA_COLOR), - x, - y, - scaleX, - scaleY, - rotation, }); layerObjectGroup.add(konvaLine); return konvaLine; @@ -77,18 +67,14 @@ export const createEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva * @param name The konva name for the rect */ export const createRectShape = (rectShape: RectShape, layerObjectGroup: Konva.Group, name: string): Konva.Rect => { - const { id, x, y, width, height, scaleX, scaleY, rotation } = rectShape; - const konvaRect = new Konva.Rect({ - id, + id: rectShape.id, + key: rectShape.id, name, - x, - y, - width, - height, - scaleX, - scaleY, - rotation, + x: rectShape.x, + y: rectShape.y, + width: rectShape.width, + height: rectShape.height, listening: false, fill: rgbaColorToString(rectShape.color), }); @@ -152,20 +138,7 @@ export const createImageObjectGroup = async ( layerObjectGroup: Konva.Group, name: string ): Promise => { - const { id, x, y, width, height, scaleX, scaleY, rotation } = imageObject; - - const konvaImageGroup = new Konva.Group({ - id, - x, - y, - width, - height, - scaleX, - scaleY, - rotation, - name, - listening: false, - }); + const konvaImageGroup = new Konva.Group({ id: imageObject.id, name, listening: false }); const placeholder = createImagePlaceholderGroup(imageObject); konvaImageGroup.add(placeholder.konvaPlaceholderGroup); layerObjectGroup.add(konvaImageGroup); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index ec214fd5ce5..25be668117c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -656,11 +656,6 @@ export const controlLayersSlice = createSlice({ layer.objects.push({ id: getBrushLineId(layer.id, lineUuid), type: 'brush_line', - x: 0, - y: 0, - scaleX: 1, - scaleY: 1, - rotation: 0, // Points must be offset by the layer's x and y coordinates // TODO: Handle this in the event listener? points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y], @@ -690,11 +685,6 @@ export const controlLayersSlice = createSlice({ layer.objects.push({ id: getEraserLineId(layer.id, lineUuid), type: 'eraser_line', - x: 0, - y: 0, - scaleX: 1, - scaleY: 1, - rotation: 0, // Points must be offset by the layer's x and y coordinates // TODO: Handle this in the event listener? points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y], @@ -738,9 +728,6 @@ export const controlLayersSlice = createSlice({ id, x: rect.x - layer.x, y: rect.y - layer.y, - scaleX: 1, - scaleY: 1, - rotation: 0, width: rect.width, height: rect.height, color, @@ -763,9 +750,6 @@ export const controlLayersSlice = createSlice({ id, x: 0, y: 0, - scaleX: 1, - scaleY: 1, - rotation: 0, width, height, image: { width, height, name }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 73654288da2..4e161069d9a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -60,16 +60,8 @@ export const DEFAULT_RGBA_COLOR: RgbaColor = { r: 255, g: 255, b: 255, a: 1 }; const zOpacity = z.number().gte(0).lte(1); -const zObjectBase = z.object({ +const zBrushLine = z.object({ id: z.string(), - x: z.number().catch(0), - y: z.number().catch(0), - scaleX: z.number().catch(1), - scaleY: z.number().catch(1), - rotation: z.number().catch(0), -}); - -const zBrushLine = zObjectBase.extend({ type: z.literal('brush_line'), strokeWidth: z.number().min(1), points: zPoints, @@ -77,39 +69,50 @@ const zBrushLine = zObjectBase.extend({ }); export type BrushLine = z.infer; -const zEraserline = zObjectBase.extend({ +const zEraserline = z.object({ + id: z.string(), type: z.literal('eraser_line'), strokeWidth: z.number().min(1), points: zPoints, }); export type EraserLine = z.infer; -const zRectShape = zObjectBase.extend({ +const zRectShape = z.object({ + id: z.string(), type: z.literal('rect_shape'), + x: z.number(), + y: z.number(), width: z.number().min(1), height: z.number().min(1), color: zRgbaColor, }); export type RectShape = z.infer; -const zEllipseShape = zObjectBase.extend({ +const zEllipseShape = z.object({ + id: z.string(), type: z.literal('ellipse_shape'), + x: z.number(), + y: z.number(), width: z.number().min(1), height: z.number().min(1), color: zRgbaColor, }); export type EllipseShape = z.infer; -const zPolygonShape = zObjectBase.extend({ +const zPolygonShape = z.object({ + id: z.string(), type: z.literal('polygon_shape'), points: zPoints, color: zRgbaColor, }); export type PolygonShape = z.infer; -const zImageObject = zObjectBase.extend({ +const zImageObject = z.object({ + id: z.string(), type: z.literal('image'), image: zImageWithDims, + x: z.number(), + y: z.number(), width: z.number().min(1), height: z.number().min(1), }); @@ -176,22 +179,12 @@ const zMaskObject = z ...rest, type: 'brush_line', color: { r: 255, g: 255, b: 255, a: 1 }, - x: 0, - y: 0, - scaleX: 1, - scaleY: 1, - rotation: 0, }; return asBrushline; } else if (tool === 'eraser') { const asEraserLine: EraserLine = { ...rest, type: 'eraser_line', - x: 0, - y: 0, - scaleX: 1, - scaleY: 1, - rotation: 0, }; return asEraserLine; } @@ -200,11 +193,6 @@ const zMaskObject = z ...val, type: 'rect_shape', color: { r: 255, g: 255, b: 255, a: 1 }, - x: 0, - y: 0, - scaleX: 1, - scaleY: 1, - rotation: 0, }; return asRectShape; } else { From e66e4fefedb931a8c17f4d6121a11ed02cc88bca Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 7 Jun 2024 21:51:41 +1000 Subject: [PATCH 022/678] feat(ui): CL zoom and pan, some rendering optimizations --- .../components/StageComponent.tsx | 121 ++++++++----- .../controlLayers/components/ToolChooser.tsx | 14 +- .../features/controlLayers/konva/constants.ts | 15 ++ .../features/controlLayers/konva/events.ts | 120 ++++++++++--- .../features/controlLayers/konva/naming.ts | 6 +- .../konva/renderers/background.ts | 67 -------- .../controlLayers/konva/renderers/layers.ts | 34 +--- .../konva/renderers/noLayersMessage.ts | 53 ------ .../controlLayers/konva/renderers/objects.ts | 22 ++- .../konva/renderers/rasterLayer.ts | 21 +-- .../controlLayers/konva/renderers/rgLayer.ts | 20 +-- .../konva/renderers/toolPreview.ts | 162 +++++++++++------- .../controlLayers/store/controlLayersSlice.ts | 5 + .../src/features/controlLayers/store/types.ts | 2 +- 14 files changed, 348 insertions(+), 314 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts delete mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/renderers/noLayersMessage.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index a98cff2b6b5..ac12412389b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -1,23 +1,34 @@ -import { Flex } from '@invoke-ai/ui-library'; +import { Box, Flex, Heading } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { BRUSH_SPACING_PCT, MAX_BRUSH_SPACING_PX, MIN_BRUSH_SPACING_PX } from 'features/controlLayers/konva/constants'; +import { + BRUSH_SPACING_PCT, + MAX_BRUSH_SPACING_PX, + MIN_BRUSH_SPACING_PX, + TRANSPARENCY_CHECKER_PATTERN, +} from 'features/controlLayers/konva/constants'; import { setStageEventHandlers } from 'features/controlLayers/konva/events'; import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers/layers'; +import { renderImageDimsPreview } from 'features/controlLayers/konva/renderers/toolPreview'; import { $brushColor, $brushSize, $brushSpacingPx, $isDrawing, + $isMouseDown, + $isSpaceDown, $lastAddedPoint, $lastCursorPos, $lastMouseDownPos, $selectedLayer, $shouldInvertBrushSizeScrollDirection, + $stagePos, + $stageScale, $tool, + $toolBuffer, brushLineAdded, brushSizeChanged, eraserLineAdded, @@ -38,6 +49,7 @@ import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; import { clamp } from 'lodash-es'; import { memo, useCallback, useLayoutEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { getImageDTO } from 'services/api/endpoints/images'; import { useDevicePixelRatio } from 'use-device-pixel-ratio'; import { v4 as uuidv4 } from 'uuid'; @@ -63,6 +75,11 @@ const selectSelectedLayer = createSelector(selectControlLayersSlice, (controlLay return controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId) ?? null; }); +const selectLayerCount = createSelector( + selectControlLayersSlice, + (controlLayers) => controlLayers.present.layers.length +); + const useStageRenderer = ( stage: Konva.Stage, container: HTMLDivElement | null, @@ -74,11 +91,11 @@ const useStageRenderer = ( const tool = useStore($tool); const lastCursorPos = useStore($lastCursorPos); const lastMouseDownPos = useStore($lastMouseDownPos); + const isMouseDown = useStore($isMouseDown); + const stageScale = useStore($stageScale); const isDrawing = useStore($isDrawing); const brushColor = useAppSelector(selectBrushColor); const selectedLayer = useAppSelector(selectSelectedLayer); - const layerIds = useMemo(() => state.layers.map((l) => l.id), [state.layers]); - const layerCount = useMemo(() => state.layers.length, [state.layers]); const renderers = useMemo(() => (asPreview ? debouncedRenderers : normalRenderers), [asPreview]); const dpr = useDevicePixelRatio({ round: false }); const shouldInvertBrushSizeScrollDirection = useAppSelector((s) => s.canvas.shouldInvertBrushSizeScrollDirection); @@ -166,28 +183,23 @@ const useStageRenderer = ( return; } - const cancelShape = (e: KeyboardEvent) => { - // Cancel shape drawing on escape - if (e.key === 'Escape') { - $isDrawing.set(false); - $lastMouseDownPos.set(null); - } - }; - - container.addEventListener('keydown', cancelShape); - const cleanup = setStageEventHandlers({ stage, $tool, + $toolBuffer, $isDrawing, + $isMouseDown, $lastMouseDownPos, $lastCursorPos, $lastAddedPoint, + $stageScale, + $stagePos, $brushSize, $brushColor, $brushSpacingPx, $selectedLayer, $shouldInvertBrushSizeScrollDirection, + $isSpaceDown, onBrushSizeChanged, onBrushLineAdded, onEraserLineAdded, @@ -198,7 +210,6 @@ const useStageRenderer = ( return () => { log.trace('Removing stage listeners'); cleanup(); - container.removeEventListener('keydown', cancelShape); }; }, [ asPreview, @@ -251,7 +262,8 @@ const useStageRenderer = ( lastCursorPos, lastMouseDownPos, state.brushSize, - isDrawing + isDrawing, + isMouseDown ); }, [ asPreview, @@ -265,6 +277,32 @@ const useStageRenderer = ( state.brushSize, renderers, isDrawing, + isMouseDown, + ]); + + useLayoutEffect(() => { + if (asPreview) { + // Preview should not display tool + return; + } + log.trace('Rendering tool preview'); + renderImageDimsPreview(stage, state.size.width, state.size.height, stageScale); + }, [ + asPreview, + stage, + tool, + brushColor, + selectedLayer, + state.globalMaskLayerOpacity, + lastCursorPos, + lastMouseDownPos, + state.brushSize, + renderers, + isDrawing, + isMouseDown, + state.size.width, + state.size.height, + stageScale, ]); useLayoutEffect(() => { @@ -290,29 +328,6 @@ const useStageRenderer = ( debouncedRenderers.updateBboxes(stage, state.layers, onBboxChanged); }, [stage, asPreview, state.layers, onBboxChanged]); - useLayoutEffect(() => { - if (asPreview) { - // The preview should not have a background - return; - } - log.trace('Rendering background'); - renderers.renderBackground(stage, state.size.width, state.size.height); - }, [stage, asPreview, state.size.width, state.size.height, renderers]); - - useLayoutEffect(() => { - log.trace('Arranging layers'); - renderers.arrangeLayers(stage, layerIds); - }, [stage, layerIds, renderers]); - - useLayoutEffect(() => { - if (asPreview) { - // The preview should not display the no layers message - return; - } - log.trace('Rendering no layers message'); - renderers.renderNoLayersMessage(stage, layerCount, state.size.width, state.size.height); - }, [stage, layerCount, renderers, asPreview, state.size.width, state.size.height]); - useLayoutEffect(() => { Konva.pixelRatio = dpr; }, [dpr]); @@ -323,8 +338,15 @@ type Props = { }; export const StageComponent = memo(({ asPreview = false }: Props) => { + const { t } = useTranslation(); + const layerCount = useAppSelector(selectLayerCount); const [stage] = useState( - () => new Konva.Stage({ id: uuidv4(), container: document.createElement('div'), listening: !asPreview }) + () => + new Konva.Stage({ + id: uuidv4(), + container: document.createElement('div'), + listening: !asPreview, + }) ); const [container, setContainer] = useState(null); const [wrapper, setWrapper] = useState(null); @@ -341,14 +363,29 @@ export const StageComponent = memo(({ asPreview = false }: Props) => { return ( - + + + {layerCount === 0 && !asPreview && ( + + {t('controlLayers.noLayersAdded')} + + )} + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx index b9ea0af4597..0327d7de9b4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx @@ -11,7 +11,7 @@ import { import { useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; -import { PiArrowsOutCardinalBold, PiEraserBold, PiPaintBrushBold, PiRectangleBold } from 'react-icons/pi'; +import { PiArrowsOutCardinalBold, PiEraserBold, PiHandBold, PiPaintBrushBold, PiRectangleBold } from 'react-icons/pi'; const selectIsDisabled = createSelector(selectControlLayersSlice, (controlLayers) => { const selectedLayer = controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId); @@ -41,6 +41,10 @@ export const ToolChooser: React.FC = () => { $tool.set('move'); }, []); useHotkeys('v', setToolToMove, { enabled: !isDisabled }, [isDisabled]); + const setToolToView = useCallback(() => { + $tool.set('view'); + }, []); + useHotkeys('h', setToolToView, { enabled: !isDisabled }, [isDisabled]); const resetSelectedLayer = useCallback(() => { if (selectedLayerId === null) { @@ -89,6 +93,14 @@ export const ToolChooser: React.FC = () => { onClick={setToolToMove} isDisabled={isDisabled} /> + } + variant={tool === 'view' ? 'solid' : 'outline'} + onClick={setToolToView} + isDisabled={isDisabled} + /> ); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts b/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts index 638b6da7486..9fd691bbdae 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts @@ -39,3 +39,18 @@ export const MAX_BRUSH_SPACING_PX = 15; * The debounce time in milliseconds for debounced renderers. */ export const DEBOUNCE_MS = 300; + +/** + * Konva wheel zoom exponential scale factor + */ +export const CANVAS_SCALE_BY = 0.999; + +/** + * Minimum (furthest-zoomed-out) scale + */ +export const MIN_CANVAS_SCALE = 0.1; + +/** + * Maximum (furthest-zoomed-in) scale + */ +export const MAX_CANVAS_SCALE = 20; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 6ee781edaf7..55aea09bfd8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -1,4 +1,5 @@ import { calculateNewBrushSize } from 'features/canvas/hooks/useCanvasZoom'; +import { CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/canvas/util/constants'; import { getIsMouseDown, getScaledFlooredCursorPosition, snapPosToStage } from 'features/controlLayers/konva/util'; import { type AddBrushLineArg, @@ -11,23 +12,29 @@ import { } from 'features/controlLayers/store/types'; import type Konva from 'konva'; import type { Vector2d } from 'konva/lib/types'; +import { clamp } from 'lodash-es'; import type { WritableAtom } from 'nanostores'; import type { RgbaColor } from 'react-colorful'; -import { TOOL_PREVIEW_LAYER_ID } from './naming'; +import { TOOL_PREVIEW_TOOL_GROUP_ID } from './naming'; type SetStageEventHandlersArg = { stage: Konva.Stage; $tool: WritableAtom; + $toolBuffer: WritableAtom; $isDrawing: WritableAtom; + $isMouseDown: WritableAtom; $lastMouseDownPos: WritableAtom; $lastCursorPos: WritableAtom; $lastAddedPoint: WritableAtom; + $stageScale: WritableAtom; + $stagePos: WritableAtom; $brushColor: WritableAtom; $brushSize: WritableAtom; $brushSpacingPx: WritableAtom; $selectedLayer: WritableAtom; $shouldInvertBrushSizeScrollDirection: WritableAtom; + $isSpaceDown: WritableAtom; onBrushLineAdded: (arg: AddBrushLineArg) => void; onEraserLineAdded: (arg: AddEraserLineArg) => void; onPointAddedToLine: (arg: AddPointToLineArg) => void; @@ -80,15 +87,20 @@ const maybeAddNextPoint = ( export const setStageEventHandlers = ({ stage, $tool, + $toolBuffer, $isDrawing, + $isMouseDown, $lastMouseDownPos, $lastCursorPos, $lastAddedPoint, + $stagePos, + $stageScale, $brushColor, $brushSize, $brushSpacingPx, $selectedLayer, $shouldInvertBrushSizeScrollDirection, + $isSpaceDown, onBrushLineAdded, onEraserLineAdded, onPointAddedToLine, @@ -102,7 +114,7 @@ export const setStageEventHandlers = ({ return; } const tool = $tool.get(); - stage.findOne(`#${TOOL_PREVIEW_LAYER_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); + stage.findOne(`#${TOOL_PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); }); //#region mousedown @@ -111,6 +123,7 @@ export const setStageEventHandlers = ({ if (!stage) { return; } + $isMouseDown.set(true); const tool = $tool.get(); const pos = updateLastCursorPos(stage, $lastCursorPos); const selectedLayer = $selectedLayer.get(); @@ -120,6 +133,12 @@ export const setStageEventHandlers = ({ if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { return; } + + if ($isSpaceDown.get()) { + // No drawing when space is down - we are panning the stage + return; + } + if (tool === 'brush') { onBrushLineAdded({ layerId: selectedLayer.id, @@ -151,6 +170,7 @@ export const setStageEventHandlers = ({ if (!stage) { return; } + $isMouseDown.set(false); const pos = $lastCursorPos.get(); const selectedLayer = $selectedLayer.get(); @@ -160,6 +180,12 @@ export const setStageEventHandlers = ({ if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { return; } + + if ($isSpaceDown.get()) { + // No drawing when space is down - we are panning the stage + return; + } + const tool = $tool.get(); if (tool === 'rect') { @@ -193,7 +219,7 @@ export const setStageEventHandlers = ({ const pos = updateLastCursorPos(stage, $lastCursorPos); const selectedLayer = $selectedLayer.get(); - stage.findOne(`#${TOOL_PREVIEW_LAYER_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); + stage.findOne(`#${TOOL_PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); if (!pos || !selectedLayer) { return; @@ -202,6 +228,11 @@ export const setStageEventHandlers = ({ return; } + if ($isSpaceDown.get()) { + // No drawing when space is down - we are panning the stage + return; + } + if (!getIsMouseDown(e)) { return; } @@ -246,7 +277,7 @@ export const setStageEventHandlers = ({ const selectedLayer = $selectedLayer.get(); const tool = $tool.get(); - stage.findOne(`#${TOOL_PREVIEW_LAYER_ID}`)?.visible(false); + stage.findOne(`#${TOOL_PREVIEW_TOOL_GROUP_ID}`)?.visible(false); if (!pos || !selectedLayer) { return; @@ -254,6 +285,10 @@ export const setStageEventHandlers = ({ if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { return; } + if ($isSpaceDown.get()) { + // No drawing when space is down - we are panning the stage + return; + } if (getIsMouseDown(e)) { if (tool === 'brush') { onPointAddedToLine({ layerId: selectedLayer.id, point: [pos.x, pos.y] }); @@ -267,28 +302,73 @@ export const setStageEventHandlers = ({ stage.on('wheel', (e) => { e.evt.preventDefault(); - const tool = $tool.get(); - const selectedLayer = $selectedLayer.get(); - if (tool !== 'brush' && tool !== 'eraser') { - return; - } - if (!selectedLayer) { - return; + if (e.evt.ctrlKey || e.evt.metaKey) { + let delta = e.evt.deltaY; + if ($shouldInvertBrushSizeScrollDirection.get()) { + delta = -delta; + } + // Holding ctrl or meta while scrolling changes the brush size + onBrushSizeChanged(calculateNewBrushSize($brushSize.get(), delta)); + } else { + // We need the absolute cursor position - not the scaled position + const cursorPos = stage.getPointerPosition(); + if (!cursorPos) { + return; + } + // Stage's x and y scale are always the same + const stageScale = stage.scaleX(); + // When wheeling on trackpad, e.evt.ctrlKey is true - in that case, let's reverse the direction + const delta = e.evt.ctrlKey ? -e.evt.deltaY : e.evt.deltaY; + const mousePointTo = { + x: (cursorPos.x - stage.x()) / stageScale, + y: (cursorPos.y - stage.y()) / stageScale, + }; + const newScale = clamp(stageScale * CANVAS_SCALE_BY ** delta, MIN_CANVAS_SCALE, MAX_CANVAS_SCALE); + const newPos = { + x: cursorPos.x - mousePointTo.x * newScale, + y: cursorPos.y - mousePointTo.y * newScale, + }; + + stage.scaleX(newScale); + stage.scaleY(newScale); + stage.position(newPos); + $stageScale.set(newScale); + $stagePos.set(newPos); } - if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { + }); + + const onKeyDown = (e: KeyboardEvent) => { + if (e.repeat) { return; } - // Invert the delta if the property is set to true - let delta = e.evt.deltaY; - if ($shouldInvertBrushSizeScrollDirection.get()) { - delta = -delta; + // Cancel shape drawing on escape + if (e.key === 'Escape') { + $isDrawing.set(false); + $lastMouseDownPos.set(null); + } else if (e.key === ' ') { + $toolBuffer.set($tool.get()); + $tool.set('view'); } + }; + window.addEventListener('keydown', onKeyDown); - if (e.evt.ctrlKey || e.evt.metaKey) { - onBrushSizeChanged(calculateNewBrushSize($brushSize.get(), delta)); + const onKeyUp = (e: KeyboardEvent) => { + // Cancel shape drawing on escape + if (e.repeat) { + return; } - }); + if (e.key === ' ') { + const toolBuffer = $toolBuffer.get(); + $tool.set(toolBuffer ?? 'move'); + $toolBuffer.set(null); + } + }; + window.addEventListener('keyup', onKeyUp); - return () => stage.off('mousedown mouseup mousemove mouseenter mouseleave wheel'); + return () => { + stage.off('mousedown mouseup mousemove mouseenter mouseleave wheel'); + window.removeEventListener('keydown', onKeyDown); + window.removeEventListener('keyup', onKeyUp); + }; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts index b5ceefbf14b..2f61c6cd229 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts @@ -4,14 +4,14 @@ // IDs for singleton Konva layers and objects export const TOOL_PREVIEW_LAYER_ID = 'tool_preview_layer'; +export const TOOL_PREVIEW_TOOL_GROUP_ID = 'tool_preview_layer.tool_group'; export const TOOL_PREVIEW_BRUSH_GROUP_ID = 'tool_preview_layer.brush_group'; export const TOOL_PREVIEW_BRUSH_FILL_ID = 'tool_preview_layer.brush_fill'; export const TOOL_PREVIEW_BRUSH_BORDER_INNER_ID = 'tool_preview_layer.brush_border_inner'; export const TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID = 'tool_preview_layer.brush_border_outer'; export const TOOL_PREVIEW_RECT_ID = 'tool_preview_layer.rect'; -export const BACKGROUND_LAYER_ID = 'background_layer'; -export const BACKGROUND_RECT_ID = 'background_layer.rect'; -export const NO_LAYERS_MESSAGE_LAYER_ID = 'no_layers_message'; +export const TOOL_PREVIEW_IMAGE_DIMS_RECT = 'tool_preview_layer.image_dims_rect'; + // Names for Konva layers and objects (comparable to CSS classes) export const LAYER_BBOX_NAME = 'layer.bbox'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts deleted file mode 100644 index d5dcfddcda1..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants'; -import { BACKGROUND_LAYER_ID, BACKGROUND_RECT_ID } from 'features/controlLayers/konva/naming'; -import Konva from 'konva'; -import { assert } from 'tsafe'; - -/** - * The stage background is a semi-transparent checkerboard pattern. We use konva's `fillPatternImage` to apply the - * a data URL of the pattern image to the background rect. Some scaling and positioning is required to ensure the - * everything lines up correctly. - */ - -/** - * Creates the background layer for the stage. - * @param stage The konva stage - */ -const createBackgroundLayer = (stage: Konva.Stage): Konva.Layer => { - const layer = new Konva.Layer({ - id: BACKGROUND_LAYER_ID, - }); - const background = new Konva.Rect({ - id: BACKGROUND_RECT_ID, - x: stage.x(), - y: 0, - width: stage.width() / stage.scaleX(), - height: stage.height() / stage.scaleY(), - listening: false, - opacity: 0.2, - }); - layer.add(background); - stage.add(layer); - const image = new Image(); - image.onload = () => { - background.fillPatternImage(image); - }; - image.src = TRANSPARENCY_CHECKER_PATTERN; - return layer; -}; - -/** - * Renders the background layer for the stage. - * @param stage The konva stage - * @param width The unscaled width of the canvas - * @param height The unscaled height of the canvas - */ -export const renderBackground = (stage: Konva.Stage, width: number, height: number): void => { - const layer = stage.findOne(`#${BACKGROUND_LAYER_ID}`) ?? createBackgroundLayer(stage); - - const background = layer.findOne(`#${BACKGROUND_RECT_ID}`); - assert(background, 'Background rect not found'); - // ensure background rect is in the top-left of the canvas - background.absolutePosition({ x: 0, y: 0 }); - - // set the dimensions of the background rect to match the canvas - not the stage!!! - background.size({ - width: width / stage.scaleX(), - height: height / stage.scaleY(), - }); - - // Calculate the amount the stage is moved - including the effect of scaling - const stagePos = { - x: -stage.x() / stage.scaleX(), - y: -stage.y() / stage.scaleY(), - }; - - // Apply that movement to the fill pattern - background.fillPatternOffset(stagePos); -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts index 2b5fea493f4..d50bfae6b26 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -1,10 +1,8 @@ import { DEBOUNCE_MS } from 'features/controlLayers/konva/constants'; -import { BACKGROUND_LAYER_ID, TOOL_PREVIEW_LAYER_ID } from 'features/controlLayers/konva/naming'; -import { renderBackground } from 'features/controlLayers/konva/renderers/background'; +import { TOOL_PREVIEW_LAYER_ID } from 'features/controlLayers/konva/naming'; import { updateBboxes } from 'features/controlLayers/konva/renderers/bbox'; import { renderCALayer } from 'features/controlLayers/konva/renderers/caLayer'; import { renderIILayer } from 'features/controlLayers/konva/renderers/iiLayer'; -import { renderNoLayersMessage } from 'features/controlLayers/konva/renderers/noLayersMessage'; import { renderRasterLayer } from 'features/controlLayers/konva/renderers/rasterLayer'; import { renderRGLayer } from 'features/controlLayers/konva/renderers/rgLayer'; import { renderToolPreview } from 'features/controlLayers/konva/renderers/toolPreview'; @@ -25,23 +23,6 @@ import type { ImageDTO } from 'services/api/types'; * Logic for rendering arranging and rendering all layers. */ -/** - * Arranges all layers in the z-axis by updating their z-indices. - * @param stage The konva stage - * @param layerIds An array of redux layer ids, in their z-index order - */ -const arrangeLayers = (stage: Konva.Stage, layerIds: string[]): void => { - let nextZIndex = 0; - // Background is the first layer - stage.findOne(`#${BACKGROUND_LAYER_ID}`)?.zIndex(nextZIndex++); - // Then arrange the redux layers in order - for (const layerId of layerIds) { - stage.findOne(`#${layerId}`)?.zIndex(nextZIndex++); - } - // Finally, the tool preview layer is always on top - stage.findOne(`#${TOOL_PREVIEW_LAYER_ID}`)?.zIndex(nextZIndex++); -}; - /** * Renders the layers on the stage. * @param stage The konva stage @@ -66,7 +47,8 @@ const renderLayers = ( konvaLayer.destroy(); } } - + // We'll need to ensure the tool preview layer is on top of the rest of the layers + let toolLayerZIndex = 0; for (const layer of layerStates) { if (isRegionalGuidanceLayer(layer)) { renderRGLayer(stage, layer, globalMaskLayerOpacity, tool, onLayerPosChanged); @@ -81,7 +63,11 @@ const renderLayers = ( renderRasterLayer(stage, layer, tool, onLayerPosChanged); } // IP Adapter layers are not rendered + // Increment the z-index for the tool layer + toolLayerZIndex++; } + // Arrange the tool preview layer + stage.findOne(`#${TOOL_PREVIEW_LAYER_ID}`)?.zIndex(toolLayerZIndex); }; /** @@ -90,9 +76,6 @@ const renderLayers = ( export const renderers = { renderToolPreview, renderLayers, - renderBackground, - renderNoLayersMessage, - arrangeLayers, updateBboxes, }; @@ -104,9 +87,6 @@ export const renderers = { const getDebouncedRenderers = (ms = DEBOUNCE_MS): typeof renderers => ({ renderToolPreview: debounce(renderToolPreview, ms), renderLayers: debounce(renderLayers, ms), - renderBackground: debounce(renderBackground, ms), - renderNoLayersMessage: debounce(renderNoLayersMessage, ms), - arrangeLayers: debounce(arrangeLayers, ms), updateBboxes: debounce(updateBboxes, ms), }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/noLayersMessage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/noLayersMessage.ts deleted file mode 100644 index eae41d70d8d..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/noLayersMessage.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { NO_LAYERS_MESSAGE_LAYER_ID } from 'features/controlLayers/konva/naming'; -import { t } from 'i18next'; -import Konva from 'konva'; - -/** - * Logic for creating and rendering a fallback message when there are no layers to render. - */ - -/** - * Creates the "no layers" fallback layer - * @param stage The konva stage - */ -const createNoLayersMessageLayer = (stage: Konva.Stage): Konva.Layer => { - const noLayersMessageLayer = new Konva.Layer({ - id: NO_LAYERS_MESSAGE_LAYER_ID, - opacity: 0.7, - listening: false, - }); - const text = new Konva.Text({ - x: 0, - y: 0, - align: 'center', - verticalAlign: 'middle', - text: t('controlLayers.noLayersAdded', 'No Layers Added'), - fontFamily: '"Inter Variable", sans-serif', - fontStyle: '600', - fill: 'white', - }); - noLayersMessageLayer.add(text); - stage.add(noLayersMessageLayer); - return noLayersMessageLayer; -}; - -/** - * 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 - */ -export const renderNoLayersMessage = (stage: Konva.Stage, layerCount: number, width: number, height: number): void => { - const noLayersMessageLayer = - stage.findOne(`#${NO_LAYERS_MESSAGE_LAYER_ID}`) ?? createNoLayersMessageLayer(stage); - if (layerCount === 0) { - noLayersMessageLayer.findOne('Text')?.setAttrs({ - width, - height, - fontSize: 32 / stage.scaleX(), - }); - } else { - noLayersMessageLayer?.destroy(); - } -}; 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 7f09e87f85c..32628a39d27 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -1,5 +1,10 @@ import { rgbaColorToString } from 'features/canvas/util/colorToString'; -import { getLayerBboxId, getObjectGroupId, LAYER_BBOX_NAME } from 'features/controlLayers/konva/naming'; +import { + getLayerBboxId, + getObjectGroupId, + LAYER_BBOX_NAME, + TOOL_PREVIEW_IMAGE_DIMS_RECT, +} from 'features/controlLayers/konva/naming'; import type { BrushLine, EraserLine, ImageObject, Layer, RectShape } from 'features/controlLayers/store/types'; import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types'; import { t } from 'i18next'; @@ -198,3 +203,18 @@ export const createObjectGroup = (konvaLayer: Konva.Layer, name: string): Konva. konvaLayer.add(konvaObjectGroup); return konvaObjectGroup; }; + +export const createImageDimsPreview = (konvaLayer: Konva.Layer, width: number, height: number): Konva.Rect => { + const imageDimsPreview = new Konva.Rect({ + id: TOOL_PREVIEW_IMAGE_DIMS_RECT, + x: 0, + y: 0, + width, + height, + stroke: 'rgb(255,0,255)', + strokeWidth: 1 / konvaLayer.getStage().scaleX(), + listening: false, + }); + konvaLayer.add(imageDimsPreview); + return imageDimsPreview; +}; 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 af55f7f1f2f..81f3d570a77 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts @@ -16,7 +16,7 @@ import { createObjectGroup, createRectShape, } from 'features/controlLayers/konva/renderers/objects'; -import { getScaledFlooredCursorPosition, mapId, selectRasterObjects } from 'features/controlLayers/konva/util'; +import { mapId, selectRasterObjects } from 'features/controlLayers/konva/util'; import type { RasterLayer, Tool } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -51,24 +51,6 @@ const createRasterLayer = ( }); } - // The dragBoundFunc limits how far the layer can be dragged - konvaLayer.dragBoundFunc(function (pos) { - const cursorPos = getScaledFlooredCursorPosition(stage); - if (!cursorPos) { - return this.getAbsolutePosition(); - } - // Prevent the user from dragging the layer out of the stage bounds by constaining the cursor position to the stage bounds - if ( - cursorPos.x < 0 || - cursorPos.x > stage.width() / stage.scaleX() || - cursorPos.y < 0 || - cursorPos.y > stage.height() / stage.scaleY() - ) { - return this.getAbsolutePosition(); - } - return pos; - }); - stage.add(konvaLayer); return konvaLayer; @@ -156,6 +138,7 @@ export const renderRasterLayer = async ( width: layerState.bbox.width, height: layerState.bbox.height, stroke: layerState.isSelected ? BBOX_SELECTED_STROKE : '', + strokeWidth: 1 / stage.scaleX(), }); } else { bboxRect.visible(false); 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 32a3f0e3bbc..d6ece5c3933 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts @@ -17,7 +17,7 @@ import { createObjectGroup, createRectShape, } from 'features/controlLayers/konva/renderers/objects'; -import { getScaledFlooredCursorPosition, mapId, selectVectorMaskObjects } from 'features/controlLayers/konva/util'; +import { mapId, selectVectorMaskObjects } from 'features/controlLayers/konva/util'; import type { RegionalGuidanceLayer, Tool } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -65,24 +65,6 @@ const createRGLayer = ( }); } - // The dragBoundFunc limits how far the layer can be dragged - konvaLayer.dragBoundFunc(function (pos) { - const cursorPos = getScaledFlooredCursorPosition(stage); - if (!cursorPos) { - return this.getAbsolutePosition(); - } - // Prevent the user from dragging the layer out of the stage bounds by constaining the cursor position to the stage bounds - if ( - cursorPos.x < 0 || - cursorPos.x > stage.width() / stage.scaleX() || - cursorPos.y < 0 || - cursorPos.y > stage.height() / stage.scaleY() - ) { - return this.getAbsolutePosition(); - } - return pos; - }); - stage.add(konvaLayer); return konvaLayer; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/toolPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/toolPreview.ts index 5cf963334ff..95d41bb2f6a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/toolPreview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/toolPreview.ts @@ -9,8 +9,10 @@ import { TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID, TOOL_PREVIEW_BRUSH_FILL_ID, TOOL_PREVIEW_BRUSH_GROUP_ID, + TOOL_PREVIEW_IMAGE_DIMS_RECT, TOOL_PREVIEW_LAYER_ID, TOOL_PREVIEW_RECT_ID, + TOOL_PREVIEW_TOOL_GROUP_ID, } from 'features/controlLayers/konva/naming'; import { selectRenderableLayers, snapPosToStage } from 'features/controlLayers/konva/util'; import type { Layer, RgbaColor, Tool } from 'features/controlLayers/store/types'; @@ -26,9 +28,13 @@ import { assert } from 'tsafe'; * Creates the singleton tool preview layer and all its objects. * @param stage The konva stage */ -const createToolPreviewLayer = (stage: Konva.Stage): Konva.Layer => { +const getToolPreviewLayer = (stage: Konva.Stage): Konva.Layer => { + let toolPreviewLayer = stage.findOne(`#${TOOL_PREVIEW_LAYER_ID}`); + if (toolPreviewLayer) { + return toolPreviewLayer; + } // Initialize the brush preview layer & add to the stage - const toolPreviewLayer = new Konva.Layer({ id: TOOL_PREVIEW_LAYER_ID, visible: false, listening: false }); + toolPreviewLayer = new Konva.Layer({ id: TOOL_PREVIEW_LAYER_ID, listening: false }); stage.add(toolPreviewLayer); // Create the brush preview group & circles @@ -55,7 +61,6 @@ const createToolPreviewLayer = (stage: Konva.Stage): Konva.Layer => { strokeEnabled: true, }); brushPreviewGroup.add(brushPreviewBorderOuter); - toolPreviewLayer.add(brushPreviewGroup); // 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({ @@ -64,11 +69,38 @@ const createToolPreviewLayer = (stage: Konva.Stage): Konva.Layer => { stroke: BBOX_SELECTED_STROKE, strokeWidth: 1, }); - toolPreviewLayer.add(rectPreview); + + const toolGroup = new Konva.Group({ id: TOOL_PREVIEW_TOOL_GROUP_ID }); + + toolGroup.add(rectPreview); + toolGroup.add(brushPreviewGroup); + + const imageDimsPreview = new Konva.Rect({ + id: TOOL_PREVIEW_IMAGE_DIMS_RECT, + x: 0, + y: 0, + width: 0, + height: 0, + stroke: 'rgb(255,0,255)', + strokeWidth: 1 / toolPreviewLayer.getStage().scaleX(), + listening: false, + }); + + toolPreviewLayer.add(toolGroup); + toolPreviewLayer.add(imageDimsPreview); return toolPreviewLayer; }; +export const renderImageDimsPreview = (stage: Konva.Stage, width: number, height: number, stageScale: number): void => { + const imageDimsPreview = stage.findOne(`#${TOOL_PREVIEW_IMAGE_DIMS_RECT}`); + imageDimsPreview?.setAttrs({ + width, + height, + strokeWidth: 1 / stageScale, + }); +}; + /** * Renders the brush preview for the selected tool. * @param stage The konva stage @@ -89,11 +121,15 @@ export const renderToolPreview = ( cursorPos: Vector2d | null, lastMouseDownPos: Vector2d | null, brushSize: number, - isDrawing: boolean + isDrawing: boolean, + isMouseDown: boolean ): void => { const layerCount = stage.find(selectRenderableLayers).length; // Update the stage's pointer style - if (layerCount === 0) { + if (tool === 'view') { + // View gets a hand + stage.container().style.cursor = isMouseDown ? 'grabbing' : 'grab'; + } else if (layerCount === 0) { // We have no layers, so we should not render any tool stage.container().style.cursor = 'default'; } else if (selectedLayerType !== 'regional_guidance_layer' && selectedLayerType !== 'raster_layer') { @@ -103,70 +139,74 @@ export const renderToolPreview = ( // Move tool gets a pointer stage.container().style.cursor = 'default'; } else if (tool === 'rect') { - // Move rect gets a crosshair + // Rect gets a crosshair stage.container().style.cursor = 'crosshair'; } else { // Else we hide the native cursor and use the konva-rendered brush preview stage.container().style.cursor = 'none'; } - const toolPreviewLayer = stage.findOne(`#${TOOL_PREVIEW_LAYER_ID}`) ?? createToolPreviewLayer(stage); + stage.draggable(tool === 'view'); - if (!cursorPos || layerCount === 0) { - // We can bail early if the mouse isn't over the stage or there are no layers - toolPreviewLayer.visible(false); - return; - } + const toolPreviewLayer = getToolPreviewLayer(stage); + const toolGroup = toolPreviewLayer.findOne(`#${TOOL_PREVIEW_TOOL_GROUP_ID}`); - toolPreviewLayer.visible(true); - - const brushPreviewGroup = stage.findOne(`#${TOOL_PREVIEW_BRUSH_GROUP_ID}`); - assert(brushPreviewGroup, 'Brush preview group not found'); - - const rectPreview = stage.findOne(`#${TOOL_PREVIEW_RECT_ID}`); - assert(rectPreview, 'Rect preview not found'); - - // No need to render the brush preview if the cursor position or color is missing - if (cursorPos && (tool === 'brush' || tool === 'eraser')) { - // Update the fill circle - const brushPreviewFill = brushPreviewGroup.findOne(`#${TOOL_PREVIEW_BRUSH_FILL_ID}`); - brushPreviewFill?.setAttrs({ - x: cursorPos.x, - y: cursorPos.y, - radius: brushSize / 2, - fill: isDrawing ? '' : rgbaColorToString(brushColor), - globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out', - }); - - // Update the inner border of the brush preview - const brushPreviewInner = toolPreviewLayer.findOne(`#${TOOL_PREVIEW_BRUSH_BORDER_INNER_ID}`); - brushPreviewInner?.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: brushSize / 2 }); - - // Update the outer border of the brush preview - const brushPreviewOuter = toolPreviewLayer.findOne(`#${TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID}`); - brushPreviewOuter?.setAttrs({ - x: cursorPos.x, - y: cursorPos.y, - radius: brushSize / 2 + 1, - }); - - brushPreviewGroup.visible(true); - } else { - brushPreviewGroup.visible(false); - } + assert(toolGroup, 'Tool group not found'); - if (cursorPos && lastMouseDownPos && tool === 'rect') { - const snappedPos = snapPosToStage(cursorPos, stage); - const rectPreview = toolPreviewLayer.findOne(`#${TOOL_PREVIEW_RECT_ID}`); - rectPreview?.setAttrs({ - x: Math.min(snappedPos.x, lastMouseDownPos.x), - y: Math.min(snappedPos.y, lastMouseDownPos.y), - width: Math.abs(snappedPos.x - lastMouseDownPos.x), - height: Math.abs(snappedPos.y - lastMouseDownPos.y), - fill: rgbaColorToString(brushColor), - }); - rectPreview?.visible(true); + if (!cursorPos || layerCount === 0) { + // We can bail early if the mouse isn't over the stage or there are no layers + toolGroup.visible(false); } else { - rectPreview?.visible(false); + toolGroup.visible(true); + + const brushPreviewGroup = stage.findOne(`#${TOOL_PREVIEW_BRUSH_GROUP_ID}`); + assert(brushPreviewGroup, 'Brush preview group not found'); + + const rectPreview = stage.findOne(`#${TOOL_PREVIEW_RECT_ID}`); + assert(rectPreview, 'Rect preview not found'); + + // No need to render the brush preview if the cursor position or color is missing + if (cursorPos && (tool === 'brush' || tool === 'eraser')) { + // Update the fill circle + const brushPreviewFill = brushPreviewGroup.findOne(`#${TOOL_PREVIEW_BRUSH_FILL_ID}`); + brushPreviewFill?.setAttrs({ + x: cursorPos.x, + y: cursorPos.y, + radius: brushSize / 2, + fill: isDrawing ? '' : rgbaColorToString(brushColor), + globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out', + }); + + // Update the inner border of the brush preview + const brushPreviewInner = toolPreviewLayer.findOne(`#${TOOL_PREVIEW_BRUSH_BORDER_INNER_ID}`); + brushPreviewInner?.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: brushSize / 2 }); + + // Update the outer border of the brush preview + const brushPreviewOuter = toolPreviewLayer.findOne(`#${TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID}`); + brushPreviewOuter?.setAttrs({ + x: cursorPos.x, + y: cursorPos.y, + radius: brushSize / 2 + 1, + }); + + brushPreviewGroup.visible(true); + } else { + brushPreviewGroup.visible(false); + } + + if (cursorPos && lastMouseDownPos && tool === 'rect') { + const snappedPos = snapPosToStage(cursorPos, stage); + const rectPreview = toolPreviewLayer.findOne(`#${TOOL_PREVIEW_RECT_ID}`); + rectPreview?.setAttrs({ + x: Math.min(snappedPos.x, lastMouseDownPos.x), + y: Math.min(snappedPos.y, lastMouseDownPos.y), + width: Math.abs(snappedPos.x - lastMouseDownPos.x), + height: Math.abs(snappedPos.y - lastMouseDownPos.y), + fill: rgbaColorToString(brushColor), + }); + rectPreview?.visible(true); + } else { + rectPreview?.visible(false); + } } }; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 25be668117c..53141e4ff75 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -979,11 +979,16 @@ const migrateControlLayersState = (state: any): any => { // Ephemeral interaction state export const $isDrawing = atom(false); +export const $isMouseDown = atom(false); export const $lastMouseDownPos = atom(null); export const $tool = atom('brush'); +export const $toolBuffer = atom(null); export const $lastCursorPos = atom(null); export const $isPreviewVisible = atom(true); export const $lastAddedPoint = atom(null); +export const $isSpaceDown = atom(false); +export const $stageScale = atom(1); +export const $stagePos = atom({ x: 0, y: 0 }); // Some nanostores that are manually synced to redux state to provide imperative access // TODO(psyche): This is a hack, figure out another way to handle this... diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 4e161069d9a..377d632400d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -23,7 +23,7 @@ import type { IRect } from 'konva/lib/types'; import type { ImageDTO } from 'services/api/types'; import { z } from 'zod'; -const zTool = z.enum(['brush', 'eraser', 'move', 'rect']); +const zTool = z.enum(['brush', 'eraser', 'move', 'rect', 'view']); export type Tool = z.infer; const zDrawingTool = zTool.extract(['brush', 'eraser']); From 8530f8ddcccf190fcb5affddb1c4f18715d93fca Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 8 Jun 2024 10:51:30 +1000 Subject: [PATCH 023/678] feat(ui): wip generation bbox --- .../components/StageComponent.tsx | 153 ++++----- .../features/controlLayers/konva/events.ts | 16 +- .../features/controlLayers/konva/naming.ts | 19 +- .../controlLayers/konva/renderers/caLayer.ts | 31 +- .../controlLayers/konva/renderers/iiLayer.ts | 6 + .../controlLayers/konva/renderers/layers.ts | 24 +- .../controlLayers/konva/renderers/objects.ts | 4 +- .../konva/renderers/previewLayer.ts | 301 ++++++++++++++++++ .../konva/renderers/rasterLayer.ts | 2 + .../controlLayers/konva/renderers/rgLayer.ts | 2 + .../konva/renderers/toolPreview.ts | 212 ------------ .../controlLayers/store/controlLayersSlice.ts | 10 + .../src/features/controlLayers/store/types.ts | 2 + 13 files changed, 446 insertions(+), 336 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts delete mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/renderers/toolPreview.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index ac12412389b..ab64063e156 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -12,11 +12,12 @@ import { } from 'features/controlLayers/konva/constants'; import { setStageEventHandlers } from 'features/controlLayers/konva/events'; import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers/layers'; -import { renderImageDimsPreview } from 'features/controlLayers/konva/renderers/toolPreview'; +import { renderImageDimsPreview } from 'features/controlLayers/konva/renderers/previewLayer'; import { $brushColor, $brushSize, $brushSpacingPx, + $genBbox, $isDrawing, $isMouseDown, $isSpaceDown, @@ -29,6 +30,7 @@ import { $stageScale, $tool, $toolBuffer, + bboxChanged, brushLineAdded, brushSizeChanged, eraserLineAdded, @@ -80,12 +82,7 @@ const selectLayerCount = createSelector( (controlLayers) => controlLayers.present.layers.length ); -const useStageRenderer = ( - stage: Konva.Stage, - container: HTMLDivElement | null, - wrapper: HTMLDivElement | null, - asPreview: boolean -) => { +const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, asPreview: boolean) => { const dispatch = useAppDispatch(); const state = useAppSelector((s) => s.controlLayers.present); const tool = useStore($tool); @@ -103,6 +100,10 @@ const useStageRenderer = ( () => clamp(state.brushSize / BRUSH_SPACING_PCT, MIN_BRUSH_SPACING_PX, MAX_BRUSH_SPACING_PX), [state.brushSize] ); + const bbox = useMemo( + () => ({ x: state.x, y: state.y, width: state.size.width, height: state.size.height }), + [state.x, state.y, state.size.width, state.size.height] + ); useLayoutEffect(() => { $brushColor.set(brushColor); @@ -110,6 +111,7 @@ const useStageRenderer = ( $brushSpacingPx.set(brushSpacingPx); $selectedLayer.set(selectedLayer); $shouldInvertBrushSizeScrollDirection.set(shouldInvertBrushSizeScrollDirection); + $genBbox.set(bbox); }, [ brushSpacingPx, brushColor, @@ -118,6 +120,7 @@ const useStageRenderer = ( state.brushSize, state.selectedLayerId, state.brushColor, + bbox, ]); const onLayerPosChanged = useCallback( @@ -164,6 +167,12 @@ const useStageRenderer = ( }, [dispatch] ); + const onBboxTransformed = useCallback( + (bbox: IRect) => { + dispatch(bboxChanged(bbox)); + }, + [dispatch] + ); useLayoutEffect(() => { log.trace('Initializing stage'); @@ -224,28 +233,23 @@ const useStageRenderer = ( useLayoutEffect(() => { log.trace('Updating stage dimensions'); - if (!wrapper) { + if (!container) { return; } const fitStageToContainer = () => { - const newXScale = wrapper.offsetWidth / state.size.width; - const newYScale = wrapper.offsetHeight / state.size.height; - const newScale = Math.min(newXScale, newYScale, 1); - stage.width(state.size.width * newScale); - stage.height(state.size.height * newScale); - stage.scaleX(newScale); - stage.scaleY(newScale); + stage.width(container.offsetWidth); + stage.height(container.offsetHeight); }; const resizeObserver = new ResizeObserver(fitStageToContainer); - resizeObserver.observe(wrapper); + resizeObserver.observe(container); fitStageToContainer(); return () => { resizeObserver.disconnect(); }; - }, [stage, state.size.width, state.size.height, wrapper]); + }, [stage, container]); useLayoutEffect(() => { if (asPreview) { @@ -253,7 +257,7 @@ const useStageRenderer = ( return; } log.trace('Rendering tool preview'); - renderers.renderToolPreview( + renderers.renderPreviewLayer( stage, tool, brushColor, @@ -263,8 +267,11 @@ const useStageRenderer = ( lastMouseDownPos, state.brushSize, isDrawing, - isMouseDown + isMouseDown, + $genBbox, + onBboxTransformed ); + renderImageDimsPreview(stage, bbox, tool); }, [ asPreview, stage, @@ -278,46 +285,23 @@ const useStageRenderer = ( renderers, isDrawing, isMouseDown, - ]); - - useLayoutEffect(() => { - if (asPreview) { - // Preview should not display tool - return; - } - log.trace('Rendering tool preview'); - renderImageDimsPreview(stage, state.size.width, state.size.height, stageScale); - }, [ - asPreview, - stage, - tool, - brushColor, - selectedLayer, - state.globalMaskLayerOpacity, - lastCursorPos, - lastMouseDownPos, - state.brushSize, - renderers, - isDrawing, - isMouseDown, - state.size.width, - state.size.height, + bbox, stageScale, + onBboxTransformed, ]); useLayoutEffect(() => { log.trace('Rendering layers'); - renderers.renderLayers(stage, state.layers, state.globalMaskLayerOpacity, tool, getImageDTO, onLayerPosChanged); - }, [ - stage, - state.layers, - state.globalMaskLayerOpacity, - tool, - onLayerPosChanged, - renderers, - state.size.width, - state.size.height, - ]); + renderers.renderLayers( + stage, + bbox, + state.layers, + state.globalMaskLayerOpacity, + tool, + getImageDTO, + onLayerPosChanged + ); + }, [stage, state.layers, state.globalMaskLayerOpacity, tool, onLayerPosChanged, renderers, bbox]); useLayoutEffect(() => { if (asPreview) { @@ -349,45 +333,42 @@ export const StageComponent = memo(({ asPreview = false }: Props) => { }) ); const [container, setContainer] = useState(null); - const [wrapper, setWrapper] = useState(null); const containerRef = useCallback((el: HTMLDivElement | null) => { setContainer(el); }, []); - const wrapperRef = useCallback((el: HTMLDivElement | null) => { - setWrapper(el); - }, []); - - useStageRenderer(stage, container, wrapper, asPreview); + useStageRenderer(stage, container, asPreview); return ( - - - - {layerCount === 0 && !asPreview && ( - - {t('controlLayers.noLayersAdded')} - - )} - - - + + + {layerCount === 0 && !asPreview && ( + + {t('controlLayers.noLayersAdded')} + + )} + + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 55aea09bfd8..cf95e8902cd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -16,7 +16,7 @@ import { clamp } from 'lodash-es'; import type { WritableAtom } from 'nanostores'; import type { RgbaColor } from 'react-colorful'; -import { TOOL_PREVIEW_TOOL_GROUP_ID } from './naming'; +import { PREVIEW_TOOL_GROUP_ID } from './naming'; type SetStageEventHandlersArg = { stage: Konva.Stage; @@ -114,7 +114,7 @@ export const setStageEventHandlers = ({ return; } const tool = $tool.get(); - stage.findOne(`#${TOOL_PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); + stage.findOne(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); }); //#region mousedown @@ -219,7 +219,7 @@ export const setStageEventHandlers = ({ const pos = updateLastCursorPos(stage, $lastCursorPos); const selectedLayer = $selectedLayer.get(); - stage.findOne(`#${TOOL_PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); + stage.findOne(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); if (!pos || !selectedLayer) { return; @@ -277,7 +277,7 @@ export const setStageEventHandlers = ({ const selectedLayer = $selectedLayer.get(); const tool = $tool.get(); - stage.findOne(`#${TOOL_PREVIEW_TOOL_GROUP_ID}`)?.visible(false); + stage.findOne(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(false); if (!pos || !selectedLayer) { return; @@ -338,6 +338,12 @@ export const setStageEventHandlers = ({ } }); + stage.on('dragend', () => { + // Stage position should always be an integer, else we get fractional pixels which are blurry + stage.x(Math.floor(stage.x())); + stage.y(Math.floor(stage.y())); + }); + const onKeyDown = (e: KeyboardEvent) => { if (e.repeat) { return; @@ -367,7 +373,7 @@ export const setStageEventHandlers = ({ window.addEventListener('keyup', onKeyUp); return () => { - stage.off('mousedown mouseup mousemove mouseenter mouseleave wheel'); + stage.off('mousedown mouseup mousemove mouseenter mouseleave wheel dragend'); window.removeEventListener('keydown', onKeyDown); window.removeEventListener('keyup', onKeyUp); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts index 2f61c6cd229..f3483937674 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts @@ -3,15 +3,16 @@ */ // IDs for singleton Konva layers and objects -export const TOOL_PREVIEW_LAYER_ID = 'tool_preview_layer'; -export const TOOL_PREVIEW_TOOL_GROUP_ID = 'tool_preview_layer.tool_group'; -export const TOOL_PREVIEW_BRUSH_GROUP_ID = 'tool_preview_layer.brush_group'; -export const TOOL_PREVIEW_BRUSH_FILL_ID = 'tool_preview_layer.brush_fill'; -export const TOOL_PREVIEW_BRUSH_BORDER_INNER_ID = 'tool_preview_layer.brush_border_inner'; -export const TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID = 'tool_preview_layer.brush_border_outer'; -export const TOOL_PREVIEW_RECT_ID = 'tool_preview_layer.rect'; -export const TOOL_PREVIEW_IMAGE_DIMS_RECT = 'tool_preview_layer.image_dims_rect'; - +export const PREVIEW_LAYER_ID = 'preview_layer'; +export const PREVIEW_TOOL_GROUP_ID = 'preview_layer.tool_group'; +export const PREVIEW_BRUSH_GROUP_ID = 'preview_layer.brush_group'; +export const PREVIEW_BRUSH_FILL_ID = 'preview_layer.brush_fill'; +export const PREVIEW_BRUSH_BORDER_INNER_ID = 'preview_layer.brush_border_inner'; +export const PREVIEW_BRUSH_BORDER_OUTER_ID = 'preview_layer.brush_border_outer'; +export const PREVIEW_RECT_ID = 'preview_layer.rect'; +export const PREVIEW_GENERATION_BBOX_GROUP = 'preview_layer.gen_bbox_group'; +export const PREVIEW_GENERATION_BBOX_TRANSFORMER = 'preview_layer.gen_bbox_transformer'; +export const PREVIEW_GENERATION_BBOX_DUMMY_RECT = 'preview_layer.gen_bbox_dummy_rect'; // Names for Konva layers and objects (comparable to CSS classes) export const LAYER_BBOX_NAME = 'layer.bbox'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts index d08d0bd60ef..dfc8a15f7cb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts @@ -2,6 +2,7 @@ import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; import { CA_LAYER_IMAGE_NAME, CA_LAYER_NAME, getCALayerImageId } from 'features/controlLayers/konva/naming'; import type { ControlAdapterLayer } from 'features/controlLayers/store/types'; import Konva from 'konva'; +import type { IRect } from 'konva/lib/types'; import type { ImageDTO } from 'services/api/types'; /** @@ -18,7 +19,7 @@ const createCALayer = (stage: Konva.Stage, layerState: ControlAdapterLayer): Kon const konvaLayer = new Konva.Layer({ id: layerState.id, name: CA_LAYER_NAME, - imageSmoothingEnabled: true, + imageSmoothingEnabled: false, listening: false, }); stage.add(konvaLayer); @@ -51,6 +52,7 @@ const updateCALayerImageSource = async ( stage: Konva.Stage, konvaLayer: Konva.Layer, layerState: ControlAdapterLayer, + bbox: IRect, getImageDTO: (imageName: string) => Promise ): Promise => { const image = layerState.controlAdapter.processedImage ?? layerState.controlAdapter.image; @@ -72,7 +74,7 @@ const updateCALayerImageSource = async ( id: imageId, image: imageEl, }); - updateCALayerImageAttrs(stage, konvaImage, layerState); + updateCALayerImageAttrs(stage, konvaImage, layerState, bbox); // Must cache after this to apply the filters konvaImage.cache(); imageEl.id = imageId; @@ -93,18 +95,19 @@ const updateCALayerImageSource = async ( const updateCALayerImageAttrs = ( stage: Konva.Stage, konvaImage: Konva.Image, - layerState: ControlAdapterLayer + layerState: ControlAdapterLayer, + bbox: IRect ): void => { let needsCache = false; // 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. // TODO(psyche): Investigate and report upstream. - const newWidth = stage.width() / stage.scaleX(); - const newHeight = stage.height() / stage.scaleY(); const hasFilter = konvaImage.filters() !== null && konvaImage.filters().length > 0; if ( - konvaImage.width() !== newWidth || - konvaImage.height() !== newHeight || + konvaImage.x() !== bbox.x || + konvaImage.y() !== bbox.y || + konvaImage.width() !== bbox.width || + konvaImage.height() !== bbox.height || konvaImage.visible() !== layerState.isEnabled || hasFilter !== layerState.isFilterEnabled ) { @@ -112,8 +115,7 @@ const updateCALayerImageAttrs = ( opacity: layerState.opacity, scaleX: 1, scaleY: 1, - width: stage.width() / stage.scaleX(), - height: stage.height() / stage.scaleY(), + ...bbox, visible: layerState.isEnabled, filters: layerState.isFilterEnabled ? [LightnessToAlphaFilter] : [], }); @@ -137,12 +139,19 @@ const updateCALayerImageAttrs = ( export const renderCALayer = ( stage: Konva.Stage, layerState: ControlAdapterLayer, + bbox: IRect, + zIndex: number, getImageDTO: (imageName: string) => Promise ): void => { const konvaLayer = stage.findOne(`#${layerState.id}`) ?? createCALayer(stage, layerState); + + konvaLayer.zIndex(zIndex); + const konvaImage = konvaLayer.findOne(`.${CA_LAYER_IMAGE_NAME}`); const canvasImageSource = konvaImage?.image(); + let imageSourceNeedsUpdate = false; + if (canvasImageSource instanceof HTMLImageElement) { const image = layerState.controlAdapter.processedImage ?? layerState.controlAdapter.image; if (image && canvasImageSource.id !== getCALayerImageId(layerState.id, image.name)) { @@ -155,8 +164,8 @@ export const renderCALayer = ( } if (imageSourceNeedsUpdate) { - updateCALayerImageSource(stage, konvaLayer, layerState, getImageDTO); + updateCALayerImageSource(stage, konvaLayer, layerState, bbox, getImageDTO); } else if (konvaImage) { - updateCALayerImageAttrs(stage, konvaImage, layerState); + updateCALayerImageAttrs(stage, konvaImage, layerState, bbox); } }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/iiLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/iiLayer.ts index cf1b69d6668..a638f69f397 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/iiLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/iiLayer.ts @@ -124,12 +124,18 @@ const updateIILayerImageSource = async ( export const renderIILayer = ( stage: Konva.Stage, layerState: InitialImageLayer, + zIndex: number, getImageDTO: (imageName: string) => Promise ): void => { const konvaLayer = stage.findOne(`#${layerState.id}`) ?? createIILayer(stage, layerState); + + konvaLayer.zIndex(zIndex); + const konvaImage = konvaLayer.findOne(`.${INITIAL_IMAGE_LAYER_IMAGE_NAME}`); const canvasImageSource = konvaImage?.image(); + let imageSourceNeedsUpdate = false; + if (canvasImageSource instanceof HTMLImageElement) { const image = layerState.image; if (image && canvasImageSource.id !== getCALayerImageId(layerState.id, image.name)) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts index d50bfae6b26..a2e7f4735c5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -1,11 +1,11 @@ import { DEBOUNCE_MS } from 'features/controlLayers/konva/constants'; -import { TOOL_PREVIEW_LAYER_ID } from 'features/controlLayers/konva/naming'; +import { PREVIEW_LAYER_ID } from 'features/controlLayers/konva/naming'; import { updateBboxes } from 'features/controlLayers/konva/renderers/bbox'; import { renderCALayer } from 'features/controlLayers/konva/renderers/caLayer'; import { renderIILayer } from 'features/controlLayers/konva/renderers/iiLayer'; +import { renderPreviewLayer } from 'features/controlLayers/konva/renderers/previewLayer'; import { renderRasterLayer } from 'features/controlLayers/konva/renderers/rasterLayer'; import { renderRGLayer } from 'features/controlLayers/konva/renderers/rgLayer'; -import { renderToolPreview } from 'features/controlLayers/konva/renderers/toolPreview'; import { mapId, selectRenderableLayers } from 'features/controlLayers/konva/util'; import type { Layer, Tool } from 'features/controlLayers/store/types'; import { @@ -16,6 +16,7 @@ import { isRenderableLayer, } from 'features/controlLayers/store/types'; import type Konva from 'konva'; +import type { IRect } from 'konva/lib/types'; import { debounce } from 'lodash-es'; import type { ImageDTO } from 'services/api/types'; @@ -34,6 +35,7 @@ import type { ImageDTO } from 'services/api/types'; */ const renderLayers = ( stage: Konva.Stage, + bbox: IRect, layerStates: Layer[], globalMaskLayerOpacity: number, tool: Tool, @@ -48,33 +50,33 @@ const renderLayers = ( } } // We'll need to ensure the tool preview layer is on top of the rest of the layers - let toolLayerZIndex = 0; + let zIndex = 0; for (const layer of layerStates) { if (isRegionalGuidanceLayer(layer)) { - renderRGLayer(stage, layer, globalMaskLayerOpacity, tool, onLayerPosChanged); + renderRGLayer(stage, layer, globalMaskLayerOpacity, tool, zIndex, onLayerPosChanged); } if (isControlAdapterLayer(layer)) { - renderCALayer(stage, layer, getImageDTO); + renderCALayer(stage, layer, bbox, zIndex, getImageDTO); } if (isInitialImageLayer(layer)) { - renderIILayer(stage, layer, getImageDTO); + renderIILayer(stage, layer, zIndex, getImageDTO); } if (isRasterLayer(layer)) { - renderRasterLayer(stage, layer, tool, onLayerPosChanged); + renderRasterLayer(stage, layer, tool, zIndex, onLayerPosChanged); } // IP Adapter layers are not rendered // Increment the z-index for the tool layer - toolLayerZIndex++; + zIndex++; } // Arrange the tool preview layer - stage.findOne(`#${TOOL_PREVIEW_LAYER_ID}`)?.zIndex(toolLayerZIndex); + stage.findOne(`#${PREVIEW_LAYER_ID}`)?.zIndex(zIndex); }; /** * All the renderers for the Konva stage. */ export const renderers = { - renderToolPreview, + renderPreviewLayer, renderLayers, updateBboxes, }; @@ -85,7 +87,7 @@ export const renderers = { * @returns The renderers with debouncing applied */ const getDebouncedRenderers = (ms = DEBOUNCE_MS): typeof renderers => ({ - renderToolPreview: debounce(renderToolPreview, ms), + renderPreviewLayer: debounce(renderPreviewLayer, ms), renderLayers: debounce(renderLayers, ms), updateBboxes: debounce(updateBboxes, ms), }); 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 32628a39d27..d9ea85e9cab 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -3,7 +3,7 @@ import { getLayerBboxId, getObjectGroupId, LAYER_BBOX_NAME, - TOOL_PREVIEW_IMAGE_DIMS_RECT, + PREVIEW_GENERATION_BBOX_DUMMY_RECT, } from 'features/controlLayers/konva/naming'; import type { BrushLine, EraserLine, ImageObject, Layer, RectShape } from 'features/controlLayers/store/types'; import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types'; @@ -206,7 +206,7 @@ export const createObjectGroup = (konvaLayer: Konva.Layer, name: string): Konva. export const createImageDimsPreview = (konvaLayer: Konva.Layer, width: number, height: number): Konva.Rect => { const imageDimsPreview = new Konva.Rect({ - id: TOOL_PREVIEW_IMAGE_DIMS_RECT, + id: PREVIEW_GENERATION_BBOX_DUMMY_RECT, x: 0, y: 0, width, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts new file mode 100644 index 00000000000..2b5a0e6f3f1 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts @@ -0,0 +1,301 @@ +import { roundToMultiple } from 'common/util/roundDownToMultiple'; +import { rgbaColorToString } from 'features/canvas/util/colorToString'; +import { + BBOX_SELECTED_STROKE, + BRUSH_BORDER_INNER_COLOR, + BRUSH_BORDER_OUTER_COLOR, +} from 'features/controlLayers/konva/constants'; +import { + PREVIEW_BRUSH_BORDER_INNER_ID, + PREVIEW_BRUSH_BORDER_OUTER_ID, + PREVIEW_BRUSH_FILL_ID, + PREVIEW_BRUSH_GROUP_ID, + PREVIEW_GENERATION_BBOX_DUMMY_RECT, + PREVIEW_GENERATION_BBOX_GROUP, + PREVIEW_GENERATION_BBOX_TRANSFORMER, + PREVIEW_LAYER_ID, + PREVIEW_RECT_ID, + PREVIEW_TOOL_GROUP_ID, +} from 'features/controlLayers/konva/naming'; +import { selectRenderableLayers, snapPosToStage } from 'features/controlLayers/konva/util'; +import type { Layer, RgbaColor, Tool } from 'features/controlLayers/store/types'; +import Konva from 'konva'; +import type { IRect, Vector2d } from 'konva/lib/types'; +import type { WritableAtom } from 'nanostores'; +import { assert } from 'tsafe'; + +/** + * Creates the singleton preview layer and all its objects. + * @param stage The konva stage + */ +const getPreviewLayer = ( + stage: Konva.Stage, + $genBbox: WritableAtom, + onBboxTransformed: (bbox: IRect) => void +): Konva.Layer => { + let previewLayer = stage.findOne(`#${PREVIEW_LAYER_ID}`); + if (previewLayer) { + return previewLayer; + } + // Initialize the preview layer & add to the stage + previewLayer = new Konva.Layer({ id: PREVIEW_LAYER_ID, listening: true }); + stage.add(previewLayer); + + // Create the brush preview group & circles + const brushPreviewGroup = new Konva.Group({ id: PREVIEW_BRUSH_GROUP_ID }); + const brushPreviewFill = new Konva.Circle({ + id: PREVIEW_BRUSH_FILL_ID, + listening: false, + strokeEnabled: false, + }); + brushPreviewGroup.add(brushPreviewFill); + const brushPreviewBorderInner = new Konva.Circle({ + id: PREVIEW_BRUSH_BORDER_INNER_ID, + listening: false, + stroke: BRUSH_BORDER_INNER_COLOR, + strokeWidth: 1, + strokeEnabled: true, + }); + brushPreviewGroup.add(brushPreviewBorderInner); + const brushPreviewBorderOuter = new Konva.Circle({ + id: PREVIEW_BRUSH_BORDER_OUTER_ID, + listening: false, + stroke: BRUSH_BORDER_OUTER_COLOR, + strokeWidth: 1, + strokeEnabled: true, + }); + brushPreviewGroup.add(brushPreviewBorderOuter); + + // 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: PREVIEW_RECT_ID, + listening: false, + stroke: BBOX_SELECTED_STROKE, + strokeWidth: 1, + }); + + const toolGroup = new Konva.Group({ id: PREVIEW_TOOL_GROUP_ID }); + + toolGroup.add(rectPreview); + toolGroup.add(brushPreviewGroup); + + // Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully + // transparent rect for this purpose. + const generationBboxGroup = new Konva.Group({ id: PREVIEW_GENERATION_BBOX_GROUP }); + const generationBboxDummyRect = new Konva.Rect({ + id: PREVIEW_GENERATION_BBOX_DUMMY_RECT, + listening: false, + strokeEnabled: false, + draggable: true, + }); + generationBboxDummyRect.on('dragmove', (e) => { + const bbox: IRect = { + x: roundToMultiple(Math.round(generationBboxDummyRect.x()), 64), + y: roundToMultiple(Math.round(generationBboxDummyRect.y()), 64), + width: Math.round(generationBboxDummyRect.width() * generationBboxDummyRect.scaleX()), + height: Math.round(generationBboxDummyRect.height() * generationBboxDummyRect.scaleY()), + }; + generationBboxDummyRect.setAttrs(bbox); + const genBbox = $genBbox.get(); + if ( + genBbox.x !== bbox.x || + genBbox.y !== bbox.y || + genBbox.width !== bbox.width || + genBbox.height !== bbox.height + ) { + onBboxTransformed(bbox); + } + }); + const generationBboxTransformer = new Konva.Transformer({ + id: PREVIEW_GENERATION_BBOX_TRANSFORMER, + borderDash: [5, 5], + borderStroke: 'rgba(212,216,234,1)', + borderEnabled: true, + rotateEnabled: false, + keepRatio: false, + ignoreStroke: true, + listening: false, + flipEnabled: false, + anchorFill: 'rgba(212,216,234,1)', + anchorStroke: 'rgb(42,42,42)', + anchorSize: 12, + anchorCornerRadius: 3, + anchorStyleFunc: (anchor) => { + // Make the x/y resize anchors little bars + if (anchor.hasName('top-center') || anchor.hasName('bottom-center')) { + anchor.height(8); + anchor.offsetY(4); + anchor.width(30); + anchor.offsetX(15); + } + if (anchor.hasName('middle-left') || anchor.hasName('middle-right')) { + anchor.height(30); + anchor.offsetY(15); + anchor.width(8); + anchor.offsetX(4); + } + }, + }); + generationBboxTransformer.on('transform', (e) => { + const bbox: IRect = { + x: Math.round(generationBboxDummyRect.x()), + y: Math.round(generationBboxDummyRect.y()), + width: Math.round(generationBboxDummyRect.width() * generationBboxDummyRect.scaleX()), + height: Math.round(generationBboxDummyRect.height() * generationBboxDummyRect.scaleY()), + }; + generationBboxDummyRect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1 }); + onBboxTransformed(bbox); + }); + // The transformer will always be transforming the dummy rect + generationBboxTransformer.nodes([generationBboxDummyRect]); + generationBboxGroup.add(generationBboxDummyRect); + generationBboxGroup.add(generationBboxTransformer); + previewLayer.add(toolGroup); + previewLayer.add(generationBboxGroup); + + return previewLayer; +}; + +const ALL_ANCHORS: string[] = [ + 'top-left', + 'top-center', + 'top-right', + 'middle-right', + 'middle-left', + 'bottom-left', + 'bottom-center', + 'bottom-right', +]; +const NO_ANCHORS: string[] = []; + +export const renderImageDimsPreview = (stage: Konva.Stage, bbox: IRect, tool: Tool): void => { + const previewLayer = stage.findOne(`#${PREVIEW_LAYER_ID}`); + const generationBboxGroup = stage.findOne(`#${PREVIEW_GENERATION_BBOX_GROUP}`); + const generationBboxDummyRect = stage.findOne(`#${PREVIEW_GENERATION_BBOX_DUMMY_RECT}`); + const generationBboxTransformer = stage.findOne(`#${PREVIEW_GENERATION_BBOX_TRANSFORMER}`); + assert( + previewLayer && generationBboxGroup && generationBboxDummyRect && generationBboxTransformer, + 'Generation bbox konva objects not found' + ); + generationBboxDummyRect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1, listening: tool === 'move' }); + generationBboxTransformer.setAttrs({ + listening: tool === 'move', + enabledAnchors: tool === 'move' ? ALL_ANCHORS : NO_ANCHORS, + }); +}; + +/** + * Renders the preview layer. + * @param stage The konva stage + * @param tool The selected tool + * @param color The selected layer's color + * @param selectedLayerType The selected layer's type + * @param globalMaskLayerOpacity The global mask layer opacity + * @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 + */ +export const renderPreviewLayer = ( + stage: Konva.Stage, + tool: Tool, + brushColor: RgbaColor, + selectedLayerType: Layer['type'] | null, + globalMaskLayerOpacity: number, + cursorPos: Vector2d | null, + lastMouseDownPos: Vector2d | null, + brushSize: number, + isDrawing: boolean, + isMouseDown: boolean, + $genBbox: WritableAtom, + onBboxTransformed: (bbox: IRect) => void +): void => { + const layerCount = stage.find(selectRenderableLayers).length; + // Update the stage's pointer style + if (tool === 'view') { + // View gets a hand + stage.container().style.cursor = isMouseDown ? 'grabbing' : 'grab'; + } else if (layerCount === 0) { + // We have no layers, so we should not render any tool + stage.container().style.cursor = 'default'; + } else if (selectedLayerType !== 'regional_guidance_layer' && selectedLayerType !== 'raster_layer') { + // Non-mask-guidance layers don't have tools + stage.container().style.cursor = 'not-allowed'; + } else if (tool === 'move') { + // Move tool gets a pointer + stage.container().style.cursor = 'default'; + } else if (tool === 'rect') { + // Rect gets a crosshair + stage.container().style.cursor = 'crosshair'; + } else { + // Else we hide the native cursor and use the konva-rendered brush preview + stage.container().style.cursor = 'none'; + } + + stage.draggable(tool === 'view'); + + const previewLayer = getPreviewLayer(stage, $genBbox, onBboxTransformed); + const toolGroup = previewLayer.findOne(`#${PREVIEW_TOOL_GROUP_ID}`); + + assert(toolGroup, 'Tool group not found'); + + if ( + !cursorPos || + layerCount === 0 || + (selectedLayerType !== 'regional_guidance_layer' && selectedLayerType !== 'raster_layer') + ) { + // We can bail early if the mouse isn't over the stage or there are no layers + toolGroup.visible(false); + } else { + toolGroup.visible(true); + + const brushPreviewGroup = stage.findOne(`#${PREVIEW_BRUSH_GROUP_ID}`); + assert(brushPreviewGroup, 'Brush preview group not found'); + + const rectPreview = stage.findOne(`#${PREVIEW_RECT_ID}`); + assert(rectPreview, 'Rect preview not found'); + + // No need to render the brush preview if the cursor position or color is missing + if (cursorPos && (tool === 'brush' || tool === 'eraser')) { + // Update the fill circle + const brushPreviewFill = brushPreviewGroup.findOne(`#${PREVIEW_BRUSH_FILL_ID}`); + brushPreviewFill?.setAttrs({ + x: cursorPos.x, + y: cursorPos.y, + radius: brushSize / 2, + fill: isDrawing ? '' : rgbaColorToString(brushColor), + globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out', + }); + + // Update the inner border of the brush preview + const brushPreviewInner = previewLayer.findOne(`#${PREVIEW_BRUSH_BORDER_INNER_ID}`); + brushPreviewInner?.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: brushSize / 2 }); + + // Update the outer border of the brush preview + const brushPreviewOuter = previewLayer.findOne(`#${PREVIEW_BRUSH_BORDER_OUTER_ID}`); + brushPreviewOuter?.setAttrs({ + x: cursorPos.x, + y: cursorPos.y, + radius: brushSize / 2 + 1, + }); + + brushPreviewGroup.visible(true); + } else { + brushPreviewGroup.visible(false); + } + + if (cursorPos && lastMouseDownPos && tool === 'rect') { + const snappedPos = snapPosToStage(cursorPos, stage); + const rectPreview = previewLayer.findOne(`#${PREVIEW_RECT_ID}`); + rectPreview?.setAttrs({ + x: Math.min(snappedPos.x, lastMouseDownPos.x), + y: Math.min(snappedPos.y, lastMouseDownPos.y), + width: Math.abs(snappedPos.x - lastMouseDownPos.x), + height: Math.abs(snappedPos.y - lastMouseDownPos.y), + fill: rgbaColorToString(brushColor), + }); + rectPreview?.visible(true); + } else { + rectPreview?.visible(false); + } + } +}; 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 81f3d570a77..061c2ed0e6d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts @@ -67,6 +67,7 @@ export const renderRasterLayer = async ( stage: Konva.Stage, layerState: RasterLayer, tool: Tool, + zIndex: number, onLayerPosChanged?: (layerId: string, x: number, y: number) => void ) => { const konvaLayer = @@ -77,6 +78,7 @@ export const renderRasterLayer = async ( listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events x: Math.floor(layerState.x), y: Math.floor(layerState.y), + zIndex, }); const konvaObjectGroup = 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 d6ece5c3933..e6bc7e1212a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts @@ -83,6 +83,7 @@ export const renderRGLayer = ( layerState: RegionalGuidanceLayer, globalMaskLayerOpacity: number, tool: Tool, + zIndex: number, onLayerPosChanged?: (layerId: string, x: number, y: number) => void ): void => { const konvaLayer = @@ -93,6 +94,7 @@ export const renderRGLayer = ( listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events x: Math.floor(layerState.x), y: Math.floor(layerState.y), + zIndex, }); // Convert the color to a string, stripping the alpha - the object group will handle opacity. diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/toolPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/toolPreview.ts deleted file mode 100644 index 95d41bb2f6a..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/toolPreview.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { rgbaColorToString } from 'features/canvas/util/colorToString'; -import { - BBOX_SELECTED_STROKE, - BRUSH_BORDER_INNER_COLOR, - BRUSH_BORDER_OUTER_COLOR, -} from 'features/controlLayers/konva/constants'; -import { - TOOL_PREVIEW_BRUSH_BORDER_INNER_ID, - TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID, - TOOL_PREVIEW_BRUSH_FILL_ID, - TOOL_PREVIEW_BRUSH_GROUP_ID, - TOOL_PREVIEW_IMAGE_DIMS_RECT, - TOOL_PREVIEW_LAYER_ID, - TOOL_PREVIEW_RECT_ID, - TOOL_PREVIEW_TOOL_GROUP_ID, -} from 'features/controlLayers/konva/naming'; -import { selectRenderableLayers, snapPosToStage } from 'features/controlLayers/konva/util'; -import type { Layer, RgbaColor, Tool } from 'features/controlLayers/store/types'; -import Konva from 'konva'; -import type { Vector2d } from 'konva/lib/types'; -import { assert } from 'tsafe'; - -/** - * Logic to create and render the singleton tool preview layer. - */ - -/** - * Creates the singleton tool preview layer and all its objects. - * @param stage The konva stage - */ -const getToolPreviewLayer = (stage: Konva.Stage): Konva.Layer => { - let toolPreviewLayer = stage.findOne(`#${TOOL_PREVIEW_LAYER_ID}`); - if (toolPreviewLayer) { - return toolPreviewLayer; - } - // Initialize the brush preview layer & add to the stage - toolPreviewLayer = new Konva.Layer({ id: TOOL_PREVIEW_LAYER_ID, listening: false }); - stage.add(toolPreviewLayer); - - // Create the brush preview group & circles - const brushPreviewGroup = new Konva.Group({ id: TOOL_PREVIEW_BRUSH_GROUP_ID }); - const brushPreviewFill = new Konva.Circle({ - id: TOOL_PREVIEW_BRUSH_FILL_ID, - listening: false, - strokeEnabled: false, - }); - brushPreviewGroup.add(brushPreviewFill); - const brushPreviewBorderInner = new Konva.Circle({ - id: TOOL_PREVIEW_BRUSH_BORDER_INNER_ID, - listening: false, - stroke: BRUSH_BORDER_INNER_COLOR, - strokeWidth: 1, - strokeEnabled: true, - }); - brushPreviewGroup.add(brushPreviewBorderInner); - const brushPreviewBorderOuter = new Konva.Circle({ - id: TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID, - listening: false, - stroke: BRUSH_BORDER_OUTER_COLOR, - strokeWidth: 1, - strokeEnabled: true, - }); - brushPreviewGroup.add(brushPreviewBorderOuter); - - // 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: BBOX_SELECTED_STROKE, - strokeWidth: 1, - }); - - const toolGroup = new Konva.Group({ id: TOOL_PREVIEW_TOOL_GROUP_ID }); - - toolGroup.add(rectPreview); - toolGroup.add(brushPreviewGroup); - - const imageDimsPreview = new Konva.Rect({ - id: TOOL_PREVIEW_IMAGE_DIMS_RECT, - x: 0, - y: 0, - width: 0, - height: 0, - stroke: 'rgb(255,0,255)', - strokeWidth: 1 / toolPreviewLayer.getStage().scaleX(), - listening: false, - }); - - toolPreviewLayer.add(toolGroup); - toolPreviewLayer.add(imageDimsPreview); - - return toolPreviewLayer; -}; - -export const renderImageDimsPreview = (stage: Konva.Stage, width: number, height: number, stageScale: number): void => { - const imageDimsPreview = stage.findOne(`#${TOOL_PREVIEW_IMAGE_DIMS_RECT}`); - imageDimsPreview?.setAttrs({ - width, - height, - strokeWidth: 1 / stageScale, - }); -}; - -/** - * Renders the brush preview for the selected tool. - * @param stage The konva stage - * @param tool The selected tool - * @param color The selected layer's color - * @param selectedLayerType The selected layer's type - * @param globalMaskLayerOpacity The global mask layer opacity - * @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 - */ -export const renderToolPreview = ( - stage: Konva.Stage, - tool: Tool, - brushColor: RgbaColor, - selectedLayerType: Layer['type'] | null, - globalMaskLayerOpacity: number, - cursorPos: Vector2d | null, - lastMouseDownPos: Vector2d | null, - brushSize: number, - isDrawing: boolean, - isMouseDown: boolean -): void => { - const layerCount = stage.find(selectRenderableLayers).length; - // Update the stage's pointer style - if (tool === 'view') { - // View gets a hand - stage.container().style.cursor = isMouseDown ? 'grabbing' : 'grab'; - } else if (layerCount === 0) { - // We have no layers, so we should not render any tool - stage.container().style.cursor = 'default'; - } else if (selectedLayerType !== 'regional_guidance_layer' && selectedLayerType !== 'raster_layer') { - // Non-mask-guidance layers don't have tools - stage.container().style.cursor = 'not-allowed'; - } else if (tool === 'move') { - // Move tool gets a pointer - stage.container().style.cursor = 'default'; - } else if (tool === 'rect') { - // Rect gets a crosshair - stage.container().style.cursor = 'crosshair'; - } else { - // Else we hide the native cursor and use the konva-rendered brush preview - stage.container().style.cursor = 'none'; - } - - stage.draggable(tool === 'view'); - - const toolPreviewLayer = getToolPreviewLayer(stage); - const toolGroup = toolPreviewLayer.findOne(`#${TOOL_PREVIEW_TOOL_GROUP_ID}`); - - assert(toolGroup, 'Tool group not found'); - - if (!cursorPos || layerCount === 0) { - // We can bail early if the mouse isn't over the stage or there are no layers - toolGroup.visible(false); - } else { - toolGroup.visible(true); - - const brushPreviewGroup = stage.findOne(`#${TOOL_PREVIEW_BRUSH_GROUP_ID}`); - assert(brushPreviewGroup, 'Brush preview group not found'); - - const rectPreview = stage.findOne(`#${TOOL_PREVIEW_RECT_ID}`); - assert(rectPreview, 'Rect preview not found'); - - // No need to render the brush preview if the cursor position or color is missing - if (cursorPos && (tool === 'brush' || tool === 'eraser')) { - // Update the fill circle - const brushPreviewFill = brushPreviewGroup.findOne(`#${TOOL_PREVIEW_BRUSH_FILL_ID}`); - brushPreviewFill?.setAttrs({ - x: cursorPos.x, - y: cursorPos.y, - radius: brushSize / 2, - fill: isDrawing ? '' : rgbaColorToString(brushColor), - globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out', - }); - - // Update the inner border of the brush preview - const brushPreviewInner = toolPreviewLayer.findOne(`#${TOOL_PREVIEW_BRUSH_BORDER_INNER_ID}`); - brushPreviewInner?.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: brushSize / 2 }); - - // Update the outer border of the brush preview - const brushPreviewOuter = toolPreviewLayer.findOne(`#${TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID}`); - brushPreviewOuter?.setAttrs({ - x: cursorPos.x, - y: cursorPos.y, - radius: brushSize / 2 + 1, - }); - - brushPreviewGroup.visible(true); - } else { - brushPreviewGroup.visible(false); - } - - if (cursorPos && lastMouseDownPos && tool === 'rect') { - const snappedPos = snapPosToStage(cursorPos, stage); - const rectPreview = toolPreviewLayer.findOne(`#${TOOL_PREVIEW_RECT_ID}`); - rectPreview?.setAttrs({ - x: Math.min(snappedPos.x, lastMouseDownPos.x), - y: Math.min(snappedPos.y, lastMouseDownPos.y), - width: Math.abs(snappedPos.x - lastMouseDownPos.x), - height: Math.abs(snappedPos.y - lastMouseDownPos.y), - fill: rgbaColorToString(brushColor), - }); - rectPreview?.visible(true); - } else { - rectPreview?.visible(false); - } - } -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 53141e4ff75..7d590ba4cd5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -92,6 +92,8 @@ export const initialControlLayersState: ControlLayersState = { height: 512, aspectRatio: deepClone(initialAspectRatioState), }, + x: 0, + y: 0, }; /** @@ -797,6 +799,12 @@ export const controlLayersSlice = createSlice({ aspectRatioChanged: (state, action: PayloadAction) => { state.size.aspectRatio = action.payload; }, + bboxChanged: (state, action: PayloadAction) => { + state.x = action.payload.x; + state.y = action.payload.y; + state.size.width = action.payload.width; + state.size.height = action.payload.height; + }, brushSizeChanged: (state, action: PayloadAction) => { state.brushSize = Math.round(action.payload); }, @@ -950,6 +958,7 @@ export const { widthChanged, heightChanged, aspectRatioChanged, + bboxChanged, brushSizeChanged, brushColorChanged, globalMaskLayerOpacityChanged, @@ -989,6 +998,7 @@ export const $lastAddedPoint = atom(null); export const $isSpaceDown = atom(false); export const $stageScale = atom(1); export const $stagePos = atom({ x: 0, y: 0 }); +export const $genBbox = atom({ x: 0, y: 0, width: 0, height: 0 }); // Some nanostores that are manually synced to redux state to provide imperative access // TODO(psyche): This is a hack, figure out another way to handle this... diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 377d632400d..d229eb1e45e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -269,6 +269,8 @@ export type ControlLayersState = { height: ParameterHeight; aspectRatio: AspectRatioState; }; + x: number; + y: number; }; export type AddEraserLineArg = { layerId: string; points: [number, number, number, number] }; From 1172a331178fbe580c0210bb9ae7ebc4dfd2d963 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 12 Jun 2024 09:13:25 +1000 Subject: [PATCH 024/678] feat(ui): wip generation bbox --- .../src/common/util/roundDownToMultiple.ts | 4 + .../components/HeadsUpDisplay.tsx | 40 +++ .../components/StageComponent.tsx | 64 +++-- .../controlLayers/konva/renderers/caLayer.ts | 19 +- .../controlLayers/konva/renderers/layers.ts | 12 +- .../konva/renderers/previewLayer.ts | 248 +++++++++++------- .../controlLayers/store/controlLayersSlice.ts | 17 +- .../src/features/controlLayers/store/types.ts | 3 +- 8 files changed, 244 insertions(+), 163 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx diff --git a/invokeai/frontend/web/src/common/util/roundDownToMultiple.ts b/invokeai/frontend/web/src/common/util/roundDownToMultiple.ts index 792b6d38e49..01c164d4393 100644 --- a/invokeai/frontend/web/src/common/util/roundDownToMultiple.ts +++ b/invokeai/frontend/web/src/common/util/roundDownToMultiple.ts @@ -8,3 +8,7 @@ export const roundDownToMultipleMin = (num: number, multiple: number): number => export const roundToMultiple = (num: number, multiple: number): number => { return Math.round(num / multiple) * multiple; }; + +export const roundToMultipleMin = (num: number, multiple: number): number => { + return Math.max(Math.round(num / multiple) * multiple, multiple); +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx new file mode 100644 index 00000000000..f36898e10ca --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx @@ -0,0 +1,40 @@ +import { Box, Flex, Text } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { useAppSelector } from 'app/store/storeHooks'; +import { $stageScale } from 'features/controlLayers/store/controlLayersSlice'; +import { round } from 'lodash-es'; +import { memo } from 'react'; + +export const HeadsUpDisplay = memo(() => { + const stageScale = useStore($stageScale); + const layerCount = useAppSelector((s) => s.controlLayers.present.layers.length); + const bbox = useAppSelector((s) => s.controlLayers.present.bbox); + + return ( + + + + + + + + + + + ); +}); + +HeadsUpDisplay.displayName = 'HeadsUpDisplay'; + +const HUDItem = memo(({ label, value }: { label: string; value: string | number }) => { + return ( + + {label}: + + {value} + + + ); +}); + +HUDItem.displayName = 'HUDItem'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index ab64063e156..0e089104d2f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -1,9 +1,10 @@ -import { Box, Flex, Heading } from '@invoke-ai/ui-library'; +import { $alt, $meta, $shift, Box, Flex, Heading } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay'; import { BRUSH_SPACING_PCT, MAX_BRUSH_SPACING_PX, @@ -12,12 +13,11 @@ import { } from 'features/controlLayers/konva/constants'; import { setStageEventHandlers } from 'features/controlLayers/konva/events'; import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers/layers'; -import { renderImageDimsPreview } from 'features/controlLayers/konva/renderers/previewLayer'; import { + $bbox, $brushColor, $brushSize, $brushSpacingPx, - $genBbox, $isDrawing, $isMouseDown, $isSpaceDown, @@ -100,10 +100,6 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, () => clamp(state.brushSize / BRUSH_SPACING_PCT, MIN_BRUSH_SPACING_PX, MAX_BRUSH_SPACING_PX), [state.brushSize] ); - const bbox = useMemo( - () => ({ x: state.x, y: state.y, width: state.size.width, height: state.size.height }), - [state.x, state.y, state.size.width, state.size.height] - ); useLayoutEffect(() => { $brushColor.set(brushColor); @@ -111,7 +107,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, $brushSpacingPx.set(brushSpacingPx); $selectedLayer.set(selectedLayer); $shouldInvertBrushSizeScrollDirection.set(shouldInvertBrushSizeScrollDirection); - $genBbox.set(bbox); + $bbox.set(state.bbox); }, [ brushSpacingPx, brushColor, @@ -120,7 +116,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, state.brushSize, state.selectedLayerId, state.brushColor, - bbox, + state.bbox, ]); const onLayerPosChanged = useCallback( @@ -257,7 +253,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, return; } log.trace('Rendering tool preview'); - renderers.renderPreviewLayer( + renderers.renderToolPreview( stage, tool, brushColor, @@ -267,41 +263,36 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, lastMouseDownPos, state.brushSize, isDrawing, - isMouseDown, - $genBbox, - onBboxTransformed + isMouseDown ); - renderImageDimsPreview(stage, bbox, tool); }, [ asPreview, - stage, - tool, brushColor, - selectedLayer, - state.globalMaskLayerOpacity, + isDrawing, + isMouseDown, lastCursorPos, lastMouseDownPos, - state.brushSize, renderers, - isDrawing, - isMouseDown, - bbox, - stageScale, - onBboxTransformed, + selectedLayer?.type, + stage, + state.brushSize, + state.globalMaskLayerOpacity, + tool, ]); + useLayoutEffect(() => { + if (asPreview) { + // Preview should not display tool + return; + } + log.trace('Rendering bbox preview'); + renderers.renderBboxPreview(stage, state.bbox, tool, $bbox.get, onBboxTransformed, $shift.get, $meta.get, $alt.get); + }, [asPreview, onBboxTransformed, renderers, stage, state.bbox, tool]); + useLayoutEffect(() => { log.trace('Rendering layers'); - renderers.renderLayers( - stage, - bbox, - state.layers, - state.globalMaskLayerOpacity, - tool, - getImageDTO, - onLayerPosChanged - ); - }, [stage, state.layers, state.globalMaskLayerOpacity, tool, onLayerPosChanged, renderers, bbox]); + renderers.renderLayers(stage, state.layers, state.globalMaskLayerOpacity, tool, getImageDTO, onLayerPosChanged); + }, [stage, state.layers, state.globalMaskLayerOpacity, tool, onLayerPosChanged, renderers]); useLayoutEffect(() => { if (asPreview) { @@ -369,6 +360,11 @@ export const StageComponent = memo(({ asPreview = false }: Props) => { overflow="hidden" data-testid="control-layers-canvas" /> + {!asPreview && ( + + + + )} ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts index dfc8a15f7cb..feb91867ea1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts @@ -2,7 +2,6 @@ import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; import { CA_LAYER_IMAGE_NAME, CA_LAYER_NAME, getCALayerImageId } from 'features/controlLayers/konva/naming'; import type { ControlAdapterLayer } from 'features/controlLayers/store/types'; import Konva from 'konva'; -import type { IRect } from 'konva/lib/types'; import type { ImageDTO } from 'services/api/types'; /** @@ -52,7 +51,6 @@ const updateCALayerImageSource = async ( stage: Konva.Stage, konvaLayer: Konva.Layer, layerState: ControlAdapterLayer, - bbox: IRect, getImageDTO: (imageName: string) => Promise ): Promise => { const image = layerState.controlAdapter.processedImage ?? layerState.controlAdapter.image; @@ -74,7 +72,7 @@ const updateCALayerImageSource = async ( id: imageId, image: imageEl, }); - updateCALayerImageAttrs(stage, konvaImage, layerState, bbox); + updateCALayerImageAttrs(stage, konvaImage, layerState); // Must cache after this to apply the filters konvaImage.cache(); imageEl.id = imageId; @@ -95,8 +93,7 @@ const updateCALayerImageSource = async ( const updateCALayerImageAttrs = ( stage: Konva.Stage, konvaImage: Konva.Image, - layerState: ControlAdapterLayer, - bbox: IRect + layerState: ControlAdapterLayer ): void => { let needsCache = false; // Konva erroneously reports NaN for width and height when the stage is hidden. This causes errors when caching, @@ -104,10 +101,8 @@ const updateCALayerImageAttrs = ( // TODO(psyche): Investigate and report upstream. const hasFilter = konvaImage.filters() !== null && konvaImage.filters().length > 0; if ( - konvaImage.x() !== bbox.x || - konvaImage.y() !== bbox.y || - konvaImage.width() !== bbox.width || - konvaImage.height() !== bbox.height || + konvaImage.x() !== layerState.x || + konvaImage.y() !== layerState.y || konvaImage.visible() !== layerState.isEnabled || hasFilter !== layerState.isFilterEnabled ) { @@ -115,7 +110,6 @@ const updateCALayerImageAttrs = ( opacity: layerState.opacity, scaleX: 1, scaleY: 1, - ...bbox, visible: layerState.isEnabled, filters: layerState.isFilterEnabled ? [LightnessToAlphaFilter] : [], }); @@ -139,7 +133,6 @@ const updateCALayerImageAttrs = ( export const renderCALayer = ( stage: Konva.Stage, layerState: ControlAdapterLayer, - bbox: IRect, zIndex: number, getImageDTO: (imageName: string) => Promise ): void => { @@ -164,8 +157,8 @@ export const renderCALayer = ( } if (imageSourceNeedsUpdate) { - updateCALayerImageSource(stage, konvaLayer, layerState, bbox, getImageDTO); + updateCALayerImageSource(stage, konvaLayer, layerState, getImageDTO); } else if (konvaImage) { - updateCALayerImageAttrs(stage, konvaImage, layerState, bbox); + updateCALayerImageAttrs(stage, konvaImage, layerState); } }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts index a2e7f4735c5..18e893fb243 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -3,7 +3,7 @@ import { PREVIEW_LAYER_ID } from 'features/controlLayers/konva/naming'; import { updateBboxes } from 'features/controlLayers/konva/renderers/bbox'; import { renderCALayer } from 'features/controlLayers/konva/renderers/caLayer'; import { renderIILayer } from 'features/controlLayers/konva/renderers/iiLayer'; -import { renderPreviewLayer } from 'features/controlLayers/konva/renderers/previewLayer'; +import { renderBboxPreview, renderToolPreview } from 'features/controlLayers/konva/renderers/previewLayer'; import { renderRasterLayer } from 'features/controlLayers/konva/renderers/rasterLayer'; import { renderRGLayer } from 'features/controlLayers/konva/renderers/rgLayer'; import { mapId, selectRenderableLayers } from 'features/controlLayers/konva/util'; @@ -16,7 +16,6 @@ import { isRenderableLayer, } from 'features/controlLayers/store/types'; import type Konva from 'konva'; -import type { IRect } from 'konva/lib/types'; import { debounce } from 'lodash-es'; import type { ImageDTO } from 'services/api/types'; @@ -35,7 +34,6 @@ import type { ImageDTO } from 'services/api/types'; */ const renderLayers = ( stage: Konva.Stage, - bbox: IRect, layerStates: Layer[], globalMaskLayerOpacity: number, tool: Tool, @@ -56,7 +54,7 @@ const renderLayers = ( renderRGLayer(stage, layer, globalMaskLayerOpacity, tool, zIndex, onLayerPosChanged); } if (isControlAdapterLayer(layer)) { - renderCALayer(stage, layer, bbox, zIndex, getImageDTO); + renderCALayer(stage, layer, zIndex, getImageDTO); } if (isInitialImageLayer(layer)) { renderIILayer(stage, layer, zIndex, getImageDTO); @@ -76,7 +74,8 @@ const renderLayers = ( * All the renderers for the Konva stage. */ export const renderers = { - renderPreviewLayer, + renderToolPreview, + renderBboxPreview, renderLayers, updateBboxes, }; @@ -87,7 +86,8 @@ export const renderers = { * @returns The renderers with debouncing applied */ const getDebouncedRenderers = (ms = DEBOUNCE_MS): typeof renderers => ({ - renderPreviewLayer: debounce(renderPreviewLayer, ms), + renderToolPreview: debounce(renderToolPreview, ms), + renderBboxPreview: debounce(renderBboxPreview, ms), renderLayers: debounce(renderLayers, ms), updateBboxes: debounce(updateBboxes, ms), }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts index 2b5a0e6f3f1..36c6d6c8a14 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts @@ -1,4 +1,4 @@ -import { roundToMultiple } from 'common/util/roundDownToMultiple'; +import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMultiple'; import { rgbaColorToString } from 'features/canvas/util/colorToString'; import { BBOX_SELECTED_STROKE, @@ -21,18 +21,13 @@ import { selectRenderableLayers, snapPosToStage } from 'features/controlLayers/k import type { Layer, RgbaColor, Tool } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect, Vector2d } from 'konva/lib/types'; -import type { WritableAtom } from 'nanostores'; import { assert } from 'tsafe'; /** * Creates the singleton preview layer and all its objects. * @param stage The konva stage */ -const getPreviewLayer = ( - stage: Konva.Stage, - $genBbox: WritableAtom, - onBboxTransformed: (bbox: IRect) => void -): Konva.Layer => { +const getPreviewLayer = (stage: Konva.Stage): Konva.Layer => { let previewLayer = stage.findOne(`#${PREVIEW_LAYER_ID}`); if (previewLayer) { return previewLayer; @@ -40,73 +35,50 @@ const getPreviewLayer = ( // Initialize the preview layer & add to the stage previewLayer = new Konva.Layer({ id: PREVIEW_LAYER_ID, listening: true }); stage.add(previewLayer); + return previewLayer; +}; - // Create the brush preview group & circles - const brushPreviewGroup = new Konva.Group({ id: PREVIEW_BRUSH_GROUP_ID }); - const brushPreviewFill = new Konva.Circle({ - id: PREVIEW_BRUSH_FILL_ID, - listening: false, - strokeEnabled: false, - }); - brushPreviewGroup.add(brushPreviewFill); - const brushPreviewBorderInner = new Konva.Circle({ - id: PREVIEW_BRUSH_BORDER_INNER_ID, - listening: false, - stroke: BRUSH_BORDER_INNER_COLOR, - strokeWidth: 1, - strokeEnabled: true, - }); - brushPreviewGroup.add(brushPreviewBorderInner); - const brushPreviewBorderOuter = new Konva.Circle({ - id: PREVIEW_BRUSH_BORDER_OUTER_ID, - listening: false, - stroke: BRUSH_BORDER_OUTER_COLOR, - strokeWidth: 1, - strokeEnabled: true, - }); - brushPreviewGroup.add(brushPreviewBorderOuter); - - // 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: PREVIEW_RECT_ID, - listening: false, - stroke: BBOX_SELECTED_STROKE, - strokeWidth: 1, - }); - - const toolGroup = new Konva.Group({ id: PREVIEW_TOOL_GROUP_ID }); +export const getBboxPreviewGroup = ( + stage: Konva.Stage, + getBbox: () => IRect, + onBboxTransformed: (bbox: IRect) => void, + getShiftKey: () => boolean, + getMetaKey: () => boolean, + getAltKey: () => boolean +): Konva.Group => { + const previewLayer = getPreviewLayer(stage); + let bboxPreviewGroup = previewLayer.findOne(`#${PREVIEW_GENERATION_BBOX_GROUP}`); - toolGroup.add(rectPreview); - toolGroup.add(brushPreviewGroup); + if (bboxPreviewGroup) { + return bboxPreviewGroup; + } + console.log('creating new bbox'); // Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully // transparent rect for this purpose. - const generationBboxGroup = new Konva.Group({ id: PREVIEW_GENERATION_BBOX_GROUP }); - const generationBboxDummyRect = new Konva.Rect({ + bboxPreviewGroup = new Konva.Group({ id: PREVIEW_GENERATION_BBOX_GROUP }); + const bboxRect = new Konva.Rect({ id: PREVIEW_GENERATION_BBOX_DUMMY_RECT, - listening: false, + listening: true, strokeEnabled: false, draggable: true, + fill: 'rgba(255,0,0,0.3)', + ...getBbox(), }); - generationBboxDummyRect.on('dragmove', (e) => { - const bbox: IRect = { - x: roundToMultiple(Math.round(generationBboxDummyRect.x()), 64), - y: roundToMultiple(Math.round(generationBboxDummyRect.y()), 64), - width: Math.round(generationBboxDummyRect.width() * generationBboxDummyRect.scaleX()), - height: Math.round(generationBboxDummyRect.height() * generationBboxDummyRect.scaleY()), + bboxRect.on('dragmove', () => { + const gridSize = getMetaKey() ? 8 : 64; + const oldBbox = getBbox(); + const newBbox: IRect = { + ...oldBbox, + x: roundToMultiple(bboxRect.x(), gridSize), + y: roundToMultiple(bboxRect.y(), gridSize), }; - generationBboxDummyRect.setAttrs(bbox); - const genBbox = $genBbox.get(); - if ( - genBbox.x !== bbox.x || - genBbox.y !== bbox.y || - genBbox.width !== bbox.width || - genBbox.height !== bbox.height - ) { - onBboxTransformed(bbox); + bboxRect.setAttrs(newBbox); + if (oldBbox.x !== newBbox.x || oldBbox.y !== newBbox.y) { + onBboxTransformed(newBbox); } }); - const generationBboxTransformer = new Konva.Transformer({ + const bboxTransformer = new Konva.Transformer({ id: PREVIEW_GENERATION_BBOX_TRANSFORMER, borderDash: [5, 5], borderStroke: 'rgba(212,216,234,1)', @@ -120,6 +92,8 @@ const getPreviewLayer = ( anchorStroke: 'rgb(42,42,42)', anchorSize: 12, anchorCornerRadius: 3, + // shiftBehavior: 'none', + centeredScaling: false, anchorStyleFunc: (anchor) => { // Make the x/y resize anchors little bars if (anchor.hasName('top-center') || anchor.hasName('bottom-center')) { @@ -135,25 +109,52 @@ const getPreviewLayer = ( anchor.offsetX(4); } }, + anchorDragBoundFunc: (oldAbsPos, newAbsPos) => { + const gridSize = getMetaKey() ? 8 : 64; + const scaledGridSize = gridSize * stage.scaleX(); + // Calculate the offset of the grid. + const stageAbsPos = stage.getAbsolutePosition(); + const offsetX = stageAbsPos.x % scaledGridSize; + const offsetY = stageAbsPos.y % scaledGridSize; + const finalPos = { + x: roundToMultiple(newAbsPos.x, scaledGridSize) + offsetX, + y: roundToMultiple(newAbsPos.y, scaledGridSize) + offsetY, + }; + console.log('scaledGridSize', scaledGridSize); + console.log('offsetX', offsetX); + console.log('offsetY', offsetY); + console.log('newAbsPosX', newAbsPos.x); + console.log('newAbsPosY', newAbsPos.y); + console.log('finalPos', finalPos); + console.log('finalPosScaled', { x: finalPos.x * stage.scaleX(), y: finalPos.y * stage.scaleY() }); + + return finalPos; + }, }); - generationBboxTransformer.on('transform', (e) => { - const bbox: IRect = { - x: Math.round(generationBboxDummyRect.x()), - y: Math.round(generationBboxDummyRect.y()), - width: Math.round(generationBboxDummyRect.width() * generationBboxDummyRect.scaleX()), - height: Math.round(generationBboxDummyRect.height() * generationBboxDummyRect.scaleY()), + + bboxTransformer.on('transform', () => { + let gridSize = getMetaKey() ? 8 : 64; + + if (getAltKey()) { + gridSize = gridSize * 2; + } + + const bbox = { + x: Math.round(bboxRect.x()), + y: Math.round(bboxRect.y()), + width: roundToMultipleMin(bboxRect.width() * bboxRect.scaleX(), gridSize), + height: roundToMultipleMin(bboxRect.height() * bboxRect.scaleY(), gridSize), }; - generationBboxDummyRect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1 }); + bboxRect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1 }); onBboxTransformed(bbox); }); - // The transformer will always be transforming the dummy rect - generationBboxTransformer.nodes([generationBboxDummyRect]); - generationBboxGroup.add(generationBboxDummyRect); - generationBboxGroup.add(generationBboxTransformer); - previewLayer.add(toolGroup); - previewLayer.add(generationBboxGroup); - return previewLayer; + // The transformer will always be transforming the dummy rect + bboxTransformer.nodes([bboxRect]); + bboxPreviewGroup.add(bboxRect); + bboxPreviewGroup.add(bboxTransformer); + previewLayer.add(bboxPreviewGroup); + return bboxPreviewGroup; }; const ALL_ANCHORS: string[] = [ @@ -168,22 +169,74 @@ const ALL_ANCHORS: string[] = [ ]; const NO_ANCHORS: string[] = []; -export const renderImageDimsPreview = (stage: Konva.Stage, bbox: IRect, tool: Tool): void => { - const previewLayer = stage.findOne(`#${PREVIEW_LAYER_ID}`); - const generationBboxGroup = stage.findOne(`#${PREVIEW_GENERATION_BBOX_GROUP}`); - const generationBboxDummyRect = stage.findOne(`#${PREVIEW_GENERATION_BBOX_DUMMY_RECT}`); - const generationBboxTransformer = stage.findOne(`#${PREVIEW_GENERATION_BBOX_TRANSFORMER}`); - assert( - previewLayer && generationBboxGroup && generationBboxDummyRect && generationBboxTransformer, - 'Generation bbox konva objects not found' - ); - generationBboxDummyRect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1, listening: tool === 'move' }); - generationBboxTransformer.setAttrs({ +export const renderBboxPreview = ( + stage: Konva.Stage, + bbox: IRect, + tool: Tool, + getBbox: () => IRect, + onBboxTransformed: (bbox: IRect) => void, + getShiftKey: () => boolean, + getMetaKey: () => boolean, + getAltKey: () => boolean +): void => { + const bboxGroup = getBboxPreviewGroup(stage, getBbox, onBboxTransformed, getShiftKey, getMetaKey, getAltKey); + const bboxRect = bboxGroup.findOne(`#${PREVIEW_GENERATION_BBOX_DUMMY_RECT}`); + const bboxTransformer = bboxGroup.findOne(`#${PREVIEW_GENERATION_BBOX_TRANSFORMER}`); + bboxRect?.setAttrs({ ...bbox, listening: tool === 'move' }); + bboxTransformer?.setAttrs({ listening: tool === 'move', enabledAnchors: tool === 'move' ? ALL_ANCHORS : NO_ANCHORS, }); }; +export const getToolPreviewGroup = (stage: Konva.Stage): Konva.Group => { + const previewLayer = getPreviewLayer(stage); + let toolPreviewGroup = previewLayer.findOne(`#${PREVIEW_TOOL_GROUP_ID}`); + if (toolPreviewGroup) { + return toolPreviewGroup; + } + + toolPreviewGroup = new Konva.Group({ id: PREVIEW_TOOL_GROUP_ID }); + + // Create the brush preview group & circles + const brushPreviewGroup = new Konva.Group({ id: PREVIEW_BRUSH_GROUP_ID }); + const brushPreviewFill = new Konva.Circle({ + id: PREVIEW_BRUSH_FILL_ID, + listening: false, + strokeEnabled: false, + }); + brushPreviewGroup.add(brushPreviewFill); + const brushPreviewBorderInner = new Konva.Circle({ + id: PREVIEW_BRUSH_BORDER_INNER_ID, + listening: false, + stroke: BRUSH_BORDER_INNER_COLOR, + strokeWidth: 1, + strokeEnabled: true, + }); + brushPreviewGroup.add(brushPreviewBorderInner); + const brushPreviewBorderOuter = new Konva.Circle({ + id: PREVIEW_BRUSH_BORDER_OUTER_ID, + listening: false, + stroke: BRUSH_BORDER_OUTER_COLOR, + strokeWidth: 1, + strokeEnabled: true, + }); + brushPreviewGroup.add(brushPreviewBorderOuter); + + // 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: PREVIEW_RECT_ID, + listening: false, + stroke: BBOX_SELECTED_STROKE, + strokeWidth: 1, + }); + + toolPreviewGroup.add(rectPreview); + toolPreviewGroup.add(brushPreviewGroup); + previewLayer.add(toolPreviewGroup); + return toolPreviewGroup; +}; + /** * Renders the preview layer. * @param stage The konva stage @@ -195,7 +248,7 @@ export const renderImageDimsPreview = (stage: Konva.Stage, bbox: IRect, tool: To * @param lastMouseDownPos The position of the last mouse down event - used for the rect tool * @param brushSize The brush size */ -export const renderPreviewLayer = ( +export const renderToolPreview = ( stage: Konva.Stage, tool: Tool, brushColor: RgbaColor, @@ -205,9 +258,7 @@ export const renderPreviewLayer = ( lastMouseDownPos: Vector2d | null, brushSize: number, isDrawing: boolean, - isMouseDown: boolean, - $genBbox: WritableAtom, - onBboxTransformed: (bbox: IRect) => void + isMouseDown: boolean ): void => { const layerCount = stage.find(selectRenderableLayers).length; // Update the stage's pointer style @@ -233,10 +284,7 @@ export const renderPreviewLayer = ( stage.draggable(tool === 'view'); - const previewLayer = getPreviewLayer(stage, $genBbox, onBboxTransformed); - const toolGroup = previewLayer.findOne(`#${PREVIEW_TOOL_GROUP_ID}`); - - assert(toolGroup, 'Tool group not found'); + const toolPreviewGroup = getToolPreviewGroup(stage); if ( !cursorPos || @@ -244,9 +292,9 @@ export const renderPreviewLayer = ( (selectedLayerType !== 'regional_guidance_layer' && selectedLayerType !== 'raster_layer') ) { // We can bail early if the mouse isn't over the stage or there are no layers - toolGroup.visible(false); + toolPreviewGroup.visible(false); } else { - toolGroup.visible(true); + toolPreviewGroup.visible(true); const brushPreviewGroup = stage.findOne(`#${PREVIEW_BRUSH_GROUP_ID}`); assert(brushPreviewGroup, 'Brush preview group not found'); @@ -267,11 +315,11 @@ export const renderPreviewLayer = ( }); // Update the inner border of the brush preview - const brushPreviewInner = previewLayer.findOne(`#${PREVIEW_BRUSH_BORDER_INNER_ID}`); + const brushPreviewInner = brushPreviewGroup.findOne(`#${PREVIEW_BRUSH_BORDER_INNER_ID}`); brushPreviewInner?.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: brushSize / 2 }); // Update the outer border of the brush preview - const brushPreviewOuter = previewLayer.findOne(`#${PREVIEW_BRUSH_BORDER_OUTER_ID}`); + const brushPreviewOuter = brushPreviewGroup.findOne(`#${PREVIEW_BRUSH_BORDER_OUTER_ID}`); brushPreviewOuter?.setAttrs({ x: cursorPos.x, y: cursorPos.y, @@ -285,7 +333,7 @@ export const renderPreviewLayer = ( if (cursorPos && lastMouseDownPos && tool === 'rect') { const snappedPos = snapPosToStage(cursorPos, stage); - const rectPreview = previewLayer.findOne(`#${PREVIEW_RECT_ID}`); + const rectPreview = brushPreviewGroup.findOne(`#${PREVIEW_RECT_ID}`); rectPreview?.setAttrs({ x: Math.min(snappedPos.x, lastMouseDownPos.x), y: Math.min(snappedPos.y, lastMouseDownPos.y), diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 7d590ba4cd5..573b5912958 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -92,8 +92,12 @@ export const initialControlLayersState: ControlLayersState = { height: 512, aspectRatio: deepClone(initialAspectRatioState), }, - x: 0, - y: 0, + bbox: { + x: 0, + y: 0, + width: 512, + height: 512, + }, }; /** @@ -800,10 +804,7 @@ export const controlLayersSlice = createSlice({ state.size.aspectRatio = action.payload; }, bboxChanged: (state, action: PayloadAction) => { - state.x = action.payload.x; - state.y = action.payload.y; - state.size.width = action.payload.width; - state.size.height = action.payload.height; + state.bbox = action.payload; }, brushSizeChanged: (state, action: PayloadAction) => { state.brushSize = Math.round(action.payload); @@ -998,7 +999,6 @@ export const $lastAddedPoint = atom(null); export const $isSpaceDown = atom(false); export const $stageScale = atom(1); export const $stagePos = atom({ x: 0, y: 0 }); -export const $genBbox = atom({ x: 0, y: 0, width: 0, height: 0 }); // Some nanostores that are manually synced to redux state to provide imperative access // TODO(psyche): This is a hack, figure out another way to handle this... @@ -1007,12 +1007,13 @@ export const $brushColor = atom(DEFAULT_RGBA_COLOR); export const $brushSpacingPx = atom(0); export const $selectedLayer = atom(null); export const $shouldInvertBrushSizeScrollDirection = atom(false); +export const $bbox = atom({ x: 0, y: 0, width: 0, height: 0 }); export const controlLayersPersistConfig: PersistConfig = { name: controlLayersSlice.name, initialState: initialControlLayersState, migrate: migrateControlLayersState, - persistDenylist: [], + persistDenylist: ['bbox'], }; // These actions are _individually_ grouped together as single undoable actions diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index d229eb1e45e..04a18b3839e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -269,8 +269,7 @@ export type ControlLayersState = { height: ParameterHeight; aspectRatio: AspectRatioState; }; - x: number; - y: number; + bbox: IRect; }; export type AddEraserLineArg = { layerId: string; points: [number, number, number, number] }; From 6d5b4e44715bf503163db6c0235de8bbefb51c57 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 12 Jun 2024 10:39:35 +1000 Subject: [PATCH 025/678] feat(ui): generation bbox transformation working whew --- .../components/StageComponent.tsx | 14 +- .../konva/renderers/previewLayer.ts | 137 +++++++++++++++--- 2 files changed, 125 insertions(+), 26 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index 0e089104d2f..736f7b655d3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -1,4 +1,4 @@ -import { $alt, $meta, $shift, Box, Flex, Heading } from '@invoke-ai/ui-library'; +import { $alt, $ctrl, $meta, $shift, Box, Flex, Heading } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; @@ -286,7 +286,17 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, return; } log.trace('Rendering bbox preview'); - renderers.renderBboxPreview(stage, state.bbox, tool, $bbox.get, onBboxTransformed, $shift.get, $meta.get, $alt.get); + renderers.renderBboxPreview( + stage, + state.bbox, + tool, + $bbox.get, + onBboxTransformed, + $shift.get, + $ctrl.get, + $meta.get, + $alt.get + ); }, [asPreview, onBboxTransformed, renderers, stage, state.bbox, tool]); useLayoutEffect(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts index 36c6d6c8a14..b2d7a0f00a2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts @@ -21,6 +21,7 @@ import { selectRenderableLayers, snapPosToStage } from 'features/controlLayers/k import type { Layer, RgbaColor, Tool } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect, Vector2d } from 'konva/lib/types'; +import { atom } from 'nanostores'; import { assert } from 'tsafe'; /** @@ -43,6 +44,7 @@ export const getBboxPreviewGroup = ( getBbox: () => IRect, onBboxTransformed: (bbox: IRect) => void, getShiftKey: () => boolean, + getCtrlKey: () => boolean, getMetaKey: () => boolean, getAltKey: () => boolean ): Konva.Group => { @@ -52,7 +54,11 @@ export const getBboxPreviewGroup = ( if (bboxPreviewGroup) { return bboxPreviewGroup; } - console.log('creating new bbox'); + + // Create a stash to hold onto the last aspect ratio of the bbox - this allows for locking the aspect ratio when + // transforming the bbox. + const bbox = getBbox(); + const $aspectRatioBuffer = atom(bbox.width / bbox.height); // Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully // transparent rect for this purpose. @@ -66,7 +72,7 @@ export const getBboxPreviewGroup = ( ...getBbox(), }); bboxRect.on('dragmove', () => { - const gridSize = getMetaKey() ? 8 : 64; + const gridSize = getCtrlKey() || getMetaKey() ? 8 : 64; const oldBbox = getBbox(); const newBbox: IRect = { ...oldBbox, @@ -92,7 +98,7 @@ export const getBboxPreviewGroup = ( anchorStroke: 'rgb(42,42,42)', anchorSize: 12, anchorCornerRadius: 3, - // shiftBehavior: 'none', + shiftBehavior: 'none', // we will implement our own shift behavior centeredScaling: false, anchorStyleFunc: (anchor) => { // Make the x/y resize anchors little bars @@ -109,44 +115,116 @@ export const getBboxPreviewGroup = ( anchor.offsetX(4); } }, - anchorDragBoundFunc: (oldAbsPos, newAbsPos) => { - const gridSize = getMetaKey() ? 8 : 64; + anchorDragBoundFunc: (_oldAbsPos, newAbsPos) => { + // This function works with absolute position - that is, a position in "physical" pixels on the screen, as opposed + // to konva's internal coordinate system. + + // We need to snap the anchors to the grid. If the user is holding ctrl/meta, we use the finer 8px grid. + const gridSize = getCtrlKey() || getMetaKey() ? 8 : 64; + // Because we are working in absolute coordinates, we need to scale the grid size by the stage scale. const scaledGridSize = gridSize * stage.scaleX(); - // Calculate the offset of the grid. + // To snap the anchor to the grid, we need to calculate an offset from the stage's absolute position. const stageAbsPos = stage.getAbsolutePosition(); + // The offset is the remainder of the stage's absolute position divided by the scaled grid size. const offsetX = stageAbsPos.x % scaledGridSize; const offsetY = stageAbsPos.y % scaledGridSize; - const finalPos = { + // Finally, calculate the position by rounding to the grid and adding the offset. + return { x: roundToMultiple(newAbsPos.x, scaledGridSize) + offsetX, y: roundToMultiple(newAbsPos.y, scaledGridSize) + offsetY, }; - console.log('scaledGridSize', scaledGridSize); - console.log('offsetX', offsetX); - console.log('offsetY', offsetY); - console.log('newAbsPosX', newAbsPos.x); - console.log('newAbsPosY', newAbsPos.y); - console.log('finalPos', finalPos); - console.log('finalPosScaled', { x: finalPos.x * stage.scaleX(), y: finalPos.y * stage.scaleY() }); - - return finalPos; }, }); bboxTransformer.on('transform', () => { - let gridSize = getMetaKey() ? 8 : 64; + // In the transform callback, we calculate the bbox's new dims and pos and update the konva object. + + // Some special handling is needed depending on the anchor being dragged. + const anchor = bboxTransformer.getActiveAnchor(); + if (!anchor) { + // Pretty sure we should always have an anchor here? + return; + } + + const alt = getAltKey(); + const ctrl = getCtrlKey(); + const meta = getMetaKey(); + const shift = getShiftKey(); + // Grid size depends on the modifier keys + let gridSize = ctrl || meta ? 8 : 64; + + // Alt key indicates we are using centered scaling. We need to double the gride size used when calculating the + // new dimensions so that each size scales in the correct increments and doesn't mis-place the bbox. For example, if + // we snapped the width and height to 8px increments, the bbox would be mis-placed by 4px in the x and y axes. + // Doubling the grid size ensures the bbox's coords remain aligned to the 8px/64px grid. if (getAltKey()) { gridSize = gridSize * 2; } + // The coords should be correct per the anchorDragBoundFunc. + let x = bboxRect.x(); + let y = bboxRect.y(); + + // Konva transforms by scaling the dims, not directly changing width and height. At this point, the width and height + // *have not changed*, only the scale has changed. To get the final height, we need to scale the dims and then snap + // them to the grid. + let width = roundToMultipleMin(bboxRect.width() * bboxRect.scaleX(), gridSize); + let height = roundToMultipleMin(bboxRect.height() * bboxRect.scaleY(), gridSize); + + // If shift is held and we are resizing from a corner, retain aspect ratio - needs special handling. We skip this + // if alt/opt is held - this requires math too big for my brain. + if (shift && CORNER_ANCHORS.includes(anchor) && !alt) { + // Fit the bbox to the last aspect ratio + let fittedWidth = Math.sqrt(width * height * $aspectRatioBuffer.get()); + let fittedHeight = fittedWidth / $aspectRatioBuffer.get(); + fittedWidth = roundToMultipleMin(fittedWidth, gridSize); + fittedHeight = roundToMultipleMin(fittedHeight, gridSize); + + // We need to adjust the x and y coords to have the resize occur from the right origin. + if (anchor === 'top-left') { + // The transform origin is the bottom-right anchor. Both x and y need to be updated. + x = x - (fittedWidth - width); + y = y - (fittedHeight - height); + } + if (anchor === 'top-right') { + // The transform origin is the bottom-left anchor. Only y needs to be updated. + y = y - (fittedHeight - height); + } + if (anchor === 'bottom-left') { + // The transform origin is the top-right anchor. Only x needs to be updated. + x = x - (fittedWidth - width); + } + // Update the width and height to the fitted dims. + width = fittedWidth; + height = fittedHeight; + } + const bbox = { - x: Math.round(bboxRect.x()), - y: Math.round(bboxRect.y()), - width: roundToMultipleMin(bboxRect.width() * bboxRect.scaleX(), gridSize), - height: roundToMultipleMin(bboxRect.height() * bboxRect.scaleY(), gridSize), + x: Math.round(x), + y: Math.round(y), + width, + height, }; - bboxRect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1 }); + + // Here we _could_ go ahead and update the bboxRect's attrs directly with the new transform, and reset its scale to 1. + // However, we have another function that renders the bbox when its internal state changes, so we will rely on that + // to set the new attrs. + + // Update the bbox in internal state. onBboxTransformed(bbox); + + // Update the aspect ratio buffer whenever the shift key is not held - this allows for a nice UX where you can start + // a transform, get the right aspect ratio, then hold shift to lock it in. + if (!shift) { + $aspectRatioBuffer.set(bbox.width / bbox.height); + } + }); + + bboxTransformer.on('transformend', () => { + // Always update the aspect ratio buffer when the transform ends, so if the next transform starts with shift held, + // we have the correct aspect ratio to start from. + $aspectRatioBuffer.set(bboxRect.width() / bboxRect.height()); }); // The transformer will always be transforming the dummy rect @@ -167,6 +245,7 @@ const ALL_ANCHORS: string[] = [ 'bottom-center', 'bottom-right', ]; +const CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right']; const NO_ANCHORS: string[] = []; export const renderBboxPreview = ( @@ -176,13 +255,23 @@ export const renderBboxPreview = ( getBbox: () => IRect, onBboxTransformed: (bbox: IRect) => void, getShiftKey: () => boolean, + getCtrlKey: () => boolean, getMetaKey: () => boolean, getAltKey: () => boolean ): void => { - const bboxGroup = getBboxPreviewGroup(stage, getBbox, onBboxTransformed, getShiftKey, getMetaKey, getAltKey); + const bboxGroup = getBboxPreviewGroup( + stage, + getBbox, + onBboxTransformed, + getShiftKey, + getCtrlKey, + getMetaKey, + getAltKey + ); const bboxRect = bboxGroup.findOne(`#${PREVIEW_GENERATION_BBOX_DUMMY_RECT}`); const bboxTransformer = bboxGroup.findOne(`#${PREVIEW_GENERATION_BBOX_TRANSFORMER}`); - bboxRect?.setAttrs({ ...bbox, listening: tool === 'move' }); + // This updates the bbox during transformation + bboxRect?.setAttrs({ ...bbox, scaleX: 1, scaleY: 1, listening: tool === 'move' }); bboxTransformer?.setAttrs({ listening: tool === 'move', enabledAnchors: tool === 'move' ? ALL_ANCHORS : NO_ANCHORS, From f90fa85e77ee2ba6855fc84792744a0c0cf8ea04 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 12 Jun 2024 10:56:45 +1000 Subject: [PATCH 026/678] chore(ui): bump konva --- invokeai/frontend/web/pnpm-lock.yaml | 39 ++++++++++++++-------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index 90b52e6306c..bab6e3b1e5b 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -2043,7 +2043,7 @@ packages: dependencies: '@chakra-ui/dom-utils': 2.1.0 react: 18.3.1 - react-focus-lock: 2.12.1(@types/react@18.3.3)(react@18.3.1) + react-focus-lock: 2.11.1(@types/react@18.3.3)(react@18.3.1) transitivePeerDependencies: - '@types/react' dev: false @@ -2236,11 +2236,11 @@ packages: '@chakra-ui/shared-utils': 2.0.5 '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) '@chakra-ui/transition': 2.1.0(framer-motion@10.18.0)(react@18.3.1) - aria-hidden: 1.2.4 + aria-hidden: 1.2.3 framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.5.10(@types/react@18.3.3)(react@18.3.1) + react-remove-scroll: 2.5.7(@types/react@18.3.3)(react@18.3.1) transitivePeerDependencies: - '@types/react' dev: false @@ -5773,8 +5773,8 @@ packages: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} dev: true - /aria-hidden@1.2.4: - resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} + /aria-hidden@1.2.3: + resolution: {integrity: sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==} engines: {node: '>=10'} dependencies: tslib: 2.6.3 @@ -7565,8 +7565,8 @@ packages: engines: {node: '>=0.4.0'} dev: true - /focus-lock@1.3.5: - resolution: {integrity: sha512-QFaHbhv9WPUeLYBDe/PAuLKJ4Dd9OPvKs9xZBr3yLXnUrDNaVXKu2baDBXe3naPY30hgHYSsf2JW4jzas2mDEQ==} + /focus-lock@1.3.3: + resolution: {integrity: sha512-hfXkZha7Xt4RQtrL1HBfspAuIj89Y0fb6GX0dfJilb8S2G/lvL4akPAcHq6xoD2NuZnDMCnZL/zQesMyeu6Psg==} engines: {node: '>=10'} dependencies: tslib: 2.6.3 @@ -9700,8 +9700,8 @@ packages: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} dev: false - /react-focus-lock@2.12.1(@types/react@18.3.3)(react@18.3.1): - resolution: {integrity: sha512-lfp8Dve4yJagkHiFrC1bGtib3mF2ktqwPJw4/WGcgPW+pJ/AVQA5X2vI7xgp13FcxFEpYBBHpXai/N2DBNC0Jw==} + /react-focus-lock@2.11.1(@types/react@18.3.3)(react@18.3.1): + resolution: {integrity: sha512-IXLwnTBrLTlKTpASZXqqXJ8oymWrgAlOfuuDYN4XCuN1YJ72dwX198UCaF1QqGUk5C3QOnlMik//n3ufcfe8Ig==} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -9711,11 +9711,11 @@ packages: dependencies: '@babel/runtime': 7.25.0 '@types/react': 18.3.3 - focus-lock: 1.3.5 + focus-lock: 1.3.3 prop-types: 15.8.1 react: 18.3.1 react-clientside-effect: 1.2.6(react@18.3.1) - use-callback-ref: 1.3.2(@types/react@18.3.3)(react@18.3.1) + use-callback-ref: 1.3.1(@types/react@18.3.3)(react@18.3.1) use-sidecar: 1.1.2(@types/react@18.3.3)(react@18.3.1) dev: false @@ -9847,9 +9847,10 @@ packages: use-sync-external-store: 1.2.2(react@18.3.1) dev: false - /react-remove-scroll-bar@2.3.6(@types/react@18.3.3)(react@18.3.1): - resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==} + /react-remove-scroll-bar@2.3.5(@types/react@18.3.3)(react@18.3.1): + resolution: {integrity: sha512-3cqjOqg6s0XbOjWvmasmqHch+RLxIEk2r/70rzGXuz3iIGQsQheEQyqYCBb5EECoD01Vo2SIbDqW4paLeLTASw==} engines: {node: '>=10'} + deprecated: please update to the following version as this contains a bug (https://github.com/theKashey/react-remove-scroll-bar/issues/57) peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -9863,8 +9864,8 @@ packages: tslib: 2.6.3 dev: false - /react-remove-scroll@2.5.10(@types/react@18.3.3)(react@18.3.1): - resolution: {integrity: sha512-m3zvBRANPBw3qxVVjEIPEQinkcwlFZ4qyomuWVpNJdv4c6MvHfXV0C3L9Jx5rr3HeBHKNRX+1jreB5QloDIJjA==} + /react-remove-scroll@2.5.7(@types/react@18.3.3)(react@18.3.1): + resolution: {integrity: sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==} engines: {node: '>=10'} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -9875,10 +9876,10 @@ packages: dependencies: '@types/react': 18.3.3 react: 18.3.1 - react-remove-scroll-bar: 2.3.6(@types/react@18.3.3)(react@18.3.1) + react-remove-scroll-bar: 2.3.5(@types/react@18.3.3)(react@18.3.1) react-style-singleton: 2.2.1(@types/react@18.3.3)(react@18.3.1) tslib: 2.6.3 - use-callback-ref: 1.3.2(@types/react@18.3.3)(react@18.3.1) + use-callback-ref: 1.3.1(@types/react@18.3.3)(react@18.3.1) use-sidecar: 1.1.2(@types/react@18.3.3)(react@18.3.1) dev: false @@ -11298,8 +11299,8 @@ packages: punycode: 2.3.1 dev: true - /use-callback-ref@1.3.2(@types/react@18.3.3)(react@18.3.1): - resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==} + /use-callback-ref@1.3.1(@types/react@18.3.3)(react@18.3.1): + resolution: {integrity: sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==} engines: {node: '>=10'} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 From e0b8b82a152ea54ba26c50dd5d4b3575188b7b4a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 12 Jun 2024 11:48:04 +1000 Subject: [PATCH 027/678] feat(ui): round stage scale --- .../src/features/controlLayers/components/HeadsUpDisplay.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx index f36898e10ca..20ee186a4c8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx @@ -12,7 +12,7 @@ export const HeadsUpDisplay = memo(() => { return ( - + From fe0c4767c78d63f46acf48213bbd601d5a203465 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 12 Jun 2024 11:57:14 +1000 Subject: [PATCH 028/678] feat(ui): store all stage attrs in nanostores --- .../components/HeadsUpDisplay.tsx | 8 ++-- .../components/StageComponent.tsx | 14 +++--- .../features/controlLayers/konva/events.ts | 43 +++++++++++++------ .../features/controlLayers/konva/naming.ts | 3 ++ .../controlLayers/store/controlLayersSlice.ts | 10 ++++- .../src/features/controlLayers/store/types.ts | 1 + 6 files changed, 55 insertions(+), 24 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx index 20ee186a4c8..834903c0f76 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx @@ -1,18 +1,20 @@ import { Box, Flex, Text } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; -import { $stageScale } from 'features/controlLayers/store/controlLayersSlice'; +import { $stageAttrs } from 'features/controlLayers/store/controlLayersSlice'; import { round } from 'lodash-es'; import { memo } from 'react'; export const HeadsUpDisplay = memo(() => { - const stageScale = useStore($stageScale); + const stageAttrs = useStore($stageAttrs); const layerCount = useAppSelector((s) => s.controlLayers.present.layers.length); const bbox = useAppSelector((s) => s.controlLayers.present.bbox); return ( - + + + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index 736f7b655d3..6782ccb36f2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -26,8 +26,7 @@ import { $lastMouseDownPos, $selectedLayer, $shouldInvertBrushSizeScrollDirection, - $stagePos, - $stageScale, + $stageAttrs, $tool, $toolBuffer, bboxChanged, @@ -89,7 +88,6 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, const lastCursorPos = useStore($lastCursorPos); const lastMouseDownPos = useStore($lastMouseDownPos); const isMouseDown = useStore($isMouseDown); - const stageScale = useStore($stageScale); const isDrawing = useStore($isDrawing); const brushColor = useAppSelector(selectBrushColor); const selectedLayer = useAppSelector(selectSelectedLayer); @@ -197,8 +195,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, $lastMouseDownPos, $lastCursorPos, $lastAddedPoint, - $stageScale, - $stagePos, + $stageAttrs, $brushSize, $brushColor, $brushSpacingPx, @@ -236,6 +233,13 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, const fitStageToContainer = () => { stage.width(container.offsetWidth); stage.height(container.offsetHeight); + $stageAttrs.set({ + x: stage.x(), + y: stage.y(), + width: stage.width(), + height: stage.height(), + scale: stage.scaleX(), + }); }; const resizeObserver = new ResizeObserver(fitStageToContainer); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index cf95e8902cd..d4818bfee30 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -1,15 +1,16 @@ import { calculateNewBrushSize } from 'features/canvas/hooks/useCanvasZoom'; import { CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/canvas/util/constants'; import { getIsMouseDown, getScaledFlooredCursorPosition, snapPosToStage } from 'features/controlLayers/konva/util'; -import { - type AddBrushLineArg, - type AddEraserLineArg, - type AddPointToLineArg, - type AddRectShapeArg, - DEFAULT_RGBA_COLOR, - type Layer, - type Tool, +import type { + AddBrushLineArg, + AddEraserLineArg, + AddPointToLineArg, + AddRectShapeArg, + Layer, + StageAttrs, + Tool, } from 'features/controlLayers/store/types'; +import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types'; import type Konva from 'konva'; import type { Vector2d } from 'konva/lib/types'; import { clamp } from 'lodash-es'; @@ -27,8 +28,7 @@ type SetStageEventHandlersArg = { $lastMouseDownPos: WritableAtom; $lastCursorPos: WritableAtom; $lastAddedPoint: WritableAtom; - $stageScale: WritableAtom; - $stagePos: WritableAtom; + $stageAttrs: WritableAtom; $brushColor: WritableAtom; $brushSize: WritableAtom; $brushSpacingPx: WritableAtom; @@ -93,8 +93,7 @@ export const setStageEventHandlers = ({ $lastMouseDownPos, $lastCursorPos, $lastAddedPoint, - $stagePos, - $stageScale, + $stageAttrs, $brushColor, $brushSize, $brushSpacingPx, @@ -333,15 +332,31 @@ export const setStageEventHandlers = ({ stage.scaleX(newScale); stage.scaleY(newScale); stage.position(newPos); - $stageScale.set(newScale); - $stagePos.set(newPos); + $stageAttrs.set({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale }); } }); + stage.on('dragmove', () => { + $stageAttrs.set({ + x: stage.x(), + y: stage.y(), + width: stage.width(), + height: stage.height(), + scale: stage.scaleX(), + }); + }); + stage.on('dragend', () => { // Stage position should always be an integer, else we get fractional pixels which are blurry stage.x(Math.floor(stage.x())); stage.y(Math.floor(stage.y())); + $stageAttrs.set({ + x: stage.x(), + y: stage.y(), + width: stage.width(), + height: stage.height(), + scale: stage.scaleX(), + }); }); const onKeyDown = (e: KeyboardEvent) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts index f3483937674..7fd088dc732 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts @@ -13,6 +13,9 @@ export const PREVIEW_RECT_ID = 'preview_layer.rect'; export const PREVIEW_GENERATION_BBOX_GROUP = 'preview_layer.gen_bbox_group'; export const PREVIEW_GENERATION_BBOX_TRANSFORMER = 'preview_layer.gen_bbox_transformer'; export const PREVIEW_GENERATION_BBOX_DUMMY_RECT = 'preview_layer.gen_bbox_dummy_rect'; +export const PREVIEW_DOCUMENT_SIZE_GROUP = 'preview_layer.doc_size_group'; +export const PREVIEW_DOCUMENT_SIZE_STAGE_RECT = 'preview_layer.doc_size_stage_rect'; +export const PREVIEW_DOCUMENT_SIZE_DOCUMENT_RECT = 'preview_layer.doc_size_doc_rect'; // Names for Konva layers and objects (comparable to CSS classes) export const LAYER_BBOX_NAME = 'layer.bbox'; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 573b5912958..80a2a4cc4c0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -60,6 +60,7 @@ import type { RasterLayer, RegionalGuidanceLayer, RgbaColor, + StageAttrs, Tool, } from './types'; import { @@ -997,8 +998,13 @@ export const $lastCursorPos = atom(null); export const $isPreviewVisible = atom(true); export const $lastAddedPoint = atom(null); export const $isSpaceDown = atom(false); -export const $stageScale = atom(1); -export const $stagePos = atom({ x: 0, y: 0 }); +export const $stageAttrs = atom({ + x: 0, + y: 0, + width: 0, + height: 0, + scale: 0, +}); // Some nanostores that are manually synced to redux state to provide imperative access // TODO(psyche): This is a hack, figure out another way to handle this... diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 04a18b3839e..860c8b5586d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -272,6 +272,7 @@ export type ControlLayersState = { bbox: IRect; }; +export type StageAttrs = { x: number; y: number; width: number; height: number; scale: number }; export type AddEraserLineArg = { layerId: string; points: [number, number, number, number] }; export type AddBrushLineArg = AddEraserLineArg & { color: RgbaColor }; export type AddPointToLineArg = { layerId: string; point: [number, number] }; From a2585a8bb1a259ebaf8a76a3fccc5b943c4e4cf1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 12 Jun 2024 16:08:03 +1000 Subject: [PATCH 029/678] feat(ui): decouple konva logic from nanostores --- .../components/StageComponent.tsx | 37 +-- .../features/controlLayers/konva/events.ts | 218 ++++++++++-------- .../controlLayers/store/controlLayersSlice.ts | 2 +- 3 files changed, 146 insertions(+), 111 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index 6782ccb36f2..23e21392d9d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -20,12 +20,12 @@ import { $brushSpacingPx, $isDrawing, $isMouseDown, - $isSpaceDown, $lastAddedPoint, $lastCursorPos, $lastMouseDownPos, $selectedLayer, $shouldInvertBrushSizeScrollDirection, + $spaceKey, $stageAttrs, $tool, $toolBuffer, @@ -188,20 +188,27 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, const cleanup = setStageEventHandlers({ stage, - $tool, - $toolBuffer, - $isDrawing, - $isMouseDown, - $lastMouseDownPos, - $lastCursorPos, - $lastAddedPoint, - $stageAttrs, - $brushSize, - $brushColor, - $brushSpacingPx, - $selectedLayer, - $shouldInvertBrushSizeScrollDirection, - $isSpaceDown, + getTool: $tool.get, + setTool: $tool.set, + getToolBuffer: $toolBuffer.get, + setToolBuffer: $toolBuffer.set, + getIsDrawing: $isDrawing.get, + setIsDrawing: $isDrawing.set, + getIsMouseDown: $isMouseDown.get, + setIsMouseDown: $isMouseDown.set, + getBrushColor: $brushColor.get, + getBrushSize: $brushSize.get, + getBrushSpacingPx: $brushSpacingPx.get, + getSelectedLayer: $selectedLayer.get, + getLastAddedPoint: $lastAddedPoint.get, + setLastAddedPoint: $lastAddedPoint.set, + getLastCursorPos: $lastCursorPos.get, + setLastCursorPos: $lastCursorPos.set, + getLastMouseDownPos: $lastMouseDownPos.get, + setLastMouseDownPos: $lastMouseDownPos.set, + getShouldInvert: $shouldInvertBrushSizeScrollDirection.get, + getSpaceKey: $spaceKey.get, + setStageAttrs: $stageAttrs.set, onBrushSizeChanged, onBrushLineAdded, onEraserLineAdded, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index d4818bfee30..fcc4c5b14b7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -1,6 +1,6 @@ import { calculateNewBrushSize } from 'features/canvas/hooks/useCanvasZoom'; import { CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/canvas/util/constants'; -import { getIsMouseDown, getScaledFlooredCursorPosition, snapPosToStage } from 'features/controlLayers/konva/util'; +import { getScaledFlooredCursorPosition, snapPosToStage } from 'features/controlLayers/konva/util'; import type { AddBrushLineArg, AddEraserLineArg, @@ -14,27 +14,33 @@ import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types'; import type Konva from 'konva'; import type { Vector2d } from 'konva/lib/types'; import { clamp } from 'lodash-es'; -import type { WritableAtom } from 'nanostores'; import type { RgbaColor } from 'react-colorful'; import { PREVIEW_TOOL_GROUP_ID } from './naming'; -type SetStageEventHandlersArg = { +type Arg = { stage: Konva.Stage; - $tool: WritableAtom; - $toolBuffer: WritableAtom; - $isDrawing: WritableAtom; - $isMouseDown: WritableAtom; - $lastMouseDownPos: WritableAtom; - $lastCursorPos: WritableAtom; - $lastAddedPoint: WritableAtom; - $stageAttrs: WritableAtom; - $brushColor: WritableAtom; - $brushSize: WritableAtom; - $brushSpacingPx: WritableAtom; - $selectedLayer: WritableAtom; - $shouldInvertBrushSizeScrollDirection: WritableAtom; - $isSpaceDown: WritableAtom; + getTool: () => Tool; + setTool: (tool: Tool) => void; + getToolBuffer: () => Tool | null; + setToolBuffer: (tool: Tool | null) => void; + getIsDrawing: () => boolean; + setIsDrawing: (isDrawing: boolean) => void; + getIsMouseDown: () => boolean; + setIsMouseDown: (isMouseDown: boolean) => void; + getLastMouseDownPos: () => Vector2d | null; + setLastMouseDownPos: (pos: Vector2d | null) => void; + getLastCursorPos: () => Vector2d | null; + setLastCursorPos: (pos: Vector2d | null) => void; + getLastAddedPoint: () => Vector2d | null; + setLastAddedPoint: (pos: Vector2d | null) => void; + setStageAttrs: (attrs: StageAttrs) => void; + getBrushColor: () => RgbaColor; + getBrushSize: () => number; + getBrushSpacingPx: () => number; + getSelectedLayer: () => Layer | null; + getShouldInvert: () => boolean; + getSpaceKey: () => boolean; onBrushLineAdded: (arg: AddBrushLineArg) => void; onEraserLineAdded: (arg: AddEraserLineArg) => void; onPointAddedToLine: (arg: AddPointToLineArg) => void; @@ -46,14 +52,14 @@ type SetStageEventHandlersArg = { * Updates the last cursor position atom with the current cursor position, returning the new position or `null` if the * cursor is not over the stage. * @param stage The konva stage - * @param $lastCursorPos The last cursor pos as a nanostores atom + * @param setLastCursorPos The callback to store the cursor pos */ -const updateLastCursorPos = (stage: Konva.Stage, $lastCursorPos: WritableAtom) => { +const updateLastCursorPos = (stage: Konva.Stage, setLastCursorPos: Arg['setLastCursorPos']) => { const pos = getScaledFlooredCursorPosition(stage); if (!pos) { return null; } - $lastCursorPos.set(pos); + setLastCursorPos(pos); return pos; }; @@ -68,51 +74,59 @@ const updateLastCursorPos = (stage: Konva.Stage, $lastCursorPos: WritableAtom, - $brushSpacingPx: WritableAtom, - onPointAddedToLine: (arg: AddPointToLineArg) => void + getLastAddedPoint: Arg['getLastAddedPoint'], + setLastAddedPoint: Arg['setLastAddedPoint'], + getBrushSpacingPx: Arg['getBrushSpacingPx'], + onPointAddedToLine: Arg['onPointAddedToLine'] ) => { // Continue the last line - const lastAddedPoint = $lastAddedPoint.get(); + const lastAddedPoint = getLastAddedPoint(); if (lastAddedPoint) { // Dispatching redux events impacts perf substantially - using brush spacing keeps dispatches to a reasonable number - if (Math.hypot(lastAddedPoint.x - currentPos.x, lastAddedPoint.y - currentPos.y) < $brushSpacingPx.get()) { + if (Math.hypot(lastAddedPoint.x - currentPos.x, lastAddedPoint.y - currentPos.y) < getBrushSpacingPx()) { return; } } - $lastAddedPoint.set(currentPos); + setLastAddedPoint(currentPos); onPointAddedToLine({ layerId, point: [currentPos.x, currentPos.y] }); }; export const setStageEventHandlers = ({ stage, - $tool, - $toolBuffer, - $isDrawing, - $isMouseDown, - $lastMouseDownPos, - $lastCursorPos, - $lastAddedPoint, - $stageAttrs, - $brushColor, - $brushSize, - $brushSpacingPx, - $selectedLayer, - $shouldInvertBrushSizeScrollDirection, - $isSpaceDown, + getTool, + setTool, + getToolBuffer, + setToolBuffer, + getIsDrawing, + setIsDrawing, + getIsMouseDown, + setIsMouseDown, + getLastMouseDownPos, + setLastMouseDownPos, + getLastCursorPos, + setLastCursorPos, + getLastAddedPoint, + setLastAddedPoint, + setStageAttrs, + getBrushColor, + getBrushSize, + getBrushSpacingPx, + getSelectedLayer, + getShouldInvert, + getSpaceKey, onBrushLineAdded, onEraserLineAdded, onPointAddedToLine, onRectShapeAdded, onBrushSizeChanged, -}: SetStageEventHandlersArg): (() => void) => { +}: Arg): (() => void) => { //#region mouseenter stage.on('mouseenter', (e) => { const stage = e.target.getStage(); if (!stage) { return; } - const tool = $tool.get(); + const tool = getTool(); stage.findOne(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); }); @@ -122,10 +136,10 @@ export const setStageEventHandlers = ({ if (!stage) { return; } - $isMouseDown.set(true); - const tool = $tool.get(); - const pos = updateLastCursorPos(stage, $lastCursorPos); - const selectedLayer = $selectedLayer.get(); + setIsMouseDown(true); + const tool = getTool(); + const pos = updateLastCursorPos(stage, setLastCursorPos); + const selectedLayer = getSelectedLayer(); if (!pos || !selectedLayer) { return; } @@ -133,7 +147,7 @@ export const setStageEventHandlers = ({ return; } - if ($isSpaceDown.get()) { + if (getSpaceKey()) { // No drawing when space is down - we are panning the stage return; } @@ -142,10 +156,10 @@ export const setStageEventHandlers = ({ onBrushLineAdded({ layerId: selectedLayer.id, points: [pos.x, pos.y, pos.x, pos.y], - color: selectedLayer.type === 'raster_layer' ? $brushColor.get() : DEFAULT_RGBA_COLOR, + color: selectedLayer.type === 'raster_layer' ? getBrushColor() : DEFAULT_RGBA_COLOR, }); - $isDrawing.set(true); - $lastMouseDownPos.set(pos); + setIsDrawing(true); + setLastMouseDownPos(pos); } if (tool === 'eraser') { @@ -153,13 +167,13 @@ export const setStageEventHandlers = ({ layerId: selectedLayer.id, points: [pos.x, pos.y, pos.x, pos.y], }); - $isDrawing.set(true); - $lastMouseDownPos.set(pos); + setIsDrawing(true); + setLastMouseDownPos(pos); } if (tool === 'rect') { - $isDrawing.set(true); - $lastMouseDownPos.set(snapPosToStage(pos, stage)); + setIsDrawing(true); + setLastMouseDownPos(snapPosToStage(pos, stage)); } }); @@ -169,9 +183,9 @@ export const setStageEventHandlers = ({ if (!stage) { return; } - $isMouseDown.set(false); - const pos = $lastCursorPos.get(); - const selectedLayer = $selectedLayer.get(); + setIsMouseDown(false); + const pos = getLastCursorPos(); + const selectedLayer = getSelectedLayer(); if (!pos || !selectedLayer) { return; @@ -180,15 +194,15 @@ export const setStageEventHandlers = ({ return; } - if ($isSpaceDown.get()) { + if (getSpaceKey()) { // No drawing when space is down - we are panning the stage return; } - const tool = $tool.get(); + const tool = getTool(); if (tool === 'rect') { - const lastMouseDownPos = $lastMouseDownPos.get(); + const lastMouseDownPos = getLastMouseDownPos(); if (lastMouseDownPos) { const snappedPos = snapPosToStage(pos, stage); onRectShapeAdded({ @@ -199,13 +213,13 @@ export const setStageEventHandlers = ({ width: Math.abs(snappedPos.x - lastMouseDownPos.x), height: Math.abs(snappedPos.y - lastMouseDownPos.y), }, - color: selectedLayer.type === 'raster_layer' ? $brushColor.get() : DEFAULT_RGBA_COLOR, + color: selectedLayer.type === 'raster_layer' ? getBrushColor() : DEFAULT_RGBA_COLOR, }); } } - $isDrawing.set(false); - $lastMouseDownPos.set(null); + setIsDrawing(false); + setLastMouseDownPos(null); }); //#region mousemove @@ -214,9 +228,9 @@ export const setStageEventHandlers = ({ if (!stage) { return; } - const tool = $tool.get(); - const pos = updateLastCursorPos(stage, $lastCursorPos); - const selectedLayer = $selectedLayer.get(); + const tool = getTool(); + const pos = updateLastCursorPos(stage, setLastCursorPos); + const selectedLayer = getSelectedLayer(); stage.findOne(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); @@ -227,38 +241,52 @@ export const setStageEventHandlers = ({ return; } - if ($isSpaceDown.get()) { + if (getSpaceKey()) { // No drawing when space is down - we are panning the stage return; } - if (!getIsMouseDown(e)) { + if (!getIsMouseDown()) { return; } if (tool === 'brush') { - if ($isDrawing.get()) { + if (getIsDrawing()) { // Continue the last line - maybeAddNextPoint(selectedLayer.id, pos, $lastAddedPoint, $brushSpacingPx, onPointAddedToLine); + maybeAddNextPoint( + selectedLayer.id, + pos, + getLastAddedPoint, + setLastAddedPoint, + getBrushSpacingPx, + onPointAddedToLine + ); } else { // Start a new line onBrushLineAdded({ layerId: selectedLayer.id, points: [pos.x, pos.y, pos.x, pos.y], - color: selectedLayer.type === 'raster_layer' ? $brushColor.get() : DEFAULT_RGBA_COLOR, + color: selectedLayer.type === 'raster_layer' ? getBrushColor() : DEFAULT_RGBA_COLOR, }); - $isDrawing.set(true); + setIsDrawing(true); } } if (tool === 'eraser') { - if ($isDrawing.get()) { + if (getIsDrawing()) { // Continue the last line - maybeAddNextPoint(selectedLayer.id, pos, $lastAddedPoint, $brushSpacingPx, onPointAddedToLine); + maybeAddNextPoint( + selectedLayer.id, + pos, + getLastAddedPoint, + setLastAddedPoint, + getBrushSpacingPx, + onPointAddedToLine + ); } else { // Start a new line onEraserLineAdded({ layerId: selectedLayer.id, points: [pos.x, pos.y, pos.x, pos.y] }); - $isDrawing.set(true); + setIsDrawing(true); } } }); @@ -269,12 +297,12 @@ export const setStageEventHandlers = ({ if (!stage) { return; } - const pos = updateLastCursorPos(stage, $lastCursorPos); - $isDrawing.set(false); - $lastCursorPos.set(null); - $lastMouseDownPos.set(null); - const selectedLayer = $selectedLayer.get(); - const tool = $tool.get(); + const pos = updateLastCursorPos(stage, setLastCursorPos); + setIsDrawing(false); + setLastCursorPos(null); + setLastMouseDownPos(null); + const selectedLayer = getSelectedLayer(); + const tool = getTool(); stage.findOne(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(false); @@ -284,11 +312,11 @@ export const setStageEventHandlers = ({ if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { return; } - if ($isSpaceDown.get()) { + if (getSpaceKey()) { // No drawing when space is down - we are panning the stage return; } - if (getIsMouseDown(e)) { + if (getIsMouseDown()) { if (tool === 'brush') { onPointAddedToLine({ layerId: selectedLayer.id, point: [pos.x, pos.y] }); } @@ -304,11 +332,11 @@ export const setStageEventHandlers = ({ if (e.evt.ctrlKey || e.evt.metaKey) { let delta = e.evt.deltaY; - if ($shouldInvertBrushSizeScrollDirection.get()) { + if (getShouldInvert()) { delta = -delta; } // Holding ctrl or meta while scrolling changes the brush size - onBrushSizeChanged(calculateNewBrushSize($brushSize.get(), delta)); + onBrushSizeChanged(calculateNewBrushSize(getBrushSize(), delta)); } else { // We need the absolute cursor position - not the scaled position const cursorPos = stage.getPointerPosition(); @@ -332,12 +360,12 @@ export const setStageEventHandlers = ({ stage.scaleX(newScale); stage.scaleY(newScale); stage.position(newPos); - $stageAttrs.set({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale }); + setStageAttrs({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale }); } }); stage.on('dragmove', () => { - $stageAttrs.set({ + setStageAttrs({ x: stage.x(), y: stage.y(), width: stage.width(), @@ -350,7 +378,7 @@ export const setStageEventHandlers = ({ // Stage position should always be an integer, else we get fractional pixels which are blurry stage.x(Math.floor(stage.x())); stage.y(Math.floor(stage.y())); - $stageAttrs.set({ + setStageAttrs({ x: stage.x(), y: stage.y(), width: stage.width(), @@ -365,11 +393,11 @@ export const setStageEventHandlers = ({ } // Cancel shape drawing on escape if (e.key === 'Escape') { - $isDrawing.set(false); - $lastMouseDownPos.set(null); + setIsDrawing(false); + setLastMouseDownPos(null); } else if (e.key === ' ') { - $toolBuffer.set($tool.get()); - $tool.set('view'); + setToolBuffer(getTool()); + setTool('view'); } }; window.addEventListener('keydown', onKeyDown); @@ -380,9 +408,9 @@ export const setStageEventHandlers = ({ return; } if (e.key === ' ') { - const toolBuffer = $toolBuffer.get(); - $tool.set(toolBuffer ?? 'move'); - $toolBuffer.set(null); + const toolBuffer = getToolBuffer(); + setTool(toolBuffer ?? 'move'); + setToolBuffer(null); } }; window.addEventListener('keyup', onKeyUp); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 80a2a4cc4c0..444eca89467 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -997,7 +997,7 @@ export const $toolBuffer = atom(null); export const $lastCursorPos = atom(null); export const $isPreviewVisible = atom(true); export const $lastAddedPoint = atom(null); -export const $isSpaceDown = atom(false); +export const $spaceKey = atom(false); export const $stageAttrs = atom({ x: 0, y: 0, From 7b39b31f6c382527c35eacf23dc3bf47e8c473ce Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 12 Jun 2024 16:31:18 +1000 Subject: [PATCH 030/678] fix(ui): multiple stages --- .../components/StageComponent.tsx | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index 23e21392d9d..c03c985cfa3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -49,7 +49,7 @@ import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; import { clamp } from 'lodash-es'; -import { memo, useCallback, useLayoutEffect, useMemo, useState } from 'react'; +import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { getImageDTO } from 'services/api/endpoints/images'; import { useDevicePixelRatio } from 'use-device-pixel-ratio'; @@ -327,6 +327,13 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, useLayoutEffect(() => { Konva.pixelRatio = dpr; }, [dpr]); + + useEffect( + () => () => { + stage.destroy(); + }, + [stage] + ); }; type Props = { @@ -334,8 +341,6 @@ type Props = { }; export const StageComponent = memo(({ asPreview = false }: Props) => { - const { t } = useTranslation(); - const layerCount = useAppSelector(selectLayerCount); const [stage] = useState( () => new Konva.Stage({ @@ -363,12 +368,7 @@ export const StageComponent = memo(({ asPreview = false }: Props) => { backgroundRepeat="repeat" opacity={0.2} /> - {layerCount === 0 && !asPreview && ( - - {t('controlLayers.noLayersAdded')} - - )} - + {!asPreview && } { }); StageComponent.displayName = 'StageComponent'; + +const NoLayersFallback = () => { + const { t } = useTranslation(); + const layerCount = useAppSelector(selectLayerCount); + if (layerCount) { + return null; + } + + return ( + + {t('controlLayers.noLayersAdded')} + + ); +}; From d76802f5638a84cfe51b8a13bcae29ef12b97c4a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 12 Jun 2024 16:35:23 +1000 Subject: [PATCH 031/678] fix(ui): rect tool preview --- .../features/controlLayers/konva/events.ts | 21 ++++++++----------- .../konva/renderers/previewLayer.ts | 14 ++++++------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index fcc4c5b14b7..12a0efdfb7c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -1,6 +1,6 @@ import { calculateNewBrushSize } from 'features/canvas/hooks/useCanvasZoom'; import { CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/canvas/util/constants'; -import { getScaledFlooredCursorPosition, snapPosToStage } from 'features/controlLayers/konva/util'; +import { getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util'; import type { AddBrushLineArg, AddEraserLineArg, @@ -152,14 +152,15 @@ export const setStageEventHandlers = ({ return; } + setIsDrawing(true); + setLastMouseDownPos(pos); + if (tool === 'brush') { onBrushLineAdded({ layerId: selectedLayer.id, points: [pos.x, pos.y, pos.x, pos.y], color: selectedLayer.type === 'raster_layer' ? getBrushColor() : DEFAULT_RGBA_COLOR, }); - setIsDrawing(true); - setLastMouseDownPos(pos); } if (tool === 'eraser') { @@ -167,13 +168,10 @@ export const setStageEventHandlers = ({ layerId: selectedLayer.id, points: [pos.x, pos.y, pos.x, pos.y], }); - setIsDrawing(true); - setLastMouseDownPos(pos); } if (tool === 'rect') { - setIsDrawing(true); - setLastMouseDownPos(snapPosToStage(pos, stage)); + // Setting the last mouse down pos starts a rect } }); @@ -204,14 +202,13 @@ export const setStageEventHandlers = ({ if (tool === 'rect') { const lastMouseDownPos = getLastMouseDownPos(); if (lastMouseDownPos) { - const snappedPos = snapPosToStage(pos, stage); onRectShapeAdded({ layerId: selectedLayer.id, rect: { - x: Math.min(snappedPos.x, lastMouseDownPos.x), - y: Math.min(snappedPos.y, lastMouseDownPos.y), - width: Math.abs(snappedPos.x - lastMouseDownPos.x), - height: Math.abs(snappedPos.y - lastMouseDownPos.y), + x: Math.min(pos.x, lastMouseDownPos.x), + y: Math.min(pos.y, lastMouseDownPos.y), + width: Math.abs(pos.x - lastMouseDownPos.x), + height: Math.abs(pos.y - lastMouseDownPos.y), }, color: selectedLayer.type === 'raster_layer' ? getBrushColor() : DEFAULT_RGBA_COLOR, }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts index b2d7a0f00a2..d26f2a29cb6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts @@ -17,7 +17,7 @@ import { PREVIEW_RECT_ID, PREVIEW_TOOL_GROUP_ID, } from 'features/controlLayers/konva/naming'; -import { selectRenderableLayers, snapPosToStage } from 'features/controlLayers/konva/util'; +import { selectRenderableLayers } from 'features/controlLayers/konva/util'; import type { Layer, RgbaColor, Tool } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect, Vector2d } from 'konva/lib/types'; @@ -68,7 +68,6 @@ export const getBboxPreviewGroup = ( listening: true, strokeEnabled: false, draggable: true, - fill: 'rgba(255,0,0,0.3)', ...getBbox(), }); bboxRect.on('dragmove', () => { @@ -421,13 +420,12 @@ export const renderToolPreview = ( } if (cursorPos && lastMouseDownPos && tool === 'rect') { - const snappedPos = snapPosToStage(cursorPos, stage); - const rectPreview = brushPreviewGroup.findOne(`#${PREVIEW_RECT_ID}`); + const rectPreview = toolPreviewGroup.findOne(`#${PREVIEW_RECT_ID}`); rectPreview?.setAttrs({ - x: Math.min(snappedPos.x, lastMouseDownPos.x), - y: Math.min(snappedPos.y, lastMouseDownPos.y), - width: Math.abs(snappedPos.x - lastMouseDownPos.x), - height: Math.abs(snappedPos.y - lastMouseDownPos.y), + x: Math.min(cursorPos.x, lastMouseDownPos.x), + y: Math.min(cursorPos.y, lastMouseDownPos.y), + width: Math.abs(cursorPos.x - lastMouseDownPos.x), + height: Math.abs(cursorPos.y - lastMouseDownPos.y), fill: rgbaColorToString(brushColor), }); rectPreview?.visible(true); From c60f1c0031c89364e59f678ce81b82ca6ac0b0e1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 12 Jun 2024 17:12:53 +1000 Subject: [PATCH 032/678] feat(ui): bbox tool --- .../controlLayers/components/ToolChooser.tsx | 21 ++++++++++++++++++- .../konva/renderers/previewLayer.ts | 18 ++++++++-------- .../src/features/controlLayers/store/types.ts | 2 +- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx index 0327d7de9b4..9e26fed592a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx @@ -11,7 +11,14 @@ import { import { useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; -import { PiArrowsOutCardinalBold, PiEraserBold, PiHandBold, PiPaintBrushBold, PiRectangleBold } from 'react-icons/pi'; +import { + PiArrowsOutCardinalBold, + PiBoundingBoxBold, + PiEraserBold, + PiHandBold, + PiPaintBrushBold, + PiRectangleBold, +} from 'react-icons/pi'; const selectIsDisabled = createSelector(selectControlLayersSlice, (controlLayers) => { const selectedLayer = controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId); @@ -45,6 +52,10 @@ export const ToolChooser: React.FC = () => { $tool.set('view'); }, []); useHotkeys('h', setToolToView, { enabled: !isDisabled }, [isDisabled]); + const setToolToBbox = useCallback(() => { + $tool.set('bbox'); + }, []); + useHotkeys('q', setToolToBbox, { enabled: !isDisabled }, [isDisabled]); const resetSelectedLayer = useCallback(() => { if (selectedLayerId === null) { @@ -101,6 +112,14 @@ export const ToolChooser: React.FC = () => { onClick={setToolToView} isDisabled={isDisabled} /> + } + variant={tool === 'bbox' ? 'solid' : 'outline'} + onClick={setToolToBbox} + isDisabled={isDisabled} + /> ); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts index d26f2a29cb6..1947d868e33 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts @@ -62,10 +62,10 @@ export const getBboxPreviewGroup = ( // Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully // transparent rect for this purpose. - bboxPreviewGroup = new Konva.Group({ id: PREVIEW_GENERATION_BBOX_GROUP }); + bboxPreviewGroup = new Konva.Group({ id: PREVIEW_GENERATION_BBOX_GROUP, listening: false }); const bboxRect = new Konva.Rect({ id: PREVIEW_GENERATION_BBOX_DUMMY_RECT, - listening: true, + listening: false, strokeEnabled: false, draggable: true, ...getBbox(), @@ -269,12 +269,10 @@ export const renderBboxPreview = ( ); const bboxRect = bboxGroup.findOne(`#${PREVIEW_GENERATION_BBOX_DUMMY_RECT}`); const bboxTransformer = bboxGroup.findOne(`#${PREVIEW_GENERATION_BBOX_TRANSFORMER}`); + bboxGroup.listening(tool === 'bbox'); // This updates the bbox during transformation - bboxRect?.setAttrs({ ...bbox, scaleX: 1, scaleY: 1, listening: tool === 'move' }); - bboxTransformer?.setAttrs({ - listening: tool === 'move', - enabledAnchors: tool === 'move' ? ALL_ANCHORS : NO_ANCHORS, - }); + bboxRect?.setAttrs({ ...bbox, scaleX: 1, scaleY: 1, listening: tool === 'bbox' }); + bboxTransformer?.setAttrs({ listening: tool === 'bbox', enabledAnchors: tool === 'bbox' ? ALL_ANCHORS : NO_ANCHORS }); }; export const getToolPreviewGroup = (stage: Konva.Stage): Konva.Group => { @@ -365,9 +363,11 @@ export const renderToolPreview = ( } else if (tool === 'rect') { // Rect gets a crosshair stage.container().style.cursor = 'crosshair'; - } else { - // Else we hide the native cursor and use the konva-rendered brush preview + } else if (tool === 'brush' || tool === 'eraser') { + // Hide the native cursor and use the konva-rendered brush preview stage.container().style.cursor = 'none'; + } else if (tool === 'bbox') { + stage.container().style.cursor = 'default'; } stage.draggable(tool === 'view'); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 860c8b5586d..5380fe79d5e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -23,7 +23,7 @@ import type { IRect } from 'konva/lib/types'; import type { ImageDTO } from 'services/api/types'; import { z } from 'zod'; -const zTool = z.enum(['brush', 'eraser', 'move', 'rect', 'view']); +const zTool = z.enum(['brush', 'eraser', 'move', 'rect', 'view', 'bbox']); export type Tool = z.infer; const zDrawingTool = zTool.extract(['brush', 'eraser']); From bcfdae62e364734f30f29815ead65c2b760606cb Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 14 Jun 2024 14:19:35 +1000 Subject: [PATCH 033/678] refactor(ui): canvas v2 (wip) --- .../listeners/controlAdapterPreprocessor.ts | 36 +- .../listeners/imageDropped.ts | 16 +- .../listeners/imageUploaded.ts | 12 +- invokeai/frontend/web/src/app/store/store.ts | 32 +- .../src/common/hooks/useIsReadyToEnqueue.ts | 8 +- .../web/src/common/util/arrayUtils.test.ts | 199 +++- .../web/src/common/util/arrayUtils.ts | 44 +- .../components/AddLayerButton.tsx | 6 +- .../components/AddPromptButtons.tsx | 12 +- .../CALayer/CALayerControlAdapterWrapper.tsx | 22 +- .../components/ControlLayersPanelContent.tsx | 8 +- .../IPALayer/IPALayerIPAdapterWrapper.tsx | 16 +- .../LayerCommon/LayerMenuArrangeActions.tsx | 4 +- .../LayerCommon/LayerMenuRGActions.tsx | 12 +- .../components/LayerCommon/LayerOpacity.tsx | 4 +- .../components/LayerCommon/LayerTitle.tsx | 4 +- .../components/RGLayer/RGLayer.tsx | 4 +- .../RGLayer/RGLayerAutoNegativeCheckbox.tsx | 6 +- .../components/RGLayer/RGLayerColorPicker.tsx | 6 +- .../RGLayer/RGLayerIPAdapterList.tsx | 4 +- .../RGLayer/RGLayerIPAdapterWrapper.tsx | 28 +- .../RGLayer/RGLayerNegativePrompt.tsx | 4 +- .../RGLayer/RGLayerPositivePrompt.tsx | 4 +- .../RGLayer/RGLayerPromptDeleteButton.tsx | 8 +- .../components/StageComponent.tsx | 53 +- .../controlLayers/components/ToolChooser.tsx | 4 +- .../controlLayers/hooks/addLayerHooks.ts | 12 +- .../controlLayers/hooks/layerStateHooks.ts | 12 +- .../features/controlLayers/konva/events.ts | 19 +- .../features/controlLayers/konva/naming.ts | 2 + .../controlLayers/konva/renderers/bbox.ts | 8 +- .../controlLayers/konva/renderers/layers.ts | 16 +- .../controlLayers/konva/renderers/objects.ts | 4 +- .../konva/renderers/previewLayer.ts | 4 +- .../src/features/controlLayers/konva/util.ts | 4 +- .../store/controlAdaptersSlice.ts | 282 +++++ .../controlLayers/store/controlLayersSlice.ts | 1019 ++--------------- .../controlLayers/store/inpaintMaskSlice.ts | 0 .../controlLayers/store/ipAdaptersSlice.ts | 140 +++ .../controlLayers/store/layersSlice.ts | 268 +++++ .../store/regionalGuidanceSlice.ts | 440 +++++++ .../src/features/controlLayers/store/types.ts | 263 +++-- .../controlLayers/util/controlAdapters.ts | 12 +- .../components/DeleteImageModal.tsx | 4 +- .../deleteImageModal/store/selectors.ts | 8 +- .../components/Boards/DeleteBoardModal.tsx | 4 +- .../metadata/components/MetadataLayers.tsx | 8 +- .../src/features/metadata/util/handlers.ts | 6 +- .../web/src/features/metadata/util/parsers.ts | 10 +- .../src/features/metadata/util/recallers.ts | 18 +- .../src/features/metadata/util/validators.ts | 8 +- .../util/graph/generation/addControlLayers.ts | 10 +- .../queue/components/QueueButtonTooltip.tsx | 4 +- .../ImageSettingsAccordion.tsx | 4 +- 54 files changed, 1755 insertions(+), 1390 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskSlice.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersSlice.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts index cd8fb69ca08..3c7d626e773 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts @@ -4,12 +4,12 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware' import type { AppDispatch } from 'app/store/store'; import { parseify } from 'common/util/serialize'; import { - caLayerImageChanged, - caLayerModelChanged, - caLayerProcessedImageChanged, - caLayerProcessorConfigChanged, - caLayerProcessorPendingBatchIdChanged, - caLayerRecalled, + controlAdapterImageChanged, + controlAdapterModelChanged, + controlAdapterProcessedImageChanged, + controlAdapterProcessorConfigChanged, + controlAdapterProcessorPendingBatchIdChanged, + controlAdapterRecalled, } from 'features/controlLayers/store/controlLayersSlice'; import { isControlAdapterLayer } from 'features/controlLayers/store/types'; import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; @@ -23,11 +23,11 @@ import { socketInvocationComplete } from 'services/events/actions'; import { assert } from 'tsafe'; const matcher = isAnyOf( - caLayerImageChanged, - caLayerProcessedImageChanged, - caLayerProcessorConfigChanged, - caLayerModelChanged, - caLayerRecalled + controlAdapterImageChanged, + controlAdapterProcessedImageChanged, + controlAdapterProcessorConfigChanged, + controlAdapterModelChanged, + controlAdapterRecalled ); const DEBOUNCE_MS = 300; @@ -46,7 +46,7 @@ const cancelProcessorBatch = async (dispatch: AppDispatch, layerId: string, batc } finally { req.reset(); // Always reset the pending batch ID - the cancel req could fail if the batch doesn't exist - dispatch(caLayerProcessorPendingBatchIdChanged({ layerId, batchId: null })); + dispatch(controlAdapterProcessorPendingBatchIdChanged({ layerId, batchId: null })); } }; @@ -54,7 +54,7 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni startAppListening({ matcher, effect: async (action, { dispatch, getState, getOriginalState, cancelActiveListeners, delay, take, signal }) => { - const layerId = caLayerRecalled.match(action) ? action.payload.id : action.payload.layerId; + const layerId = controlAdapterRecalled.match(action) ? action.payload.id : action.payload.layerId; const state = getState(); const originalState = getOriginalState(); @@ -91,7 +91,7 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni // - If we have no image, we have nothing to process // - If we have no processor config, we have nothing to process // Clear the processed image and bail - dispatch(caLayerProcessedImageChanged({ layerId, imageDTO: null })); + dispatch(controlAdapterProcessedImageChanged({ layerId, imageDTO: null })); return; } @@ -132,7 +132,7 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni const enqueueResult = await req.unwrap(); // TODO(psyche): Update the pydantic models, pretty sure we will _always_ have a batch_id here, but the model says it's optional assert(enqueueResult.batch.batch_id, 'Batch ID not returned from queue'); - dispatch(caLayerProcessorPendingBatchIdChanged({ layerId, batchId: enqueueResult.batch.batch_id })); + dispatch(controlAdapterProcessorPendingBatchIdChanged({ layerId, batchId: enqueueResult.batch.batch_id })); log.debug({ enqueueResult: parseify(enqueueResult) }, t('queue.graphQueued')); // Wait for the processor node to complete @@ -155,8 +155,8 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni // Whew! We made it. Update the layer with the processed image log.debug({ layerId, imageDTO }, 'ControlNet image processed'); - dispatch(caLayerProcessedImageChanged({ layerId, imageDTO })); - dispatch(caLayerProcessorPendingBatchIdChanged({ layerId, batchId: null })); + dispatch(controlAdapterProcessedImageChanged({ layerId, imageDTO })); + dispatch(controlAdapterProcessorPendingBatchIdChanged({ layerId, batchId: null })); } catch (error) { if (signal.aborted) { // The listener was canceled - we need to cancel the pending processor batch, if there is one (could have changed by now). @@ -174,7 +174,7 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni if (error instanceof Object) { if ('data' in error && 'status' in error) { if (error.status === 403) { - dispatch(caLayerImageChanged({ layerId, imageDTO: null })); + dispatch(controlAdapterImageChanged({ layerId, imageDTO: null })); return; } } diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index b5431508cf7..663a8f7d193 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -8,11 +8,11 @@ import { controlAdapterIsEnabledChanged, } from 'features/controlAdapters/store/controlAdaptersSlice'; import { - caLayerImageChanged, + controlAdapterImageChanged, iiLayerImageChanged, - imageAdded, - ipaLayerImageChanged, - rgLayerIPAdapterImageChanged, + layerImageAdded, + ipAdapterImageChanged, + regionalGuidanceIPAdapterImageChanged, } from 'features/controlLayers/store/controlLayersSlice'; import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import { isValidDrop } from 'features/dnd/util/isValidDrop'; @@ -99,7 +99,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => ) { const { layerId } = overData.context; dispatch( - caLayerImageChanged({ + controlAdapterImageChanged({ layerId, imageDTO: activeData.payload.imageDTO, }) @@ -117,7 +117,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => ) { const { layerId } = overData.context; dispatch( - ipaLayerImageChanged({ + ipAdapterImageChanged({ layerId, imageDTO: activeData.payload.imageDTO, }) @@ -135,7 +135,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => ) { const { layerId, ipAdapterId } = overData.context; dispatch( - rgLayerIPAdapterImageChanged({ + regionalGuidanceIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO: activeData.payload.imageDTO, @@ -172,7 +172,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => ) { const { layerId } = overData.context; dispatch( - imageAdded({ + layerImageAdded({ layerId, imageDTO: activeData.payload.imageDTO, }) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts index 1aa47345e18..3c9059d9c95 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts @@ -6,10 +6,10 @@ import { controlAdapterIsEnabledChanged, } from 'features/controlAdapters/store/controlAdaptersSlice'; import { - caLayerImageChanged, + controlAdapterImageChanged, iiLayerImageChanged, - ipaLayerImageChanged, - rgLayerIPAdapterImageChanged, + ipAdapterImageChanged, + regionalGuidanceIPAdapterImageChanged, } from 'features/controlLayers/store/controlLayersSlice'; import { selectListBoardsQueryArgs } from 'features/gallery/store/gallerySelectors'; import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; @@ -122,7 +122,7 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis if (postUploadAction?.type === 'SET_CA_LAYER_IMAGE') { const { layerId } = postUploadAction; - dispatch(caLayerImageChanged({ layerId, imageDTO })); + dispatch(controlAdapterImageChanged({ layerId, imageDTO })); toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage'), @@ -131,7 +131,7 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis if (postUploadAction?.type === 'SET_IPA_LAYER_IMAGE') { const { layerId } = postUploadAction; - dispatch(ipaLayerImageChanged({ layerId, imageDTO })); + dispatch(ipAdapterImageChanged({ layerId, imageDTO })); toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage'), @@ -140,7 +140,7 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis if (postUploadAction?.type === 'SET_RG_LAYER_IP_ADAPTER_IMAGE') { const { layerId, ipAdapterId } = postUploadAction; - dispatch(rgLayerIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO })); + dispatch(regionalGuidanceIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO })); toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage'), diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index cacea30e482..0fcb842db3f 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -7,14 +7,16 @@ import type { JSONObject } from 'common/types'; import { canvasPersistConfig, canvasSlice } from 'features/canvas/store/canvasSlice'; import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice'; import { - controlAdaptersPersistConfig, - controlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; + controlAdaptersV2PersistConfig, + controlAdaptersV2Slice, +} from 'features/controlLayers/store/controlAdaptersSlice'; +import { canvasV2PersistConfig, canvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; +import { ipAdaptersPersistConfig, ipAdaptersSlice } from 'features/controlLayers/store/ipAdaptersSlice'; +import { layersPersistConfig, layersSlice } from 'features/controlLayers/store/layersSlice'; import { - controlLayersPersistConfig, - controlLayersSlice, - controlLayersUndoableConfig, -} from 'features/controlLayers/store/controlLayersSlice'; + regionalGuidancePersistConfig, + regionalGuidanceSlice, +} from 'features/controlLayers/store/regionalGuidanceSlice'; import { deleteImageModalSlice } from 'features/deleteImageModal/store/slice'; import { dynamicPromptsPersistConfig, dynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/gallerySlice'; @@ -49,6 +51,7 @@ import { stateSanitizer } from './middleware/devtools/stateSanitizer'; import { listenerMiddleware } from './middleware/listenerMiddleware'; const allReducers = { + [api.reducerPath]: api.reducer, [canvasSlice.name]: canvasSlice.reducer, [gallerySlice.name]: gallerySlice.reducer, [generationSlice.name]: generationSlice.reducer, @@ -56,7 +59,6 @@ const allReducers = { [systemSlice.name]: systemSlice.reducer, [configSlice.name]: configSlice.reducer, [uiSlice.name]: uiSlice.reducer, - [controlAdaptersSlice.name]: controlAdaptersSlice.reducer, [dynamicPromptsSlice.name]: dynamicPromptsSlice.reducer, [deleteImageModalSlice.name]: deleteImageModalSlice.reducer, [changeBoardModalSlice.name]: changeBoardModalSlice.reducer, @@ -66,11 +68,14 @@ const allReducers = { [queueSlice.name]: queueSlice.reducer, [workflowSlice.name]: workflowSlice.reducer, [hrfSlice.name]: hrfSlice.reducer, - [controlLayersSlice.name]: undoable(controlLayersSlice.reducer, controlLayersUndoableConfig), + [canvasV2Slice.name]: canvasV2Slice.reducer, [workflowSettingsSlice.name]: workflowSettingsSlice.reducer, - [api.reducerPath]: api.reducer, [upscaleSlice.name]: upscaleSlice.reducer, [stylePresetSlice.name]: stylePresetSlice.reducer, + [layersSlice.name]: layersSlice.reducer, + [controlAdaptersV2Slice.name]: controlAdaptersV2Slice.reducer, + [ipAdaptersSlice.name]: ipAdaptersSlice.reducer, + [regionalGuidanceSlice.name]: regionalGuidanceSlice.reducer, }; const rootReducer = combineReducers(allReducers); @@ -107,16 +112,19 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = { [systemPersistConfig.name]: systemPersistConfig, [workflowPersistConfig.name]: workflowPersistConfig, [uiPersistConfig.name]: uiPersistConfig, - [controlAdaptersPersistConfig.name]: controlAdaptersPersistConfig, [dynamicPromptsPersistConfig.name]: dynamicPromptsPersistConfig, [sdxlPersistConfig.name]: sdxlPersistConfig, [loraPersistConfig.name]: loraPersistConfig, [modelManagerV2PersistConfig.name]: modelManagerV2PersistConfig, [hrfPersistConfig.name]: hrfPersistConfig, - [controlLayersPersistConfig.name]: controlLayersPersistConfig, + [canvasV2PersistConfig.name]: canvasV2PersistConfig, [workflowSettingsPersistConfig.name]: workflowSettingsPersistConfig, [upscalePersistConfig.name]: upscalePersistConfig, [stylePresetPersistConfig.name]: stylePresetPersistConfig, + [layersPersistConfig.name]: layersPersistConfig, + [controlAdaptersV2PersistConfig.name]: controlAdaptersV2PersistConfig, + [ipAdaptersPersistConfig.name]: ipAdaptersPersistConfig, + [regionalGuidancePersistConfig.name]: regionalGuidancePersistConfig, }; const unserialize: UnserializeFunction = (data, key) => { diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index 1d610d32c22..ac47d9005da 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -6,8 +6,8 @@ import { selectControlAdaptersSlice, } from 'features/controlAdapters/store/controlAdaptersSlice'; import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; -import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; -import type { Layer } from 'features/controlLayers/store/types'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; +import type { LayerData } from 'features/controlLayers/store/types'; import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; @@ -24,7 +24,7 @@ import { forEach, upperFirst } from 'lodash-es'; import { useMemo } from 'react'; import { getConnectedEdges } from 'reactflow'; -const LAYER_TYPE_TO_TKEY: Record = { +const LAYER_TYPE_TO_TKEY: Record = { initial_image_layer: 'controlLayers.globalInitialImage', control_adapter_layer: 'controlLayers.globalControlAdapter', ip_adapter_layer: 'controlLayers.globalIPAdapter', @@ -41,7 +41,7 @@ const createSelector = (templates: Templates) => selectNodesSlice, selectWorkflowSettingsSlice, selectDynamicPromptsSlice, - selectControlLayersSlice, + selectCanvasV2Slice, activeTabNameSelector, selectUpscalelice, selectConfigSlice, diff --git a/invokeai/frontend/web/src/common/util/arrayUtils.test.ts b/invokeai/frontend/web/src/common/util/arrayUtils.test.ts index 5d0fd090f75..e1922fdbbeb 100644 --- a/invokeai/frontend/web/src/common/util/arrayUtils.test.ts +++ b/invokeai/frontend/web/src/common/util/arrayUtils.test.ts @@ -1,85 +1,170 @@ -import { moveBackward, moveForward, moveToBack, moveToFront } from 'common/util/arrayUtils'; +import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { describe, expect, it } from 'vitest'; describe('Array Manipulation Functions', () => { const originalArray = ['a', 'b', 'c', 'd']; - describe('moveForwardOne', () => { - it('should move an item forward by one position', () => { - const array = [...originalArray]; - const result = moveForward(array, (item) => item === 'b'); - expect(result).toEqual(['a', 'c', 'b', 'd']); - }); - it('should do nothing if the item is at the end', () => { - const array = [...originalArray]; - const result = moveForward(array, (item) => item === 'd'); - expect(result).toEqual(['a', 'b', 'c', 'd']); + describe('moveOneToEnd', () => { + describe('with callback', () => { + it('should move an item forward by one position', () => { + const array = [...originalArray]; + const result = moveOneToEnd(array, (item) => item === 'b'); + expect(result).toEqual(['a', 'c', 'b', 'd']); + }); + + it('should do nothing if the item is at the end', () => { + const array = [...originalArray]; + const result = moveOneToEnd(array, (item) => item === 'd'); + expect(result).toEqual(['a', 'b', 'c', 'd']); + }); + + it("should leave the array unchanged if the item isn't in the array", () => { + const array = [...originalArray]; + const result = moveOneToEnd(array, (item) => item === 'z'); + expect(result).toEqual(originalArray); + }); }); + describe('with item', () => { + it('should move an item forward by one position', () => { + const array = [...originalArray]; + const result = moveOneToEnd(array, (item) => item === 'b'); + expect(result).toEqual(['a', 'c', 'b', 'd']); + }); + + it('should do nothing if the item is at the end', () => { + const array = [...originalArray]; + const result = moveOneToEnd(array, (item) => item === 'd'); + expect(result).toEqual(['a', 'b', 'c', 'd']); + }); - it("should leave the array unchanged if the item isn't in the array", () => { - const array = [...originalArray]; - const result = moveForward(array, (item) => item === 'z'); - expect(result).toEqual(originalArray); + it("should leave the array unchanged if the item isn't in the array", () => { + const array = [...originalArray]; + const result = moveOneToEnd(array, (item) => item === 'z'); + expect(result).toEqual(originalArray); + }); }); }); - describe('moveToFront', () => { - it('should move an item to the front', () => { - const array = [...originalArray]; - const result = moveToFront(array, (item) => item === 'c'); - expect(result).toEqual(['c', 'a', 'b', 'd']); - }); + describe('moveToStart', () => { + describe('with callback', () => { + it('should move an item to the front', () => { + const array = [...originalArray]; + const result = moveToStart(array, (item) => item === 'c'); + expect(result).toEqual(['c', 'a', 'b', 'd']); + }); + + it('should do nothing if the item is already at the front', () => { + const array = [...originalArray]; + const result = moveToStart(array, (item) => item === 'a'); + expect(result).toEqual(['a', 'b', 'c', 'd']); + }); - it('should do nothing if the item is already at the front', () => { - const array = [...originalArray]; - const result = moveToFront(array, (item) => item === 'a'); - expect(result).toEqual(['a', 'b', 'c', 'd']); + it("should leave the array unchanged if the item isn't in the array", () => { + const array = [...originalArray]; + const result = moveToStart(array, (item) => item === 'z'); + expect(result).toEqual(originalArray); + }); }); + describe('with item', () => { + it('should move an item to the front', () => { + const array = [...originalArray]; + const result = moveToStart(array, 'c'); + expect(result).toEqual(['c', 'a', 'b', 'd']); + }); + + it('should do nothing if the item is already at the front', () => { + const array = [...originalArray]; + const result = moveToStart(array, 'a'); + expect(result).toEqual(['a', 'b', 'c', 'd']); + }); - it("should leave the array unchanged if the item isn't in the array", () => { - const array = [...originalArray]; - const result = moveToFront(array, (item) => item === 'z'); - expect(result).toEqual(originalArray); + it("should leave the array unchanged if the item isn't in the array", () => { + const array = [...originalArray]; + const result = moveToStart(array, 'z'); + expect(result).toEqual(originalArray); + }); }); }); - describe('moveBackwardsOne', () => { - it('should move an item backward by one position', () => { - const array = [...originalArray]; - const result = moveBackward(array, (item) => item === 'c'); - expect(result).toEqual(['a', 'c', 'b', 'd']); - }); + describe('moveOneToStart', () => { + describe('with callback', () => { + it('should move an item backward by one position', () => { + const array = [...originalArray]; + const result = moveOneToStart(array, (item) => item === 'c'); + expect(result).toEqual(['a', 'c', 'b', 'd']); + }); + + it('should do nothing if the item is at the beginning', () => { + const array = [...originalArray]; + const result = moveOneToStart(array, (item) => item === 'a'); + expect(result).toEqual(['a', 'b', 'c', 'd']); + }); - it('should do nothing if the item is at the beginning', () => { - const array = [...originalArray]; - const result = moveBackward(array, (item) => item === 'a'); - expect(result).toEqual(['a', 'b', 'c', 'd']); + it("should leave the array unchanged if the item isn't in the array", () => { + const array = [...originalArray]; + const result = moveOneToStart(array, (item) => item === 'z'); + expect(result).toEqual(originalArray); + }); }); + describe('with item', () => { + it('should move an item backward by one position', () => { + const array = [...originalArray]; + const result = moveOneToStart(array, 'c'); + expect(result).toEqual(['a', 'c', 'b', 'd']); + }); + + it('should do nothing if the item is at the beginning', () => { + const array = [...originalArray]; + const result = moveOneToStart(array, 'a'); + expect(result).toEqual(['a', 'b', 'c', 'd']); + }); - it("should leave the array unchanged if the item isn't in the array", () => { - const array = [...originalArray]; - const result = moveBackward(array, (item) => item === 'z'); - expect(result).toEqual(originalArray); + it("should leave the array unchanged if the item isn't in the array", () => { + const array = [...originalArray]; + const result = moveOneToStart(array, 'z'); + expect(result).toEqual(originalArray); + }); }); }); - describe('moveToBack', () => { - it('should move an item to the back', () => { - const array = [...originalArray]; - const result = moveToBack(array, (item) => item === 'b'); - expect(result).toEqual(['a', 'c', 'd', 'b']); - }); + describe('moveToEnd', () => { + describe('with callback', () => { + it('should move an item to the back', () => { + const array = [...originalArray]; + const result = moveToEnd(array, (item) => item === 'b'); + expect(result).toEqual(['a', 'c', 'd', 'b']); + }); + + it('should do nothing if the item is already at the back', () => { + const array = [...originalArray]; + const result = moveToEnd(array, (item) => item === 'd'); + expect(result).toEqual(['a', 'b', 'c', 'd']); + }); - it('should do nothing if the item is already at the back', () => { - const array = [...originalArray]; - const result = moveToBack(array, (item) => item === 'd'); - expect(result).toEqual(['a', 'b', 'c', 'd']); + it("should leave the array unchanged if the item isn't in the array", () => { + const array = [...originalArray]; + const result = moveToEnd(array, (item) => item === 'z'); + expect(result).toEqual(originalArray); + }); }); + describe('with item', () => { + it('should move an item to the back', () => { + const array = [...originalArray]; + const result = moveToEnd(array, 'b'); + expect(result).toEqual(['a', 'c', 'd', 'b']); + }); + + it('should do nothing if the item is already at the back', () => { + const array = [...originalArray]; + const result = moveToEnd(array, 'd'); + expect(result).toEqual(['a', 'b', 'c', 'd']); + }); - it("should leave the array unchanged if the item isn't in the array", () => { - const array = [...originalArray]; - const result = moveToBack(array, (item) => item === 'z'); - expect(result).toEqual(originalArray); + it("should leave the array unchanged if the item isn't in the array", () => { + const array = [...originalArray]; + const result = moveToEnd(array, 'z'); + expect(result).toEqual(originalArray); + }); }); }); }); diff --git a/invokeai/frontend/web/src/common/util/arrayUtils.ts b/invokeai/frontend/web/src/common/util/arrayUtils.ts index 38c99b63ec4..9f0d4cfbf6c 100644 --- a/invokeai/frontend/web/src/common/util/arrayUtils.ts +++ b/invokeai/frontend/web/src/common/util/arrayUtils.ts @@ -1,37 +1,45 @@ -export const moveForward = (array: T[], callback: (item: T) => boolean): T[] => { - const index = array.findIndex(callback); - if (index >= 0 && index < array.length - 1) { - //@ts-expect-error - These indicies are safe per the previous check - [array[index], array[index + 1]] = [array[index + 1], array[index]]; - } - return array; -}; - -export const moveToFront = (array: T[], callback: (item: T) => boolean): T[] => { - const index = array.findIndex(callback); +export function moveToStart(array: T[], selectItemCallback: (item: T) => boolean): T[]; +export function moveToStart(array: T[], item: T): T[]; +export function moveToStart(array: T[], arg1: T | ((item: T) => boolean)): T[] { + const index = arg1 instanceof Function ? array.findIndex(arg1) : array.indexOf(arg1); if (index > 0) { const [item] = array.splice(index, 1); //@ts-expect-error - These indicies are safe per the previous check array.unshift(item); } return array; -}; +} -export const moveBackward = (array: T[], callback: (item: T) => boolean): T[] => { - const index = array.findIndex(callback); +export function moveOneToStart(array: T[], selectItemCallback: (item: T) => boolean): T[]; +export function moveOneToStart(array: T[], item: T): T[]; +export function moveOneToStart(array: T[], arg1: T | ((item: T) => boolean)): T[] { + const index = arg1 instanceof Function ? array.findIndex(arg1) : array.indexOf(arg1); if (index > 0) { //@ts-expect-error - These indicies are safe per the previous check [array[index], array[index - 1]] = [array[index - 1], array[index]]; } return array; -}; +} -export const moveToBack = (array: T[], callback: (item: T) => boolean): T[] => { - const index = array.findIndex(callback); +export function moveToEnd(array: T[], selectItemCallback: (item: T) => boolean): T[]; +export function moveToEnd(array: T[], item: T): T[]; +export function moveToEnd(array: T[], arg1: T | ((item: T) => boolean)): T[] { + const index = arg1 instanceof Function ? array.findIndex(arg1) : array.indexOf(arg1); if (index >= 0 && index < array.length - 1) { const [item] = array.splice(index, 1); //@ts-expect-error - These indicies are safe per the previous check array.push(item); } return array; -}; +} + +export function moveOneToEnd(array: T[], selectItemCallback: (item: T) => boolean): T[]; +export function moveOneToEnd(array: T[], item: T): T[]; +export function moveOneToEnd(array: T[], arg1: T | ((item: T) => boolean)): T[] { + const index = arg1 instanceof Function ? array.findIndex(arg1) : array.indexOf(arg1); + if (index >= 0 && index < array.length - 1) { + //@ts-expect-error - These indicies are safe per the previous check + [array[index], array[index + 1]] = [array[index + 1], array[index]]; + } + return array; +} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx index fdfa70ae2cd..72d18c3d177 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx @@ -1,7 +1,7 @@ import { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { useAddCALayer, useAddIILayer, useAddIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; -import { rasterLayerAdded, rgLayerAdded } from 'features/controlLayers/store/controlLayersSlice'; +import { layerAdded, regionalGuidanceAdded } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPlusBold } from 'react-icons/pi'; @@ -13,10 +13,10 @@ export const AddLayerButton = memo(() => { const [addIPALayer, isAddIPALayerDisabled] = useAddIPALayer(); const [addIILayer, isAddIILayerDisabled] = useAddIILayer(); const addRGLayer = useCallback(() => { - dispatch(rgLayerAdded()); + dispatch(regionalGuidanceAdded()); }, [dispatch]); const addRasterLayer = useCallback(() => { - dispatch(rasterLayerAdded()); + dispatch(layerAdded()); }, [dispatch]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx index e339d8315e0..073a1888717 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx @@ -3,9 +3,9 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAddIPAdapterToIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; import { - rgLayerNegativePromptChanged, - rgLayerPositivePromptChanged, - selectControlLayersSlice, + regionalGuidanceNegativePromptChanged, + regionalGuidancePositivePromptChanged, + selectCanvasV2Slice, } from 'features/controlLayers/store/controlLayersSlice'; import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import { useCallback, useMemo } from 'react'; @@ -22,7 +22,7 @@ export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => { const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToIPALayer(layerId); const selectValidActions = useMemo( () => - createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { + createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { const layer = controlLayers.present.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); return { @@ -34,10 +34,10 @@ export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => { ); const validActions = useAppSelector(selectValidActions); const addPositivePrompt = useCallback(() => { - dispatch(rgLayerPositivePromptChanged({ layerId, prompt: '' })); + dispatch(regionalGuidancePositivePromptChanged({ layerId, prompt: '' })); }, [dispatch, layerId]); const addNegativePrompt = useCallback(() => { - dispatch(rgLayerNegativePromptChanged({ layerId, prompt: '' })); + dispatch(regionalGuidanceNegativePromptChanged({ layerId, prompt: '' })); }, [dispatch, layerId]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx index 6c498fe1aa5..7b0e3aa3327 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx @@ -1,13 +1,13 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { ControlAdapter } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapter'; import { - caLayerControlModeChanged, - caLayerImageChanged, - caLayerModelChanged, - caLayerProcessedImageChanged, - caLayerProcessorConfigChanged, caOrIPALayerBeginEndStepPctChanged, caOrIPALayerWeightChanged, + controlAdapterControlModeChanged, + controlAdapterImageChanged, + controlAdapterModelChanged, + controlAdapterProcessedImageChanged, + controlAdapterProcessorConfigChanged, selectLayerOrThrow, } from 'features/controlLayers/store/controlLayersSlice'; import { isControlAdapterLayer } from 'features/controlLayers/store/types'; @@ -46,7 +46,7 @@ export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => { const onChangeControlMode = useCallback( (controlMode: ControlModeV2) => { dispatch( - caLayerControlModeChanged({ + controlAdapterControlModeChanged({ layerId, controlMode, }) @@ -64,7 +64,7 @@ export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => { const onChangeProcessorConfig = useCallback( (processorConfig: ProcessorConfig | null) => { - dispatch(caLayerProcessorConfigChanged({ layerId, processorConfig })); + dispatch(controlAdapterProcessorConfigChanged({ layerId, processorConfig })); }, [dispatch, layerId] ); @@ -72,7 +72,7 @@ export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => { const onChangeModel = useCallback( (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => { dispatch( - caLayerModelChanged({ + controlAdapterModelChanged({ layerId, modelConfig, }) @@ -83,17 +83,17 @@ export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => { const onChangeImage = useCallback( (imageDTO: ImageDTO | null) => { - dispatch(caLayerImageChanged({ layerId, imageDTO })); + dispatch(controlAdapterImageChanged({ layerId, imageDTO })); }, [dispatch, layerId] ); const onErrorLoadingImage = useCallback(() => { - dispatch(caLayerImageChanged({ layerId, imageDTO: null })); + dispatch(controlAdapterImageChanged({ layerId, imageDTO: null })); }, [dispatch, layerId]); const onErrorLoadingProcessedImage = useCallback(() => { - dispatch(caLayerProcessedImageChanged({ layerId, imageDTO: null })); + dispatch(controlAdapterProcessedImageChanged({ layerId, imageDTO: null })); }, [dispatch, layerId]); const droppableData = useMemo( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index d4baabab8b6..90f21a7253c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -11,14 +11,14 @@ import { IILayer } from 'features/controlLayers/components/IILayer/IILayer'; import { IPALayer } from 'features/controlLayers/components/IPALayer/IPALayer'; import { RasterLayer } from 'features/controlLayers/components/RasterLayer/RasterLayer'; import { RGLayer } from 'features/controlLayers/components/RGLayer/RGLayer'; -import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; -import type { Layer } from 'features/controlLayers/store/types'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; +import type { LayerData } from 'features/controlLayers/store/types'; import { isRenderableLayer } from 'features/controlLayers/store/types'; import { partition } from 'lodash-es'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -const selectLayerIdTypePairs = createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { +const selectLayerIdTypePairs = createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { const [renderableLayers, ipAdapterLayers] = partition(controlLayers.present.layers, isRenderableLayer); return [...ipAdapterLayers, ...renderableLayers].map((l) => ({ id: l.id, type: l.type })).reverse(); }); @@ -50,7 +50,7 @@ ControlLayersPanelContent.displayName = 'ControlLayersPanelContent'; type LayerWrapperProps = { id: string; - type: Layer['type']; + type: LayerData['type']; }; const LayerWrapper = memo(({ id, type }: LayerWrapperProps) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx index 6492e3cf32f..08737e6e604 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx @@ -3,10 +3,10 @@ import { IPAdapter } from 'features/controlLayers/components/ControlAndIPAdapter import { caOrIPALayerBeginEndStepPctChanged, caOrIPALayerWeightChanged, - ipaLayerCLIPVisionModelChanged, - ipaLayerImageChanged, - ipaLayerMethodChanged, - ipaLayerModelChanged, + ipAdapterCLIPVisionModelChanged, + ipAdapterImageChanged, + ipAdapterMethodChanged, + ipAdapterModelChanged, selectLayerOrThrow, } from 'features/controlLayers/store/controlLayersSlice'; import { isIPAdapterLayer } from 'features/controlLayers/store/types'; @@ -46,28 +46,28 @@ export const IPALayerIPAdapterWrapper = memo(({ layerId }: Props) => { const onChangeIPMethod = useCallback( (method: IPMethodV2) => { - dispatch(ipaLayerMethodChanged({ layerId, method })); + dispatch(ipAdapterMethodChanged({ layerId, method })); }, [dispatch, layerId] ); const onChangeModel = useCallback( (modelConfig: IPAdapterModelConfig) => { - dispatch(ipaLayerModelChanged({ layerId, modelConfig })); + dispatch(ipAdapterModelChanged({ layerId, modelConfig })); }, [dispatch, layerId] ); const onChangeCLIPVisionModel = useCallback( (clipVisionModel: CLIPVisionModelV2) => { - dispatch(ipaLayerCLIPVisionModelChanged({ layerId, clipVisionModel })); + dispatch(ipAdapterCLIPVisionModelChanged({ layerId, clipVisionModel })); }, [dispatch, layerId] ); const onChangeImage = useCallback( (imageDTO: ImageDTO | null) => { - dispatch(ipaLayerImageChanged({ layerId, imageDTO })); + dispatch(ipAdapterImageChanged({ layerId, imageDTO })); }, [dispatch, layerId] ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuArrangeActions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuArrangeActions.tsx index 3e65eda783a..67cf856c817 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuArrangeActions.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuArrangeActions.tsx @@ -6,7 +6,7 @@ import { layerMovedForward, layerMovedToBack, layerMovedToFront, - selectControlLayersSlice, + selectCanvasV2Slice, } from 'features/controlLayers/store/controlLayersSlice'; import { isRenderableLayer } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; @@ -21,7 +21,7 @@ export const LayerMenuArrangeActions = memo(({ layerId }: Props) => { const { t } = useTranslation(); const selectValidActions = useMemo( () => - createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { + createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { const layer = controlLayers.present.layers.find((l) => l.id === layerId); assert(isRenderableLayer(layer), `Layer ${layerId} not found or not an RP layer`); const layerIndex = controlLayers.present.layers.findIndex((l) => l.id === layerId); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx index 905abfd00df..7addbf45eb8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx @@ -3,9 +3,9 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAddIPAdapterToIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; import { - rgLayerNegativePromptChanged, - rgLayerPositivePromptChanged, - selectControlLayersSlice, + regionalGuidanceNegativePromptChanged, + regionalGuidancePositivePromptChanged, + selectCanvasV2Slice, } from 'features/controlLayers/store/controlLayersSlice'; import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; @@ -21,7 +21,7 @@ export const LayerMenuRGActions = memo(({ layerId }: Props) => { const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToIPALayer(layerId); const selectValidActions = useMemo( () => - createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { + createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { const layer = controlLayers.present.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); return { @@ -33,10 +33,10 @@ export const LayerMenuRGActions = memo(({ layerId }: Props) => { ); const validActions = useAppSelector(selectValidActions); const addPositivePrompt = useCallback(() => { - dispatch(rgLayerPositivePromptChanged({ layerId, prompt: '' })); + dispatch(regionalGuidancePositivePromptChanged({ layerId, prompt: '' })); }, [dispatch, layerId]); const addNegativePrompt = useCallback(() => { - dispatch(rgLayerNegativePromptChanged({ layerId, prompt: '' })); + dispatch(regionalGuidanceNegativePromptChanged({ layerId, prompt: '' })); }, [dispatch, layerId]); return ( <> diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerOpacity.tsx index f488d9600a4..481a6597a8c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerOpacity.tsx @@ -16,7 +16,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; import { layerOpacityChanged, - selectControlLayersSlice, + selectCanvasV2Slice, selectLayerOrThrow, } from 'features/controlLayers/store/controlLayersSlice'; import { isLayerWithOpacity } from 'features/controlLayers/store/types'; @@ -36,7 +36,7 @@ export const LayerOpacity = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); const selectOpacity = useMemo( () => - createSelector(selectControlLayersSlice, (controlLayers) => { + createSelector(selectCanvasV2Slice, (controlLayers) => { const layer = selectLayerOrThrow(controlLayers.present, layerId, isLayerWithOpacity); return Math.round(layer.opacity * 100); }), diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx index a74729d91bb..053fbd234e0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx @@ -1,10 +1,10 @@ import { Text } from '@invoke-ai/ui-library'; -import type { Layer } from 'features/controlLayers/store/types'; +import type { LayerData } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; type Props = { - type: Layer['type']; + type: LayerData['type']; }; export const LayerTitle = memo(({ type }: Props) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx index fa552dd4cf2..54e6b502e65 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx @@ -8,7 +8,7 @@ import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMe import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; -import { layerSelected, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import { layerSelected, selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -29,7 +29,7 @@ export const RGLayer = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); const selector = useMemo( () => - createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { + createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { const layer = controlLayers.present.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); return { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx index c5a7be1c3e6..ec52062062f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx @@ -1,7 +1,7 @@ import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { rgLayerAutoNegativeChanged, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import { regionalGuidanceAutoNegativeChanged, selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import type { ChangeEvent } from 'react'; import { memo, useCallback, useMemo } from 'react'; @@ -15,7 +15,7 @@ type Props = { const useAutoNegative = (layerId: string) => { const selectAutoNegative = useMemo( () => - createSelector(selectControlLayersSlice, (controlLayers) => { + createSelector(selectCanvasV2Slice, (controlLayers) => { const layer = controlLayers.present.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); return layer.autoNegative; @@ -32,7 +32,7 @@ export const RGLayerAutoNegativeCheckbox = memo(({ layerId }: Props) => { const autoNegative = useAutoNegative(layerId); const onChange = useCallback( (e: ChangeEvent) => { - dispatch(rgLayerAutoNegativeChanged({ layerId, autoNegative: e.target.checked ? 'invert' : 'off' })); + dispatch(regionalGuidanceAutoNegativeChanged({ layerId, autoNegative: e.target.checked ? 'invert' : 'off' })); }, [dispatch, layerId] ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx index 78c16a773b4..40660a1ac22 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx @@ -4,7 +4,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import RgbColorPicker from 'common/components/RgbColorPicker'; import { stopPropagation } from 'common/util/stopPropagation'; import { rgbColorToString } from 'features/canvas/util/colorToString'; -import { rgLayerPreviewColorChanged, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import { rgFillChanged, selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; import type { RgbColor } from 'react-colorful'; @@ -19,7 +19,7 @@ export const RGLayerColorPicker = memo(({ layerId }: Props) => { const { t } = useTranslation(); const selectColor = useMemo( () => - createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { + createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { const layer = controlLayers.present.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an vector mask layer`); return layer.previewColor; @@ -30,7 +30,7 @@ export const RGLayerColorPicker = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); const onColorChange = useCallback( (color: RgbColor) => { - dispatch(rgLayerPreviewColorChanged({ layerId, color })); + dispatch(rgFillChanged({ layerId, color })); }, [dispatch, layerId] ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx index 1d5698ce031..9a38123d4b1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx @@ -2,7 +2,7 @@ import { Divider, Flex } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { RGLayerIPAdapterWrapper } from 'features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper'; -import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; import { assert } from 'tsafe'; @@ -14,7 +14,7 @@ type Props = { export const RGLayerIPAdapterList = memo(({ layerId }: Props) => { const selectIPAdapterIds = useMemo( () => - createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { + createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { const layer = controlLayers.present.layers.filter(isRegionalGuidanceLayer).find((l) => l.id === layerId); assert(layer, `Layer ${layerId} not found`); return layer.ipAdapters; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx index f7be62eb0a2..f0879f07cfc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx @@ -2,13 +2,13 @@ import { Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { IPAdapter } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapter'; import { - rgLayerIPAdapterBeginEndStepPctChanged, - rgLayerIPAdapterCLIPVisionModelChanged, - rgLayerIPAdapterDeleted, - rgLayerIPAdapterImageChanged, - rgLayerIPAdapterMethodChanged, - rgLayerIPAdapterModelChanged, - rgLayerIPAdapterWeightChanged, + regionalGuidanceIPAdapterBeginEndStepPctChanged, + regionalGuidanceIPAdapterCLIPVisionModelChanged, + regionalGuidanceIPAdapterDeleted, + regionalGuidanceIPAdapterImageChanged, + regionalGuidanceIPAdapterMethodChanged, + regionalGuidanceIPAdapterModelChanged, + regionalGuidanceIPAdapterWeightChanged, selectRGLayerIPAdapterOrThrow, } from 'features/controlLayers/store/controlLayersSlice'; import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; @@ -26,14 +26,14 @@ type Props = { export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNumber }: Props) => { const dispatch = useAppDispatch(); const onDeleteIPAdapter = useCallback(() => { - dispatch(rgLayerIPAdapterDeleted({ layerId, ipAdapterId })); + dispatch(regionalGuidanceIPAdapterDeleted({ layerId, ipAdapterId })); }, [dispatch, ipAdapterId, layerId]); const ipAdapter = useAppSelector((s) => selectRGLayerIPAdapterOrThrow(s.controlLayers.present, layerId, ipAdapterId)); const onChangeBeginEndStepPct = useCallback( (beginEndStepPct: [number, number]) => { dispatch( - rgLayerIPAdapterBeginEndStepPctChanged({ + regionalGuidanceIPAdapterBeginEndStepPctChanged({ layerId, ipAdapterId, beginEndStepPct, @@ -45,35 +45,35 @@ export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNu const onChangeWeight = useCallback( (weight: number) => { - dispatch(rgLayerIPAdapterWeightChanged({ layerId, ipAdapterId, weight })); + dispatch(regionalGuidanceIPAdapterWeightChanged({ layerId, ipAdapterId, weight })); }, [dispatch, ipAdapterId, layerId] ); const onChangeIPMethod = useCallback( (method: IPMethodV2) => { - dispatch(rgLayerIPAdapterMethodChanged({ layerId, ipAdapterId, method })); + dispatch(regionalGuidanceIPAdapterMethodChanged({ layerId, ipAdapterId, method })); }, [dispatch, ipAdapterId, layerId] ); const onChangeModel = useCallback( (modelConfig: IPAdapterModelConfig) => { - dispatch(rgLayerIPAdapterModelChanged({ layerId, ipAdapterId, modelConfig })); + dispatch(regionalGuidanceIPAdapterModelChanged({ layerId, ipAdapterId, modelConfig })); }, [dispatch, ipAdapterId, layerId] ); const onChangeCLIPVisionModel = useCallback( (clipVisionModel: CLIPVisionModelV2) => { - dispatch(rgLayerIPAdapterCLIPVisionModelChanged({ layerId, ipAdapterId, clipVisionModel })); + dispatch(regionalGuidanceIPAdapterCLIPVisionModelChanged({ layerId, ipAdapterId, clipVisionModel })); }, [dispatch, ipAdapterId, layerId] ); const onChangeImage = useCallback( (imageDTO: ImageDTO | null) => { - dispatch(rgLayerIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO })); + dispatch(regionalGuidanceIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO })); }, [dispatch, ipAdapterId, layerId] ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerNegativePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerNegativePrompt.tsx index ba02aa92422..92ae46a1319 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerNegativePrompt.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerNegativePrompt.tsx @@ -2,7 +2,7 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { RGLayerPromptDeleteButton } from 'features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton'; import { useLayerNegativePrompt } from 'features/controlLayers/hooks/layerStateHooks'; -import { rgLayerNegativePromptChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { regionalGuidanceNegativePromptChanged } from 'features/controlLayers/store/controlLayersSlice'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; @@ -21,7 +21,7 @@ export const RGLayerNegativePrompt = memo(({ layerId }: Props) => { const { t } = useTranslation(); const _onChange = useCallback( (v: string) => { - dispatch(rgLayerNegativePromptChanged({ layerId, prompt: v })); + dispatch(regionalGuidanceNegativePromptChanged({ layerId, prompt: v })); }, [dispatch, layerId] ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPositivePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPositivePrompt.tsx index 6f85ea077c6..34c4366dfc7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPositivePrompt.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPositivePrompt.tsx @@ -2,7 +2,7 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { RGLayerPromptDeleteButton } from 'features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton'; import { useLayerPositivePrompt } from 'features/controlLayers/hooks/layerStateHooks'; -import { rgLayerPositivePromptChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { regionalGuidancePositivePromptChanged } from 'features/controlLayers/store/controlLayersSlice'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; @@ -21,7 +21,7 @@ export const RGLayerPositivePrompt = memo(({ layerId }: Props) => { const { t } = useTranslation(); const _onChange = useCallback( (v: string) => { - dispatch(rgLayerPositivePromptChanged({ layerId, prompt: v })); + dispatch(regionalGuidancePositivePromptChanged({ layerId, prompt: v })); }, [dispatch, layerId] ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton.tsx index 62a4ddfaeb7..cbc99e70ded 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton.tsx @@ -1,8 +1,8 @@ import { IconButton, Tooltip } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { - rgLayerNegativePromptChanged, - rgLayerPositivePromptChanged, + regionalGuidanceNegativePromptChanged, + regionalGuidancePositivePromptChanged, } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -18,9 +18,9 @@ export const RGLayerPromptDeleteButton = memo(({ layerId, polarity }: Props) => const dispatch = useAppDispatch(); const onClick = useCallback(() => { if (polarity === 'positive') { - dispatch(rgLayerPositivePromptChanged({ layerId, prompt: null })); + dispatch(regionalGuidancePositivePromptChanged({ layerId, prompt: null })); } else { - dispatch(rgLayerNegativePromptChanged({ layerId, prompt: null })); + dispatch(regionalGuidanceNegativePromptChanged({ layerId, prompt: null })); } }, [dispatch, layerId, polarity]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index c03c985cfa3..f285624fe61 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -2,8 +2,8 @@ import { $alt, $ctrl, $meta, $shift, Box, Flex, Heading } from '@invoke-ai/ui-li import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { rgbaColorToString } from 'features/canvas/util/colorToString'; import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay'; import { BRUSH_SPACING_PCT, @@ -15,16 +15,16 @@ import { setStageEventHandlers } from 'features/controlLayers/konva/events'; import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers/layers'; import { $bbox, - $brushColor, - $brushSize, $brushSpacingPx, + $brushWidth, + $fill, + $invertScroll, $isDrawing, $isMouseDown, $lastAddedPoint, $lastCursorPos, $lastMouseDownPos, $selectedLayer, - $shouldInvertBrushSizeScrollDirection, $spaceKey, $stageAttrs, $tool, @@ -37,15 +37,16 @@ import { layerTranslated, linePointsAdded, rectAdded, - selectControlLayersSlice, + selectCanvasV2Slice, } from 'features/controlLayers/store/controlLayersSlice'; +import { selectLayersSlice } from 'features/controlLayers/store/layersSlice'; +import { selectRegionalGuidanceSlice } from 'features/controlLayers/store/regionalGuidanceSlice'; import type { AddBrushLineArg, AddEraserLineArg, AddPointToLineArg, AddRectShapeArg, } from 'features/controlLayers/store/types'; -import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; import { clamp } from 'lodash-es'; @@ -60,26 +61,26 @@ Konva.showWarnings = false; const log = logger('controlLayers'); -const selectBrushColor = createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { - const layer = controlLayers.present.layers - .filter(isRegionalGuidanceLayer) - .find((l) => l.id === controlLayers.present.selectedLayerId); +const selectBrushColor = createSelector( + selectCanvasV2Slice, + selectLayersSlice, + selectRegionalGuidanceSlice, + (canvas, layers, regionalGuidance) => { + const rg = regionalGuidance.regions.find((i) => i.id === canvas.lastSelectedItem?.id); - if (layer) { - return { ...layer.previewColor, a: controlLayers.present.globalMaskLayerOpacity }; - } + if (rg) { + return rgbaColorToString({ ...rg.fill, a: regionalGuidance.opacity }); + } - return controlLayers.present.brushColor; -}); + return rgbaColorToString(canvas.tool.fill); + } +); -const selectSelectedLayer = createSelector(selectControlLayersSlice, (controlLayers) => { +const selectSelectedLayer = createSelector(selectCanvasV2Slice, (controlLayers) => { return controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId) ?? null; }); -const selectLayerCount = createSelector( - selectControlLayersSlice, - (controlLayers) => controlLayers.present.layers.length -); +const selectLayerCount = createSelector(selectCanvasV2Slice, (controlLayers) => controlLayers.present.layers.length); const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, asPreview: boolean) => { const dispatch = useAppDispatch(); @@ -100,11 +101,11 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, ); useLayoutEffect(() => { - $brushColor.set(brushColor); - $brushSize.set(state.brushSize); + $fill.set(brushColor); + $brushWidth.set(state.brushSize); $brushSpacingPx.set(brushSpacingPx); $selectedLayer.set(selectedLayer); - $shouldInvertBrushSizeScrollDirection.set(shouldInvertBrushSizeScrollDirection); + $invertScroll.set(shouldInvertBrushSizeScrollDirection); $bbox.set(state.bbox); }, [ brushSpacingPx, @@ -196,8 +197,8 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, setIsDrawing: $isDrawing.set, getIsMouseDown: $isMouseDown.get, setIsMouseDown: $isMouseDown.set, - getBrushColor: $brushColor.get, - getBrushSize: $brushSize.get, + getBrushColor: $fill.get, + getBrushSize: $brushWidth.get, getBrushSpacingPx: $brushSpacingPx.get, getSelectedLayer: $selectedLayer.get, getLastAddedPoint: $lastAddedPoint.get, @@ -206,7 +207,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, setLastCursorPos: $lastCursorPos.set, getLastMouseDownPos: $lastMouseDownPos.get, setLastMouseDownPos: $lastMouseDownPos.set, - getShouldInvert: $shouldInvertBrushSizeScrollDirection.get, + getShouldInvert: $invertScroll.get, getSpaceKey: $spaceKey.get, setStageAttrs: $stageAttrs.set, onBrushSizeChanged, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx index 9e26fed592a..c1daa11df44 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx @@ -5,7 +5,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { $tool, layerReset, - selectControlLayersSlice, + selectCanvasV2Slice, selectedLayerDeleted, } from 'features/controlLayers/store/controlLayersSlice'; import { useCallback } from 'react'; @@ -20,7 +20,7 @@ import { PiRectangleBold, } from 'react-icons/pi'; -const selectIsDisabled = createSelector(selectControlLayersSlice, (controlLayers) => { +const selectIsDisabled = createSelector(selectCanvasV2Slice, (controlLayers) => { const selectedLayer = controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId); return selectedLayer?.type !== 'regional_guidance_layer' && selectedLayer?.type !== 'raster_layer'; }); diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts index 244e57c6558..5e0976ed594 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts @@ -1,9 +1,9 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { - caLayerAdded, + controlAdapterAdded, iiLayerAdded, - ipaLayerAdded, - rgLayerIPAdapterAdded, + ipAdapterAdded, + regionalGuidanceIPAdapterAdded, } from 'features/controlLayers/store/controlLayersSlice'; import { isInitialImageLayer } from 'features/controlLayers/store/types'; import { @@ -46,7 +46,7 @@ export const useAddCALayer = () => { processorConfig, }); - dispatch(caLayerAdded(controlAdapter)); + dispatch(controlAdapterAdded(controlAdapter)); }, [dispatch, model, baseModel]); return [addCALayer, isDisabled] as const; @@ -70,7 +70,7 @@ export const useAddIPALayer = () => { const ipAdapter = buildIPAdapter(id, { model: zModelIdentifierField.parse(model), }); - dispatch(ipaLayerAdded(ipAdapter)); + dispatch(ipAdapterAdded(ipAdapter)); }, [dispatch, model]); return [addIPALayer, isDisabled] as const; @@ -94,7 +94,7 @@ export const useAddIPAdapterToIPALayer = (layerId: string) => { const ipAdapter = buildIPAdapter(id, { model: zModelIdentifierField.parse(model), }); - dispatch(rgLayerIPAdapterAdded({ layerId, ipAdapter })); + dispatch(regionalGuidanceIPAdapterAdded({ layerId, ipAdapter })); }, [dispatch, model, layerId]); return [addIPAdapter, isDisabled] as const; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts index c643b863fd6..c9dfeb4e129 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts @@ -1,7 +1,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; import { isControlAdapterLayer, isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; import { assert } from 'tsafe'; @@ -9,7 +9,7 @@ import { assert } from 'tsafe'; export const useLayerPositivePrompt = (layerId: string) => { const selectLayer = useMemo( () => - createSelector(selectControlLayersSlice, (controlLayers) => { + createSelector(selectCanvasV2Slice, (controlLayers) => { const layer = controlLayers.present.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); assert(layer.positivePrompt !== null, `Layer ${layerId} does not have a positive prompt`); @@ -24,7 +24,7 @@ export const useLayerPositivePrompt = (layerId: string) => { export const useLayerNegativePrompt = (layerId: string) => { const selectLayer = useMemo( () => - createSelector(selectControlLayersSlice, (controlLayers) => { + createSelector(selectCanvasV2Slice, (controlLayers) => { const layer = controlLayers.present.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); assert(layer.negativePrompt !== null, `Layer ${layerId} does not have a negative prompt`); @@ -39,7 +39,7 @@ export const useLayerNegativePrompt = (layerId: string) => { export const useLayerIsEnabled = (layerId: string) => { const selectLayer = useMemo( () => - createSelector(selectControlLayersSlice, (controlLayers) => { + createSelector(selectCanvasV2Slice, (controlLayers) => { const layer = controlLayers.present.layers.find((l) => l.id === layerId); assert(layer, `Layer ${layerId} not found`); return layer.isEnabled; @@ -53,7 +53,7 @@ export const useLayerIsEnabled = (layerId: string) => { export const useLayerType = (layerId: string) => { const selectLayer = useMemo( () => - createSelector(selectControlLayersSlice, (controlLayers) => { + createSelector(selectCanvasV2Slice, (controlLayers) => { const layer = controlLayers.present.layers.find((l) => l.id === layerId); assert(layer, `Layer ${layerId} not found`); return layer.type; @@ -67,7 +67,7 @@ export const useLayerType = (layerId: string) => { export const useCALayerOpacity = (layerId: string) => { const selectLayer = useMemo( () => - createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { + createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { const layer = controlLayers.present.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId); assert(layer, `Layer ${layerId} not found`); return { opacity: Math.round(layer.opacity * 100), isFilterEnabled: layer.isFilterEnabled }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 12a0efdfb7c..1f1e99f3167 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -6,7 +6,7 @@ import type { AddEraserLineArg, AddPointToLineArg, AddRectShapeArg, - Layer, + LayerData, StageAttrs, Tool, } from 'features/controlLayers/store/types'; @@ -38,7 +38,7 @@ type Arg = { getBrushColor: () => RgbaColor; getBrushSize: () => number; getBrushSpacingPx: () => number; - getSelectedLayer: () => Layer | null; + getSelectedLayer: () => LayerData | null; getShouldInvert: () => boolean; getSpaceKey: () => boolean; onBrushLineAdded: (arg: AddBrushLineArg) => void; @@ -72,7 +72,7 @@ const updateLastCursorPos = (stage: Konva.Stage, setLastCursorPos: Arg['setLastC * @param onPointAddedToLine The callback to add a point to a line */ const maybeAddNextPoint = ( - layerId: string, + selectedLayer: LayerData, currentPos: Vector2d, getLastAddedPoint: Arg['getLastAddedPoint'], setLastAddedPoint: Arg['setLastAddedPoint'], @@ -88,7 +88,7 @@ const maybeAddNextPoint = ( } } setLastAddedPoint(currentPos); - onPointAddedToLine({ layerId, point: [currentPos.x, currentPos.y] }); + onPointAddedToLine({ layerId, point: [currentPos.x - selectedLayer.x, currentPos.y - selectedLayer.y] }); }; export const setStageEventHandlers = ({ @@ -158,7 +158,7 @@ export const setStageEventHandlers = ({ if (tool === 'brush') { onBrushLineAdded({ layerId: selectedLayer.id, - points: [pos.x, pos.y, pos.x, pos.y], + points: [pos.x - selectedLayer.x, pos.y - selectedLayer.y, pos.x - selectedLayer.x, pos.y - selectedLayer.y], color: selectedLayer.type === 'raster_layer' ? getBrushColor() : DEFAULT_RGBA_COLOR, }); } @@ -166,7 +166,7 @@ export const setStageEventHandlers = ({ if (tool === 'eraser') { onEraserLineAdded({ layerId: selectedLayer.id, - points: [pos.x, pos.y, pos.x, pos.y], + points: [pos.x - selectedLayer.x, pos.y - selectedLayer.y, pos.x - selectedLayer.x, pos.y - selectedLayer.y], }); } @@ -262,7 +262,7 @@ export const setStageEventHandlers = ({ // Start a new line onBrushLineAdded({ layerId: selectedLayer.id, - points: [pos.x, pos.y, pos.x, pos.y], + points: [pos.x - selectedLayer.x, pos.y - selectedLayer.y, pos.x - selectedLayer.x, pos.y - selectedLayer.y], color: selectedLayer.type === 'raster_layer' ? getBrushColor() : DEFAULT_RGBA_COLOR, }); setIsDrawing(true); @@ -282,7 +282,10 @@ export const setStageEventHandlers = ({ ); } else { // Start a new line - onEraserLineAdded({ layerId: selectedLayer.id, points: [pos.x, pos.y, pos.x, pos.y] }); + onEraserLineAdded({ + layerId: selectedLayer.id, + points: [pos.x - selectedLayer.x, pos.y - selectedLayer.y, pos.x - selectedLayer.x, pos.y - selectedLayer.y], + }); setIsDrawing(true); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts index 7fd088dc732..6c2b019fd8e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts @@ -41,6 +41,8 @@ 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'; +export const INPAINT_MASK_LAYER_NAME = 'inpaint_mask_layer'; + // 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}`; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts index 316ef85110c..bc4c42fbd28 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts @@ -6,8 +6,8 @@ import { RG_LAYER_OBJECT_GROUP_NAME, } from 'features/controlLayers/konva/naming'; import { createBboxRect } from 'features/controlLayers/konva/renderers/objects'; -import type { Layer } from 'features/controlLayers/store/types'; -import { isRegionalGuidanceLayer, isRGOrRasterlayer } from 'features/controlLayers/store/types'; +import type { LayerData } from 'features/controlLayers/store/types'; +import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; import { assert } from 'tsafe'; @@ -185,10 +185,10 @@ const filterRasterChildren = (node: Konva.Node): boolean => node.name() === RAST */ export const updateBboxes = ( stage: Konva.Stage, - layerStates: Layer[], + layerStates: LayerData[], onBboxChanged: (layerId: string, bbox: IRect | null) => void ): void => { - for (const layerState of layerStates.filter(isRGOrRasterlayer)) { + for (const layerState of layerStates) { const konvaLayer = stage.findOne(`#${layerState.id}`); assert(konvaLayer, `Layer ${layerState.id} not found in stage`); // We only need to recalculate the bbox if the layer has changed diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts index 18e893fb243..266bfd4aaa1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -7,10 +7,11 @@ import { renderBboxPreview, renderToolPreview } from 'features/controlLayers/kon import { renderRasterLayer } from 'features/controlLayers/konva/renderers/rasterLayer'; import { renderRGLayer } from 'features/controlLayers/konva/renderers/rgLayer'; import { mapId, selectRenderableLayers } from 'features/controlLayers/konva/util'; -import type { Layer, Tool } from 'features/controlLayers/store/types'; +import type { LayerData, Tool } from 'features/controlLayers/store/types'; import { isControlAdapterLayer, isInitialImageLayer, + isInpaintMaskLayer, isRasterLayer, isRegionalGuidanceLayer, isRenderableLayer, @@ -34,7 +35,7 @@ import type { ImageDTO } from 'services/api/types'; */ const renderLayers = ( stage: Konva.Stage, - layerStates: Layer[], + layerStates: LayerData[], globalMaskLayerOpacity: number, tool: Tool, getImageDTO: (imageName: string) => Promise, @@ -52,15 +53,14 @@ const renderLayers = ( for (const layer of layerStates) { if (isRegionalGuidanceLayer(layer)) { renderRGLayer(stage, layer, globalMaskLayerOpacity, tool, zIndex, onLayerPosChanged); - } - if (isControlAdapterLayer(layer)) { + } else if (isControlAdapterLayer(layer)) { renderCALayer(stage, layer, zIndex, getImageDTO); - } - if (isInitialImageLayer(layer)) { + } else if (isInitialImageLayer(layer)) { renderIILayer(stage, layer, zIndex, getImageDTO); - } - if (isRasterLayer(layer)) { + } else if (isRasterLayer(layer)) { renderRasterLayer(stage, layer, tool, zIndex, onLayerPosChanged); + } else if (isInpaintMaskLayer(layer)) { + // } // IP Adapter layers are not rendered // Increment the z-index for the tool layer 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 d9ea85e9cab..351a39301ec 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -5,7 +5,7 @@ import { LAYER_BBOX_NAME, PREVIEW_GENERATION_BBOX_DUMMY_RECT, } from 'features/controlLayers/konva/naming'; -import type { BrushLine, EraserLine, ImageObject, Layer, RectShape } from 'features/controlLayers/store/types'; +import type { BrushLine, EraserLine, ImageObject, LayerData, RectShape } from 'features/controlLayers/store/types'; import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types'; import { t } from 'i18next'; import Konva from 'konva'; @@ -177,7 +177,7 @@ export const createImageObjectGroup = async ( * @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 */ -export const createBboxRect = (layerState: Layer, konvaLayer: Konva.Layer): Konva.Rect => { +export const createBboxRect = (layerState: LayerData, konvaLayer: Konva.Layer): Konva.Rect => { const rect = new Konva.Rect({ id: getLayerBboxId(layerState.id), name: LAYER_BBOX_NAME, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts index 1947d868e33..d58d459ee00 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts @@ -18,7 +18,7 @@ import { PREVIEW_TOOL_GROUP_ID, } from 'features/controlLayers/konva/naming'; import { selectRenderableLayers } from 'features/controlLayers/konva/util'; -import type { Layer, RgbaColor, Tool } from 'features/controlLayers/store/types'; +import type { LayerData, RgbaColor, Tool } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect, Vector2d } from 'konva/lib/types'; import { atom } from 'nanostores'; @@ -338,7 +338,7 @@ export const renderToolPreview = ( stage: Konva.Stage, tool: Tool, brushColor: RgbaColor, - selectedLayerType: Layer['type'] | null, + selectedLayerType: LayerData['type'] | null, globalMaskLayerOpacity: number, cursorPos: Vector2d | null, lastMouseDownPos: Vector2d | null, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index 1143940bfea..1e28392f2af 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -1,6 +1,7 @@ import { CA_LAYER_NAME, INITIAL_IMAGE_LAYER_NAME, + INPAINT_MASK_LAYER_NAME, RASTER_LAYER_BRUSH_LINE_NAME, RASTER_LAYER_ERASER_LINE_NAME, RASTER_LAYER_IMAGE_NAME, @@ -98,7 +99,8 @@ 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; + node.name() === RASTER_LAYER_NAME || + node.name() === INPAINT_MASK_LAYER_NAME; /** * Konva selection callback to select RG mask objects. This includes lines and rects. diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts new file mode 100644 index 00000000000..f53de4eb5a9 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts @@ -0,0 +1,282 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; +import type { PersistConfig, RootState } from 'app/store/store'; +import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; +import type { ControlModeV2, ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import { buildControlAdapterProcessorV2, imageDTOToImageWithDims } from 'features/controlLayers/util/controlAdapters'; +import { zModelIdentifierField } from 'features/nodes/types/common'; +import type { IRect } from 'konva/lib/types'; +import { isEqual } from 'lodash-es'; +import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types'; +import { v4 as uuidv4 } from 'uuid'; + +import type { ControlAdapterConfig, ControlAdapterData, Filter } from './types'; + +type ControlAdaptersV2State = { + _version: 1; + controlAdapters: ControlAdapterData[]; +}; + +const initialState: ControlAdaptersV2State = { + _version: 1, + controlAdapters: [], +}; + +const selectCa = (state: ControlAdaptersV2State, id: string) => state.controlAdapters.find((ca) => ca.id === id); + +export const controlAdaptersV2Slice = createSlice({ + name: 'controlAdaptersV2', + initialState, + reducers: { + caAdded: { + reducer: (state, action: PayloadAction<{ id: string; config: ControlAdapterConfig }>) => { + const { id, config } = action.payload; + state.controlAdapters.push({ + id, + type: 'control_adapter', + x: 0, + y: 0, + bbox: null, + bboxNeedsUpdate: false, + isEnabled: true, + opacity: 1, + filter: 'lightness_to_alpha', + processorPendingBatchId: null, + ...config, + }); + }, + prepare: (config: ControlAdapterConfig) => ({ + payload: { id: uuidv4(), config }, + }), + }, + caRecalled: (state, action: PayloadAction<{ data: ControlAdapterData }>) => { + state.controlAdapters.push(action.payload.data); + }, + caIsEnabledChanged: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => { + const { id, isEnabled } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + ca.isEnabled = isEnabled; + }, + caTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => { + const { id, x, y } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + ca.x = x; + ca.y = y; + }, + caBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { + const { id, bbox } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + ca.bbox = bbox; + ca.bboxNeedsUpdate = false; + }, + caDeleted: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + state.controlAdapters = state.controlAdapters.filter((ca) => ca.id !== id); + }, + caOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => { + const { id, opacity } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + ca.opacity = opacity; + }, + caMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + moveOneToEnd(state.controlAdapters, ca); + }, + caMovedToFront: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + moveToEnd(state.controlAdapters, ca); + }, + caMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + moveOneToStart(state.controlAdapters, ca); + }, + caMovedToBack: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + moveToStart(state.controlAdapters, ca); + }, + caImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => { + const { id, imageDTO } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + ca.bbox = null; + ca.bboxNeedsUpdate = true; + ca.isEnabled = true; + if (imageDTO) { + const newImage = imageDTOToImageWithDims(imageDTO); + if (isEqual(newImage, ca.image)) { + return; + } + ca.image = newImage; + ca.processedImage = null; + } else { + ca.image = null; + ca.processedImage = null; + } + }, + caProcessedImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => { + const { id, imageDTO } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + ca.bbox = null; + ca.bboxNeedsUpdate = true; + ca.isEnabled = true; + ca.processedImage = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; + }, + caModelChanged: ( + state, + action: PayloadAction<{ + id: string; + modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null; + }> + ) => { + const { id, modelConfig } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + if (!modelConfig) { + ca.model = null; + return; + } + ca.model = zModelIdentifierField.parse(modelConfig); + + // We may need to convert the CA to match the model + if (!ca.controlMode && ca.model.type === 'controlnet') { + ca.controlMode = 'balanced'; + } else if (ca.controlMode && ca.model.type === 't2i_adapter') { + ca.controlMode = null; + } + + const candidateProcessorConfig = buildControlAdapterProcessorV2(modelConfig); + if (candidateProcessorConfig?.type !== ca.processorConfig?.type) { + // The processor has changed. For example, the previous model was a Canny model and the new model is a Depth + // model. We need to use the new processor. + ca.processedImage = null; + ca.processorConfig = candidateProcessorConfig; + } + }, + caControlModeChanged: (state, action: PayloadAction<{ id: string; controlMode: ControlModeV2 }>) => { + const { id, controlMode } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + ca.controlMode = controlMode; + }, + caProcessorConfigChanged: ( + state, + action: PayloadAction<{ id: string; processorConfig: ProcessorConfig | null }> + ) => { + const { id, processorConfig } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + ca.processorConfig = processorConfig; + if (!processorConfig) { + ca.processedImage = null; + } + }, + caFilterChanged: (state, action: PayloadAction<{ id: string; filter: Filter }>) => { + const { id, filter } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + ca.filter = filter; + }, + caProcessorPendingBatchIdChanged: (state, action: PayloadAction<{ id: string; batchId: string | null }>) => { + const { id, batchId } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + ca.processorPendingBatchId = batchId; + }, + caWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { + const { id, weight } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + ca.weight = weight; + }, + caBeginEndStepPctChanged: (state, action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }>) => { + const { id, beginEndStepPct } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + ca.beginEndStepPct = beginEndStepPct; + }, + }, +}); + +export const { + caAdded, + caBboxChanged, + caDeleted, + caIsEnabledChanged, + caMovedBackwardOne, + caMovedForwardOne, + caMovedToBack, + caMovedToFront, + caOpacityChanged, + caTranslated, + caRecalled, + caImageChanged, + caProcessedImageChanged, + caModelChanged, + caControlModeChanged, + caProcessorConfigChanged, + caFilterChanged, + caProcessorPendingBatchIdChanged, + caWeightChanged, + caBeginEndStepPctChanged, +} = controlAdaptersV2Slice.actions; + +export const selectControlAdaptersV2Slice = (state: RootState) => state.controlAdaptersV2; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +const migrate = (state: any): any => { + return state; +}; + +export const controlAdaptersV2PersistConfig: PersistConfig = { + name: controlAdaptersV2Slice.name, + initialState, + migrate, + persistDenylist: [], +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 444eca89467..71df7102585 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -1,93 +1,50 @@ -import type { PayloadAction, UnknownAction } from '@reduxjs/toolkit'; -import { createSlice, isAnyOf } from '@reduxjs/toolkit'; +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; -import { moveBackward, moveForward, moveToBack, moveToFront } from 'common/util/arrayUtils'; import { deepClone } from 'common/util/deepClone'; import { roundDownToMultiple } from 'common/util/roundDownToMultiple'; -import { - getBrushLineId, - getCALayerId, - getEraserLineId, - getImageObjectId, - getIPALayerId, - getRasterLayerId, - getRectShapeId, - getRGLayerId, - INITIAL_IMAGE_LAYER_ID, -} from 'features/controlLayers/konva/naming'; -import type { - CLIPVisionModelV2, - ControlModeV2, - ControlNetConfigV2, - IPAdapterConfigV2, - IPMethodV2, - ProcessorConfig, - T2IAdapterConfigV2, -} from 'features/controlLayers/util/controlAdapters'; -import { - buildControlAdapterProcessorV2, - controlNetToT2IAdapter, - imageDTOToImageWithDims, - t2iAdapterToControlNet, -} from 'features/controlLayers/util/controlAdapters'; -import { zModelIdentifierField } from 'features/nodes/types/common'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; import { modelChanged } from 'features/parameters/store/generationSlice'; -import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas'; import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension'; import type { IRect, Vector2d } from 'konva/lib/types'; -import { isEqual, partition, unset } from 'lodash-es'; import { atom } from 'nanostores'; -import type { RgbColor } from 'react-colorful'; -import type { UndoableOptions } from 'redux-undo'; -import type { ControlNetModelConfig, ImageDTO, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types'; -import { assert } from 'tsafe'; -import { v4 as uuidv4 } from 'uuid'; import type { - AddBrushLineArg, - AddEraserLineArg, - AddImageObjectArg, - AddPointToLineArg, - AddRectShapeArg, - ControlAdapterLayer, - ControlLayersState, - InitialImageLayer, - IPAdapterLayer, - Layer, - RasterLayer, - RegionalGuidanceLayer, + CanvasV2State, + ControlAdapterData, + IPAdapterData, + LayerData, + RegionalGuidanceData, RgbaColor, StageAttrs, Tool, } from './types'; -import { - DEFAULT_RGBA_COLOR, - isCAOrIPALayer, - isControlAdapterLayer, - isInitialImageLayer, - isIPAdapterLayer, - isLine, - isRasterLayer, - isRegionalGuidanceLayer, - isRenderableLayer, - isRGOrRasterlayer, -} from './types'; +import { DEFAULT_RGBA_COLOR } from './types'; -export const initialControlLayersState: ControlLayersState = { +const initialState: CanvasV2State = { _version: 3, - selectedLayerId: null, - brushSize: 100, - brushColor: DEFAULT_RGBA_COLOR, - layers: [], - globalMaskLayerOpacity: 0.3, // this globally changes all mask layers' opacity - positivePrompt: '', - negativePrompt: '', - positivePrompt2: '', - negativePrompt2: '', - shouldConcatPrompts: true, + lastSelectedItem: null, + prompts: { + positivePrompt: '', + negativePrompt: '', + positivePrompt2: '', + negativePrompt2: '', + shouldConcatPrompts: true, + }, + tool: { + selected: 'bbox', + selectedBuffer: null, + invertScroll: false, + fill: DEFAULT_RGBA_COLOR, + brush: { + width: 50, + }, + eraser: { + width: 50, + }, + }, size: { width: 512, height: 512, @@ -101,687 +58,24 @@ export const initialControlLayersState: ControlLayersState = { }, }; -/** - * A selector that accepts a type guard and returns the first layer that matches the guard. - * Throws if the layer is not found or does not match the guard. - */ -export const selectLayerOrThrow = ( - state: ControlLayersState, - layerId: string, - predicate: (layer: Layer) => layer is T -): T => { - const layer = state.layers.find((l) => l.id === layerId); - assert(layer && predicate(layer)); - return layer; -}; - -export const selectRGLayerIPAdapterOrThrow = ( - state: ControlLayersState, - layerId: string, - ipAdapterId: string -): IPAdapterConfigV2 => { - const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer); - const ipAdapter = layer.ipAdapters.find((ipAdapter) => ipAdapter.id === ipAdapterId); - assert(ipAdapter); - return ipAdapter; -}; - -const getVectorMaskPreviewColor = (state: ControlLayersState): RgbColor => { - const rgLayers = state.layers.filter(isRegionalGuidanceLayer); - const lastColor = rgLayers[rgLayers.length - 1]?.previewColor; - return LayerColors.next(lastColor); -}; -const exclusivelySelectLayer = (state: ControlLayersState, layerId: string) => { - for (const layer of state.layers) { - layer.isSelected = layer.id === layerId; - } - state.selectedLayerId = layerId; -}; - -export const controlLayersSlice = createSlice({ - name: 'controlLayers', - initialState: initialControlLayersState, +export const canvasV2Slice = createSlice({ + name: 'canvasV2', + initialState, reducers: { - //#region Any Layer Type - layerSelected: (state, action: PayloadAction) => { - exclusivelySelectLayer(state, action.payload); - }, - layerIsEnabledToggled: (state, action: PayloadAction) => { - const layer = state.layers.find((l) => l.id === action.payload); - if (layer) { - layer.isEnabled = !layer.isEnabled; - } - }, - layerTranslated: (state, action: PayloadAction<{ layerId: string; x: number; y: number }>) => { - const { layerId, x, y } = action.payload; - const layer = state.layers.find((l) => l.id === layerId); - if (isRenderableLayer(layer)) { - layer.x = x; - layer.y = y; - } - if (isRegionalGuidanceLayer(layer)) { - layer.uploadedMaskImage = null; - } - }, - layerBboxChanged: (state, action: PayloadAction<{ layerId: string; bbox: IRect | null }>) => { - const { layerId, bbox } = action.payload; - const layer = state.layers.find((l) => l.id === layerId); - if (isRenderableLayer(layer)) { - layer.bbox = bbox; - layer.bboxNeedsUpdate = false; - if (bbox === null) { - // The layer was fully erased, empty its objects to prevent accumulation of invisible objects - if (isRegionalGuidanceLayer(layer)) { - layer.objects = []; - layer.uploadedMaskImage = null; - } - if (isRasterLayer(layer)) { - layer.objects = []; - } - } - } - }, - layerReset: (state, action: PayloadAction) => { - const layer = state.layers.find((l) => l.id === action.payload); - // TODO(psyche): Should other layer types also have reset functionality? - if (isRegionalGuidanceLayer(layer)) { - layer.objects = []; - layer.bbox = null; - layer.isEnabled = true; - 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); - state.selectedLayerId = state.layers[0]?.id ?? null; - }, - layerOpacityChanged: (state, action: PayloadAction<{ layerId: string; opacity: number }>) => { - const { layerId, opacity } = action.payload; - const layer = state.layers.find((l) => l.id === layerId); - if (isControlAdapterLayer(layer) || isInitialImageLayer(layer) || isRasterLayer(layer)) { - layer.opacity = opacity; - } - }, - layerMovedForward: (state, action: PayloadAction) => { - const cb = (l: Layer) => l.id === action.payload; - const [renderableLayers, ipAdapterLayers] = partition(state.layers, isRenderableLayer); - moveForward(renderableLayers, cb); - state.layers = [...ipAdapterLayers, ...renderableLayers]; - }, - layerMovedToFront: (state, action: PayloadAction) => { - const cb = (l: Layer) => l.id === action.payload; - const [renderableLayers, ipAdapterLayers] = partition(state.layers, isRenderableLayer); - // Because the layers are in reverse order, moving to the front is equivalent to moving to the back - moveToBack(renderableLayers, cb); - state.layers = [...ipAdapterLayers, ...renderableLayers]; - }, - layerMovedBackward: (state, action: PayloadAction) => { - const cb = (l: Layer) => l.id === action.payload; - const [renderableLayers, ipAdapterLayers] = partition(state.layers, isRenderableLayer); - moveBackward(renderableLayers, cb); - state.layers = [...ipAdapterLayers, ...renderableLayers]; - }, - layerMovedToBack: (state, action: PayloadAction) => { - const cb = (l: Layer) => l.id === action.payload; - const [renderableLayers, ipAdapterLayers] = partition(state.layers, isRenderableLayer); - // Because the layers are in reverse order, moving to the back is equivalent to moving to the front - moveToFront(renderableLayers, cb); - state.layers = [...ipAdapterLayers, ...renderableLayers]; - }, - selectedLayerDeleted: (state) => { - state.layers = state.layers.filter((l) => l.id !== state.selectedLayerId); - state.selectedLayerId = state.layers[0]?.id ?? null; - }, - allLayersDeleted: (state) => { - state.layers = []; - state.selectedLayerId = null; - }, - //#endregion - - //#region CA Layers - caLayerAdded: { - reducer: ( - state, - action: PayloadAction<{ layerId: string; controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2 }> - ) => { - const { layerId, controlAdapter } = action.payload; - const layer: ControlAdapterLayer = { - id: getCALayerId(layerId), - type: 'control_adapter_layer', - x: 0, - y: 0, - bbox: null, - bboxNeedsUpdate: false, - isEnabled: true, - opacity: 1, - isSelected: true, - isFilterEnabled: true, - controlAdapter, - }; - state.layers.push(layer); - exclusivelySelectLayer(state, layer.id); - }, - prepare: (controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2) => ({ - payload: { layerId: uuidv4(), controlAdapter }, - }), - }, - caLayerRecalled: (state, action: PayloadAction) => { - state.layers.push({ ...action.payload, isSelected: true }); - exclusivelySelectLayer(state, action.payload.id); - }, - caLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { - const { layerId, imageDTO } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isControlAdapterLayer); - layer.bbox = null; - layer.bboxNeedsUpdate = true; - layer.isEnabled = true; - if (imageDTO) { - const newImage = imageDTOToImageWithDims(imageDTO); - if (isEqual(newImage, layer.controlAdapter.image)) { - return; - } - layer.controlAdapter.image = newImage; - layer.controlAdapter.processedImage = null; - } else { - layer.controlAdapter.image = null; - layer.controlAdapter.processedImage = null; - } - }, - caLayerProcessedImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { - const { layerId, imageDTO } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isControlAdapterLayer); - layer.bbox = null; - layer.bboxNeedsUpdate = true; - layer.isEnabled = true; - layer.controlAdapter.processedImage = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; - }, - caLayerModelChanged: ( - state, - action: PayloadAction<{ - layerId: string; - modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null; - }> - ) => { - const { layerId, modelConfig } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isControlAdapterLayer); - if (!modelConfig) { - layer.controlAdapter.model = null; - return; - } - layer.controlAdapter.model = zModelIdentifierField.parse(modelConfig); - - // We may need to convert the CA to match the model - if (layer.controlAdapter.type === 't2i_adapter' && layer.controlAdapter.model.type === 'controlnet') { - layer.controlAdapter = t2iAdapterToControlNet(layer.controlAdapter); - } else if (layer.controlAdapter.type === 'controlnet' && layer.controlAdapter.model.type === 't2i_adapter') { - layer.controlAdapter = controlNetToT2IAdapter(layer.controlAdapter); - } - - const candidateProcessorConfig = buildControlAdapterProcessorV2(modelConfig); - if (candidateProcessorConfig?.type !== layer.controlAdapter.processorConfig?.type) { - // The processor has changed. For example, the previous model was a Canny model and the new model is a Depth - // model. We need to use the new processor. - layer.controlAdapter.processedImage = null; - layer.controlAdapter.processorConfig = candidateProcessorConfig; - } - }, - caLayerControlModeChanged: (state, action: PayloadAction<{ layerId: string; controlMode: ControlModeV2 }>) => { - const { layerId, controlMode } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isControlAdapterLayer); - assert(layer.controlAdapter.type === 'controlnet'); - layer.controlAdapter.controlMode = controlMode; - }, - caLayerProcessorConfigChanged: ( - state, - action: PayloadAction<{ layerId: string; processorConfig: ProcessorConfig | null }> - ) => { - const { layerId, processorConfig } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isControlAdapterLayer); - layer.controlAdapter.processorConfig = processorConfig; - if (!processorConfig) { - layer.controlAdapter.processedImage = null; - } - }, - caLayerIsFilterEnabledChanged: (state, action: PayloadAction<{ layerId: string; isFilterEnabled: boolean }>) => { - const { layerId, isFilterEnabled } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isControlAdapterLayer); - layer.isFilterEnabled = isFilterEnabled; - }, - caLayerProcessorPendingBatchIdChanged: ( - state, - action: PayloadAction<{ layerId: string; batchId: string | null }> - ) => { - const { layerId, batchId } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isControlAdapterLayer); - layer.controlAdapter.processorPendingBatchId = batchId; - }, - //#endregion - - //#region IP Adapter Layers - ipaLayerAdded: { - reducer: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfigV2 }>) => { - const { layerId, ipAdapter } = action.payload; - const layer: IPAdapterLayer = { - id: getIPALayerId(layerId), - type: 'ip_adapter_layer', - isEnabled: true, - isSelected: true, - ipAdapter, - }; - state.layers.push(layer); - exclusivelySelectLayer(state, layer.id); - }, - prepare: (ipAdapter: IPAdapterConfigV2) => ({ payload: { layerId: uuidv4(), ipAdapter } }), - }, - ipaLayerRecalled: (state, action: PayloadAction) => { - state.layers.push(action.payload); - }, - ipaLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { - const { layerId, imageDTO } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isIPAdapterLayer); - layer.ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; - }, - ipaLayerMethodChanged: (state, action: PayloadAction<{ layerId: string; method: IPMethodV2 }>) => { - const { layerId, method } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isIPAdapterLayer); - layer.ipAdapter.method = method; - }, - ipaLayerModelChanged: ( - state, - action: PayloadAction<{ - layerId: string; - modelConfig: IPAdapterModelConfig | null; - }> - ) => { - const { layerId, modelConfig } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isIPAdapterLayer); - if (!modelConfig) { - layer.ipAdapter.model = null; - return; - } - layer.ipAdapter.model = zModelIdentifierField.parse(modelConfig); - }, - ipaLayerCLIPVisionModelChanged: ( - state, - action: PayloadAction<{ layerId: string; clipVisionModel: CLIPVisionModelV2 }> - ) => { - const { layerId, clipVisionModel } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isIPAdapterLayer); - layer.ipAdapter.clipVisionModel = clipVisionModel; - }, - //#endregion - - //#region CA or IPA Layers - caOrIPALayerWeightChanged: (state, action: PayloadAction<{ layerId: string; weight: number }>) => { - const { layerId, weight } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isCAOrIPALayer); - if (layer.type === 'control_adapter_layer') { - layer.controlAdapter.weight = weight; - } else { - layer.ipAdapter.weight = weight; - } - }, - caOrIPALayerBeginEndStepPctChanged: ( - state, - action: PayloadAction<{ layerId: string; beginEndStepPct: [number, number] }> - ) => { - const { layerId, beginEndStepPct } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isCAOrIPALayer); - if (layer.type === 'control_adapter_layer') { - layer.controlAdapter.beginEndStepPct = beginEndStepPct; - } else { - layer.ipAdapter.beginEndStepPct = beginEndStepPct; - } - }, - //#endregion - - //#region RG Layers - rgLayerAdded: { - reducer: (state, action: PayloadAction<{ layerId: string }>) => { - const { layerId } = action.payload; - const layer: RegionalGuidanceLayer = { - id: getRGLayerId(layerId), - type: 'regional_guidance_layer', - isEnabled: true, - bbox: null, - bboxNeedsUpdate: false, - objects: [], - previewColor: getVectorMaskPreviewColor(state), - x: 0, - y: 0, - autoNegative: 'invert', - positivePrompt: '', - negativePrompt: null, - ipAdapters: [], - isSelected: true, - uploadedMaskImage: null, - }; - state.layers.push(layer); - exclusivelySelectLayer(state, layer.id); - }, - prepare: () => ({ payload: { layerId: uuidv4() } }), - }, - rgLayerRecalled: (state, action: PayloadAction) => { - state.layers.push({ ...action.payload, isSelected: true }); - exclusivelySelectLayer(state, action.payload.id); - }, - rgLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => { - const { layerId, prompt } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer); - layer.positivePrompt = prompt; - }, - rgLayerNegativePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => { - const { layerId, prompt } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer); - layer.negativePrompt = prompt; - }, - rgLayerPreviewColorChanged: (state, action: PayloadAction<{ layerId: string; color: RgbColor }>) => { - const { layerId, color } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer); - layer.previewColor = color; - }, - - rgLayerMaskImageUploaded: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO }>) => { - const { layerId, imageDTO } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer); - layer.uploadedMaskImage = imageDTOToImageWithDims(imageDTO); - }, - rgLayerAutoNegativeChanged: ( - state, - action: PayloadAction<{ layerId: string; autoNegative: ParameterAutoNegative }> - ) => { - const { layerId, autoNegative } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer); - layer.autoNegative = autoNegative; - }, - rgLayerIPAdapterAdded: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfigV2 }>) => { - const { layerId, ipAdapter } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer); - layer.ipAdapters.push(ipAdapter); - }, - rgLayerIPAdapterDeleted: (state, action: PayloadAction<{ layerId: string; ipAdapterId: string }>) => { - const { layerId, ipAdapterId } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer); - layer.ipAdapters = layer.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId); - }, - rgLayerIPAdapterImageChanged: ( - state, - action: PayloadAction<{ layerId: string; ipAdapterId: string; imageDTO: ImageDTO | null }> - ) => { - const { layerId, ipAdapterId, imageDTO } = action.payload; - const ipAdapter = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId); - ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; - }, - rgLayerIPAdapterWeightChanged: ( - state, - action: PayloadAction<{ layerId: string; ipAdapterId: string; weight: number }> - ) => { - const { layerId, ipAdapterId, weight } = action.payload; - const ipAdapter = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId); - ipAdapter.weight = weight; - }, - rgLayerIPAdapterBeginEndStepPctChanged: ( - state, - action: PayloadAction<{ layerId: string; ipAdapterId: string; beginEndStepPct: [number, number] }> - ) => { - const { layerId, ipAdapterId, beginEndStepPct } = action.payload; - const ipAdapter = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId); - ipAdapter.beginEndStepPct = beginEndStepPct; - }, - rgLayerIPAdapterMethodChanged: ( - state, - action: PayloadAction<{ layerId: string; ipAdapterId: string; method: IPMethodV2 }> - ) => { - const { layerId, ipAdapterId, method } = action.payload; - const ipAdapter = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId); - ipAdapter.method = method; - }, - rgLayerIPAdapterModelChanged: ( - state, - action: PayloadAction<{ - layerId: string; - ipAdapterId: string; - modelConfig: IPAdapterModelConfig | null; - }> - ) => { - const { layerId, ipAdapterId, modelConfig } = action.payload; - const ipAdapter = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId); - if (!modelConfig) { - ipAdapter.model = null; - return; - } - ipAdapter.model = zModelIdentifierField.parse(modelConfig); - }, - rgLayerIPAdapterCLIPVisionModelChanged: ( - state, - action: PayloadAction<{ layerId: string; ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }> - ) => { - const { layerId, ipAdapterId, clipVisionModel } = action.payload; - const ipAdapter = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId); - ipAdapter.clipVisionModel = clipVisionModel; - }, - //#endregion - - //#region Initial Image Layer - iiLayerAdded: { - reducer: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { - const { layerId, imageDTO } = action.payload; - - // Retain opacity and denoising strength of existing initial image layer if exists - let opacity = 1; - let denoisingStrength = 0.75; - const iiLayer = state.layers.find((l) => l.id === layerId); - if (iiLayer) { - assert(isInitialImageLayer(iiLayer)); - opacity = iiLayer.opacity; - denoisingStrength = iiLayer.denoisingStrength; - } - - // Highlander! There can be only one! - state.layers = state.layers.filter((l) => (isInitialImageLayer(l) ? false : true)); - - const layer: InitialImageLayer = { - id: layerId, - type: 'initial_image_layer', - opacity, - x: 0, - y: 0, - bbox: null, - bboxNeedsUpdate: false, - isEnabled: true, - image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null, - isSelected: true, - denoisingStrength, - }; - state.layers.push(layer); - exclusivelySelectLayer(state, layer.id); - }, - prepare: (imageDTO: ImageDTO | null) => ({ payload: { layerId: INITIAL_IMAGE_LAYER_ID, imageDTO } }), - }, - iiLayerRecalled: (state, action: PayloadAction) => { - state.layers = state.layers.filter((l) => (isInitialImageLayer(l) ? false : true)); - state.layers.push({ ...action.payload, isSelected: true }); - exclusivelySelectLayer(state, action.payload.id); - }, - iiLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { - const { layerId, imageDTO } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isInitialImageLayer); - layer.bbox = null; - layer.bboxNeedsUpdate = true; - layer.isEnabled = true; - layer.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; - }, - iiLayerDenoisingStrengthChanged: (state, action: PayloadAction<{ layerId: string; denoisingStrength: number }>) => { - const { layerId, denoisingStrength } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isInitialImageLayer); - layer.denoisingStrength = denoisingStrength; - }, - //#endregion - - //#region Raster Layers - rasterLayerAdded: { - reducer: (state, action: PayloadAction<{ layerId: string }>) => { - const { layerId } = action.payload; - const layer: RasterLayer = { - id: getRasterLayerId(layerId), - type: 'raster_layer', - isEnabled: true, - bbox: null, - bboxNeedsUpdate: false, - objects: [], - opacity: 1, - x: 0, - y: 0, - isSelected: true, - }; - state.layers.push(layer); - exclusivelySelectLayer(state, layer.id); - }, - prepare: () => ({ payload: { layerId: uuidv4() } }), - }, - //#endregion - - //#region Objects - brushLineAdded: { - reducer: ( - state, - action: PayloadAction< - AddBrushLineArg & { - lineUuid: string; - } - > - ) => { - const { layerId, points, lineUuid, color } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isRGOrRasterlayer); - layer.objects.push({ - id: getBrushLineId(layer.id, lineUuid), - type: 'brush_line', - // Points must be offset by the layer's x and y coordinates - // TODO: Handle this in the event listener? - points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y], - strokeWidth: state.brushSize, - color, - }); - layer.bboxNeedsUpdate = true; - if (layer.type === 'regional_guidance_layer') { - layer.uploadedMaskImage = null; - } - }, - prepare: (payload: AddBrushLineArg) => ({ - payload: { ...payload, lineUuid: uuidv4() }, - }), - }, - eraserLineAdded: { - reducer: ( - state, - action: PayloadAction< - AddEraserLineArg & { - lineUuid: string; - } - > - ) => { - const { layerId, points, lineUuid } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isRGOrRasterlayer); - layer.objects.push({ - id: getEraserLineId(layer.id, lineUuid), - type: 'eraser_line', - // Points must be offset by the layer's x and y coordinates - // TODO: Handle this in the event listener? - points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y], - strokeWidth: state.brushSize, - }); - layer.bboxNeedsUpdate = true; - if (isRegionalGuidanceLayer(layer)) { - layer.uploadedMaskImage = null; - } - }, - prepare: (payload: AddEraserLineArg) => ({ - payload: { ...payload, lineUuid: uuidv4() }, - }), - }, - linePointsAdded: (state, action: PayloadAction) => { - const { layerId, point } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isRGOrRasterlayer); - const lastLine = layer.objects.findLast(isLine); - if (!lastLine || !isLine(lastLine)) { - return; - } - // Points must be offset by the layer's x and y coordinates - // TODO: Handle this in the event listener - lastLine.points.push(point[0] - layer.x, point[1] - layer.y); - layer.bboxNeedsUpdate = true; - if (isRegionalGuidanceLayer(layer)) { - layer.uploadedMaskImage = null; - } - }, - rectAdded: { - reducer: (state, action: PayloadAction) => { - const { layerId, rect, rectUuid, color } = action.payload; - if (rect.height === 0 || rect.width === 0) { - // Ignore zero-area rectangles - return; - } - const layer = selectLayerOrThrow(state, layerId, isRGOrRasterlayer); - const id = getRectShapeId(layer.id, rectUuid); - layer.objects.push({ - type: 'rect_shape', - id, - x: rect.x - layer.x, - y: rect.y - layer.y, - width: rect.width, - height: rect.height, - color, - }); - layer.bboxNeedsUpdate = true; - if (isRegionalGuidanceLayer(layer)) { - layer.uploadedMaskImage = null; - } - }, - prepare: (payload: AddRectShapeArg) => ({ payload: { ...payload, rectUuid: uuidv4() } }), - }, - imageAdded: { - reducer: (state, action: PayloadAction) => { - const { layerId, imageUuid, imageDTO } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isRasterLayer); - const id = getImageObjectId(layer.id, imageUuid); - const { width, height, image_name: name } = imageDTO; - layer.objects.push({ - type: 'image', - id, - x: 0, - y: 0, - width, - height, - image: { width, height, name }, - }); - layer.bboxNeedsUpdate = true; - }, - prepare: (payload: AddImageObjectArg) => ({ payload: { ...payload, imageUuid: uuidv4() } }), - }, - //#endregion - - //#region Globals positivePromptChanged: (state, action: PayloadAction) => { - state.positivePrompt = action.payload; + state.prompts.positivePrompt = action.payload; }, negativePromptChanged: (state, action: PayloadAction) => { - state.negativePrompt = action.payload; + state.prompts.negativePrompt = action.payload; }, positivePrompt2Changed: (state, action: PayloadAction) => { - state.positivePrompt2 = action.payload; + state.prompts.positivePrompt2 = action.payload; }, negativePrompt2Changed: (state, action: PayloadAction) => { - state.negativePrompt2 = action.payload; + state.prompts.negativePrompt2 = action.payload; }, shouldConcatPromptsChanged: (state, action: PayloadAction) => { - state.shouldConcatPrompts = action.payload; + state.prompts.shouldConcatPrompts = action.payload; }, widthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }>) => { const { width, updateAspectRatio, clamp } = action.payload; @@ -807,28 +101,18 @@ export const controlLayersSlice = createSlice({ bboxChanged: (state, action: PayloadAction) => { state.bbox = action.payload; }, - brushSizeChanged: (state, action: PayloadAction) => { - state.brushSize = Math.round(action.payload); - }, - brushColorChanged: (state, action: PayloadAction) => { - state.brushColor = action.payload; + brushWidthChanged: (state, action: PayloadAction) => { + state.tool.brush.width = Math.round(action.payload); }, - globalMaskLayerOpacityChanged: (state, action: PayloadAction) => { - state.globalMaskLayerOpacity = action.payload; + eraserWidthChanged: (state, action: PayloadAction) => { + state.tool.eraser.width = Math.round(action.payload); }, - undo: (state) => { - // Invalidate the bbox for all layers to prevent stale bboxes - for (const layer of state.layers.filter(isRenderableLayer)) { - layer.bboxNeedsUpdate = true; - } + fillChanged: (state, action: PayloadAction) => { + state.tool.fill = action.payload; }, - redo: (state) => { - // Invalidate the bbox for all layers to prevent stale bboxes - for (const layer of state.layers.filter(isRenderableLayer)) { - layer.bboxNeedsUpdate = true; - } + invertScrollChanged: (state, action: PayloadAction) => { + state.tool.invertScroll = action.payload; }, - //#endregion }, extraReducers(builder) { builder.addCase(modelChanged, (state, action) => { @@ -845,113 +129,10 @@ export const controlLayersSlice = createSlice({ state.size.width = width; state.size.height = height; }); - - // // TODO: This is a temp fix to reduce issues with T2I adapter having a different downscaling - // // factor than the UNet. Hopefully we get an upstream fix in diffusers. - // builder.addMatcher(isAnyControlAdapterAdded, (state, action) => { - // if (action.payload.type === 't2i_adapter') { - // state.size.width = roundToMultiple(state.size.width, 64); - // state.size.height = roundToMultiple(state.size.height, 64); - // } - // }); }, }); -/** - * This class is used to cycle through a set of colors for the prompt region layers. - */ -class LayerColors { - static COLORS: RgbColor[] = [ - { r: 121, g: 157, b: 219 }, // rgb(121, 157, 219) - { r: 131, g: 214, b: 131 }, // rgb(131, 214, 131) - { r: 250, g: 225, b: 80 }, // rgb(250, 225, 80) - { r: 220, g: 144, b: 101 }, // rgb(220, 144, 101) - { r: 224, g: 117, b: 117 }, // rgb(224, 117, 117) - { r: 213, g: 139, b: 202 }, // rgb(213, 139, 202) - { r: 161, g: 120, b: 214 }, // rgb(161, 120, 214) - ]; - static i = this.COLORS.length - 1; - /** - * Get the next color in the sequence. If a known color is provided, the next color will be the one after it. - */ - static next(currentColor?: RgbColor): RgbColor { - if (currentColor) { - const i = this.COLORS.findIndex((c) => isEqual(c, currentColor)); - if (i !== -1) { - this.i = i; - } - } - this.i = (this.i + 1) % this.COLORS.length; - const color = this.COLORS[this.i]; - assert(color); - return color; - } -} - export const { - // Any Layer Type - layerSelected, - layerIsEnabledToggled, - layerTranslated, - layerBboxChanged, - layerReset, - layerDeleted, - layerOpacityChanged, - layerMovedForward, - layerMovedToFront, - layerMovedBackward, - layerMovedToBack, - selectedLayerDeleted, - allLayersDeleted, - // CA Layers - caLayerAdded, - caLayerRecalled, - caLayerImageChanged, - caLayerProcessedImageChanged, - caLayerModelChanged, - caLayerControlModeChanged, - caLayerProcessorConfigChanged, - caLayerIsFilterEnabledChanged, - caLayerProcessorPendingBatchIdChanged, - // IPA Layers - ipaLayerAdded, - ipaLayerRecalled, - ipaLayerImageChanged, - ipaLayerMethodChanged, - ipaLayerModelChanged, - ipaLayerCLIPVisionModelChanged, - // CA or IPA Layers - caOrIPALayerWeightChanged, - caOrIPALayerBeginEndStepPctChanged, - // RG Layers - rgLayerAdded, - rgLayerRecalled, - rgLayerPositivePromptChanged, - rgLayerNegativePromptChanged, - rgLayerPreviewColorChanged, - brushLineAdded, - eraserLineAdded, - linePointsAdded, - rectAdded, - imageAdded, - rgLayerMaskImageUploaded, - rgLayerAutoNegativeChanged, - rgLayerIPAdapterAdded, - rgLayerIPAdapterDeleted, - rgLayerIPAdapterImageChanged, - rgLayerIPAdapterWeightChanged, - rgLayerIPAdapterBeginEndStepPctChanged, - rgLayerIPAdapterMethodChanged, - rgLayerIPAdapterModelChanged, - rgLayerIPAdapterCLIPVisionModelChanged, - // II Layer - iiLayerAdded, - iiLayerRecalled, - iiLayerImageChanged, - iiLayerDenoisingStrengthChanged, - // Raster layers - rasterLayerAdded, - // Globals positivePromptChanged, negativePromptChanged, positivePrompt2Changed, @@ -961,30 +142,16 @@ export const { heightChanged, aspectRatioChanged, bboxChanged, - brushSizeChanged, - brushColorChanged, - globalMaskLayerOpacityChanged, - undo, - redo, -} = controlLayersSlice.actions; + brushWidthChanged, + eraserWidthChanged, + fillChanged, + invertScrollChanged, +} = canvasV2Slice.actions; -export const selectControlLayersSlice = (state: RootState) => state.controlLayers; +export const selectCanvasV2Slice = (state: RootState) => state.canvasV2; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrateControlLayersState = (state: any): any => { - if (state._version === 1) { - // Reset state for users on v1 (e.g. beta users), some changes could cause - state = deepClone(initialControlLayersState); - } - if (state._version === 2) { - // The CA `isProcessingImage` flag was replaced with a `processorPendingBatchId` property, fix up CA layers - for (const layer of (state as ControlLayersState).layers) { - if (layer.type === 'control_adapter_layer') { - layer.controlAdapter.processorPendingBatchId = null; - unset(layer.controlAdapter, 'isProcessingImage'); - } - } - } +const migrate = (state: any): any => { return state; }; @@ -992,8 +159,6 @@ const migrateControlLayersState = (state: any): any => { export const $isDrawing = atom(false); export const $isMouseDown = atom(false); export const $lastMouseDownPos = atom(null); -export const $tool = atom('brush'); -export const $toolBuffer = atom(null); export const $lastCursorPos = atom(null); export const $isPreviewVisible = atom(true); export const $lastAddedPoint = atom(null); @@ -1007,72 +172,24 @@ export const $stageAttrs = atom({ }); // Some nanostores that are manually synced to redux state to provide imperative access -// TODO(psyche): This is a hack, figure out another way to handle this... -export const $brushSize = atom(0); -export const $brushColor = atom(DEFAULT_RGBA_COLOR); +// TODO(psyche): +export const $tool = atom('brush'); +export const $toolBuffer = atom(null); +export const $brushWidth = atom(0); export const $brushSpacingPx = atom(0); -export const $selectedLayer = atom(null); -export const $shouldInvertBrushSizeScrollDirection = atom(false); +export const $eraserWidth = atom(0); +export const $eraserSpacingPx = atom(0); +export const $fill = atom(DEFAULT_RGBA_COLOR); +export const $selectedLayer = atom(null); +export const $selectedRG = atom(null); +export const $selectedCA = atom(null); +export const $selectedIPA = atom(null); +export const $invertScroll = atom(false); export const $bbox = atom({ x: 0, y: 0, width: 0, height: 0 }); -export const controlLayersPersistConfig: PersistConfig = { - name: controlLayersSlice.name, - initialState: initialControlLayersState, - migrate: migrateControlLayersState, +export const canvasV2PersistConfig: PersistConfig = { + name: canvasV2Slice.name, + initialState, + migrate, persistDenylist: ['bbox'], }; - -// These actions are _individually_ grouped together as single undoable actions -const undoableGroupByMatcher = isAnyOf( - layerTranslated, - brushSizeChanged, - globalMaskLayerOpacityChanged, - positivePromptChanged, - negativePromptChanged, - positivePrompt2Changed, - negativePrompt2Changed, - rgLayerPositivePromptChanged, - rgLayerNegativePromptChanged, - rgLayerPreviewColorChanged -); - -// These are used to group actions into logical lines below (hate typos) -const LINE_1 = 'LINE_1'; -const LINE_2 = 'LINE_2'; - -export const controlLayersUndoableConfig: UndoableOptions = { - limit: 64, - undoType: controlLayersSlice.actions.undo.type, - redoType: controlLayersSlice.actions.redo.type, - groupBy: (action, state, history) => { - // Lines are started with `rgLayerLineAdded` and may have any number of subsequent `rgLayerPointsAdded` events. - // We can use a double-buffer-esque trick to group each "logical" line as a single undoable action, without grouping - // separate logical lines as a single undo action. - if (brushLineAdded.match(action)) { - return history.group === LINE_1 ? LINE_2 : LINE_1; - } - if (linePointsAdded.match(action)) { - if (history.group === LINE_1 || history.group === LINE_2) { - return history.group; - } - } - if (undoableGroupByMatcher(action)) { - return action.type; - } - return null; - }, - filter: (action, _state, _history) => { - // TODO(psyche): TEMP OVERRIDE - return false; - // // Ignore all actions from other slices - // if (!action.type.startsWith(controlLayersSlice.name)) { - // return false; - // } - // // This action is triggered on state changes, including when we undo. If we do not ignore this action, when we - // // undo, this action triggers and empties the future states array. Therefore, we must ignore this action. - // if (layerBboxChanged.match(action)) { - // return false; - // } - // return true; - }, -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskSlice.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersSlice.ts new file mode 100644 index 00000000000..7424d41903f --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersSlice.ts @@ -0,0 +1,140 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; +import type { PersistConfig, RootState } from 'app/store/store'; +import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; +import { imageDTOToImageWithDims } from 'features/controlLayers/util/controlAdapters'; +import { zModelIdentifierField } from 'features/nodes/types/common'; +import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; +import { v4 as uuidv4 } from 'uuid'; + +import type { IPAdapterConfig, IPAdapterData } from './types'; + +type IPAdaptersState = { + _version: 1; + ipAdapters: IPAdapterData[]; +}; + +const initialState: IPAdaptersState = { + _version: 1, + ipAdapters: [], +}; + +const selectIpa = (state: IPAdaptersState, id: string) => state.ipAdapters.find((ipa) => ipa.id === id); + +export const ipAdaptersSlice = createSlice({ + name: 'ipAdapters', + initialState, + reducers: { + ipaAdded: { + reducer: (state, action: PayloadAction<{ id: string; config: IPAdapterConfig }>) => { + const { id, config } = action.payload; + const layer: IPAdapterData = { + id, + type: 'ip_adapter', + isEnabled: true, + ...config, + }; + state.ipAdapters.push(layer); + }, + prepare: (config: IPAdapterConfig) => ({ payload: { id: uuidv4(), config } }), + }, + ipaRecalled: (state, action: PayloadAction<{ data: IPAdapterData }>) => { + state.ipAdapters.push(action.payload.data); + }, + ipaIsEnabledChanged: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => { + const { id, isEnabled } = action.payload; + const ipa = selectIpa(state, id); + if (ipa) { + ipa.isEnabled = isEnabled; + } + }, + ipaDeleted: (state, action: PayloadAction<{ id: string }>) => { + state.ipAdapters = state.ipAdapters.filter((ipa) => ipa.id !== action.payload.id); + }, + ipaImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => { + const { id, imageDTO } = action.payload; + const ipa = selectIpa(state, id); + if (!ipa) { + return; + } + ipa.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; + }, + ipaMethodChanged: (state, action: PayloadAction<{ id: string; method: IPMethodV2 }>) => { + const { id, method } = action.payload; + const ipa = selectIpa(state, id); + if (!ipa) { + return; + } + ipa.method = method; + }, + ipaModelChanged: ( + state, + action: PayloadAction<{ + id: string; + modelConfig: IPAdapterModelConfig | null; + }> + ) => { + const { id, modelConfig } = action.payload; + const ipa = selectIpa(state, id); + if (!ipa) { + return; + } + if (modelConfig) { + ipa.model = zModelIdentifierField.parse(modelConfig); + } else { + ipa.model = null; + } + }, + ipaCLIPVisionModelChanged: (state, action: PayloadAction<{ id: string; clipVisionModel: CLIPVisionModelV2 }>) => { + const { id, clipVisionModel } = action.payload; + const ipa = selectIpa(state, id); + if (!ipa) { + return; + } + ipa.clipVisionModel = clipVisionModel; + }, + ipaWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { + const { id, weight } = action.payload; + const ipa = selectIpa(state, id); + if (!ipa) { + return; + } + ipa.weight = weight; + }, + ipaBeginEndStepPctChanged: (state, action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }>) => { + const { id, beginEndStepPct } = action.payload; + const ipa = selectIpa(state, id); + if (!ipa) { + return; + } + ipa.beginEndStepPct = beginEndStepPct; + }, + }, +}); + +export const { + ipaAdded, + ipaRecalled, + ipaIsEnabledChanged, + ipaDeleted, + ipaImageChanged, + ipaMethodChanged, + ipaModelChanged, + ipaCLIPVisionModelChanged, + ipaWeightChanged, + ipaBeginEndStepPctChanged, +} = ipAdaptersSlice.actions; + +export const selectIPAdaptersSlice = (state: RootState) => state.ipAdapters; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +const migrate = (state: any): any => { + return state; +}; + +export const ipAdaptersPersistConfig: PersistConfig = { + name: ipAdaptersSlice.name, + initialState, + migrate, + persistDenylist: [], +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts new file mode 100644 index 00000000000..45790bb2f1a --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts @@ -0,0 +1,268 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; +import type { PersistConfig, RootState } from 'app/store/store'; +import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; +import { getBrushLineId, getEraserLineId, getImageObjectId, getRectShapeId } from 'features/controlLayers/konva/naming'; +import type { IRect } from 'konva/lib/types'; +import { v4 as uuidv4 } from 'uuid'; + +import type { + AddBrushLineArg, + AddEraserLineArg, + AddImageObjectArg, + AddPointToLineArg, + AddRectShapeArg, + LayerData, +} from './types'; +import { isLine } from './types'; + +type LayersState = { + _version: 1; + layers: LayerData[]; +}; + +const initialState: LayersState = { _version: 1, layers: [] }; +const selectLayer = (state: LayersState, id: string) => state.layers.find((layer) => layer.id === id); + +export const layersSlice = createSlice({ + name: 'layers', + initialState, + reducers: { + layerAdded: { + reducer: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + state.layers.push({ + id, + type: 'layer', + isEnabled: true, + bbox: null, + bboxNeedsUpdate: false, + objects: [], + opacity: 1, + x: 0, + y: 0, + }); + }, + prepare: () => ({ payload: { id: uuidv4() } }), + }, + layerRecalled: (state, action: PayloadAction<{ data: LayerData }>) => { + state.layers.push(action.payload.data); + }, + layerIsEnabledChanged: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => { + const { id, isEnabled } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + layer.isEnabled = isEnabled; + }, + layerTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => { + const { id, x, y } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + layer.x = x; + layer.y = y; + }, + layerBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { + const { id, bbox } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + layer.bbox = bbox; + layer.bboxNeedsUpdate = false; + if (bbox === null) { + layer.objects = []; + } + }, + layerReset: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + layer.isEnabled = true; + layer.objects = []; + layer.bbox = null; + layer.bboxNeedsUpdate = false; + }, + layerDeleted: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + state.layers = state.layers.filter((l) => l.id !== id); + }, + layerOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => { + const { id, opacity } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + layer.opacity = opacity; + }, + layerMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + moveOneToEnd(state.layers, layer); + }, + layerMovedToFront: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + moveToEnd(state.layers, layer); + }, + layerMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + moveOneToStart(state.layers, layer); + }, + layerMovedToBack: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + moveToStart(state.layers, layer); + }, + layerBrushLineAdded: { + reducer: (state, action: PayloadAction) => { + const { id, points, lineId, color, width } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + + layer.objects.push({ + id: getBrushLineId(id, lineId), + type: 'brush_line', + points, + strokeWidth: width, + color, + }); + layer.bboxNeedsUpdate = true; + }, + prepare: (payload: AddBrushLineArg) => ({ + payload: { ...payload, lineId: uuidv4() }, + }), + }, + layerEraserLineAdded: { + reducer: (state, action: PayloadAction) => { + const { id, points, lineId, width } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + + layer.objects.push({ + id: getEraserLineId(id, lineId), + type: 'eraser_line', + points, + strokeWidth: width, + }); + layer.bboxNeedsUpdate = true; + }, + prepare: (payload: AddEraserLineArg) => ({ + payload: { ...payload, lineId: uuidv4() }, + }), + }, + layerLinePointAdded: (state, action: PayloadAction) => { + const { id, point } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + const lastObject = layer.objects[layer.objects.length - 1]; + if (!lastObject || !isLine(lastObject)) { + return; + } + lastObject.points.push(...point); + layer.bboxNeedsUpdate = true; + }, + layerRectAdded: { + reducer: (state, action: PayloadAction) => { + const { id, rect, rectId, color } = action.payload; + if (rect.height === 0 || rect.width === 0) { + // Ignore zero-area rectangles + return; + } + const layer = selectLayer(state, id); + if (!layer) { + return; + } + layer.objects.push({ + type: 'rect_shape', + id: getRectShapeId(id, rectId), + x: rect.x - layer.x, + y: rect.y - layer.y, + width: rect.width, + height: rect.height, + color, + }); + layer.bboxNeedsUpdate = true; + }, + prepare: (payload: AddRectShapeArg) => ({ payload: { ...payload, rectId: uuidv4() } }), + }, + layerImageAdded: { + reducer: (state, action: PayloadAction) => { + const { id, imageId, imageDTO } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + const { width, height, image_name: name } = imageDTO; + layer.objects.push({ + type: 'image', + id: getImageObjectId(id, imageId), + x: 0, + y: 0, + width, + height, + image: { width, height, name }, + }); + layer.bboxNeedsUpdate = true; + }, + prepare: (payload: AddImageObjectArg) => ({ payload: { ...payload, imageId: uuidv4() } }), + }, + }, +}); + +export const { + layerAdded, + layerDeleted, + layerReset, + layerMovedForwardOne, + layerMovedToFront, + layerMovedBackwardOne, + layerMovedToBack, + layerIsEnabledChanged, + layerOpacityChanged, + layerTranslated, + layerBboxChanged, + layerBrushLineAdded, + layerEraserLineAdded, + layerLinePointAdded, + layerRectAdded, + layerImageAdded, +} = layersSlice.actions; + +export const selectLayersSlice = (state: RootState) => state.layers; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +const migrate = (state: any): any => { + return state; +}; + +export const layersPersistConfig: PersistConfig = { + name: layersSlice.name, + initialState, + migrate, + persistDenylist: [], +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts new file mode 100644 index 00000000000..167feec396f --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts @@ -0,0 +1,440 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; +import type { PersistConfig, RootState } from 'app/store/store'; +import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; +import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; +import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; +import { imageDTOToImageWithDims } from 'features/controlLayers/util/controlAdapters'; +import { zModelIdentifierField } from 'features/nodes/types/common'; +import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas'; +import type { IRect } from 'konva/lib/types'; +import { isEqual } from 'lodash-es'; +import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; +import { assert } from 'tsafe'; +import { v4 as uuidv4 } from 'uuid'; + +import type { + AddBrushLineArg, + AddEraserLineArg, + AddPointToLineArg, + AddRectShapeArg, + IPAdapterData, + RegionalGuidanceData, + RgbColor, +} from './types'; +import { isLine } from './types'; + +type RegionalGuidanceState = { + _version: 1; + regions: RegionalGuidanceData[]; + opacity: number; +}; + +const initialState: RegionalGuidanceState = { + _version: 1, + regions: [], + opacity: 0.3, +}; + +const selectRg = (state: RegionalGuidanceState, id: string) => state.regions.find((rg) => rg.id === id); + +const DEFAULT_MASK_COLORS: RgbColor[] = [ + { r: 121, g: 157, b: 219 }, // rgb(121, 157, 219) + { r: 131, g: 214, b: 131 }, // rgb(131, 214, 131) + { r: 250, g: 225, b: 80 }, // rgb(250, 225, 80) + { r: 220, g: 144, b: 101 }, // rgb(220, 144, 101) + { r: 224, g: 117, b: 117 }, // rgb(224, 117, 117) + { r: 213, g: 139, b: 202 }, // rgb(213, 139, 202) + { r: 161, g: 120, b: 214 }, // rgb(161, 120, 214) +]; + +const getRGMaskFill = (state: RegionalGuidanceState): RgbColor => { + const lastFill = state.regions.slice(-1)[0]?.fill; + let i = DEFAULT_MASK_COLORS.findIndex((c) => isEqual(c, lastFill)); + if (i === -1) { + i = 0; + } + i = (i + 1) % DEFAULT_MASK_COLORS.length; + const fill = DEFAULT_MASK_COLORS[i]; + assert(fill, 'This should never happen'); + return fill; +}; + +export const regionalGuidanceSlice = createSlice({ + name: 'regionalGuidance', + initialState, + reducers: { + rgAdded: { + reducer: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const rg: RegionalGuidanceData = { + id, + type: 'regional_guidance', + isEnabled: true, + bbox: null, + bboxNeedsUpdate: false, + objects: [], + fill: getRGMaskFill(state), + x: 0, + y: 0, + autoNegative: 'invert', + positivePrompt: '', + negativePrompt: null, + ipAdapters: [], + imageCache: null, + }; + state.regions.push(rg); + }, + prepare: () => ({ payload: { id: uuidv4() } }), + }, + rgRecalled: (state, action: PayloadAction<{ data: RegionalGuidanceData }>) => { + const { data } = action.payload; + state.regions.push(data); + }, + rgIsEnabledToggled: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => { + const { id, isEnabled } = action.payload; + const rg = selectRg(state, id); + if (rg) { + rg.isEnabled = isEnabled; + } + }, + rgTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => { + const { id, x, y } = action.payload; + const rg = selectRg(state, id); + if (rg) { + rg.x = x; + rg.y = y; + } + }, + rgBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { + const { id, bbox } = action.payload; + const rg = selectRg(state, id); + if (rg) { + rg.bbox = bbox; + rg.bboxNeedsUpdate = false; + } + }, + rgDeleted: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + state.regions = state.regions.filter((ca) => ca.id !== id); + }, + rgGlobalOpacityChanged: (state, action: PayloadAction<{ opacity: number }>) => { + const { opacity } = action.payload; + state.opacity = opacity; + }, + rgMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + moveOneToEnd(state.regions, rg); + }, + rgMovedToFront: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + moveToEnd(state.regions, rg); + }, + rgMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + moveOneToStart(state.regions, rg); + }, + rgMovedToBack: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + moveToStart(state.regions, rg); + }, + rgPositivePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => { + const { id, prompt } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + rg.positivePrompt = prompt; + }, + rgNegativePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => { + const { id, prompt } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + rg.negativePrompt = prompt; + }, + rgFillChanged: (state, action: PayloadAction<{ id: string; fill: RgbColor }>) => { + const { id, fill } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + rg.fill = fill; + }, + rgMaskImageUploaded: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO }>) => { + const { id, imageDTO } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + rg.imageCache = imageDTOToImageWithDims(imageDTO); + }, + rgAutoNegativeChanged: (state, action: PayloadAction<{ id: string; autoNegative: ParameterAutoNegative }>) => { + const { id, autoNegative } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + rg.autoNegative = autoNegative; + }, + rgIPAdapterAdded: (state, action: PayloadAction<{ id: string; ipAdapter: IPAdapterData }>) => { + const { id, ipAdapter } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + rg.ipAdapters.push(ipAdapter); + }, + rgIPAdapterDeleted: (state, action: PayloadAction<{ id: string; ipAdapterId: string }>) => { + const { id, ipAdapterId } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + rg.ipAdapters = rg.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId); + }, + rgIPAdapterImageChanged: ( + state, + action: PayloadAction<{ id: string; ipAdapterId: string; imageDTO: ImageDTO | null }> + ) => { + const { id, ipAdapterId, imageDTO } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); + if (!ipa) { + return; + } + ipa.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; + }, + rgIPAdapterWeightChanged: (state, action: PayloadAction<{ id: string; ipAdapterId: string; weight: number }>) => { + const { id, ipAdapterId, weight } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); + if (!ipa) { + return; + } + ipa.weight = weight; + }, + rgIPAdapterBeginEndStepPctChanged: ( + state, + action: PayloadAction<{ id: string; ipAdapterId: string; beginEndStepPct: [number, number] }> + ) => { + const { id, ipAdapterId, beginEndStepPct } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); + if (!ipa) { + return; + } + ipa.beginEndStepPct = beginEndStepPct; + }, + rgIPAdapterMethodChanged: ( + state, + action: PayloadAction<{ id: string; ipAdapterId: string; method: IPMethodV2 }> + ) => { + const { id, ipAdapterId, method } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); + if (!ipa) { + return; + } + ipa.method = method; + }, + rgIPAdapterModelChanged: ( + state, + action: PayloadAction<{ + id: string; + ipAdapterId: string; + modelConfig: IPAdapterModelConfig | null; + }> + ) => { + const { id, ipAdapterId, modelConfig } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); + if (!ipa) { + return; + } + if (modelConfig) { + ipa.model = zModelIdentifierField.parse(modelConfig); + } else { + ipa.model = null; + } + }, + rgIPAdapterCLIPVisionModelChanged: ( + state, + action: PayloadAction<{ id: string; ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }> + ) => { + const { id, ipAdapterId, clipVisionModel } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); + if (!ipa) { + return; + } + ipa.clipVisionModel = clipVisionModel; + }, + rgBrushLineAdded: { + reducer: (state, action: PayloadAction) => { + const { id, points, lineId, color, width } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + rg.objects.push({ + id: getBrushLineId(id, lineId), + type: 'brush_line', + // Points must be offset by the layer's x and y coordinates + // TODO: Handle this in the event listener? + points: [points[0] - rg.x, points[1] - rg.y, points[2] - rg.x, points[3] - rg.y], + strokeWidth: width, + color, + }); + rg.bboxNeedsUpdate = true; + rg.imageCache = null; + }, + prepare: (payload: AddBrushLineArg) => ({ + payload: { ...payload, lineId: uuidv4() }, + }), + }, + rgEraserLineAdded: { + reducer: (state, action: PayloadAction) => { + const { id, points, lineId, width } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + rg.objects.push({ + id: getEraserLineId(id, lineId), + type: 'eraser_line', + // Points must be offset by the layer's x and y coordinates + // TODO: Handle this in the event listener? + points: [points[0] - rg.x, points[1] - rg.y, points[2] - rg.x, points[3] - rg.y], + strokeWidth: width, + }); + rg.bboxNeedsUpdate = true; + rg.imageCache = null; + }, + prepare: (payload: AddEraserLineArg) => ({ + payload: { ...payload, lineId: uuidv4() }, + }), + }, + rgLinePointAdded: (state, action: PayloadAction) => { + const { id, point } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + const lastObject = rg.objects[rg.objects.length - 1]; + if (!lastObject || !isLine(lastObject)) { + return; + } + // Points must be offset by the layer's x and y coordinates + // TODO: Handle this in the event listener + lastObject.points.push(point[0] - rg.x, point[1] - rg.y); + rg.bboxNeedsUpdate = true; + rg.imageCache = null; + }, + rgRectAdded: { + reducer: (state, action: PayloadAction) => { + const { id, rect, rectId, color } = action.payload; + if (rect.height === 0 || rect.width === 0) { + // Ignore zero-area rectangles + return; + } + const rg = selectRg(state, id); + if (!rg) { + return; + } + rg.objects.push({ + type: 'rect_shape', + id: getRectShapeId(id, rectId), + x: rect.x - rg.x, + y: rect.y - rg.y, + width: rect.width, + height: rect.height, + color, + }); + rg.bboxNeedsUpdate = true; + rg.imageCache = null; + }, + prepare: (payload: AddRectShapeArg) => ({ payload: { ...payload, rectId: uuidv4() } }), + }, + }, +}); + +export const { + rgAdded, + rgRecalled, + rgIsEnabledToggled, + rgTranslated, + rgBboxChanged, + rgDeleted, + rgGlobalOpacityChanged, + rgMovedForwardOne, + rgMovedToFront, + rgMovedBackwardOne, + rgMovedToBack, + rgPositivePromptChanged, + rgNegativePromptChanged, + rgFillChanged, + rgMaskImageUploaded, + rgAutoNegativeChanged, + rgIPAdapterAdded, + rgIPAdapterDeleted, + rgIPAdapterImageChanged, + rgIPAdapterWeightChanged, + rgIPAdapterBeginEndStepPctChanged, + rgIPAdapterMethodChanged, + rgIPAdapterModelChanged, + rgIPAdapterCLIPVisionModelChanged, + rgBrushLineAdded, + rgEraserLineAdded, + rgLinePointAdded, + rgRectAdded, +} = regionalGuidanceSlice.actions; + +export const selectRegionalGuidanceSlice = (state: RootState) => state.regionalGuidance; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +const migrate = (state: any): any => { + return state; +}; + +export const regionalGuidancePersistConfig: PersistConfig = { + name: regionalGuidanceSlice.name, + initialState, + migrate, + persistDenylist: [], +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 5380fe79d5e..860f3f20825 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,9 +1,13 @@ import { - zControlNetConfigV2, + zBeginEndStepPct, + zCLIPVisionModelV2, + zControlModeV2, + zId, zImageWithDims, - zIPAdapterConfigV2, - zT2IAdapterConfigV2, + zIPMethodV2, + zProcessorConfig, } from 'features/controlLayers/util/controlAdapters'; +import { zModelIdentifierField } from 'features/nodes/types/common'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; import type { ParameterHeight, @@ -17,7 +21,6 @@ import { zAutoNegative, zParameterNegativePrompt, zParameterPositivePrompt, - zParameterStrength, } from 'features/parameters/types/parameterSchemas'; import type { IRect } from 'konva/lib/types'; import type { ImageDTO } from 'services/api/types'; @@ -31,7 +34,7 @@ const zPoints = z.array(z.number()).refine((points) => points.length % 2 === 0, message: 'Must have an even number of points', }); const zOLD_VectorMaskLine = z.object({ - id: z.string(), + id: zId, type: z.literal('vector_mask_line'), tool: zDrawingTool, strokeWidth: z.number().min(1), @@ -39,7 +42,7 @@ const zOLD_VectorMaskLine = z.object({ }); const zOLD_VectorMaskRect = z.object({ - id: z.string(), + id: zId, type: z.literal('vector_mask_rect'), x: z.number(), y: z.number(), @@ -52,6 +55,7 @@ const zRgbColor = z.object({ g: z.number().int().min(0).max(255), b: z.number().int().min(0).max(255), }); +export type RgbColor = z.infer; const zRgbaColor = zRgbColor.extend({ a: z.number().min(0).max(1), }); @@ -61,7 +65,7 @@ export const DEFAULT_RGBA_COLOR: RgbaColor = { r: 255, g: 255, b: 255, a: 1 }; const zOpacity = z.number().gte(0).lte(1); const zBrushLine = z.object({ - id: z.string(), + id: zId, type: z.literal('brush_line'), strokeWidth: z.number().min(1), points: zPoints, @@ -70,7 +74,7 @@ const zBrushLine = z.object({ export type BrushLine = z.infer; const zEraserline = z.object({ - id: z.string(), + id: zId, type: z.literal('eraser_line'), strokeWidth: z.number().min(1), points: zPoints, @@ -78,7 +82,7 @@ const zEraserline = z.object({ export type EraserLine = z.infer; const zRectShape = z.object({ - id: z.string(), + id: zId, type: z.literal('rect_shape'), x: z.number(), y: z.number(), @@ -89,7 +93,7 @@ const zRectShape = z.object({ export type RectShape = z.infer; const zEllipseShape = z.object({ - id: z.string(), + id: zId, type: z.literal('ellipse_shape'), x: z.number(), y: z.number(), @@ -100,7 +104,7 @@ const zEllipseShape = z.object({ export type EllipseShape = z.infer; const zPolygonShape = z.object({ - id: z.string(), + id: zId, type: z.literal('polygon_shape'), points: zPoints, color: zRgbaColor, @@ -108,7 +112,7 @@ const zPolygonShape = z.object({ export type PolygonShape = z.infer; const zImageObject = z.object({ - id: z.string(), + id: zId, type: z.literal('image'), image: zImageWithDims, x: z.number(), @@ -118,7 +122,7 @@ const zImageObject = z.object({ }); export type ImageObject = z.infer; -const zAnyLayerObject = z.discriminatedUnion('type', [ +const zLayerObject = z.discriminatedUnion('type', [ zImageObject, zBrushLine, zEraserline, @@ -126,13 +130,7 @@ const zAnyLayerObject = z.discriminatedUnion('type', [ zEllipseShape, zPolygonShape, ]); -export type AnyLayerObject = z.infer; - -const zLayerBase = z.object({ - id: z.string(), - isEnabled: z.boolean().default(true), - isSelected: z.boolean().default(true), -}); +export type LayerObject = z.infer; const zRect = z.object({ x: z.number(), @@ -140,33 +138,36 @@ const zRect = z.object({ width: z.number().min(1), height: z.number().min(1), }); -const zRenderableLayerBase = zLayerBase.extend({ + +const zLayerData = z.object({ + id: zId, + type: z.literal('layer'), + isEnabled: z.boolean(), x: z.number(), y: z.number(), bbox: zRect.nullable(), bboxNeedsUpdate: z.boolean(), -}); - -const zRasterLayer = zRenderableLayerBase.extend({ - type: z.literal('raster_layer'), opacity: zOpacity, - objects: z.array(zAnyLayerObject), + objects: z.array(zLayerObject), }); -export type RasterLayer = z.infer; +export type LayerData = z.infer; -const zControlAdapterLayer = zRenderableLayerBase.extend({ - type: z.literal('control_adapter_layer'), - opacity: zOpacity, - isFilterEnabled: z.boolean(), - controlAdapter: z.discriminatedUnion('type', [zControlNetConfigV2, zT2IAdapterConfigV2]), -}); -export type ControlAdapterLayer = z.infer; - -const zIPAdapterLayer = zLayerBase.extend({ - type: z.literal('ip_adapter_layer'), - ipAdapter: zIPAdapterConfigV2, +const zIPAdapterData = z.object({ + id: zId, + type: z.literal('ip_adapter'), + isEnabled: z.boolean(), + weight: z.number().gte(-1).lte(2), + method: zIPMethodV2, + image: zImageWithDims.nullable(), + model: zModelIdentifierField.nullable(), + clipVisionModel: zCLIPVisionModelV2, + beginEndStepPct: zBeginEndStepPct, }); -export type IPAdapterLayer = z.infer; +export type IPAdapterData = z.infer; +export type IPAdapterConfig = Pick< + IPAdapterData, + 'weight' | 'image' | 'beginEndStepPct' | 'model' | 'clipVisionModel' | 'method' +>; const zMaskObject = z .discriminatedUnion('type', [zOLD_VectorMaskLine, zOLD_VectorMaskRect, zBrushLine, zEraserline, zRectShape]) @@ -201,69 +202,109 @@ const zMaskObject = z }) .pipe(z.discriminatedUnion('type', [zBrushLine, zEraserline, zRectShape])); -const zOLD_RegionalGuidanceLayer = zRenderableLayerBase.extend({ - type: z.literal('regional_guidance_layer'), - maskObjects: z.array(zMaskObject), - positivePrompt: zParameterPositivePrompt.nullable(), - negativePrompt: zParameterNegativePrompt.nullable(), - ipAdapters: z.array(zIPAdapterConfigV2), - previewColor: zRgbColor, - autoNegative: zAutoNegative, - uploadedMaskImage: zImageWithDims.nullable(), -}); -const zRegionalGuidanceLayer = zRenderableLayerBase.extend({ - type: z.literal('regional_guidance_layer'), +const zRegionalGuidanceData = z.object({ + id: zId, + type: z.literal('regional_guidance'), + isEnabled: z.boolean(), + x: z.number(), + y: z.number(), + bbox: zRect.nullable(), + bboxNeedsUpdate: z.boolean(), objects: z.array(zMaskObject), positivePrompt: zParameterPositivePrompt.nullable(), negativePrompt: zParameterNegativePrompt.nullable(), - ipAdapters: z.array(zIPAdapterConfigV2), - previewColor: zRgbColor, + ipAdapters: z.array(zIPAdapterData), + fill: zRgbColor, autoNegative: zAutoNegative, - uploadedMaskImage: zImageWithDims.nullable(), + imageCache: zImageWithDims.nullable(), }); -// TODO(psyche): This doesn't migrate correctly! -const zRGLayer = z - .union([zOLD_RegionalGuidanceLayer, zRegionalGuidanceLayer]) - .transform((val) => { - if ('maskObjects' in val) { - const { maskObjects, ...rest } = val; - return { ...rest, objects: maskObjects }; - } else { - return val; - } - }) - .pipe(zRegionalGuidanceLayer); -export type RegionalGuidanceLayer = z.infer; +export type RegionalGuidanceData = z.infer; -const zInitialImageLayer = zRenderableLayerBase.extend({ - type: z.literal('initial_image_layer'), +const zColorFill = z.object({ + type: z.literal('color_fill'), + color: zRgbaColor, +}); +const zImageFill = z.object({ + type: z.literal('image_fill'), + src: z.string(), +}); +const zFill = z.discriminatedUnion('type', [zColorFill, zImageFill]); +const zInpaintMaskData = z.object({ + id: zId, + type: z.literal('inpaint_mask'), + isEnabled: z.boolean(), + x: z.number(), + y: z.number(), + bbox: zRect.nullable(), + bboxNeedsUpdate: z.boolean(), + maskObjects: z.array(zMaskObject), + fill: zFill, + imageCache: zImageWithDims.nullable(), +}); +export type InpaintMaskData = z.infer; + +const zFilter = z.enum(['none', 'lightness_to_alpha']); +export type Filter = z.infer; + +const zControlAdapterData = z.object({ + id: zId, + type: z.literal('control_adapter'), + isEnabled: z.boolean(), + x: z.number(), + y: z.number(), + bbox: zRect.nullable(), + bboxNeedsUpdate: z.boolean(), opacity: zOpacity, + filter: zFilter, + weight: z.number().gte(-1).lte(2), image: zImageWithDims.nullable(), - denoisingStrength: zParameterStrength, + processedImage: zImageWithDims.nullable(), + processorConfig: zProcessorConfig.nullable(), + processorPendingBatchId: z.string().nullable().default(null), + beginEndStepPct: zBeginEndStepPct, + model: zModelIdentifierField.nullable(), + controlMode: zControlModeV2.nullable(), }); -export type InitialImageLayer = z.infer; +export type ControlAdapterData = z.infer; +export type ControlAdapterConfig = Pick< + ControlAdapterData, + 'weight' | 'image' | 'processedImage' | 'processorConfig' | 'beginEndStepPct' | 'model' | 'controlMode' +>; -export const zLayer = z.discriminatedUnion('type', [ - zRegionalGuidanceLayer, - zControlAdapterLayer, - zIPAdapterLayer, - zInitialImageLayer, - zRasterLayer, -]); -export type Layer = z.infer; +const zCanvasItemIdentifier = z.object({ + type: z.enum([ + zLayerData.shape.type.value, + zIPAdapterData.shape.type.value, + zControlAdapterData.shape.type.value, + zRegionalGuidanceData.shape.type.value, + zInpaintMaskData.shape.type.value, + ]), + id: zId, +}); +type CanvasItemIdentifier = z.infer; -export type ControlLayersState = { +export type CanvasV2State = { _version: 3; - selectedLayerId: string | null; - layers: Layer[]; - brushSize: number; - brushColor: RgbaColor; - globalMaskLayerOpacity: number; - positivePrompt: ParameterPositivePrompt; - negativePrompt: ParameterNegativePrompt; - positivePrompt2: ParameterPositiveStylePromptSDXL; - negativePrompt2: ParameterNegativeStylePromptSDXL; - shouldConcatPrompts: boolean; + lastSelectedItem: CanvasItemIdentifier | null; + prompts: { + positivePrompt: ParameterPositivePrompt; + negativePrompt: ParameterNegativePrompt; + positivePrompt2: ParameterPositiveStylePromptSDXL; + negativePrompt2: ParameterNegativeStylePromptSDXL; + shouldConcatPrompts: boolean; + }; + tool: { + selected: Tool; + selectedBuffer: Tool | null; + invertScroll: boolean; + brush: { + width: number; + }; + eraser: { + width: number; + }; + fill: RgbaColor; + }; size: { width: ParameterWidth; height: ParameterHeight; @@ -273,45 +314,13 @@ export type ControlLayersState = { }; export type StageAttrs = { x: number; y: number; width: number; height: number; scale: number }; -export type AddEraserLineArg = { layerId: string; points: [number, number, number, number] }; +export type AddEraserLineArg = { id: string; points: [number, number, number, number]; width: number }; export type AddBrushLineArg = AddEraserLineArg & { color: RgbaColor }; -export type AddPointToLineArg = { layerId: string; point: [number, number] }; -export type AddRectShapeArg = { layerId: string; rect: IRect; color: RgbaColor }; -export type AddImageObjectArg = { layerId: string; imageDTO: ImageDTO }; +export type AddPointToLineArg = { id: string; point: [number, number] }; +export type AddRectShapeArg = { id: string; rect: IRect; color: RgbaColor }; +export type AddImageObjectArg = { id: string; imageDTO: ImageDTO }; //#region Type guards -export const isLine = (obj: AnyLayerObject): obj is BrushLine | EraserLine => { +export const isLine = (obj: LayerObject): obj is BrushLine | EraserLine => { return obj.type === 'brush_line' || obj.type === 'eraser_line'; }; -export const isRegionalGuidanceLayer = (layer?: Layer): layer is RegionalGuidanceLayer => { - return layer?.type === 'regional_guidance_layer'; -}; -export const isControlAdapterLayer = (layer?: Layer): layer is ControlAdapterLayer => { - return layer?.type === 'control_adapter_layer'; -}; -export const isIPAdapterLayer = (layer?: Layer): layer is IPAdapterLayer => { - return layer?.type === 'ip_adapter_layer'; -}; -export const isInitialImageLayer = (layer?: Layer): layer is InitialImageLayer => { - return layer?.type === 'initial_image_layer'; -}; -export const isRasterLayer = (layer?: Layer): layer is RasterLayer => { - return layer?.type === 'raster_layer'; -}; -export const isRenderableLayer = ( - layer?: Layer -): layer is RegionalGuidanceLayer | ControlAdapterLayer | InitialImageLayer | RasterLayer => { - return ( - isRegionalGuidanceLayer(layer) || isControlAdapterLayer(layer) || isInitialImageLayer(layer) || isRasterLayer(layer) - ); -}; -export const isLayerWithOpacity = (layer?: Layer): layer is ControlAdapterLayer | InitialImageLayer | RasterLayer => { - return isControlAdapterLayer(layer) || isInitialImageLayer(layer) || isRasterLayer(layer); -}; -export const isCAOrIPALayer = (layer?: Layer): layer is ControlAdapterLayer | IPAdapterLayer => { - return isControlAdapterLayer(layer) || isIPAdapterLayer(layer); -}; -export const isRGOrRasterlayer = (layer?: Layer): layer is RegionalGuidanceLayer | RasterLayer => { - return isRegionalGuidanceLayer(layer) || isRasterLayer(layer); -}; -//#endregion diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts index 0330eceee17..892d2c5eaca 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts @@ -10,7 +10,7 @@ import type { } from 'services/api/types'; import { z } from 'zod'; -const zId = z.string().min(1); +export const zId = z.string().min(1); const zCannyProcessorConfig = z.object({ id: zId, @@ -120,7 +120,7 @@ const zZoeDepthProcessorConfig = z.object({ }); export type ZoeDepthProcessorConfig = z.infer; -const zProcessorConfig = z.discriminatedUnion('type', [ +export const zProcessorConfig = z.discriminatedUnion('type', [ zCannyProcessorConfig, zColorMapProcessorConfig, zContentShuffleProcessorConfig, @@ -145,7 +145,7 @@ export const zImageWithDims = z.object({ }); export type ImageWithDims = z.infer; -const zBeginEndStepPct = z +export const zBeginEndStepPct = z .tuple([z.number().gte(0).lte(1), z.number().gte(0).lte(1)]) .refine(([begin, end]) => begin < end, { message: 'Begin must be less than end', @@ -161,7 +161,7 @@ const zControlAdapterBase = z.object({ beginEndStepPct: zBeginEndStepPct, }); -const zControlModeV2 = z.enum(['balanced', 'more_prompt', 'more_control', 'unbalanced']); +export const zControlModeV2 = z.enum(['balanced', 'more_prompt', 'more_control', 'unbalanced']); export type ControlModeV2 = z.infer; export const isControlModeV2 = (v: unknown): v is ControlModeV2 => zControlModeV2.safeParse(v).success; @@ -178,11 +178,11 @@ export const zT2IAdapterConfigV2 = zControlAdapterBase.extend({ }); export type T2IAdapterConfigV2 = z.infer; -const zCLIPVisionModelV2 = z.enum(['ViT-H', 'ViT-G']); +export const zCLIPVisionModelV2 = z.enum(['ViT-H', 'ViT-G']); export type CLIPVisionModelV2 = z.infer; export const isCLIPVisionModelV2 = (v: unknown): v is CLIPVisionModelV2 => zCLIPVisionModelV2.safeParse(v).success; -const zIPMethodV2 = z.enum(['full', 'style', 'composition']); +export const zIPMethodV2 = z.enum(['full', 'style', 'composition']); export type IPMethodV2 = z.infer; export const isIPMethodV2 = (v: unknown): v is IPMethodV2 => zIPMethodV2.safeParse(v).success; diff --git a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx index 3df7a02f431..e58c0eaad7c 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx +++ b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx @@ -3,7 +3,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice'; -import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions'; import { getImageUsage, selectImageUsage } from 'features/deleteImageModal/store/selectors'; import { @@ -27,7 +27,7 @@ const selectImageUsages = createMemoizedSelector( selectCanvasSlice, selectNodesSlice, selectControlAdaptersSlice, - selectControlLayersSlice, + selectCanvasV2Slice, selectImageUsage, ], (deleteImageModal, canvas, nodes, controlAdapters, controlLayers, imagesUsage) => { diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts index aeb41c402c7..0a2a0587c7b 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts @@ -7,8 +7,8 @@ import { } from 'features/controlAdapters/store/controlAdaptersSlice'; import type { ControlAdaptersState } from 'features/controlAdapters/store/types'; import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; -import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; -import type { ControlLayersState } from 'features/controlLayers/store/types'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; +import type { CanvasV2State } from 'features/controlLayers/store/types'; import { isControlAdapterLayer, isInitialImageLayer, @@ -28,7 +28,7 @@ export const getImageUsage = ( canvas: CanvasState, nodes: NodesState, controlAdapters: ControlAdaptersState, - controlLayers: ControlLayersState, + controlLayers: CanvasV2State, image_name: string ) => { const isCanvasImage = canvas.layerState.objects.some((obj) => obj.kind === 'image' && obj.imageName === image_name); @@ -75,7 +75,7 @@ export const selectImageUsage = createMemoizedSelector( selectCanvasSlice, selectNodesSlice, selectControlAdaptersSlice, - selectControlLayersSlice, + selectCanvasV2Slice, (deleteImageModal, canvas, nodes, controlAdapters, controlLayers) => { const { imagesToDelete } = deleteImageModal; diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx index 2fe1963e76f..a0fa496ea61 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx @@ -15,7 +15,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice'; -import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; import ImageUsageMessage from 'features/deleteImageModal/components/ImageUsageMessage'; import { getImageUsage } from 'features/deleteImageModal/store/selectors'; import type { ImageUsage } from 'features/deleteImageModal/store/types'; @@ -42,7 +42,7 @@ const DeleteBoardModal = (props: Props) => { const selectImageUsageSummary = useMemo( () => createMemoizedSelector( - [selectCanvasSlice, selectNodesSlice, selectControlAdaptersSlice, selectControlLayersSlice], + [selectCanvasSlice, selectNodesSlice, selectControlAdaptersSlice, selectCanvasV2Slice], (canvas, nodes, controlAdapters, controlLayers) => { const allImageUsage = (boardImageNames ?? []).map((imageName) => getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, imageName) diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx index ab4ce039877..7bc60580374 100644 --- a/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx +++ b/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx @@ -1,4 +1,4 @@ -import type { Layer } from 'features/controlLayers/store/types'; +import type { LayerData } from 'features/controlLayers/store/types'; import { MetadataItemView } from 'features/metadata/components/MetadataItemView'; import type { MetadataHandlers } from 'features/metadata/types'; import { handlers } from 'features/metadata/util/handlers'; @@ -9,7 +9,7 @@ type Props = { }; export const MetadataLayers = ({ metadata }: Props) => { - const [layers, setLayers] = useState([]); + const [layers, setLayers] = useState([]); useEffect(() => { const parse = async () => { @@ -40,8 +40,8 @@ const MetadataViewLayer = ({ handlers, }: { label: string; - layer: Layer; - handlers: MetadataHandlers; + layer: LayerData; + handlers: MetadataHandlers; }) => { const onRecall = useCallback(() => { if (!handlers.recallItem) { diff --git a/invokeai/frontend/web/src/features/metadata/util/handlers.ts b/invokeai/frontend/web/src/features/metadata/util/handlers.ts index 33715cbbe10..f55f085b7dd 100644 --- a/invokeai/frontend/web/src/features/metadata/util/handlers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/handlers.ts @@ -2,7 +2,7 @@ import { getStore } from 'app/store/nanostores/store'; import { deepClone } from 'common/util/deepClone'; import { objectKeys } from 'common/util/objectKeys'; import { shouldConcatPromptsChanged } from 'features/controlLayers/store/controlLayersSlice'; -import type { Layer } from 'features/controlLayers/store/types'; +import type { LayerData } from 'features/controlLayers/store/types'; import type { LoRA } from 'features/lora/store/loraSlice'; import type { AnyControlAdapterConfigMetadata, @@ -49,7 +49,7 @@ const renderControlAdapterValue: MetadataRenderValueFunc = async (layer) => { +const renderLayerValue: MetadataRenderValueFunc = async (layer) => { if (layer.type === 'initial_image_layer') { let rendered = t('controlLayers.globalInitialImageLayer'); if (layer.image) { @@ -89,7 +89,7 @@ const renderLayerValue: MetadataRenderValueFunc = async (layer) => { } assert(false, 'Unknown layer type'); }; -const renderLayersValue: MetadataRenderValueFunc = async (layers) => { +const renderLayersValue: MetadataRenderValueFunc = async (layers) => { return `${layers.length} ${t('controlLayers.layers', { count: layers.length })}`; }; diff --git a/invokeai/frontend/web/src/features/metadata/util/parsers.ts b/invokeai/frontend/web/src/features/metadata/util/parsers.ts index 78d569f9878..3707d6b32d5 100644 --- a/invokeai/frontend/web/src/features/metadata/util/parsers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/parsers.ts @@ -5,7 +5,7 @@ import { } from 'features/controlAdapters/util/buildControlAdapter'; import { buildControlAdapterProcessor } from 'features/controlAdapters/util/buildControlAdapterProcessor'; import { getCALayerId, getIPALayerId, INITIAL_IMAGE_LAYER_ID } from 'features/controlLayers/konva/naming'; -import type { ControlAdapterLayer, InitialImageLayer, IPAdapterLayer, Layer } from 'features/controlLayers/store/types'; +import type { ControlAdapterLayer, InitialImageLayer, IPAdapterLayer, LayerData } from 'features/controlLayers/store/types'; import { zLayer } from 'features/controlLayers/store/types'; import { CA_PROCESSOR_DATA, @@ -431,22 +431,22 @@ const parseAllIPAdapters: MetadataParseFunc = async ( }; //#region Control Layers -const parseLayer: MetadataParseFunc = async (metadataItem) => zLayer.parseAsync(metadataItem); +const parseLayer: MetadataParseFunc = async (metadataItem) => zLayer.parseAsync(metadataItem); -const parseLayers: MetadataParseFunc = async (metadata) => { +const parseLayers: MetadataParseFunc = async (metadata) => { // We need to support recalling pre-Control Layers metadata into Control Layers. A separate set of parsers handles // taking pre-CL metadata and parsing it into layers. It doesn't always map 1-to-1, so this is best-effort. For // example, CL Control Adapters don't support resize mode, so we simply omit that property. try { - const layers: Layer[] = []; + const layers: LayerData[] = []; try { const control_layers = await getProperty(metadata, 'control_layers'); const controlLayersRaw = await getProperty(control_layers, 'layers', isArray); const controlLayersParseResults = await Promise.allSettled(controlLayersRaw.map(parseLayer)); const controlLayers = controlLayersParseResults - .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') .map((result) => result.value); layers.push(...controlLayers); } catch { diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index b69a14810dc..9e0ecf5b8bb 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -9,18 +9,18 @@ import { import { getCALayerId, getIPALayerId, getRGLayerId } from 'features/controlLayers/konva/naming'; import { allLayersDeleted, - caLayerRecalled, + controlAdapterRecalled, heightChanged, iiLayerRecalled, - ipaLayerRecalled, + ipAdapterRecalled, negativePrompt2Changed, negativePromptChanged, positivePrompt2Changed, positivePromptChanged, - rgLayerRecalled, + regionalGuidanceRecalled, widthChanged, } from 'features/controlLayers/store/controlLayersSlice'; -import type { Layer } from 'features/controlLayers/store/types'; +import type { LayerData } from 'features/controlLayers/store/types'; import { setHrfEnabled, setHrfMethod, setHrfStrength } from 'features/hrf/store/hrfSlice'; import type { LoRA } from 'features/lora/store/loraSlice'; import { loraRecalled, lorasReset } from 'features/lora/store/loraSlice'; @@ -242,7 +242,7 @@ const recallIPAdapters: MetadataRecallFunc = (ipAdapt }; //#region Control Layers -const recallLayer: MetadataRecallFunc = async (layer) => { +const recallLayer: MetadataRecallFunc = async (layer) => { const { dispatch } = getStore(); // We need to check for the existence of all images and models when recalling. If they do not exist, SMITE THEM! // Also, we need fresh IDs for all objects when recalling, to prevent multiple layers with the same ID. @@ -269,7 +269,7 @@ const recallLayer: MetadataRecallFunc = async (layer) => { } clone.id = getCALayerId(uuidv4()); clone.controlAdapter.id = uuidv4(); - dispatch(caLayerRecalled(clone)); + dispatch(controlAdapterRecalled(clone)); return; } if (layer.type === 'ip_adapter_layer') { @@ -289,7 +289,7 @@ const recallLayer: MetadataRecallFunc = async (layer) => { } clone.id = getIPALayerId(uuidv4()); clone.ipAdapter.id = uuidv4(); - dispatch(ipaLayerRecalled(clone)); + dispatch(ipAdapterRecalled(clone)); return; } @@ -315,7 +315,7 @@ const recallLayer: MetadataRecallFunc = async (layer) => { ipAdapter.id = uuidv4(); } clone.id = getRGLayerId(uuidv4()); - dispatch(rgLayerRecalled(clone)); + dispatch(regionalGuidanceRecalled(clone)); return; } @@ -325,7 +325,7 @@ const recallLayer: MetadataRecallFunc = async (layer) => { } }; -const recallLayers: MetadataRecallFunc = (layers) => { +const recallLayers: MetadataRecallFunc = (layers) => { const { dispatch } = getStore(); dispatch(allLayersDeleted()); for (const l of layers) { diff --git a/invokeai/frontend/web/src/features/metadata/util/validators.ts b/invokeai/frontend/web/src/features/metadata/util/validators.ts index 759e8ba5611..2d79854183a 100644 --- a/invokeai/frontend/web/src/features/metadata/util/validators.ts +++ b/invokeai/frontend/web/src/features/metadata/util/validators.ts @@ -1,5 +1,5 @@ import { getStore } from 'app/store/nanostores/store'; -import type { Layer } from 'features/controlLayers/store/types'; +import type { LayerData } from 'features/controlLayers/store/types'; import type { LoRA } from 'features/lora/store/loraSlice'; import type { ControlNetConfigMetadata, @@ -110,7 +110,7 @@ const validateIPAdapters: MetadataValidateFunc = (ipA return new Promise((resolve) => resolve(validatedIPAdapters)); }; -const validateLayer: MetadataValidateFunc = async (layer) => { +const validateLayer: MetadataValidateFunc = async (layer) => { if (layer.type === 'control_adapter_layer') { const model = layer.controlAdapter.model; assert(model, 'Control Adapter layer missing model'); @@ -132,8 +132,8 @@ const validateLayer: MetadataValidateFunc = async (layer) => { return layer; }; -const validateLayers: MetadataValidateFunc = async (layers) => { - const validatedLayers: Layer[] = []; +const validateLayers: MetadataValidateFunc = async (layers) => { + const validatedLayers: LayerData[] = []; for (const l of layers) { try { const validated = await validateLayer(l); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts index 6adee170649..f130aa5671d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts @@ -5,8 +5,8 @@ import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; import { blobToDataURL } from 'features/canvas/util/blobToDataURL'; import { RG_LAYER_NAME } from 'features/controlLayers/konva/naming'; import { renderers } from 'features/controlLayers/konva/renderers/layers'; -import { rgLayerMaskImageUploaded } from 'features/controlLayers/store/controlLayersSlice'; -import type { InitialImageLayer, Layer, RegionalGuidanceLayer } from 'features/controlLayers/store/types'; +import { regionalGuidanceMaskImageUploaded } from 'features/controlLayers/store/controlLayersSlice'; +import type { InitialImageLayer, LayerData, RegionalGuidanceLayer } from 'features/controlLayers/store/types'; import { isControlAdapterLayer, isInitialImageLayer, @@ -70,7 +70,7 @@ export const addControlLayers = async ( | Invocation<'vae_loader'> | Invocation<'main_model_loader'> | Invocation<'sdxl_model_loader'> -): Promise => { +): Promise => { const isSDXL = base === 'sdxl'; const validLayers = state.controlLayers.present.layers.filter((l) => isValidLayer(l, base)); @@ -492,7 +492,7 @@ const isValidIPAdapter = (ipa: IPAdapterConfigV2, base: BaseModelType): boolean return hasModel && modelMatchesBase && hasImage; }; -const isValidLayer = (layer: Layer, base: BaseModelType) => { +const isValidLayer = (layer: LayerData, base: BaseModelType) => { if (!layer.isEnabled) { return false; } @@ -532,7 +532,7 @@ const getMaskImage = async (layer: RegionalGuidanceLayer, blob: Blob): Promise getShouldProcessPrompt(controlLayers.present.positivePrompt) ? dynamicPrompts.prompts.length : 1 diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx index 47392cdb8c0..fbb6ca74dec 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx @@ -3,7 +3,7 @@ import { Expander, Flex, FormControlGroup, StandaloneAccordion } from '@invoke-a import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; import { HrfSettings } from 'features/hrf/components/HrfSettings'; import { selectHrfSlice } from 'features/hrf/store/hrfSlice'; import ParamScaleBeforeProcessing from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing'; @@ -24,7 +24,7 @@ import { ImageSizeCanvas } from './ImageSizeCanvas'; import { ImageSizeLinear } from './ImageSizeLinear'; const selector = createMemoizedSelector( - [selectGenerationSlice, selectCanvasSlice, selectHrfSlice, selectControlLayersSlice, activeTabNameSelector], + [selectGenerationSlice, selectCanvasSlice, selectHrfSlice, selectCanvasV2Slice, activeTabNameSelector], (generation, canvas, hrf, controlLayers, activeTabName) => { const { shouldRandomizeSeed, model } = generation; const { hrfEnabled } = hrf; From 47b5b7c4b4723cefe8e110428d53f06d626682c0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 14 Jun 2024 17:38:00 +1000 Subject: [PATCH 034/678] refactor(ui): canvas v2 (wip) --- .../listeners/boardAndImagesDeleted.ts | 4 +- .../listeners/controlAdapterPreprocessor.ts | 6 +- .../listeners/imageDeletionListeners.ts | 2 +- .../listeners/modelsLoaded.ts | 6 +- invokeai/frontend/web/src/app/store/store.ts | 3 +- .../src/common/hooks/useIsReadyToEnqueue.ts | 6 +- .../components/AddPromptButtons.tsx | 2 +- .../components/BrushColorPicker.tsx | 2 +- .../controlLayers/components/BrushSize.tsx | 2 +- .../components/CALayer/CALayer.tsx | 2 +- .../CALayer/CALayerControlAdapterWrapper.tsx | 2 +- .../components/ControlLayersPanelContent.tsx | 2 +- .../components/DeleteAllLayersButton.tsx | 2 +- .../components/GlobalMaskLayerOpacity.tsx | 2 +- .../components/HeadsUpDisplay.tsx | 4 +- .../components/IILayer/IILayer.tsx | 2 +- .../components/IPALayer/IPALayer.tsx | 2 +- .../IPALayer/IPALayerIPAdapterWrapper.tsx | 2 +- .../LayerCommon/LayerMenuArrangeActions.tsx | 6 +- .../LayerCommon/LayerMenuRGActions.tsx | 2 +- .../components/LayerCommon/LayerOpacity.tsx | 2 +- .../components/RGLayer/RGLayer.tsx | 4 +- .../RGLayer/RGLayerAutoNegativeCheckbox.tsx | 2 +- .../components/RGLayer/RGLayerColorPicker.tsx | 2 +- .../RGLayer/RGLayerIPAdapterList.tsx | 2 +- .../RGLayer/RGLayerIPAdapterWrapper.tsx | 2 +- .../components/RasterLayer/RasterLayer.tsx | 2 +- .../components/StageComponent.tsx | 309 +++++++++++------- .../controlLayers/components/ToolChooser.tsx | 4 +- .../controlLayers/hooks/addLayerHooks.ts | 2 +- .../controlLayers/hooks/layerStateHooks.ts | 10 +- .../features/controlLayers/konva/events.ts | 252 +++++++------- .../controlLayers/konva/renderers/caLayer.ts | 61 ++-- .../controlLayers/konva/renderers/layers.ts | 59 ++-- .../controlLayers/konva/renderers/objects.ts | 14 +- .../konva/renderers/previewLayer.ts | 30 +- .../konva/renderers/rasterLayer.ts | 55 ++-- .../controlLayers/konva/renderers/rgLayer.ts | 61 ++-- .../controlLayers/store/controlLayersSlice.ts | 36 +- .../controlLayers/store/layersSlice.ts | 33 +- .../store/regionalGuidanceSlice.ts | 39 +-- .../src/features/controlLayers/store/types.ts | 34 +- .../components/DeleteImageModal.tsx | 2 +- .../deleteImageModal/store/selectors.ts | 2 +- .../components/Boards/DeleteBoardModal.tsx | 2 +- .../util/graph/buildLinearBatchConfig.ts | 2 +- .../util/graph/generation/addControlLayers.ts | 10 +- .../nodes/util/graph/generation/addHRF.ts | 2 +- .../nodes/util/graph/graphBuilderUtils.ts | 2 +- .../components/Core/ParamNegativePrompt.tsx | 2 +- .../components/Core/ParamPositivePrompt.tsx | 2 +- .../queue/components/QueueButtonTooltip.tsx | 2 +- .../ParamSDXLNegativeStylePrompt.tsx | 2 +- .../ParamSDXLPositiveStylePrompt.tsx | 2 +- .../SDXLPrompts/SDXLConcatButton.tsx | 2 +- .../ImageSettingsAccordion.tsx | 2 +- .../ImageSizeLinear.tsx | 6 +- .../hooks/usePresetModifiedPrompts.ts | 2 +- .../ParametersPanelTextToImage.tsx | 2 +- 59 files changed, 604 insertions(+), 518 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts index 244e0cdf8a0..fb4a23912a7 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts @@ -19,9 +19,9 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS let wereControlAdaptersReset = false; let wereControlLayersReset = false; - const { canvas, nodes, controlAdapters, controlLayers } = getState(); + const { canvas, nodes, controlAdapters, canvasV2 } = getState(); deleted_images.forEach((image_name) => { - const imageUsage = getImageUsage(canvas, nodes.present, controlAdapters, controlLayers.present, image_name); + const imageUsage = getImageUsage(canvas, nodes.present, controlAdapters, canvasV2, image_name); if (imageUsage.isCanvasImage && !wasCanvasReset) { dispatch(resetCanvas()); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts index 3c7d626e773..bdd72764f2a 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts @@ -65,14 +65,14 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni // Delay before starting actual work await delay(DEBOUNCE_MS); - const layer = state.controlLayers.present.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId); + const layer = state.canvasV2.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId); if (!layer) { return; } // We should only process if the processor settings or image have changed - const originalLayer = originalState.controlLayers.present.layers + const originalLayer = originalState.canvasV2.layers .filter(isControlAdapterLayer) .find((l) => l.id === layerId); const originalImage = originalLayer?.controlAdapter.image; @@ -161,7 +161,7 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni if (signal.aborted) { // The listener was canceled - we need to cancel the pending processor batch, if there is one (could have changed by now). const pendingBatchId = getState() - .controlLayers.present.layers.filter(isControlAdapterLayer) + .canvasV2.layers.filter(isControlAdapterLayer) .find((l) => l.id === layerId)?.controlAdapter.processorPendingBatchId; if (pendingBatchId) { cancelProcessorBatch(dispatch, layerId, pendingBatchId); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts index 61df8846f04..218b0be8ee7 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts @@ -70,7 +70,7 @@ const deleteControlAdapterImages = (state: RootState, dispatch: AppDispatch, ima }; const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { - state.controlLayers.present.layers.forEach((l) => { + state.canvasV2.layers.forEach((l) => { if (isRegionalGuidanceLayer(l)) { if (l.ipAdapters.some((ipa) => ipa.image?.name === imageDTO.image_name)) { dispatch(layerDeleted(l.id)); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts index 9d02fcbfa54..30f558086e9 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts @@ -79,15 +79,15 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => { const optimalDimension = getOptimalDimension(defaultModelInList); if ( getIsSizeOptimal( - state.controlLayers.present.size.width, - state.controlLayers.present.size.height, + state.canvasV2.size.width, + state.canvasV2.size.height, optimalDimension ) ) { return; } const { width, height } = calculateNewSize( - state.controlLayers.present.size.aspectRatio.value, + state.canvasV2.size.aspectRatio.value, optimalDimension * optimalDimension ); diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 0fcb842db3f..45c0d020e89 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -4,7 +4,7 @@ import { logger } from 'app/logging/logger'; import { idbKeyValDriver } from 'app/store/enhancers/reduxRemember/driver'; import { errorHandler } from 'app/store/enhancers/reduxRemember/errors'; import type { JSONObject } from 'common/types'; -import { canvasPersistConfig, canvasSlice } from 'features/canvas/store/canvasSlice'; +import { canvasPersistConfig } from 'features/canvas/store/canvasSlice'; import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice'; import { controlAdaptersV2PersistConfig, @@ -52,7 +52,6 @@ import { listenerMiddleware } from './middleware/listenerMiddleware'; const allReducers = { [api.reducerPath]: api.reducer, - [canvasSlice.name]: canvasSlice.reducer, [gallerySlice.name]: gallerySlice.reducer, [generationSlice.name]: generationSlice.reducer, [nodesSlice.name]: undoable(nodesSlice.reducer, nodesUndoableConfig), diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index ac47d9005da..97042124b12 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -59,8 +59,8 @@ const createSelector = (templates: Templates) => config ) => { const { model } = generation; - const { size } = controlLayers.present; - const { positivePrompt } = controlLayers.present; + const { size } = canvasV2; + const { positivePrompt } = canvasV2; const { isConnected } = system; @@ -126,7 +126,7 @@ const createSelector = (templates: Templates) => if (activeTabName === 'generation') { // Handling for generation tab - controlLayers.present.layers + canvasV2.layers .filter((l) => l.isEnabled) .forEach((l, i) => { const layerLiteral = i18n.t('controlLayers.layers_one'); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx index 073a1888717..71b1ca5f1ec 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx @@ -23,7 +23,7 @@ export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => { const selectValidActions = useMemo( () => createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = controlLayers.present.layers.find((l) => l.id === layerId); + const layer = canvasV2.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); return { canAddPositivePrompt: layer.positivePrompt === null, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/BrushColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/BrushColorPicker.tsx index 4c218a87fd0..ec8e535b40c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/BrushColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/BrushColorPicker.tsx @@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next'; export const BrushColorPicker = memo(() => { const { t } = useTranslation(); - const brushColor = useAppSelector((s) => s.controlLayers.present.brushColor); + const brushColor = useAppSelector((s) => s.canvasV2.brushColor); const dispatch = useAppDispatch(); const onChange = useCallback( (color: RgbaColor) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/BrushSize.tsx b/invokeai/frontend/web/src/features/controlLayers/components/BrushSize.tsx index 731f9a1c779..4c102ea0ff9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/BrushSize.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/BrushSize.tsx @@ -20,7 +20,7 @@ const formatPx = (v: number | string) => `${v} px`; export const BrushSize = memo(() => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const brushSize = useAppSelector((s) => s.controlLayers.present.brushSize); + const brushSize = useAppSelector((s) => s.canvasV2.brushSize); const onChange = useCallback( (v: number) => { dispatch(brushSizeChanged(Math.round(v))); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx index 868693e58ce..9d543446e91 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx @@ -19,7 +19,7 @@ type Props = { export const CALayer = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); const isSelected = useAppSelector( - (s) => selectLayerOrThrow(s.controlLayers.present, layerId, isControlAdapterLayer).isSelected + (s) => selectLayerOrThrow(s.canvasV2, layerId, isControlAdapterLayer).isSelected ); const onClick = useCallback(() => { dispatch(layerSelected(layerId)); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx index 7b0e3aa3327..f5ccd49cb2b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx @@ -28,7 +28,7 @@ type Props = { export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); const controlAdapter = useAppSelector( - (s) => selectLayerOrThrow(s.controlLayers.present, layerId, isControlAdapterLayer).controlAdapter + (s) => selectLayerOrThrow(s.canvasV2, layerId, isControlAdapterLayer).controlAdapter ); const onChangeBeginEndStepPct = useCallback( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index 90f21a7253c..27ef318c1ad 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -19,7 +19,7 @@ import { memo } from 'react'; import { useTranslation } from 'react-i18next'; const selectLayerIdTypePairs = createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { - const [renderableLayers, ipAdapterLayers] = partition(controlLayers.present.layers, isRenderableLayer); + const [renderableLayers, ipAdapterLayers] = partition(canvasV2.layers, isRenderableLayer); return [...ipAdapterLayers, ...renderableLayers].map((l) => ({ id: l.id, type: l.type })).reverse(); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx index 00487fcc43e..e69b83fa791 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx @@ -8,7 +8,7 @@ import { PiTrashSimpleBold } from 'react-icons/pi'; export const DeleteAllLayersButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const isDisabled = useAppSelector((s) => s.controlLayers.present.layers.length === 0); + const isDisabled = useAppSelector((s) => s.canvasV2.layers.length === 0); const onClick = useCallback(() => { dispatch(allLayersDeleted()); }, [dispatch]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/GlobalMaskLayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/GlobalMaskLayerOpacity.tsx index 40985499db6..23720f4c227 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/GlobalMaskLayerOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/GlobalMaskLayerOpacity.tsx @@ -14,7 +14,7 @@ export const GlobalMaskLayerOpacity = memo(() => { const dispatch = useAppDispatch(); const { t } = useTranslation(); const globalMaskLayerOpacity = useAppSelector((s) => - Math.round(s.controlLayers.present.globalMaskLayerOpacity * 100) + Math.round(s.canvasV2.globalMaskLayerOpacity * 100) ); const onChange = useCallback( (v: number) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx index 834903c0f76..2aea3a422cd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx @@ -7,8 +7,8 @@ import { memo } from 'react'; export const HeadsUpDisplay = memo(() => { const stageAttrs = useStore($stageAttrs); - const layerCount = useAppSelector((s) => s.controlLayers.present.layers.length); - const bbox = useAppSelector((s) => s.controlLayers.present.bbox); + const layerCount = useAppSelector((s) => s.canvasV2.layers.length); + const bbox = useAppSelector((s) => s.canvasV2.bbox); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx index 43857b6fc3e..202b830e479 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx @@ -25,7 +25,7 @@ type Props = { export const IILayer = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); - const layer = useAppSelector((s) => selectLayerOrThrow(s.controlLayers.present, layerId, isInitialImageLayer)); + const layer = useAppSelector((s) => selectLayerOrThrow(s.canvasV2, layerId, isInitialImageLayer)); const onClick = useCallback(() => { dispatch(layerSelected(layerId)); }, [dispatch, layerId]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx index e4d3dd9e4ff..d227f73045c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx @@ -16,7 +16,7 @@ type Props = { export const IPALayer = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); const isSelected = useAppSelector( - (s) => selectLayerOrThrow(s.controlLayers.present, layerId, isIPAdapterLayer).isSelected + (s) => selectLayerOrThrow(s.canvasV2, layerId, isIPAdapterLayer).isSelected ); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); const onClick = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx index 08737e6e604..7d7ba7e1b50 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx @@ -22,7 +22,7 @@ type Props = { export const IPALayerIPAdapterWrapper = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); const ipAdapter = useAppSelector( - (s) => selectLayerOrThrow(s.controlLayers.present, layerId, isIPAdapterLayer).ipAdapter + (s) => selectLayerOrThrow(s.canvasV2, layerId, isIPAdapterLayer).ipAdapter ); const onChangeBeginEndStepPct = useCallback( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuArrangeActions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuArrangeActions.tsx index 67cf856c817..4c86a98c36c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuArrangeActions.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuArrangeActions.tsx @@ -22,10 +22,10 @@ export const LayerMenuArrangeActions = memo(({ layerId }: Props) => { const selectValidActions = useMemo( () => createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = controlLayers.present.layers.find((l) => l.id === layerId); + const layer = canvasV2.layers.find((l) => l.id === layerId); assert(isRenderableLayer(layer), `Layer ${layerId} not found or not an RP layer`); - const layerIndex = controlLayers.present.layers.findIndex((l) => l.id === layerId); - const layerCount = controlLayers.present.layers.length; + const layerIndex = canvasV2.layers.findIndex((l) => l.id === layerId); + const layerCount = canvasV2.layers.length; return { canMoveForward: layerIndex < layerCount - 1, canMoveBackward: layerIndex > 0, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx index 7addbf45eb8..d04da5e4a18 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx @@ -22,7 +22,7 @@ export const LayerMenuRGActions = memo(({ layerId }: Props) => { const selectValidActions = useMemo( () => createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = controlLayers.present.layers.find((l) => l.id === layerId); + const layer = canvasV2.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); return { canAddPositivePrompt: layer.positivePrompt === null, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerOpacity.tsx index 481a6597a8c..b92ccc41f91 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerOpacity.tsx @@ -37,7 +37,7 @@ export const LayerOpacity = memo(({ layerId }: Props) => { const selectOpacity = useMemo( () => createSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = selectLayerOrThrow(controlLayers.present, layerId, isLayerWithOpacity); + const layer = selectLayerOrThrow(canvasV2, layerId, isLayerWithOpacity); return Math.round(layer.opacity * 100); }), [layerId] diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx index 54e6b502e65..4df5670f0e3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx @@ -30,14 +30,14 @@ export const RGLayer = memo(({ layerId }: Props) => { const selector = useMemo( () => createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = controlLayers.present.layers.find((l) => l.id === layerId); + const layer = canvasV2.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); return { color: rgbColorToString(layer.previewColor), hasPositivePrompt: layer.positivePrompt !== null, hasNegativePrompt: layer.negativePrompt !== null, hasIPAdapters: layer.ipAdapters.length > 0, - isSelected: layerId === controlLayers.present.selectedLayerId, + isSelected: layerId === canvasV2.selectedLayerId, autoNegative: layer.autoNegative, }; }), diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx index ec52062062f..998682d4ccd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx @@ -16,7 +16,7 @@ const useAutoNegative = (layerId: string) => { const selectAutoNegative = useMemo( () => createSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = controlLayers.present.layers.find((l) => l.id === layerId); + const layer = canvasV2.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); return layer.autoNegative; }), diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx index 40660a1ac22..7ce002817ac 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx @@ -20,7 +20,7 @@ export const RGLayerColorPicker = memo(({ layerId }: Props) => { const selectColor = useMemo( () => createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = controlLayers.present.layers.find((l) => l.id === layerId); + const layer = canvasV2.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an vector mask layer`); return layer.previewColor; }), diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx index 9a38123d4b1..5b6be683d18 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx @@ -15,7 +15,7 @@ export const RGLayerIPAdapterList = memo(({ layerId }: Props) => { const selectIPAdapterIds = useMemo( () => createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = controlLayers.present.layers.filter(isRegionalGuidanceLayer).find((l) => l.id === layerId); + const layer = canvasV2.layers.filter(isRegionalGuidanceLayer).find((l) => l.id === layerId); assert(layer, `Layer ${layerId} not found`); return layer.ipAdapters; }), diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx index f0879f07cfc..802ad2bb3d8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx @@ -28,7 +28,7 @@ export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNu const onDeleteIPAdapter = useCallback(() => { dispatch(regionalGuidanceIPAdapterDeleted({ layerId, ipAdapterId })); }, [dispatch, ipAdapterId, layerId]); - const ipAdapter = useAppSelector((s) => selectRGLayerIPAdapterOrThrow(s.controlLayers.present, layerId, ipAdapterId)); + const ipAdapter = useAppSelector((s) => selectRGLayerIPAdapterOrThrow(s.canvasV2, layerId, ipAdapterId)); const onChangeBeginEndStepPct = useCallback( (beginEndStepPct: [number, number]) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx index e1dd5c3b78a..921122fb338 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx @@ -19,7 +19,7 @@ type Props = { export const RasterLayer = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); const isSelected = useAppSelector( - (s) => selectLayerOrThrow(s.controlLayers.present, layerId, isRasterLayer).isSelected + (s) => selectLayerOrThrow(s.canvasV2, layerId, isRasterLayer).isSelected ); const onClick = useCallback(() => { dispatch(layerSelected(layerId)); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index f285624fe61..a0b65d3a4e8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -5,51 +5,59 @@ import { logger } from 'app/logging/logger'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { rgbaColorToString } from 'features/canvas/util/colorToString'; import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay'; -import { - BRUSH_SPACING_PCT, - MAX_BRUSH_SPACING_PX, - MIN_BRUSH_SPACING_PX, - TRANSPARENCY_CHECKER_PATTERN, -} from 'features/controlLayers/konva/constants'; +import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants'; import { setStageEventHandlers } from 'features/controlLayers/konva/events'; import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers/layers'; +import { caBboxChanged, caTranslated } from 'features/controlLayers/store/controlAdaptersSlice'; import { $bbox, - $brushSpacingPx, - $brushWidth, - $fill, - $invertScroll, + $currentFill, $isDrawing, $isMouseDown, $lastAddedPoint, $lastCursorPos, $lastMouseDownPos, - $selectedLayer, + $selectedEntity, $spaceKey, $stageAttrs, - $tool, - $toolBuffer, + $toolState, bboxChanged, - brushLineAdded, - brushSizeChanged, - eraserLineAdded, - layerBboxChanged, - layerTranslated, - linePointsAdded, - rectAdded, + brushWidthChanged, + eraserWidthChanged, selectCanvasV2Slice, + toolBufferChanged, + toolChanged, } from 'features/controlLayers/store/controlLayersSlice'; -import { selectLayersSlice } from 'features/controlLayers/store/layersSlice'; -import { selectRegionalGuidanceSlice } from 'features/controlLayers/store/regionalGuidanceSlice'; +import { + layerBboxChanged, + layerBrushLineAdded, + layerEraserLineAdded, + layerLinePointAdded, + layerRectAdded, + layerTranslated, + selectLayersSlice, +} from 'features/controlLayers/store/layersSlice'; +import { + rgBboxChanged, + rgBrushLineAdded, + rgEraserLineAdded, + rgLinePointAdded, + rgRectAdded, + rgTranslated, + selectRegionalGuidanceSlice, +} from 'features/controlLayers/store/regionalGuidanceSlice'; import type { - AddBrushLineArg, - AddEraserLineArg, - AddPointToLineArg, - AddRectShapeArg, + BboxChangedArg, + BrushLineAddedArg, + CanvasEntity, + EraserLineAddedArg, + PointAddedToLineArg, + PosChangedArg, + RectShapeAddedArg, + Tool, } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; -import { clamp } from 'lodash-es'; import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { getImageDTO } from 'services/api/endpoints/images'; @@ -61,12 +69,12 @@ Konva.showWarnings = false; const log = logger('controlLayers'); -const selectBrushColor = createSelector( +const selectBrushFill = createSelector( selectCanvasV2Slice, selectLayersSlice, selectRegionalGuidanceSlice, (canvas, layers, regionalGuidance) => { - const rg = regionalGuidance.regions.find((i) => i.id === canvas.lastSelectedItem?.id); + const rg = regionalGuidance.regions.find((i) => i.id === canvas.selectedEntityIdentifier?.id); if (rg) { return rgbaColorToString({ ...rg.fill, a: regionalGuidance.opacity }); @@ -76,89 +84,121 @@ const selectBrushColor = createSelector( } ); -const selectSelectedLayer = createSelector(selectCanvasV2Slice, (controlLayers) => { - return controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId) ?? null; -}); - -const selectLayerCount = createSelector(selectCanvasV2Slice, (controlLayers) => controlLayers.present.layers.length); - const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, asPreview: boolean) => { const dispatch = useAppDispatch(); - const state = useAppSelector((s) => s.controlLayers.present); - const tool = useStore($tool); + const canvasV2State = useAppSelector(selectCanvasV2Slice); + const layersState = useAppSelector((s) => s.layers); + const controlAdaptersState = useAppSelector((s) => s.controlAdaptersV2); + const ipAdaptersState = useAppSelector((s) => s.ipAdapters); + const regionalGuidanceState = useAppSelector((s) => s.regionalGuidance); const lastCursorPos = useStore($lastCursorPos); const lastMouseDownPos = useStore($lastMouseDownPos); const isMouseDown = useStore($isMouseDown); const isDrawing = useStore($isDrawing); - const brushColor = useAppSelector(selectBrushColor); - const selectedLayer = useAppSelector(selectSelectedLayer); + const brushColor = useAppSelector(selectBrushFill); + const selectedEntity = useMemo(() => { + const identifier = canvasV2State.selectedEntityIdentifier; + if (!identifier) { + return null; + } else if (identifier.type === 'layer') { + return layersState.layers.find((i) => i.id === identifier.id) ?? null; + } else if (identifier.type === 'control_adapter') { + return controlAdaptersState.controlAdapters.find((i) => i.id === identifier.id) ?? null; + } else if (identifier.type === 'ip_adapter') { + return ipAdaptersState.ipAdapters.find((i) => i.id === identifier.id) ?? null; + } else if (identifier.type === 'regional_guidance') { + return regionalGuidanceState.regions.find((i) => i.id === identifier.id) ?? null; + } else { + return null; + } + }, [ + canvasV2State.selectedEntityIdentifier, + controlAdaptersState.controlAdapters, + ipAdaptersState.ipAdapters, + layersState.layers, + regionalGuidanceState.regions, + ]); + + const currentFill = useMemo(() => { + if (selectedEntity && selectedEntity.type === 'regional_guidance') { + return { ...selectedEntity.fill, a: regionalGuidanceState.opacity }; + } + return canvasV2State.tool.fill; + }, [canvasV2State.tool.fill, regionalGuidanceState.opacity, selectedEntity]); + const renderers = useMemo(() => (asPreview ? debouncedRenderers : normalRenderers), [asPreview]); const dpr = useDevicePixelRatio({ round: false }); - const shouldInvertBrushSizeScrollDirection = useAppSelector((s) => s.canvas.shouldInvertBrushSizeScrollDirection); - const brushSpacingPx = useMemo( - () => clamp(state.brushSize / BRUSH_SPACING_PCT, MIN_BRUSH_SPACING_PX, MAX_BRUSH_SPACING_PX), - [state.brushSize] - ); useLayoutEffect(() => { - $fill.set(brushColor); - $brushWidth.set(state.brushSize); - $brushSpacingPx.set(brushSpacingPx); - $selectedLayer.set(selectedLayer); - $invertScroll.set(shouldInvertBrushSizeScrollDirection); - $bbox.set(state.bbox); - }, [ - brushSpacingPx, - brushColor, - selectedLayer, - shouldInvertBrushSizeScrollDirection, - state.brushSize, - state.selectedLayerId, - state.brushColor, - state.bbox, - ]); - - const onLayerPosChanged = useCallback( - (layerId: string, x: number, y: number) => { - dispatch(layerTranslated({ layerId, x, y })); + $toolState.set(canvasV2State.tool); + $selectedEntity.set(selectedEntity); + $bbox.set(canvasV2State.bbox); + $currentFill.set(currentFill); + }, [selectedEntity, canvasV2State.tool, canvasV2State.bbox, currentFill]); + + const onPosChanged = useCallback( + (arg: PosChangedArg, entityType: CanvasEntity['type']) => { + if (entityType === 'layer') { + dispatch(layerTranslated(arg)); + } else if (entityType === 'control_adapter') { + dispatch(caTranslated(arg)); + } else if (entityType === 'regional_guidance') { + dispatch(rgTranslated(arg)); + } }, [dispatch] ); const onBboxChanged = useCallback( - (layerId: string, bbox: IRect | null) => { - dispatch(layerBboxChanged({ layerId, bbox })); + (arg: BboxChangedArg, entityType: CanvasEntity['type']) => { + if (entityType === 'layer') { + dispatch(layerBboxChanged(arg)); + } else if (entityType === 'control_adapter') { + dispatch(caBboxChanged(arg)); + } else if (entityType === 'regional_guidance') { + dispatch(rgBboxChanged(arg)); + } }, [dispatch] ); const onBrushLineAdded = useCallback( - (arg: AddBrushLineArg) => { - dispatch(brushLineAdded(arg)); + (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => { + if (entityType === 'layer') { + dispatch(layerBrushLineAdded(arg)); + } else if (entityType === 'regional_guidance') { + dispatch(rgBrushLineAdded(arg)); + } }, [dispatch] ); const onEraserLineAdded = useCallback( - (arg: AddEraserLineArg) => { - dispatch(eraserLineAdded(arg)); + (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => { + if (entityType === 'layer') { + dispatch(layerEraserLineAdded(arg)); + } else if (entityType === 'regional_guidance') { + dispatch(rgEraserLineAdded(arg)); + } }, [dispatch] ); const onPointAddedToLine = useCallback( - (arg: AddPointToLineArg) => { - dispatch(linePointsAdded(arg)); + (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => { + if (entityType === 'layer') { + dispatch(layerLinePointAdded(arg)); + } else if (entityType === 'regional_guidance') { + dispatch(rgLinePointAdded(arg)); + } }, [dispatch] ); const onRectShapeAdded = useCallback( - (arg: AddRectShapeArg) => { - dispatch(rectAdded(arg)); - }, - [dispatch] - ); - const onBrushSizeChanged = useCallback( - (size: number) => { - dispatch(brushSizeChanged(size)); + (arg: RectShapeAddedArg, entityType: CanvasEntity['type']) => { + if (entityType === 'layer') { + dispatch(layerRectAdded(arg)); + } else if (entityType === 'regional_guidance') { + dispatch(rgRectAdded(arg)); + } }, [dispatch] ); @@ -168,6 +208,30 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, }, [dispatch] ); + const onBrushWidthChanged = useCallback( + (width: number) => { + dispatch(brushWidthChanged(width)); + }, + [dispatch] + ); + const onEraserWidthChanged = useCallback( + (width: number) => { + dispatch(eraserWidthChanged(width)); + }, + [dispatch] + ); + const setTool = useCallback( + (tool: Tool) => { + dispatch(toolChanged(tool)); + }, + [dispatch] + ); + const setToolBuffer = useCallback( + (toolBuffer: Tool | null) => { + dispatch(toolBufferChanged(toolBuffer)); + }, + [dispatch] + ); useLayoutEffect(() => { log.trace('Initializing stage'); @@ -189,32 +253,29 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, const cleanup = setStageEventHandlers({ stage, - getTool: $tool.get, - setTool: $tool.set, - getToolBuffer: $toolBuffer.get, - setToolBuffer: $toolBuffer.set, + getToolState: $toolState.get, + setTool, + setToolBuffer, getIsDrawing: $isDrawing.get, setIsDrawing: $isDrawing.set, getIsMouseDown: $isMouseDown.get, setIsMouseDown: $isMouseDown.set, - getBrushColor: $fill.get, - getBrushSize: $brushWidth.get, - getBrushSpacingPx: $brushSpacingPx.get, - getSelectedLayer: $selectedLayer.get, + getSelectedEntity: $selectedEntity.get, getLastAddedPoint: $lastAddedPoint.get, setLastAddedPoint: $lastAddedPoint.set, getLastCursorPos: $lastCursorPos.get, setLastCursorPos: $lastCursorPos.set, getLastMouseDownPos: $lastMouseDownPos.get, setLastMouseDownPos: $lastMouseDownPos.set, - getShouldInvert: $invertScroll.get, getSpaceKey: $spaceKey.get, setStageAttrs: $stageAttrs.set, - onBrushSizeChanged, onBrushLineAdded, onEraserLineAdded, onPointAddedToLine, onRectShapeAdded, + onBrushWidthChanged, + onEraserWidthChanged, + getCurrentFill: $currentFill.get, }); return () => { @@ -224,12 +285,15 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, }, [ asPreview, onBrushLineAdded, - onBrushSizeChanged, + onBrushWidthChanged, onEraserLineAdded, onPointAddedToLine, onRectShapeAdded, stage, container, + onEraserWidthChanged, + setTool, + setToolBuffer, ]); useLayoutEffect(() => { @@ -267,29 +331,26 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, log.trace('Rendering tool preview'); renderers.renderToolPreview( stage, - tool, - brushColor, - selectedLayer?.type ?? null, - state.globalMaskLayerOpacity, + canvasV2State.tool, + currentFill, + selectedEntity, lastCursorPos, lastMouseDownPos, - state.brushSize, isDrawing, isMouseDown ); }, [ asPreview, brushColor, + canvasV2State.tool, + currentFill, isDrawing, isMouseDown, lastCursorPos, lastMouseDownPos, renderers, - selectedLayer?.type, + selectedEntity, stage, - state.brushSize, - state.globalMaskLayerOpacity, - tool, ]); useLayoutEffect(() => { @@ -300,8 +361,8 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, log.trace('Rendering bbox preview'); renderers.renderBboxPreview( stage, - state.bbox, - tool, + canvasV2State.bbox, + canvasV2State.tool.selected, $bbox.get, onBboxTransformed, $shift.get, @@ -309,21 +370,41 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, $meta.get, $alt.get ); - }, [asPreview, onBboxTransformed, renderers, stage, state.bbox, tool]); + }, [asPreview, canvasV2State.bbox, canvasV2State.tool.selected, onBboxTransformed, renderers, stage]); useLayoutEffect(() => { log.trace('Rendering layers'); - renderers.renderLayers(stage, state.layers, state.globalMaskLayerOpacity, tool, getImageDTO, onLayerPosChanged); - }, [stage, state.layers, state.globalMaskLayerOpacity, tool, onLayerPosChanged, renderers]); + renderers.renderLayers( + stage, + layersState.layers, + controlAdaptersState.controlAdapters, + regionalGuidanceState.regions, + regionalGuidanceState.opacity, + canvasV2State.tool.selected, + selectedEntity, + getImageDTO, + onPosChanged + ); + }, [ + stage, + renderers, + layersState.layers, + controlAdaptersState.controlAdapters, + regionalGuidanceState.regions, + regionalGuidanceState.opacity, + onPosChanged, + canvasV2State.tool.selected, + selectedEntity, + ]); - useLayoutEffect(() => { - if (asPreview) { - // Preview should not check for transparency - return; - } - log.trace('Updating bboxes'); - debouncedRenderers.updateBboxes(stage, state.layers, onBboxChanged); - }, [stage, asPreview, state.layers, onBboxChanged]); + // useLayoutEffect(() => { + // if (asPreview) { + // // Preview should not check for transparency + // return; + // } + // log.trace('Updating bboxes'); + // debouncedRenderers.updateBboxes(stage, state.layers, onBboxChanged); + // }, [stage, asPreview, state.layers, onBboxChanged]); useLayoutEffect(() => { Konva.pixelRatio = dpr; @@ -395,7 +476,7 @@ StageComponent.displayName = 'StageComponent'; const NoLayersFallback = () => { const { t } = useTranslation(); - const layerCount = useAppSelector(selectLayerCount); + const layerCount = useAppSelector((s) => s.layers.layers.length); if (layerCount) { return null; } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx index c1daa11df44..d9eadd1a31e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx @@ -21,7 +21,7 @@ import { } from 'react-icons/pi'; const selectIsDisabled = createSelector(selectCanvasV2Slice, (controlLayers) => { - const selectedLayer = controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId); + const selectedLayer = canvasV2.layers.find((l) => l.id === canvasV2.selectedLayerId); return selectedLayer?.type !== 'regional_guidance_layer' && selectedLayer?.type !== 'raster_layer'; }); @@ -29,7 +29,7 @@ export const ToolChooser: React.FC = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const isDisabled = useAppSelector(selectIsDisabled); - const selectedLayerId = useAppSelector((s) => s.controlLayers.present.selectedLayerId); + const selectedLayerId = useAppSelector((s) => s.canvasV2.selectedLayerId); const tool = useStore($tool); const setToolToBrush = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts index 5e0976ed594..5b9e799063b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts @@ -102,7 +102,7 @@ export const useAddIPAdapterToIPALayer = (layerId: string) => { export const useAddIILayer = () => { const dispatch = useAppDispatch(); - const isDisabled = useAppSelector((s) => Boolean(s.controlLayers.present.layers.find(isInitialImageLayer))); + const isDisabled = useAppSelector((s) => Boolean(s.canvasV2.layers.find(isInitialImageLayer))); const addIILayer = useCallback(() => { dispatch(iiLayerAdded(null)); }, [dispatch]); diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts index c9dfeb4e129..03f47bcdda3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts @@ -10,7 +10,7 @@ export const useLayerPositivePrompt = (layerId: string) => { const selectLayer = useMemo( () => createSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = controlLayers.present.layers.find((l) => l.id === layerId); + const layer = canvasV2.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); assert(layer.positivePrompt !== null, `Layer ${layerId} does not have a positive prompt`); return layer.positivePrompt; @@ -25,7 +25,7 @@ export const useLayerNegativePrompt = (layerId: string) => { const selectLayer = useMemo( () => createSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = controlLayers.present.layers.find((l) => l.id === layerId); + const layer = canvasV2.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); assert(layer.negativePrompt !== null, `Layer ${layerId} does not have a negative prompt`); return layer.negativePrompt; @@ -40,7 +40,7 @@ export const useLayerIsEnabled = (layerId: string) => { const selectLayer = useMemo( () => createSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = controlLayers.present.layers.find((l) => l.id === layerId); + const layer = canvasV2.layers.find((l) => l.id === layerId); assert(layer, `Layer ${layerId} not found`); return layer.isEnabled; }), @@ -54,7 +54,7 @@ export const useLayerType = (layerId: string) => { const selectLayer = useMemo( () => createSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = controlLayers.present.layers.find((l) => l.id === layerId); + const layer = canvasV2.layers.find((l) => l.id === layerId); assert(layer, `Layer ${layerId} not found`); return layer.type; }), @@ -68,7 +68,7 @@ export const useCALayerOpacity = (layerId: string) => { const selectLayer = useMemo( () => createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = controlLayers.present.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId); + const layer = canvasV2.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId); assert(layer, `Layer ${layerId} not found`); return { opacity: Math.round(layer.opacity * 100), isFilterEnabled: layer.isFilterEnabled }; }), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 1f1e99f3167..bc8f94d31e6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -2,27 +2,27 @@ import { calculateNewBrushSize } from 'features/canvas/hooks/useCanvasZoom'; import { CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/canvas/util/constants'; import { getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util'; import type { - AddBrushLineArg, - AddEraserLineArg, - AddPointToLineArg, - AddRectShapeArg, - LayerData, + BrushLineAddedArg, + CanvasEntity, + CanvasV2State, + EraserLineAddedArg, + PointAddedToLineArg, + RectShapeAddedArg, + RgbaColor, StageAttrs, Tool, } from 'features/controlLayers/store/types'; -import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types'; import type Konva from 'konva'; import type { Vector2d } from 'konva/lib/types'; import { clamp } from 'lodash-es'; -import type { RgbaColor } from 'react-colorful'; import { PREVIEW_TOOL_GROUP_ID } from './naming'; type Arg = { stage: Konva.Stage; - getTool: () => Tool; + getToolState: () => CanvasV2State['tool']; + getCurrentFill: () => RgbaColor; setTool: (tool: Tool) => void; - getToolBuffer: () => Tool | null; setToolBuffer: (tool: Tool | null) => void; getIsDrawing: () => boolean; setIsDrawing: (isDrawing: boolean) => void; @@ -35,17 +35,14 @@ type Arg = { getLastAddedPoint: () => Vector2d | null; setLastAddedPoint: (pos: Vector2d | null) => void; setStageAttrs: (attrs: StageAttrs) => void; - getBrushColor: () => RgbaColor; - getBrushSize: () => number; - getBrushSpacingPx: () => number; - getSelectedLayer: () => LayerData | null; - getShouldInvert: () => boolean; + getSelectedEntity: () => CanvasEntity | null; getSpaceKey: () => boolean; - onBrushLineAdded: (arg: AddBrushLineArg) => void; - onEraserLineAdded: (arg: AddEraserLineArg) => void; - onPointAddedToLine: (arg: AddPointToLineArg) => void; - onRectShapeAdded: (arg: AddRectShapeArg) => void; - onBrushSizeChanged: (size: number) => void; + onBrushLineAdded: (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => void; + onEraserLineAdded: (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => void; + onPointAddedToLine: (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => void; + onRectShapeAdded: (arg: RectShapeAddedArg, entityType: CanvasEntity['type']) => void; + onBrushWidthChanged: (size: number) => void; + onEraserWidthChanged: (size: number) => void; }; /** @@ -72,30 +69,41 @@ const updateLastCursorPos = (stage: Konva.Stage, setLastCursorPos: Arg['setLastC * @param onPointAddedToLine The callback to add a point to a line */ const maybeAddNextPoint = ( - selectedLayer: LayerData, + selectedEntity: CanvasEntity, currentPos: Vector2d, + getToolState: Arg['getToolState'], getLastAddedPoint: Arg['getLastAddedPoint'], setLastAddedPoint: Arg['setLastAddedPoint'], - getBrushSpacingPx: Arg['getBrushSpacingPx'], onPointAddedToLine: Arg['onPointAddedToLine'] ) => { + if (selectedEntity.type !== 'layer' && selectedEntity.type !== 'regional_guidance') { + return; + } // Continue the last line const lastAddedPoint = getLastAddedPoint(); + const toolState = getToolState(); + const minSpacingPx = toolState.selected === 'brush' ? toolState.brush.width * 0.05 : toolState.eraser.width * 0.05; if (lastAddedPoint) { // Dispatching redux events impacts perf substantially - using brush spacing keeps dispatches to a reasonable number - if (Math.hypot(lastAddedPoint.x - currentPos.x, lastAddedPoint.y - currentPos.y) < getBrushSpacingPx()) { + if (Math.hypot(lastAddedPoint.x - currentPos.x, lastAddedPoint.y - currentPos.y) < minSpacingPx) { return; } } setLastAddedPoint(currentPos); - onPointAddedToLine({ layerId, point: [currentPos.x - selectedLayer.x, currentPos.y - selectedLayer.y] }); + onPointAddedToLine( + { + id: selectedEntity.id, + point: [currentPos.x - selectedEntity.x, currentPos.y - selectedEntity.y], + }, + selectedEntity.type + ); }; export const setStageEventHandlers = ({ stage, - getTool, + getToolState, + getCurrentFill, setTool, - getToolBuffer, setToolBuffer, getIsDrawing, setIsDrawing, @@ -108,17 +116,14 @@ export const setStageEventHandlers = ({ getLastAddedPoint, setLastAddedPoint, setStageAttrs, - getBrushColor, - getBrushSize, - getBrushSpacingPx, - getSelectedLayer, - getShouldInvert, + getSelectedEntity, getSpaceKey, onBrushLineAdded, onEraserLineAdded, onPointAddedToLine, onRectShapeAdded, - onBrushSizeChanged, + onBrushWidthChanged: onBrushSizeChanged, + onEraserWidthChanged: onEraserSizeChanged, }: Arg): (() => void) => { //#region mouseenter stage.on('mouseenter', (e) => { @@ -126,7 +131,7 @@ export const setStageEventHandlers = ({ if (!stage) { return; } - const tool = getTool(); + const tool = getToolState().selected; stage.findOne(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); }); @@ -137,13 +142,13 @@ export const setStageEventHandlers = ({ return; } setIsMouseDown(true); - const tool = getTool(); + const toolState = getToolState(); const pos = updateLastCursorPos(stage, setLastCursorPos); - const selectedLayer = getSelectedLayer(); - if (!pos || !selectedLayer) { + const selectedEntity = getSelectedEntity(); + if (!pos || !selectedEntity) { return; } - if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { + if (selectedEntity.type !== 'regional_guidance' && selectedEntity.type !== 'layer') { return; } @@ -155,23 +160,37 @@ export const setStageEventHandlers = ({ setIsDrawing(true); setLastMouseDownPos(pos); - if (tool === 'brush') { - onBrushLineAdded({ - layerId: selectedLayer.id, - points: [pos.x - selectedLayer.x, pos.y - selectedLayer.y, pos.x - selectedLayer.x, pos.y - selectedLayer.y], - color: selectedLayer.type === 'raster_layer' ? getBrushColor() : DEFAULT_RGBA_COLOR, - }); - } - - if (tool === 'eraser') { - onEraserLineAdded({ - layerId: selectedLayer.id, - points: [pos.x - selectedLayer.x, pos.y - selectedLayer.y, pos.x - selectedLayer.x, pos.y - selectedLayer.y], - }); + if (toolState.selected === 'brush') { + onBrushLineAdded( + { + id: selectedEntity.id, + points: [ + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + ], + color: getCurrentFill(), + width: toolState.brush.width, + }, + selectedEntity.type + ); } - if (tool === 'rect') { - // Setting the last mouse down pos starts a rect + if (toolState.selected === 'eraser') { + onEraserLineAdded( + { + id: selectedEntity.id, + points: [ + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + ], + width: toolState.eraser.width, + }, + selectedEntity.type + ); } }); @@ -183,12 +202,12 @@ export const setStageEventHandlers = ({ } setIsMouseDown(false); const pos = getLastCursorPos(); - const selectedLayer = getSelectedLayer(); + const selectedEntity = getSelectedEntity(); - if (!pos || !selectedLayer) { + if (!pos || !selectedEntity) { return; } - if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { + if (selectedEntity.type !== 'regional_guidance' && selectedEntity.type !== 'layer') { return; } @@ -197,21 +216,24 @@ export const setStageEventHandlers = ({ return; } - const tool = getTool(); + const toolState = getToolState(); - if (tool === 'rect') { + if (toolState.selected === 'rect') { const lastMouseDownPos = getLastMouseDownPos(); if (lastMouseDownPos) { - onRectShapeAdded({ - layerId: selectedLayer.id, - rect: { - x: Math.min(pos.x, lastMouseDownPos.x), - y: Math.min(pos.y, lastMouseDownPos.y), - width: Math.abs(pos.x - lastMouseDownPos.x), - height: Math.abs(pos.y - lastMouseDownPos.y), + onRectShapeAdded( + { + id: selectedEntity.id, + rect: { + x: Math.min(pos.x, lastMouseDownPos.x), + y: Math.min(pos.y, lastMouseDownPos.y), + width: Math.abs(pos.x - lastMouseDownPos.x), + height: Math.abs(pos.y - lastMouseDownPos.y), + }, + color: getCurrentFill(), }, - color: selectedLayer.type === 'raster_layer' ? getBrushColor() : DEFAULT_RGBA_COLOR, - }); + selectedEntity.type + ); } } @@ -225,16 +247,18 @@ export const setStageEventHandlers = ({ if (!stage) { return; } - const tool = getTool(); + const toolState = getToolState(); const pos = updateLastCursorPos(stage, setLastCursorPos); - const selectedLayer = getSelectedLayer(); + const selectedEntity = getSelectedEntity(); - stage.findOne(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); + stage + .findOne(`#${PREVIEW_TOOL_GROUP_ID}`) + ?.visible(toolState.selected === 'brush' || toolState.selected === 'eraser'); - if (!pos || !selectedLayer) { + if (!pos || !selectedEntity) { return; } - if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { + if (selectedEntity.type !== 'regional_guidance' && selectedEntity.type !== 'layer') { return; } @@ -247,45 +271,49 @@ export const setStageEventHandlers = ({ return; } - if (tool === 'brush') { + if (toolState.selected === 'brush') { if (getIsDrawing()) { // Continue the last line - maybeAddNextPoint( - selectedLayer.id, - pos, - getLastAddedPoint, - setLastAddedPoint, - getBrushSpacingPx, - onPointAddedToLine - ); + maybeAddNextPoint(selectedEntity, pos, getToolState, getLastAddedPoint, setLastAddedPoint, onPointAddedToLine); } else { // Start a new line - onBrushLineAdded({ - layerId: selectedLayer.id, - points: [pos.x - selectedLayer.x, pos.y - selectedLayer.y, pos.x - selectedLayer.x, pos.y - selectedLayer.y], - color: selectedLayer.type === 'raster_layer' ? getBrushColor() : DEFAULT_RGBA_COLOR, - }); + onBrushLineAdded( + { + id: selectedEntity.id, + points: [ + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + ], + width: toolState.brush.width, + color: getCurrentFill(), + }, + selectedEntity.type + ); setIsDrawing(true); } } - if (tool === 'eraser') { + if (toolState.selected === 'eraser') { if (getIsDrawing()) { // Continue the last line - maybeAddNextPoint( - selectedLayer.id, - pos, - getLastAddedPoint, - setLastAddedPoint, - getBrushSpacingPx, - onPointAddedToLine - ); + maybeAddNextPoint(selectedEntity, pos, getToolState, getLastAddedPoint, setLastAddedPoint, onPointAddedToLine); } else { // Start a new line - onEraserLineAdded({ - layerId: selectedLayer.id, - points: [pos.x - selectedLayer.x, pos.y - selectedLayer.y, pos.x - selectedLayer.x, pos.y - selectedLayer.y], - }); + onEraserLineAdded( + { + id: selectedEntity.id, + points: [ + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + ], + width: toolState.eraser.width, + }, + selectedEntity.type + ); setIsDrawing(true); } } @@ -301,15 +329,15 @@ export const setStageEventHandlers = ({ setIsDrawing(false); setLastCursorPos(null); setLastMouseDownPos(null); - const selectedLayer = getSelectedLayer(); - const tool = getTool(); + const selectedEntity = getSelectedEntity(); + const toolState = getToolState(); stage.findOne(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(false); - if (!pos || !selectedLayer) { + if (!pos || !selectedEntity) { return; } - if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { + if (selectedEntity.type !== 'regional_guidance' && selectedEntity.type !== 'layer') { return; } if (getSpaceKey()) { @@ -317,12 +345,11 @@ export const setStageEventHandlers = ({ return; } if (getIsMouseDown()) { - if (tool === 'brush') { - onPointAddedToLine({ layerId: selectedLayer.id, point: [pos.x, pos.y] }); + if (toolState.selected === 'brush') { + onPointAddedToLine({ id: selectedEntity.id, point: [pos.x, pos.y] }, selectedEntity.type); } - - if (tool === 'eraser') { - onPointAddedToLine({ layerId: selectedLayer.id, point: [pos.x, pos.y] }); + if (toolState.selected === 'eraser') { + onPointAddedToLine({ id: selectedEntity.id, point: [pos.x, pos.y] }, selectedEntity.type); } } }); @@ -331,12 +358,17 @@ export const setStageEventHandlers = ({ e.evt.preventDefault(); if (e.evt.ctrlKey || e.evt.metaKey) { + const toolState = getToolState(); let delta = e.evt.deltaY; - if (getShouldInvert()) { + if (toolState.invertScroll) { delta = -delta; } // Holding ctrl or meta while scrolling changes the brush size - onBrushSizeChanged(calculateNewBrushSize(getBrushSize(), delta)); + if (toolState.selected === 'brush') { + onBrushSizeChanged(calculateNewBrushSize(toolState.brush.width, delta)); + } else if (toolState.selected === 'eraser') { + onEraserSizeChanged(calculateNewBrushSize(toolState.eraser.width, delta)); + } } else { // We need the absolute cursor position - not the scaled position const cursorPos = stage.getPointerPosition(); @@ -396,7 +428,7 @@ export const setStageEventHandlers = ({ setIsDrawing(false); setLastMouseDownPos(null); } else if (e.key === ' ') { - setToolBuffer(getTool()); + setToolBuffer(getToolState().selected); setTool('view'); } }; @@ -408,7 +440,7 @@ export const setStageEventHandlers = ({ return; } if (e.key === ' ') { - const toolBuffer = getToolBuffer(); + const toolBuffer = getToolState().selectedBuffer; setTool(toolBuffer ?? 'move'); setToolBuffer(null); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts index feb91867ea1..7b85bcfce33 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts @@ -1,6 +1,6 @@ import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; import { CA_LAYER_IMAGE_NAME, CA_LAYER_NAME, getCALayerImageId } from 'features/controlLayers/konva/naming'; -import type { ControlAdapterLayer } from 'features/controlLayers/store/types'; +import type { ControlAdapterData } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { ImageDTO } from 'services/api/types'; @@ -12,11 +12,11 @@ import type { ImageDTO } from 'services/api/types'; /** * Creates a control adapter layer. * @param stage The konva stage - * @param layerState The control adapter layer state + * @param ca The control adapter layer state */ -const createCALayer = (stage: Konva.Stage, layerState: ControlAdapterLayer): Konva.Layer => { +const createCALayer = (stage: Konva.Stage, ca: ControlAdapterData): Konva.Layer => { const konvaLayer = new Konva.Layer({ - id: layerState.id, + id: ca.id, name: CA_LAYER_NAME, imageSmoothingEnabled: false, listening: false, @@ -44,16 +44,16 @@ const createCALayerImage = (konvaLayer: Konva.Layer, imageEl: HTMLImageElement): * the konva image. * @param stage The konva stage * @param konvaLayer The konva layer - * @param layerState The control adapter layer state + * @param ca 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, konvaLayer: Konva.Layer, - layerState: ControlAdapterLayer, + ca: ControlAdapterData, getImageDTO: (imageName: string) => Promise ): Promise => { - const image = layerState.controlAdapter.processedImage ?? layerState.controlAdapter.image; + const image = ca.processedImage ?? ca.image; if (image) { const imageName = image.name; const imageDTO = await getImageDTO(imageName); @@ -61,7 +61,7 @@ const updateCALayerImageSource = async ( return; } const imageEl = new Image(); - const imageId = getCALayerImageId(layerState.id, imageName); + const imageId = getCALayerImageId(ca.id, imageName); imageEl.onload = () => { // Find the existing image or create a new one - must find using the name, bc the id may have just changed const konvaImage = @@ -72,7 +72,7 @@ const updateCALayerImageSource = async ( id: imageId, image: imageEl, }); - updateCALayerImageAttrs(stage, konvaImage, layerState); + updateCALayerImageAttrs(stage, konvaImage, ca); // Must cache after this to apply the filters konvaImage.cache(); imageEl.id = imageId; @@ -87,36 +87,33 @@ const updateCALayerImageSource = async ( * 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 + * @param ca The control adapter layer state */ -const updateCALayerImageAttrs = ( - stage: Konva.Stage, - konvaImage: Konva.Image, - layerState: ControlAdapterLayer -): void => { +const updateCALayerImageAttrs = (stage: Konva.Stage, konvaImage: Konva.Image, ca: ControlAdapterData): void => { let needsCache = false; // 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. // TODO(psyche): Investigate and report upstream. - const hasFilter = konvaImage.filters() !== null && konvaImage.filters().length > 0; + const filter = konvaImage.filters()[0] ?? null; + const filterNeedsUpdate = (filter === null && ca.filter !== 'none') || (filter && filter.name !== ca.filter); if ( - konvaImage.x() !== layerState.x || - konvaImage.y() !== layerState.y || - konvaImage.visible() !== layerState.isEnabled || - hasFilter !== layerState.isFilterEnabled + konvaImage.x() !== ca.x || + konvaImage.y() !== ca.y || + konvaImage.visible() !== ca.isEnabled || + filterNeedsUpdate ) { konvaImage.setAttrs({ - opacity: layerState.opacity, + opacity: ca.opacity, scaleX: 1, scaleY: 1, - visible: layerState.isEnabled, - filters: layerState.isFilterEnabled ? [LightnessToAlphaFilter] : [], + visible: ca.isEnabled, + filters: ca.filter === LightnessToAlphaFilter.name ? [LightnessToAlphaFilter] : [], }); needsCache = true; } - if (konvaImage.opacity() !== layerState.opacity) { - konvaImage.opacity(layerState.opacity); + if (konvaImage.opacity() !== ca.opacity) { + konvaImage.opacity(ca.opacity); } if (needsCache) { konvaImage.cache(); @@ -127,16 +124,16 @@ const updateCALayerImageAttrs = ( * 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 ca The control adapter layer state * @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source */ export const renderCALayer = ( stage: Konva.Stage, - layerState: ControlAdapterLayer, + ca: ControlAdapterData, zIndex: number, getImageDTO: (imageName: string) => Promise ): void => { - const konvaLayer = stage.findOne(`#${layerState.id}`) ?? createCALayer(stage, layerState); + const konvaLayer = stage.findOne(`#${ca.id}`) ?? createCALayer(stage, ca); konvaLayer.zIndex(zIndex); @@ -146,8 +143,8 @@ export const renderCALayer = ( let imageSourceNeedsUpdate = false; if (canvasImageSource instanceof HTMLImageElement) { - const image = layerState.controlAdapter.processedImage ?? layerState.controlAdapter.image; - if (image && canvasImageSource.id !== getCALayerImageId(layerState.id, image.name)) { + const image = ca.processedImage ?? ca.image; + if (image && canvasImageSource.id !== getCALayerImageId(ca.id, image.name)) { imageSourceNeedsUpdate = true; } else if (!image) { imageSourceNeedsUpdate = true; @@ -157,8 +154,8 @@ export const renderCALayer = ( } if (imageSourceNeedsUpdate) { - updateCALayerImageSource(stage, konvaLayer, layerState, getImageDTO); + updateCALayerImageSource(stage, konvaLayer, ca, getImageDTO); } else if (konvaImage) { - updateCALayerImageAttrs(stage, konvaImage, layerState); + updateCALayerImageAttrs(stage, konvaImage, ca); } }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts index 266bfd4aaa1..d5dc53f3be6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -2,19 +2,17 @@ import { DEBOUNCE_MS } from 'features/controlLayers/konva/constants'; import { PREVIEW_LAYER_ID } from 'features/controlLayers/konva/naming'; import { updateBboxes } from 'features/controlLayers/konva/renderers/bbox'; import { renderCALayer } from 'features/controlLayers/konva/renderers/caLayer'; -import { renderIILayer } from 'features/controlLayers/konva/renderers/iiLayer'; import { renderBboxPreview, renderToolPreview } from 'features/controlLayers/konva/renderers/previewLayer'; import { renderRasterLayer } from 'features/controlLayers/konva/renderers/rasterLayer'; import { renderRGLayer } from 'features/controlLayers/konva/renderers/rgLayer'; import { mapId, selectRenderableLayers } from 'features/controlLayers/konva/util'; -import type { LayerData, Tool } from 'features/controlLayers/store/types'; -import { - isControlAdapterLayer, - isInitialImageLayer, - isInpaintMaskLayer, - isRasterLayer, - isRegionalGuidanceLayer, - isRenderableLayer, +import type { + CanvasEntity, + ControlAdapterData, + LayerData, + PosChangedArg, + RegionalGuidanceData, + Tool, } from 'features/controlLayers/store/types'; import type Konva from 'konva'; import { debounce } from 'lodash-es'; @@ -27,43 +25,42 @@ import type { ImageDTO } from 'services/api/types'; /** * Renders the layers on the stage. * @param stage The konva stage - * @param layerStates Array of all layer states - * @param globalMaskLayerOpacity The global mask layer opacity + * @param layers Array of all layer states + * @param rgGlobalOpacity The global mask layer opacity * @param tool The current tool * @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 + * @param onPosChanged Callback for when the layer's position changes */ const renderLayers = ( stage: Konva.Stage, - layerStates: LayerData[], - globalMaskLayerOpacity: number, + layers: LayerData[], + controlAdapters: ControlAdapterData[], + regions: RegionalGuidanceData[], + rgGlobalOpacity: number, tool: Tool, + selectedEntity: CanvasEntity | null, getImageDTO: (imageName: string) => Promise, - onLayerPosChanged?: (layerId: string, x: number, y: number) => void + onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): void => { - const layerIds = layerStates.filter(isRenderableLayer).map(mapId); + const renderableIds = [...layers.map(mapId), ...controlAdapters.map(mapId), ...regions.map(mapId)]; // Remove un-rendered layers for (const konvaLayer of stage.find(selectRenderableLayers)) { - if (!layerIds.includes(konvaLayer.id())) { + if (!renderableIds.includes(konvaLayer.id())) { konvaLayer.destroy(); } } // We'll need to ensure the tool preview layer is on top of the rest of the layers let zIndex = 0; - for (const layer of layerStates) { - if (isRegionalGuidanceLayer(layer)) { - renderRGLayer(stage, layer, globalMaskLayerOpacity, tool, zIndex, onLayerPosChanged); - } else if (isControlAdapterLayer(layer)) { - renderCALayer(stage, layer, zIndex, getImageDTO); - } else if (isInitialImageLayer(layer)) { - renderIILayer(stage, layer, zIndex, getImageDTO); - } else if (isRasterLayer(layer)) { - renderRasterLayer(stage, layer, tool, zIndex, onLayerPosChanged); - } else if (isInpaintMaskLayer(layer)) { - // - } - // IP Adapter layers are not rendered - // Increment the z-index for the tool layer + for (const layer of layers) { + renderRasterLayer(stage, layer, tool, zIndex, onPosChanged); + zIndex++; + } + for (const ca of controlAdapters) { + renderCALayer(stage, ca, zIndex, getImageDTO); + zIndex++; + } + for (const rg of regions) { + renderRGLayer(stage, rg, rgGlobalOpacity, tool, zIndex, selectedEntity, onPosChanged); zIndex++; } // Arrange the tool preview layer 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 351a39301ec..9074f240460 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -5,7 +5,13 @@ import { LAYER_BBOX_NAME, PREVIEW_GENERATION_BBOX_DUMMY_RECT, } from 'features/controlLayers/konva/naming'; -import type { BrushLine, EraserLine, ImageObject, LayerData, RectShape } from 'features/controlLayers/store/types'; +import type { + BrushLine, + CanvasEntity, + EraserLine, + ImageObject, + RectShape, +} from 'features/controlLayers/store/types'; import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types'; import { t } from 'i18next'; import Konva from 'konva'; @@ -174,12 +180,12 @@ export const createImageObjectGroup = async ( /** * Creates a bounding box rect for a layer. - * @param layerState The layer state for the layer to create the bounding box for + * @param entity The layer state for the layer to create the bounding box for * @param konvaLayer The konva layer to attach the bounding box to */ -export const createBboxRect = (layerState: LayerData, konvaLayer: Konva.Layer): Konva.Rect => { +export const createBboxRect = (entity: CanvasEntity, konvaLayer: Konva.Layer): Konva.Rect => { const rect = new Konva.Rect({ - id: getLayerBboxId(layerState.id), + id: getLayerBboxId(entity.id), name: LAYER_BBOX_NAME, strokeWidth: 1, visible: false, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts index d58d459ee00..4ee22d71a6e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts @@ -18,7 +18,7 @@ import { PREVIEW_TOOL_GROUP_ID, } from 'features/controlLayers/konva/naming'; import { selectRenderableLayers } from 'features/controlLayers/konva/util'; -import type { LayerData, RgbaColor, Tool } from 'features/controlLayers/store/types'; +import type { CanvasEntity, CanvasV2State, RgbaColor, Tool } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect, Vector2d } from 'konva/lib/types'; import { atom } from 'nanostores'; @@ -327,8 +327,8 @@ export const getToolPreviewGroup = (stage: Konva.Stage): Konva.Group => { * Renders the preview layer. * @param stage The konva stage * @param tool The selected tool - * @param color The selected layer's color - * @param selectedLayerType The selected layer's type + * @param currentFill The selected layer's color + * @param selectedEntity The selected layer's type * @param globalMaskLayerOpacity The global mask layer opacity * @param cursorPos The cursor position * @param lastMouseDownPos The position of the last mouse down event - used for the rect tool @@ -336,17 +336,16 @@ export const getToolPreviewGroup = (stage: Konva.Stage): Konva.Group => { */ export const renderToolPreview = ( stage: Konva.Stage, - tool: Tool, - brushColor: RgbaColor, - selectedLayerType: LayerData['type'] | null, - globalMaskLayerOpacity: number, + toolState: CanvasV2State['tool'], + currentFill: RgbaColor, + selectedEntity: CanvasEntity | null, cursorPos: Vector2d | null, lastMouseDownPos: Vector2d | null, - brushSize: number, isDrawing: boolean, isMouseDown: boolean ): void => { const layerCount = stage.find(selectRenderableLayers).length; + const tool = toolState.selected; // Update the stage's pointer style if (tool === 'view') { // View gets a hand @@ -354,7 +353,7 @@ export const renderToolPreview = ( } else if (layerCount === 0) { // We have no layers, so we should not render any tool stage.container().style.cursor = 'default'; - } else if (selectedLayerType !== 'regional_guidance_layer' && selectedLayerType !== 'raster_layer') { + } else if (selectedEntity?.type !== 'regional_guidance' && selectedEntity?.type !== 'layer') { // Non-mask-guidance layers don't have tools stage.container().style.cursor = 'not-allowed'; } else if (tool === 'move') { @@ -377,7 +376,7 @@ export const renderToolPreview = ( if ( !cursorPos || layerCount === 0 || - (selectedLayerType !== 'regional_guidance_layer' && selectedLayerType !== 'raster_layer') + (selectedEntity?.type !== 'regional_guidance' && selectedEntity?.type !== 'layer') ) { // We can bail early if the mouse isn't over the stage or there are no layers toolPreviewGroup.visible(false); @@ -394,24 +393,25 @@ export const renderToolPreview = ( if (cursorPos && (tool === 'brush' || tool === 'eraser')) { // Update the fill circle const brushPreviewFill = brushPreviewGroup.findOne(`#${PREVIEW_BRUSH_FILL_ID}`); + const radius = (tool === 'brush' ? toolState.brush.width : toolState.eraser.width) / 2; brushPreviewFill?.setAttrs({ x: cursorPos.x, y: cursorPos.y, - radius: brushSize / 2, - fill: isDrawing ? '' : rgbaColorToString(brushColor), + radius, + fill: isDrawing ? '' : rgbaColorToString(currentFill), globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out', }); // Update the inner border of the brush preview const brushPreviewInner = brushPreviewGroup.findOne(`#${PREVIEW_BRUSH_BORDER_INNER_ID}`); - brushPreviewInner?.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: brushSize / 2 }); + brushPreviewInner?.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius }); // Update the outer border of the brush preview const brushPreviewOuter = brushPreviewGroup.findOne(`#${PREVIEW_BRUSH_BORDER_OUTER_ID}`); brushPreviewOuter?.setAttrs({ x: cursorPos.x, y: cursorPos.y, - radius: brushSize / 2 + 1, + radius: radius + 1, }); brushPreviewGroup.visible(true); @@ -426,7 +426,7 @@ export const renderToolPreview = ( y: Math.min(cursorPos.y, lastMouseDownPos.y), width: Math.abs(cursorPos.x - lastMouseDownPos.x), height: Math.abs(cursorPos.y - lastMouseDownPos.y), - fill: rgbaColorToString(brushColor), + fill: rgbaColorToString(currentFill), }); rectPreview?.visible(true); } else { 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 061c2ed0e6d..d34d48063cf 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,4 @@ -import { BBOX_SELECTED_STROKE } from 'features/controlLayers/konva/constants'; import { - LAYER_BBOX_NAME, RASTER_LAYER_BRUSH_LINE_NAME, RASTER_LAYER_ERASER_LINE_NAME, RASTER_LAYER_IMAGE_NAME, @@ -9,7 +7,6 @@ import { RASTER_LAYER_RECT_SHAPE_NAME, } from 'features/controlLayers/konva/naming'; import { - createBboxRect, createBrushLine, createEraserLine, createImageObjectGroup, @@ -17,7 +14,7 @@ import { createRectShape, } from 'features/controlLayers/konva/renderers/objects'; import { mapId, selectRasterObjects } from 'features/controlLayers/konva/util'; -import type { RasterLayer, Tool } from 'features/controlLayers/store/types'; +import type { CanvasEntity, LayerData, PosChangedArg, Tool } from 'features/controlLayers/store/types'; import Konva from 'konva'; /** @@ -28,12 +25,12 @@ import Konva from 'konva'; * Creates a raster layer. * @param stage The konva stage * @param layerState The raster layer state - * @param onLayerPosChanged Callback for when the layer's position changes + * @param onPosChanged Callback for when the layer's position changes */ const createRasterLayer = ( stage: Konva.Stage, - layerState: RasterLayer, - onLayerPosChanged?: (layerId: string, x: number, y: number) => void + layerState: LayerData, + onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): Konva.Layer => { // This layer hasn't been added to the konva state yet const konvaLayer = new Konva.Layer({ @@ -45,9 +42,9 @@ const createRasterLayer = ( // 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 (onPosChanged) { konvaLayer.on('dragend', function (e) { - onLayerPosChanged(layerState.id, Math.floor(e.target.x()), Math.floor(e.target.y())); + onPosChanged({ id: layerState.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'layer'); }); } @@ -61,17 +58,17 @@ const createRasterLayer = ( * @param stage The konva stage * @param layerState The regional guidance layer state * @param tool The current tool - * @param onLayerPosChanged Callback for when the layer's position changes + * @param onPosChanged Callback for when the layer's position changes */ export const renderRasterLayer = async ( stage: Konva.Stage, - layerState: RasterLayer, + layerState: LayerData, tool: Tool, zIndex: number, - onLayerPosChanged?: (layerId: string, x: number, y: number) => void + onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ) => { const konvaLayer = - stage.findOne(`#${layerState.id}`) ?? createRasterLayer(stage, layerState, onLayerPosChanged); + stage.findOne(`#${layerState.id}`) ?? createRasterLayer(stage, layerState, onPosChanged); // Update the layer's position and listening state konvaLayer.setAttrs({ @@ -128,23 +125,23 @@ export const renderRasterLayer = async ( konvaLayer.visible(layerState.isEnabled); } - const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer); + // const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer); - if (layerState.bbox) { - const active = !layerState.bboxNeedsUpdate && layerState.isSelected && tool === 'move'; - bboxRect.setAttrs({ - visible: active, - listening: active, - x: layerState.bbox.x, - y: layerState.bbox.y, - width: layerState.bbox.width, - height: layerState.bbox.height, - stroke: layerState.isSelected ? BBOX_SELECTED_STROKE : '', - strokeWidth: 1 / stage.scaleX(), - }); - } else { - bboxRect.visible(false); - } + // if (layerState.bbox) { + // const active = !layerState.bboxNeedsUpdate && layerState.isSelected && tool === 'move'; + // bboxRect.setAttrs({ + // visible: active, + // listening: active, + // x: layerState.bbox.x, + // y: layerState.bbox.y, + // width: layerState.bbox.width, + // height: layerState.bbox.height, + // stroke: layerState.isSelected ? BBOX_SELECTED_STROKE : '', + // strokeWidth: 1 / stage.scaleX(), + // }); + // } else { + // bboxRect.visible(false); + // } konvaObjectGroup.opacity(layerState.opacity); }; 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 e6bc7e1212a..2f7f259baa1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts @@ -18,7 +18,7 @@ import { createRectShape, } from 'features/controlLayers/konva/renderers/objects'; import { mapId, selectVectorMaskObjects } from 'features/controlLayers/konva/util'; -import type { RegionalGuidanceLayer, Tool } from 'features/controlLayers/store/types'; +import type { CanvasEntity, PosChangedArg, RegionalGuidanceData, Tool } from 'features/controlLayers/store/types'; import Konva from 'konva'; /** @@ -41,17 +41,17 @@ const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => { /** * Creates a regional guidance layer. * @param stage The konva stage - * @param layerState The regional guidance layer state + * @param rg The regional guidance layer state * @param onLayerPosChanged Callback for when the layer's position changes */ const createRGLayer = ( stage: Konva.Stage, - layerState: RegionalGuidanceLayer, - onLayerPosChanged?: (layerId: string, x: number, y: number) => void + rg: RegionalGuidanceData, + onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): Konva.Layer => { // This layer hasn't been added to the konva state yet const konvaLayer = new Konva.Layer({ - id: layerState.id, + id: rg.id, name: RG_LAYER_NAME, draggable: true, dragDistance: 0, @@ -59,9 +59,9 @@ const createRGLayer = ( // 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 (onPosChanged) { konvaLayer.on('dragend', function (e) { - onLayerPosChanged(layerState.id, Math.floor(e.target.x()), Math.floor(e.target.y())); + onPosChanged({ id: rg.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'regional_guidance'); }); } @@ -73,32 +73,32 @@ const createRGLayer = ( /** * Renders a raster layer. * @param stage The konva stage - * @param layerState The regional guidance layer state + * @param rg The regional guidance layer state * @param globalMaskLayerOpacity The global mask layer opacity * @param tool The current tool - * @param onLayerPosChanged Callback for when the layer's position changes + * @param onPosChanged Callback for when the layer's position changes */ export const renderRGLayer = ( stage: Konva.Stage, - layerState: RegionalGuidanceLayer, + rg: RegionalGuidanceData, globalMaskLayerOpacity: number, tool: Tool, zIndex: number, - onLayerPosChanged?: (layerId: string, x: number, y: number) => void + selectedEntity: CanvasEntity | null, + onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): void => { - const konvaLayer = - stage.findOne(`#${layerState.id}`) ?? createRGLayer(stage, layerState, onLayerPosChanged); + const konvaLayer = stage.findOne(`#${rg.id}`) ?? createRGLayer(stage, rg, onPosChanged); // Update the layer's position and listening state konvaLayer.setAttrs({ listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events - x: Math.floor(layerState.x), - y: Math.floor(layerState.y), + x: Math.floor(rg.x), + y: Math.floor(rg.y), zIndex, }); // Convert the color to a string, stripping the alpha - the object group will handle opacity. - const rgbColor = rgbColorToString(layerState.previewColor); + const rgbColor = rgbColorToString(rg.fill); const konvaObjectGroup = konvaLayer.findOne(`.${RG_LAYER_OBJECT_GROUP_NAME}`) ?? @@ -107,7 +107,7 @@ export const renderRGLayer = ( // We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required. let groupNeedsCache = false; - const objectIds = layerState.objects.map(mapId); + const objectIds = rg.objects.map(mapId); // Destroy any objects that are no longer in the redux state for (const objectNode of konvaObjectGroup.find(selectVectorMaskObjects)) { if (!objectIds.includes(objectNode.id())) { @@ -116,7 +116,7 @@ export const renderRGLayer = ( } } - for (const obj of layerState.objects) { + for (const obj of rg.objects) { if (obj.type === 'brush_line') { const konvaBrushLine = stage.findOne(`#${obj.id}`) ?? createBrushLine(obj, konvaObjectGroup, RG_LAYER_BRUSH_LINE_NAME); @@ -160,8 +160,8 @@ export const renderRGLayer = ( } // Only update layer visibility if it has changed. - if (konvaLayer.visible() !== layerState.isEnabled) { - konvaLayer.visible(layerState.isEnabled); + if (konvaLayer.visible() !== rg.isEnabled) { + konvaLayer.visible(rg.isEnabled); groupNeedsCache = true; } @@ -173,6 +173,7 @@ export const renderRGLayer = ( const compositingRect = konvaLayer.findOne(`.${COMPOSITING_RECT_NAME}`) ?? createCompositingRect(konvaLayer); + const isSelected = selectedEntity?.id === rg.id; /** * When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows @@ -185,7 +186,7 @@ export const renderRGLayer = ( * 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. */ - if (layerState.isSelected && tool !== 'move') { + if (isSelected && tool !== 'move') { // We must clear the cache first so Konva will re-draw the group with the new compositing rect if (konvaObjectGroup.isCached()) { konvaObjectGroup.clearCache(); @@ -195,7 +196,7 @@ export const renderRGLayer = ( 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 - ...(!layerState.bboxNeedsUpdate && layerState.bbox ? layerState.bbox : getLayerBboxFast(konvaLayer)), + ...(!rg.bboxNeedsUpdate && rg.bbox ? rg.bbox : getLayerBboxFast(konvaLayer)), fill: rgbColor, opacity: globalMaskLayerOpacity, // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) @@ -215,18 +216,18 @@ export const renderRGLayer = ( konvaObjectGroup.opacity(globalMaskLayerOpacity); } - const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer); + const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rg, konvaLayer); - if (layerState.bbox) { - const active = !layerState.bboxNeedsUpdate && layerState.isSelected && tool === 'move'; + if (rg.bbox) { + const active = !rg.bboxNeedsUpdate && isSelected && tool === 'move'; bboxRect.setAttrs({ visible: active, listening: active, - x: layerState.bbox.x, - y: layerState.bbox.y, - width: layerState.bbox.width, - height: layerState.bbox.height, - stroke: layerState.isSelected ? BBOX_SELECTED_STROKE : '', + x: rg.bbox.x, + y: rg.bbox.y, + width: rg.bbox.width, + height: rg.bbox.height, + stroke: isSelected ? BBOX_SELECTED_STROKE : '', }); } else { bboxRect.visible(false); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 71df7102585..ee2072eaf62 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -11,21 +11,12 @@ import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/ import type { IRect, Vector2d } from 'konva/lib/types'; import { atom } from 'nanostores'; -import type { - CanvasV2State, - ControlAdapterData, - IPAdapterData, - LayerData, - RegionalGuidanceData, - RgbaColor, - StageAttrs, - Tool, -} from './types'; +import type { CanvasEntity, CanvasV2State, RgbaColor, StageAttrs, Tool } from './types'; import { DEFAULT_RGBA_COLOR } from './types'; const initialState: CanvasV2State = { _version: 3, - lastSelectedItem: null, + selectedEntityIdentifier: null, prompts: { positivePrompt: '', negativePrompt: '', @@ -113,6 +104,12 @@ export const canvasV2Slice = createSlice({ invertScrollChanged: (state, action: PayloadAction) => { state.tool.invertScroll = action.payload; }, + toolChanged: (state, action: PayloadAction) => { + state.tool.selected = action.payload; + }, + toolBufferChanged: (state, action: PayloadAction) => { + state.tool.selectedBuffer = action.payload; + }, }, extraReducers(builder) { builder.addCase(modelChanged, (state, action) => { @@ -146,6 +143,8 @@ export const { eraserWidthChanged, fillChanged, invertScrollChanged, + toolChanged, + toolBufferChanged, } = canvasV2Slice.actions; export const selectCanvasV2Slice = (state: RootState) => state.canvasV2; @@ -173,18 +172,9 @@ export const $stageAttrs = atom({ // Some nanostores that are manually synced to redux state to provide imperative access // TODO(psyche): -export const $tool = atom('brush'); -export const $toolBuffer = atom(null); -export const $brushWidth = atom(0); -export const $brushSpacingPx = atom(0); -export const $eraserWidth = atom(0); -export const $eraserSpacingPx = atom(0); -export const $fill = atom(DEFAULT_RGBA_COLOR); -export const $selectedLayer = atom(null); -export const $selectedRG = atom(null); -export const $selectedCA = atom(null); -export const $selectedIPA = atom(null); -export const $invertScroll = atom(false); +export const $toolState = atom(deepClone(initialState.tool)); +export const $currentFill = atom(DEFAULT_RGBA_COLOR); +export const $selectedEntity = atom(null); export const $bbox = atom({ x: 0, y: 0, width: 0, height: 0 }); export const canvasV2PersistConfig: PersistConfig = { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts index 45790bb2f1a..7c6705ae9b9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts @@ -7,12 +7,12 @@ import type { IRect } from 'konva/lib/types'; import { v4 as uuidv4 } from 'uuid'; import type { - AddBrushLineArg, - AddEraserLineArg, - AddImageObjectArg, - AddPointToLineArg, - AddRectShapeArg, + BrushLineAddedArg, + EraserLineAddedArg, + ImageObjectAddedArg, LayerData, + PointAddedToLineArg, + RectShapeAddedArg, } from './types'; import { isLine } from './types'; @@ -133,7 +133,7 @@ export const layersSlice = createSlice({ moveToStart(state.layers, layer); }, layerBrushLineAdded: { - reducer: (state, action: PayloadAction) => { + reducer: (state, action: PayloadAction) => { const { id, points, lineId, color, width } = action.payload; const layer = selectLayer(state, id); if (!layer) { @@ -149,12 +149,12 @@ export const layersSlice = createSlice({ }); layer.bboxNeedsUpdate = true; }, - prepare: (payload: AddBrushLineArg) => ({ + prepare: (payload: BrushLineAddedArg) => ({ payload: { ...payload, lineId: uuidv4() }, }), }, layerEraserLineAdded: { - reducer: (state, action: PayloadAction) => { + reducer: (state, action: PayloadAction) => { const { id, points, lineId, width } = action.payload; const layer = selectLayer(state, id); if (!layer) { @@ -169,11 +169,11 @@ export const layersSlice = createSlice({ }); layer.bboxNeedsUpdate = true; }, - prepare: (payload: AddEraserLineArg) => ({ + prepare: (payload: EraserLineAddedArg) => ({ payload: { ...payload, lineId: uuidv4() }, }), }, - layerLinePointAdded: (state, action: PayloadAction) => { + layerLinePointAdded: (state, action: PayloadAction) => { const { id, point } = action.payload; const layer = selectLayer(state, id); if (!layer) { @@ -187,7 +187,7 @@ export const layersSlice = createSlice({ layer.bboxNeedsUpdate = true; }, layerRectAdded: { - reducer: (state, action: PayloadAction) => { + reducer: (state, action: PayloadAction) => { const { id, rect, rectId, color } = action.payload; if (rect.height === 0 || rect.width === 0) { // Ignore zero-area rectangles @@ -200,18 +200,15 @@ export const layersSlice = createSlice({ layer.objects.push({ type: 'rect_shape', id: getRectShapeId(id, rectId), - x: rect.x - layer.x, - y: rect.y - layer.y, - width: rect.width, - height: rect.height, + ...rect, color, }); layer.bboxNeedsUpdate = true; }, - prepare: (payload: AddRectShapeArg) => ({ payload: { ...payload, rectId: uuidv4() } }), + prepare: (payload: RectShapeAddedArg) => ({ payload: { ...payload, rectId: uuidv4() } }), }, layerImageAdded: { - reducer: (state, action: PayloadAction) => { + reducer: (state, action: PayloadAction) => { const { id, imageId, imageDTO } = action.payload; const layer = selectLayer(state, id); if (!layer) { @@ -229,7 +226,7 @@ export const layersSlice = createSlice({ }); layer.bboxNeedsUpdate = true; }, - prepare: (payload: AddImageObjectArg) => ({ payload: { ...payload, imageId: uuidv4() } }), + prepare: (payload: ImageObjectAddedArg) => ({ payload: { ...payload, imageId: uuidv4() } }), }, }, }); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts index 167feec396f..7d177fabbce 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts @@ -14,11 +14,11 @@ import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; import type { - AddBrushLineArg, - AddEraserLineArg, - AddPointToLineArg, - AddRectShapeArg, + BrushLineAddedArg, + EraserLineAddedArg, IPAdapterData, + PointAddedToLineArg, + RectShapeAddedArg, RegionalGuidanceData, RgbColor, } from './types'; @@ -306,7 +306,7 @@ export const regionalGuidanceSlice = createSlice({ ipa.clipVisionModel = clipVisionModel; }, rgBrushLineAdded: { - reducer: (state, action: PayloadAction) => { + reducer: (state, action: PayloadAction) => { const { id, points, lineId, color, width } = action.payload; const rg = selectRg(state, id); if (!rg) { @@ -315,21 +315,19 @@ export const regionalGuidanceSlice = createSlice({ rg.objects.push({ id: getBrushLineId(id, lineId), type: 'brush_line', - // Points must be offset by the layer's x and y coordinates - // TODO: Handle this in the event listener? - points: [points[0] - rg.x, points[1] - rg.y, points[2] - rg.x, points[3] - rg.y], + points, strokeWidth: width, color, }); rg.bboxNeedsUpdate = true; rg.imageCache = null; }, - prepare: (payload: AddBrushLineArg) => ({ + prepare: (payload: BrushLineAddedArg) => ({ payload: { ...payload, lineId: uuidv4() }, }), }, rgEraserLineAdded: { - reducer: (state, action: PayloadAction) => { + reducer: (state, action: PayloadAction) => { const { id, points, lineId, width } = action.payload; const rg = selectRg(state, id); if (!rg) { @@ -338,19 +336,17 @@ export const regionalGuidanceSlice = createSlice({ rg.objects.push({ id: getEraserLineId(id, lineId), type: 'eraser_line', - // Points must be offset by the layer's x and y coordinates - // TODO: Handle this in the event listener? - points: [points[0] - rg.x, points[1] - rg.y, points[2] - rg.x, points[3] - rg.y], + points, strokeWidth: width, }); rg.bboxNeedsUpdate = true; rg.imageCache = null; }, - prepare: (payload: AddEraserLineArg) => ({ + prepare: (payload: EraserLineAddedArg) => ({ payload: { ...payload, lineId: uuidv4() }, }), }, - rgLinePointAdded: (state, action: PayloadAction) => { + rgLinePointAdded: (state, action: PayloadAction) => { const { id, point } = action.payload; const rg = selectRg(state, id); if (!rg) { @@ -360,14 +356,12 @@ export const regionalGuidanceSlice = createSlice({ if (!lastObject || !isLine(lastObject)) { return; } - // Points must be offset by the layer's x and y coordinates - // TODO: Handle this in the event listener - lastObject.points.push(point[0] - rg.x, point[1] - rg.y); + lastObject.points.push(...point); rg.bboxNeedsUpdate = true; rg.imageCache = null; }, rgRectAdded: { - reducer: (state, action: PayloadAction) => { + reducer: (state, action: PayloadAction) => { const { id, rect, rectId, color } = action.payload; if (rect.height === 0 || rect.width === 0) { // Ignore zero-area rectangles @@ -380,16 +374,13 @@ export const regionalGuidanceSlice = createSlice({ rg.objects.push({ type: 'rect_shape', id: getRectShapeId(id, rectId), - x: rect.x - rg.x, - y: rect.y - rg.y, - width: rect.width, - height: rect.height, + ...rect, color, }); rg.bboxNeedsUpdate = true; rg.imageCache = null; }, - prepare: (payload: AddRectShapeArg) => ({ payload: { ...payload, rectId: uuidv4() } }), + prepare: (payload: RectShapeAddedArg) => ({ payload: { ...payload, rectId: uuidv4() } }), }, }, }); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 860f3f20825..fc4b010067e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,3 +1,4 @@ +import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; import { zBeginEndStepPct, zCLIPVisionModelV2, @@ -243,7 +244,7 @@ const zInpaintMaskData = z.object({ }); export type InpaintMaskData = z.infer; -const zFilter = z.enum(['none', 'lightness_to_alpha']); +const zFilter = z.enum(['none', LightnessToAlphaFilter.name]); export type Filter = z.infer; const zControlAdapterData = z.object({ @@ -271,21 +272,12 @@ export type ControlAdapterConfig = Pick< 'weight' | 'image' | 'processedImage' | 'processorConfig' | 'beginEndStepPct' | 'model' | 'controlMode' >; -const zCanvasItemIdentifier = z.object({ - type: z.enum([ - zLayerData.shape.type.value, - zIPAdapterData.shape.type.value, - zControlAdapterData.shape.type.value, - zRegionalGuidanceData.shape.type.value, - zInpaintMaskData.shape.type.value, - ]), - id: zId, -}); -type CanvasItemIdentifier = z.infer; +export type CanvasEntity = LayerData | IPAdapterData | ControlAdapterData | RegionalGuidanceData | InpaintMaskData; +export type CanvasEntityIdentifier = Pick; export type CanvasV2State = { _version: 3; - lastSelectedItem: CanvasItemIdentifier | null; + selectedEntityIdentifier: CanvasEntityIdentifier | null; prompts: { positivePrompt: ParameterPositivePrompt; negativePrompt: ParameterNegativePrompt; @@ -314,11 +306,17 @@ export type CanvasV2State = { }; export type StageAttrs = { x: number; y: number; width: number; height: number; scale: number }; -export type AddEraserLineArg = { id: string; points: [number, number, number, number]; width: number }; -export type AddBrushLineArg = AddEraserLineArg & { color: RgbaColor }; -export type AddPointToLineArg = { id: string; point: [number, number] }; -export type AddRectShapeArg = { id: string; rect: IRect; color: RgbaColor }; -export type AddImageObjectArg = { id: string; imageDTO: ImageDTO }; +export type PosChangedArg = { id: string; x: number; y: number }; +export type BboxChangedArg = { id: string; bbox: IRect }; +export type EraserLineAddedArg = { + id: string; + points: [number, number, number, number]; + width: number; +}; +export type BrushLineAddedArg = EraserLineAddedArg & { color: RgbaColor }; +export type PointAddedToLineArg = { id: string; point: [number, number] }; +export type RectShapeAddedArg = { id: string; rect: IRect; color: RgbaColor }; +export type ImageObjectAddedArg = { id: string; imageDTO: ImageDTO }; //#region Type guards export const isLine = (obj: LayerObject): obj is BrushLine | EraserLine => { diff --git a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx index e58c0eaad7c..7ff8cb1b9ee 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx +++ b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx @@ -34,7 +34,7 @@ const selectImageUsages = createMemoizedSelector( const { imagesToDelete } = deleteImageModal; const allImageUsage = (imagesToDelete ?? []).map(({ image_name }) => - getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, image_name) + getImageUsage(canvas, nodes, controlAdapters, canvasV2, image_name) ); const imageUsageSummary: ImageUsage = { diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts index 0a2a0587c7b..456872e23be 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts @@ -84,7 +84,7 @@ export const selectImageUsage = createMemoizedSelector( } const imagesUsage = imagesToDelete.map((i) => - getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, i.image_name) + getImageUsage(canvas, nodes, controlAdapters, canvasV2, i.image_name) ); return imagesUsage; diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx index a0fa496ea61..87e78f22b47 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx @@ -45,7 +45,7 @@ const DeleteBoardModal = (props: Props) => { [selectCanvasSlice, selectNodesSlice, selectControlAdaptersSlice, selectCanvasV2Slice], (canvas, nodes, controlAdapters, controlLayers) => { const allImageUsage = (boardImageNames ?? []).map((imageName) => - getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, imageName) + getImageUsage(canvas, nodes, controlAdapters, canvasV2, imageName) ); const imageUsageSummary: ImageUsage = { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts index 28eb083844f..268ec553517 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts @@ -10,7 +10,7 @@ import { CANVAS_COHERENCE_NOISE, METADATA, NOISE, POSITIVE_CONDITIONING } from ' export const prepareLinearUIBatch = (state: RootState, graph: NonNullableGraph, prepend: boolean): BatchConfig => { const { iterations, model, shouldRandomizeSeed, seed } = state.generation; - const { shouldConcatPrompts } = state.controlLayers.present; + const { shouldConcatPrompts } = state.canvasV2; const { prompts, seedBehaviour } = state.dynamicPrompts; const data: Batch['data'] = []; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts index f130aa5671d..9ee41158b4e 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts @@ -73,7 +73,7 @@ export const addControlLayers = async ( ): Promise => { const isSDXL = base === 'sdxl'; - const validLayers = state.controlLayers.present.layers.filter((l) => isValidLayer(l, base)); + const validLayers = state.canvasV2.layers.filter((l) => isValidLayer(l, base)); const validControlAdapters = validLayers.filter(isControlAdapterLayer).map((l) => l.controlAdapter); for (const ca of validControlAdapters) { @@ -259,7 +259,7 @@ export const addControlLayers = async ( } } - g.upsertMetadata({ control_layers: { layers: validLayers, version: state.controlLayers.present._version } }); + g.upsertMetadata({ control_layers: { layers: validLayers, version: state.canvasV2._version } }); return validLayers; }; //#endregion @@ -421,7 +421,7 @@ const addInitialImageLayerToGraph = ( ) => { const { vaePrecision } = state.generation; const { refinerModel, refinerStart } = state.sdxl; - const { width, height } = state.controlLayers.present.size; + const { width, height } = state.canvasV2.size; assert(layer.isEnabled, 'Initial image layer is not enabled'); assert(layer.image, 'Initial image layer has no image'); @@ -567,8 +567,8 @@ const buildControlImage = ( */ const getRGLayerBlobs = async (layerIds?: string[], preview: boolean = false): Promise> => { const state = getStore().getState(); - const { layers } = state.controlLayers.present; - const { width, height } = state.controlLayers.present.size; + const { layers } = state.canvasV2; + const { width, height } = state.canvasV2.size; const reduxLayers = layers.filter(isRegionalGuidanceLayer); const container = document.createElement('div'); const stage = new Konva.Stage({ container, width, height }); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addHRF.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addHRF.ts index 68286e337c4..e03d54ebc2a 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addHRF.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addHRF.ts @@ -74,7 +74,7 @@ export const addHRF = ( vaeSource: Invocation<'vae_loader'> | Invocation<'main_model_loader'> | Invocation<'seamless'> ): Invocation<'l2i'> => { const { hrfStrength, hrfEnabled, hrfMethod } = state.hrf; - const { width, height } = state.controlLayers.present.size; + const { width, height } = state.canvasV2.size; const optimalDimension = selectOptimalDimension(state); const { newWidth: hrfWidth, newHeight: hrfHeight } = calculateHrfRes(optimalDimension, width, height); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts index ed6dfbc224a..0c40e98052b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts @@ -22,7 +22,7 @@ export const getPresetModifiedPrompts = ( state: RootState ): { positivePrompt: string; negativePrompt: string; positiveStylePrompt?: string; negativeStylePrompt?: string } => { const { positivePrompt, negativePrompt, positivePrompt2, negativePrompt2, shouldConcatPrompts } = - state.controlLayers.present; + state.canvasV2.prompts; const { activeStylePresetId } = state.stylePreset; if (activeStylePresetId) { diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx index 0f32bdb4359..73141cf3251 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx @@ -13,7 +13,7 @@ import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets'; export const ParamNegativePrompt = memo(() => { const dispatch = useAppDispatch(); - const prompt = useAppSelector((s) => s.controlLayers.present.negativePrompt); + const prompt = useAppSelector((s) => s.canvasV2.prompts.negativePrompt); const viewMode = useAppSelector((s) => s.stylePreset.viewMode); const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId); diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx index ae79064bcfc..32c66231b92 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx @@ -17,7 +17,7 @@ import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets'; export const ParamPositivePrompt = memo(() => { const dispatch = useAppDispatch(); - const prompt = useAppSelector((s) => s.controlLayers.present.positivePrompt); + const prompt = useAppSelector((s) => s.canvasV2.positivePrompt); const baseModel = useAppSelector((s) => s.generation.model)?.base; const viewMode = useAppSelector((s) => s.stylePreset.viewMode); const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId); diff --git a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx b/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx index 1dc0533ad8a..9c2d8ebebe6 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx @@ -15,7 +15,7 @@ const selectPromptsCount = createSelector( selectCanvasV2Slice, selectDynamicPromptsSlice, (controlLayers, dynamicPrompts) => - getShouldProcessPrompt(controlLayers.present.positivePrompt) ? dynamicPrompts.prompts.length : 1 + getShouldProcessPrompt(canvasV2.positivePrompt) ? dynamicPrompts.prompts.length : 1 ); type Props = { diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt.tsx index 3167cbb6242..1f22c1b8459 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt.tsx @@ -12,7 +12,7 @@ import { useTranslation } from 'react-i18next'; export const ParamSDXLNegativeStylePrompt = memo(() => { const dispatch = useAppDispatch(); - const prompt = useAppSelector((s) => s.controlLayers.present.negativePrompt2); + const prompt = useAppSelector((s) => s.canvasV2.negativePrompt2); const textareaRef = useRef(null); const { t } = useTranslation(); const handleChange = useCallback( diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt.tsx index 272323797a1..0b86f3014d4 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt.tsx @@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next'; export const ParamSDXLPositiveStylePrompt = memo(() => { const dispatch = useAppDispatch(); - const prompt = useAppSelector((s) => s.controlLayers.present.positivePrompt2); + const prompt = useAppSelector((s) => s.canvasV2.positivePrompt2); const textareaRef = useRef(null); const { t } = useTranslation(); const handleChange = useCallback( diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/SDXLConcatButton.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/SDXLConcatButton.tsx index 0af3dfcee43..dc3b24402b8 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/SDXLConcatButton.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/SDXLConcatButton.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'; import { PiLinkSimpleBold, PiLinkSimpleBreakBold } from 'react-icons/pi'; export const SDXLConcatButton = memo(() => { - const shouldConcatPrompts = useAppSelector((s) => s.controlLayers.present.shouldConcatPrompts); + const shouldConcatPrompts = useAppSelector((s) => s.canvasV2.shouldConcatPrompts); const dispatch = useAppDispatch(); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx index fbb6ca74dec..61453be0bad 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx @@ -42,7 +42,7 @@ const selector = createMemoizedSelector( badges.push('locked'); } } else { - const { aspectRatio, width, height } = controlLayers.present.size; + const { aspectRatio, width, height } = canvasV2.size; badges.push(`${width}×${height}`); badges.push(aspectRatio.id); if (aspectRatio.isLocked) { diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx index 3c8f274ecbc..658f47196a9 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx @@ -9,9 +9,9 @@ import { memo, useCallback } from 'react'; export const ImageSizeLinear = memo(() => { const dispatch = useAppDispatch(); - const width = useAppSelector((s) => s.controlLayers.present.size.width); - const height = useAppSelector((s) => s.controlLayers.present.size.height); - const aspectRatioState = useAppSelector((s) => s.controlLayers.present.size.aspectRatio); + const width = useAppSelector((s) => s.canvasV2.size.width); + const height = useAppSelector((s) => s.canvasV2.size.height); + const aspectRatioState = useAppSelector((s) => s.canvasV2.size.aspectRatio); const onChangeWidth = useCallback( (width: number) => { diff --git a/invokeai/frontend/web/src/features/stylePresets/hooks/usePresetModifiedPrompts.ts b/invokeai/frontend/web/src/features/stylePresets/hooks/usePresetModifiedPrompts.ts index 121840db676..4a136d1d714 100644 --- a/invokeai/frontend/web/src/features/stylePresets/hooks/usePresetModifiedPrompts.ts +++ b/invokeai/frontend/web/src/features/stylePresets/hooks/usePresetModifiedPrompts.ts @@ -10,7 +10,7 @@ export const buildPresetModifiedPrompt = (presetPrompt: string, currentPrompt: s }; export const usePresetModifiedPrompts = () => { - const { positivePrompt, negativePrompt } = useAppSelector((s) => s.controlLayers.present); + const { positivePrompt, negativePrompt } = useAppSelector((s) => s.canvasV2.prompts); const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId); diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx index 166005663fa..6a98798f90b 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx @@ -44,7 +44,7 @@ const ParametersPanelTextToImage = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const activeTabName = useAppSelector(activeTabNameSelector); - const controlLayersCount = useAppSelector((s) => s.controlLayers.present.layers.length); + const controlLayersCount = useAppSelector((s) => s.canvasV2.layers.length); const controlLayersTitle = useMemo(() => { if (controlLayersCount === 0) { return t('controlLayers.controlLayers'); From d6f3b1b85fde30ce2118608c36d47acf9444584d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 14 Jun 2024 21:14:37 +1000 Subject: [PATCH 035/678] refactor(ui): canvas v2 (wip) --- .../src/app/store/createMemoizedSelector.ts | 5 +- invokeai/frontend/web/src/app/store/store.ts | 2 - .../frontend/web/src/app/types/invokeai.ts | 4 +- .../src/common/hooks/useFullscreenDropzone.ts | 7 +- .../web/src/common/hooks/useGlobalHotkeys.ts | 12 +- .../src/common/hooks/useIsReadyToEnqueue.ts | 291 +++++----- .../src/common/util/colorCodeTransformers.ts | 12 +- .../components/AddLayerButton.tsx | 11 +- .../components/AddPromptButtons.tsx | 38 +- .../{BrushSize.tsx => BrushWidth.tsx} | 25 +- .../components/CALayer/CALayer.tsx | 48 -- .../CALayer/CALayerControlAdapterWrapper.tsx | 135 ----- .../BeginEndStepPct.tsx} | 4 +- .../Weight.tsx} | 4 +- .../CAControlModeSelect.tsx} | 8 +- .../components/ControlAdapter/CAEntity.tsx | 35 ++ .../ControlAdapter/CAHeaderItems.tsx | 109 ++++ .../CAImagePreview.tsx} | 43 +- .../CAModelCombobox.tsx} | 4 +- .../CAOpacityAndFilter.tsx} | 20 +- .../CAProcessorConfig.tsx} | 29 +- .../CAProcessorTypeSelect.tsx} | 8 +- .../components/ControlAdapter/CASettings.tsx | 157 +++++ .../processors/CannyProcessor.tsx | 0 .../processors/ColorMapProcessor.tsx | 0 .../processors/ContentShuffleProcessor.tsx | 0 .../processors/DWOpenposeProcessor.tsx | 0 .../processors/DepthAnythingProcessor.tsx | 0 .../processors/HedProcessor.tsx | 0 .../processors/LineartProcessor.tsx | 0 .../processors/MediapipeFaceProcessor.tsx | 0 .../processors/MidasDepthProcessor.tsx | 0 .../processors/MlsdImageProcessor.tsx | 0 .../processors/PidiProcessor.tsx | 0 .../processors/ProcessorWrapper.tsx | 0 .../processors/types.ts | 0 .../ControlAndIPAdapter/ControlAdapter.tsx | 123 ---- .../ControlAndIPAdapter/IPAdapter.tsx | 4 +- .../components/ControlLayersPanelContent.tsx | 6 +- .../components/ControlLayersToolbar.tsx | 4 +- .../controlLayers/components/EraserWidth.tsx | 56 ++ ...ushColorPicker.tsx => FillColorPicker.tsx} | 16 +- .../components/IILayer/IILayer.tsx | 12 +- .../components/IPALayer/IPALayer.tsx | 30 +- .../IPALayer/IPALayerIPAdapterWrapper.tsx | 4 +- ...eleteButton.tsx => EntityDeleteButton.tsx} | 16 +- ...lityToggle.tsx => EntityEnabledToggle.tsx} | 19 +- .../{LayerMenu.tsx => EntityMenu.tsx} | 4 +- .../LayerCommon/EntityMenuButton.tsx | 18 + .../components/LayerCommon/EntityTitle.tsx | 16 + .../LayerCommon/LayerMenuRGActions.tsx | 4 +- .../components/LayerCommon/LayerTitle.tsx | 33 -- .../components/RGLayer/RGLayer.tsx | 14 +- .../RGLayer/RGLayerIPAdapterWrapper.tsx | 8 +- .../components/RasterLayer/RasterLayer.tsx | 16 +- .../controlLayers/hooks/addLayerHooks.ts | 35 +- .../controlLayers/konva/renderers/caLayer.ts | 2 +- .../store/controlAdaptersSlice.ts | 51 +- .../controlLayers/store/controlLayersSlice.ts | 6 +- .../store/regionalGuidanceSlice.ts | 4 +- .../types.test.ts} | 2 +- .../src/features/controlLayers/store/types.ts | 504 +++++++++++++++- .../controlLayers/util/controlAdapters.ts | 546 ------------------ .../web/src/features/dnd/types/index.ts | 39 +- .../ControlSettingsAccordion.stories.tsx | 16 - .../ControlSettingsAccordion.tsx | 125 ---- .../ImageSettingsAccordion.tsx | 33 +- .../ImageSizeCanvas.tsx | 59 -- .../SettingsModal/useClearIntermediates.ts | 6 +- .../src/features/ui/components/InvokeTabs.tsx | 11 +- .../ParametersPanelCanvas.tsx | 59 -- .../ParametersPanelTextToImage.tsx | 6 +- .../ui/components/tabs/UnifiedCanvasTab.tsx | 51 -- .../web/src/features/ui/store/tabMap.tsx | 2 +- .../frontend/web/src/services/api/types.ts | 37 +- 75 files changed, 1296 insertions(+), 1712 deletions(-) rename invokeai/frontend/web/src/features/controlLayers/components/{BrushSize.tsx => BrushWidth.tsx} (60%) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter/ControlAdapterBeginEndStepPct.tsx => Common/BeginEndStepPct.tsx} (87%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter/ControlAdapterWeight.tsx => Common/Weight.tsx} (93%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter/ControlAdapterControlModeSelect.tsx => ControlAdapter/CAControlModeSelect.tsx} (82%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntity.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAHeaderItems.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter/ControlAdapterImagePreview.tsx => ControlAdapter/CAImagePreview.tsx} (83%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter/ControlAdapterModelCombobox.tsx => ControlAdapter/CAModelCombobox.tsx} (92%) rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer/CALayerOpacity.tsx => ControlAdapter/CAOpacityAndFilter.tsx} (83%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter/ControlAdapterProcessorConfig.tsx => ControlAdapter/CAProcessorConfig.tsx} (55%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx => ControlAdapter/CAProcessorTypeSelect.tsx} (88%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter => ControlAdapter}/processors/CannyProcessor.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter => ControlAdapter}/processors/ColorMapProcessor.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter => ControlAdapter}/processors/ContentShuffleProcessor.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter => ControlAdapter}/processors/DWOpenposeProcessor.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter => ControlAdapter}/processors/DepthAnythingProcessor.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter => ControlAdapter}/processors/HedProcessor.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter => ControlAdapter}/processors/LineartProcessor.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter => ControlAdapter}/processors/MediapipeFaceProcessor.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter => ControlAdapter}/processors/MidasDepthProcessor.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter => ControlAdapter}/processors/MlsdImageProcessor.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter => ControlAdapter}/processors/PidiProcessor.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter => ControlAdapter}/processors/ProcessorWrapper.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter => ControlAdapter}/processors/types.ts (100%) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/EraserWidth.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{BrushColorPicker.tsx => FillColorPicker.tsx} (67%) rename invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/{LayerDeleteButton.tsx => EntityDeleteButton.tsx} (50%) rename invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/{LayerVisibilityToggle.tsx => EntityEnabledToggle.tsx} (50%) rename invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/{LayerMenu.tsx => EntityMenu.tsx} (96%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityMenuButton.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityTitle.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx rename invokeai/frontend/web/src/features/controlLayers/{util/controlAdapters.test.ts => store/types.test.ts} (99%) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts delete mode 100644 invokeai/frontend/web/src/features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion.stories.tsx delete mode 100644 invokeai/frontend/web/src/features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion.tsx delete mode 100644 invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeCanvas.tsx delete mode 100644 invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx delete mode 100644 invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvasTab.tsx diff --git a/invokeai/frontend/web/src/app/store/createMemoizedSelector.ts b/invokeai/frontend/web/src/app/store/createMemoizedSelector.ts index 8e2559927ad..eb096418458 100644 --- a/invokeai/frontend/web/src/app/store/createMemoizedSelector.ts +++ b/invokeai/frontend/web/src/app/store/createMemoizedSelector.ts @@ -1,5 +1,6 @@ -import { createDraftSafeSelectorCreator, createSelectorCreator, lruMemoize } from '@reduxjs/toolkit'; +import { createDraftSafeSelectorCreator, createSelector, createSelectorCreator, lruMemoize } from '@reduxjs/toolkit'; import type { GetSelectorsOptions } from '@reduxjs/toolkit/dist/entities/state_selectors'; +import type { RootState } from 'app/store/store'; import { isEqual } from 'lodash-es'; /** @@ -19,3 +20,5 @@ export const getSelectorsOptions: GetSelectorsOptions = { argsMemoize: lruMemoize, }), }; + +export const createAppSelector = createSelector.withTypes(); diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 45c0d020e89..17a4205725d 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -4,7 +4,6 @@ import { logger } from 'app/logging/logger'; import { idbKeyValDriver } from 'app/store/enhancers/reduxRemember/driver'; import { errorHandler } from 'app/store/enhancers/reduxRemember/errors'; import type { JSONObject } from 'common/types'; -import { canvasPersistConfig } from 'features/canvas/store/canvasSlice'; import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice'; import { controlAdaptersV2PersistConfig, @@ -104,7 +103,6 @@ export type PersistConfig = { }; const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = { - [canvasPersistConfig.name]: canvasPersistConfig, [galleryPersistConfig.name]: galleryPersistConfig, [generationPersistConfig.name]: generationPersistConfig, [nodesPersistConfig.name]: nodesPersistConfig, diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index ffc4e1960bc..4d54485f616 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -1,4 +1,4 @@ -import type { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants'; +import type { ProcessorTypeV2 } from 'features/controlLayers/store/types'; import type { ParameterPrecision, ParameterScheduler } from 'features/parameters/types/parameterSchemas'; import type { InvokeTabName } from 'features/ui/store/tabMap'; import type { O } from 'ts-toolbelt'; @@ -83,7 +83,7 @@ export type AppConfig = { sd: { defaultModel?: string; disabledControlNetModels: string[]; - disabledControlNetProcessors: (keyof typeof CONTROLNET_PROCESSORS)[]; + disabledControlNetProcessors: ProcessorTypeV2; // Core parameters iterations: NumericalParameterConfig; width: NumericalParameterConfig; // initial value comes from model diff --git a/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts b/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts index d8e7d70a8c5..93c9d73932a 100644 --- a/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts +++ b/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts @@ -17,10 +17,6 @@ const accept: Accept = { const selectPostUploadAction = createMemoizedSelector(activeTabNameSelector, (activeTabName) => { let postUploadAction: PostUploadAction = { type: 'TOAST' }; - if (activeTabName === 'canvas') { - postUploadAction = { type: 'SET_CANVAS_INITIAL_IMAGE' }; - } - if (activeTabName === 'upscaling') { postUploadAction = { type: 'SET_UPSCALE_INITIAL_IMAGE' }; } @@ -30,10 +26,9 @@ const selectPostUploadAction = createMemoizedSelector(activeTabNameSelector, (ac export const useFullscreenDropzone = () => { const { t } = useTranslation(); - const postUploadAction = useAppSelector(selectPostUploadAction); const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId); const [isHandlingUpload, setIsHandlingUpload] = useState(false); - + const postUploadAction = useAppSelector(selectPostUploadAction); const [uploadImage] = useUploadImageMutation(); const fileRejectionCallback = useCallback( diff --git a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts index 9ba044199f9..487622f9b79 100644 --- a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts +++ b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts @@ -74,14 +74,6 @@ export const useGlobalHotkeys = () => { useHotkeys( '2', - () => { - dispatch(setActiveTab('canvas')); - }, - [dispatch] - ); - - useHotkeys( - '3', () => { dispatch(setActiveTab('workflows')); }, @@ -89,7 +81,7 @@ export const useGlobalHotkeys = () => { ); useHotkeys( - '4', + '3', () => { if (isModelManagerEnabled) { dispatch(setActiveTab('models')); @@ -99,7 +91,7 @@ export const useGlobalHotkeys = () => { ); useHotkeys( - isModelManagerEnabled ? '5' : '4', + isModelManagerEnabled ? '4' : '3', () => { dispatch(setActiveTab('queue')); }, diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index 97042124b12..c47a285c54a 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -1,13 +1,12 @@ import { useStore } from '@nanostores/react'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterAll, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; +import { selectControlAdaptersV2Slice } from 'features/controlLayers/store/controlAdaptersSlice'; import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; -import type { LayerData } from 'features/controlLayers/store/types'; +import { selectIPAdaptersSlice } from 'features/controlLayers/store/ipAdaptersSlice'; +import { selectLayersSlice } from 'features/controlLayers/store/layersSlice'; +import { selectRegionalGuidanceSlice } from 'features/controlLayers/store/regionalGuidanceSlice'; +import type { CanvasEntity } from 'features/controlLayers/store/types'; import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; @@ -24,43 +23,49 @@ import { forEach, upperFirst } from 'lodash-es'; import { useMemo } from 'react'; import { getConnectedEdges } from 'reactflow'; -const LAYER_TYPE_TO_TKEY: Record = { - initial_image_layer: 'controlLayers.globalInitialImage', - control_adapter_layer: 'controlLayers.globalControlAdapter', - ip_adapter_layer: 'controlLayers.globalIPAdapter', - regional_guidance_layer: 'controlLayers.regionalGuidance', - raster_layer: 'controlLayers.raster', +const LAYER_TYPE_TO_TKEY: Record = { + control_adapter: 'controlLayers.globalControlAdapter', + ip_adapter: 'controlLayers.globalIPAdapter', + regional_guidance: 'controlLayers.regionalGuidance', + layer: 'controlLayers.raster', + inpaint_mask: 'controlLayers.inpaintMask', }; const createSelector = (templates: Templates) => createMemoizedSelector( [ - selectControlAdaptersSlice, selectGenerationSlice, selectSystemSlice, selectNodesSlice, selectWorkflowSettingsSlice, selectDynamicPromptsSlice, selectCanvasV2Slice, + selectLayersSlice, + selectControlAdaptersV2Slice, + selectRegionalGuidanceSlice, + selectIPAdaptersSlice, activeTabNameSelector, selectUpscalelice, selectConfigSlice, ], ( - controlAdapters, generation, system, nodes, workflowSettings, dynamicPrompts, - controlLayers, + canvasV2, + layersState, + controlAdaptersState, + regionalGuidanceState, + ipAdaptersState, activeTabName, upscale, config ) => { const { model } = generation; const { size } = canvasV2; - const { positivePrompt } = canvasV2; + const { positivePrompt } = canvasV2.prompts; const { isConnected } = system; @@ -115,6 +120,26 @@ const createSelector = (templates: Templates) => }); }); } + } else if (activeTabName === 'upscaling') { + if (!upscale.upscaleInitialImage) { + reasons.push({ content: i18n.t('upscaling.missingUpscaleInitialImage') }); + } else if (config.maxUpscaleDimension) { + const { width, height } = upscale.upscaleInitialImage; + const { scale } = upscale; + + const maxPixels = config.maxUpscaleDimension ** 2; + const upscaledPixels = width * scale * height * scale; + + if (upscaledPixels > maxPixels) { + reasons.push({ content: i18n.t('upscaling.exceedsMaxSize') }); + } + } + if (!upscale.upscaleModel) { + reasons.push({ content: i18n.t('upscaling.missingUpscaleModel') }); + } + if (!upscale.tileControlnetModel) { + reasons.push({ content: i18n.t('upscaling.missingTileControlNetModel') }); + } } else { if (dynamicPrompts.prompts.length === 0 && getShouldProcessPrompt(positivePrompt)) { reasons.push({ content: i18n.t('parameters.invoke.noPrompts') }); @@ -124,140 +149,128 @@ const createSelector = (templates: Templates) => reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') }); } - if (activeTabName === 'generation') { - // Handling for generation tab - canvasV2.layers - .filter((l) => l.isEnabled) - .forEach((l, i) => { - const layerLiteral = i18n.t('controlLayers.layers_one'); - const layerNumber = i + 1; - const layerType = i18n.t(LAYER_TYPE_TO_TKEY[l.type]); - const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; - const problems: string[] = []; - if (l.type === 'control_adapter_layer') { - // Must have model - if (!l.controlAdapter.model) { - problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoModelSelected')); - } - // Model base must match - if (l.controlAdapter.model?.base !== model?.base) { - problems.push(i18n.t('parameters.invoke.layer.controlAdapterIncompatibleBaseModel')); - } - // Must have a control image OR, if it has a processor, it must have a processed image - if (!l.controlAdapter.image) { - problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoImageSelected')); - } else if (l.controlAdapter.processorConfig && !l.controlAdapter.processedImage) { - problems.push(i18n.t('parameters.invoke.layer.controlAdapterImageNotProcessed')); - } - // T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL) - if (l.controlAdapter.type === 't2i_adapter') { - const multiple = model?.base === 'sdxl' ? 32 : 64; - if (size.width % multiple !== 0 || size.height % multiple !== 0) { - problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions', { multiple })); - } - } - } - - if (l.type === 'ip_adapter_layer') { - // Must have model - if (!l.ipAdapter.model) { - problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected')); - } - // Model base must match - if (l.ipAdapter.model?.base !== model?.base) { - problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel')); - } - // Must have an image - if (!l.ipAdapter.image) { - problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected')); - } + controlAdaptersState.controlAdapters + .filter((ca) => ca.isEnabled) + .forEach((ca, i) => { + const layerLiteral = i18n.t('controlLayers.layers_one'); + const layerNumber = i + 1; + const layerType = i18n.t(LAYER_TYPE_TO_TKEY[ca.type]); + const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; + const problems: string[] = []; + // Must have model + if (!ca.model) { + problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoModelSelected')); + } + // Model base must match + if (ca.model?.base !== model?.base) { + problems.push(i18n.t('parameters.invoke.layer.controlAdapterIncompatibleBaseModel')); + } + // Must have a control image OR, if it has a processor, it must have a processed image + if (!ca.image) { + problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoImageSelected')); + } else if (ca.processorConfig && !ca.processedImage) { + problems.push(i18n.t('parameters.invoke.layer.controlAdapterImageNotProcessed')); + } + // T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL) + if (!ca.controlMode) { + const multiple = model?.base === 'sdxl' ? 32 : 64; + if (size.width % multiple !== 0 || size.height % multiple !== 0) { + problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions', { multiple })); } + } - if (l.type === 'initial_image_layer') { - // Must have an image - if (!l.image) { - problems.push(i18n.t('parameters.invoke.layer.initialImageNoImageSelected')); - } - } + if (problems.length) { + const content = upperFirst(problems.join(', ')); + reasons.push({ prefix, content }); + } + }); - if (l.type === 'regional_guidance_layer') { - // Must have a region - if (l.objects.length === 0) { - problems.push(i18n.t('parameters.invoke.layer.rgNoRegion')); - } - // Must have at least 1 prompt or IP Adapter - if (l.positivePrompt === null && l.negativePrompt === null && l.ipAdapters.length === 0) { - problems.push(i18n.t('parameters.invoke.layer.rgNoPromptsOrIPAdapters')); - } - l.ipAdapters.forEach((ipAdapter) => { - // Must have model - if (!ipAdapter.model) { - problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected')); - } - // Model base must match - if (ipAdapter.model?.base !== model?.base) { - problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel')); - } - // Must have an image - if (!ipAdapter.image) { - problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected')); - } - }); - } + ipAdaptersState.ipAdapters + .filter((ipa) => ipa.isEnabled) + .forEach((ipa, i) => { + const layerLiteral = i18n.t('controlLayers.layers_one'); + const layerNumber = i + 1; + const layerType = i18n.t(LAYER_TYPE_TO_TKEY[ipa.type]); + const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; + const problems: string[] = []; - if (problems.length) { - const content = upperFirst(problems.join(', ')); - reasons.push({ prefix, content }); - } - }); - } else if (activeTabName === 'upscaling') { - if (!upscale.upscaleInitialImage) { - reasons.push({ content: i18n.t('upscaling.missingUpscaleInitialImage') }); - } else if (config.maxUpscaleDimension) { - const { width, height } = upscale.upscaleInitialImage; - const { scale } = upscale; + // Must have model + if (!ipa.model) { + problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected')); + } + // Model base must match + if (ipa.model?.base !== model?.base) { + problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel')); + } + // Must have an image + if (!ipa.image) { + problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected')); + } - const maxPixels = config.maxUpscaleDimension ** 2; - const upscaledPixels = width * scale * height * scale; + if (problems.length) { + const content = upperFirst(problems.join(', ')); + reasons.push({ prefix, content }); + } + }); - if (upscaledPixels > maxPixels) { - reasons.push({ content: i18n.t('upscaling.exceedsMaxSize') }); + regionalGuidanceState.regions + .filter((rg) => rg.isEnabled) + .forEach((rg, i) => { + const layerLiteral = i18n.t('controlLayers.layers_one'); + const layerNumber = i + 1; + const layerType = i18n.t(LAYER_TYPE_TO_TKEY[rg.type]); + const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; + const problems: string[] = []; + // Must have a region + if (rg.objects.length === 0) { + problems.push(i18n.t('parameters.invoke.layer.rgNoRegion')); } - } - if (!upscale.upscaleModel) { - reasons.push({ content: i18n.t('upscaling.missingUpscaleModel') }); - } - if (!upscale.tileControlnetModel) { - reasons.push({ content: i18n.t('upscaling.missingTileControlNetModel') }); - } - } else { - // Handling for all other tabs - selectControlAdapterAll(controlAdapters) - .filter((ca) => ca.isEnabled) - .forEach((ca, i) => { - if (!ca.isEnabled) { - return; + // Must have at least 1 prompt or IP Adapter + if (rg.positivePrompt === null && rg.negativePrompt === null && rg.ipAdapters.length === 0) { + problems.push(i18n.t('parameters.invoke.layer.rgNoPromptsOrIPAdapters')); + } + rg.ipAdapters.forEach((ipAdapter) => { + // Must have model + if (!ipAdapter.model) { + problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected')); } - - if (!ca.model) { - reasons.push({ content: i18n.t('parameters.invoke.noModelForControlAdapter', { number: i + 1 }) }); - } else if (ca.model.base !== model?.base) { - // This should never happen, just a sanity check - reasons.push({ - content: i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', { number: i + 1 }), - }); + // Model base must match + if (ipAdapter.model?.base !== model?.base) { + problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel')); } - - if ( - !ca.controlImage || - (isControlNetOrT2IAdapter(ca) && !ca.processedControlImage && ca.processorType !== 'none') - ) { - reasons.push({ - content: i18n.t('parameters.invoke.noControlImageForControlAdapter', { number: i + 1 }), - }); + // Must have an image + if (!ipAdapter.image) { + problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected')); } }); - } + + if (problems.length) { + const content = upperFirst(problems.join(', ')); + reasons.push({ prefix, content }); + } + }); + + layersState.layers + .filter((l) => l.isEnabled) + .forEach((l, i) => { + const layerLiteral = i18n.t('controlLayers.layers_one'); + const layerNumber = i + 1; + const layerType = i18n.t(LAYER_TYPE_TO_TKEY[l.type]); + const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; + const problems: string[] = []; + + // if (l.type === 'initial_image_layer') { + // // Must have an image + // if (!l.image) { + // problems.push(i18n.t('parameters.invoke.layer.initialImageNoImageSelected')); + // } + // } + + if (problems.length) { + const content = upperFirst(problems.join(', ')); + reasons.push({ prefix, content }); + } + }); } return { isReady: !reasons.length, reasons }; diff --git a/invokeai/frontend/web/src/common/util/colorCodeTransformers.ts b/invokeai/frontend/web/src/common/util/colorCodeTransformers.ts index 835b2a3e35b..85635f93b55 100644 --- a/invokeai/frontend/web/src/common/util/colorCodeTransformers.ts +++ b/invokeai/frontend/web/src/common/util/colorCodeTransformers.ts @@ -1,4 +1,4 @@ -import type { RgbaColor } from 'react-colorful'; +import type { RgbaColor, RgbColor } from 'react-colorful'; export function rgbaToHex(color: RgbaColor, alpha: boolean = false): string { const hex = ((1 << 24) + (color.r << 16) + (color.g << 8) + color.b).toString(16).slice(1); @@ -15,3 +15,13 @@ export function hexToRGBA(hex: string, alpha: number) { const b = parseInt(hex.substring(4, 6), 16); return { r, g, b, a: alpha }; } + +export const rgbaColorToString = (color: RgbaColor): string => { + const { r, g, b, a } = color; + return `rgba(${r}, ${g}, ${b}, ${a})`; +}; + +export const rgbColorToString = (color: RgbColor): string => { + const { r, g, b } = color; + return `rgba(${r}, ${g}, ${b})`; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx index 72d18c3d177..591f4a41a18 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx @@ -1,7 +1,8 @@ import { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; -import { useAddCALayer, useAddIILayer, useAddIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; -import { layerAdded, regionalGuidanceAdded } from 'features/controlLayers/store/controlLayersSlice'; +import { useAddCALayer, useAddIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; +import { layerAdded } from 'features/controlLayers/store/layersSlice'; +import { rgAdded } from 'features/controlLayers/store/regionalGuidanceSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPlusBold } from 'react-icons/pi'; @@ -11,9 +12,8 @@ export const AddLayerButton = memo(() => { const dispatch = useAppDispatch(); const [addCALayer, isAddCALayerDisabled] = useAddCALayer(); const [addIPALayer, isAddIPALayerDisabled] = useAddIPALayer(); - const [addIILayer, isAddIILayerDisabled] = useAddIILayer(); const addRGLayer = useCallback(() => { - dispatch(regionalGuidanceAdded()); + dispatch(rgAdded()); }, [dispatch]); const addRasterLayer = useCallback(() => { dispatch(layerAdded()); @@ -42,9 +42,6 @@ export const AddLayerButton = memo(() => { } onClick={addIPALayer} isDisabled={isAddIPALayerDisabled}> {t('controlLayers.globalIPAdapterLayer')} - } onClick={addIILayer} isDisabled={isAddIILayerDisabled}> - {t('controlLayers.globalInitialImageLayer')} - ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx index 71b1ca5f1ec..8f312aba1d9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx @@ -1,44 +1,42 @@ import { Button, Flex } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useAddIPAdapterToIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; +import { useAddIPAdapterToRGLayer } from 'features/controlLayers/hooks/addLayerHooks'; import { - regionalGuidanceNegativePromptChanged, - regionalGuidancePositivePromptChanged, - selectCanvasV2Slice, -} from 'features/controlLayers/store/controlLayersSlice'; -import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; + rgNegativePromptChanged, + rgPositivePromptChanged, + selectRegionalGuidanceSlice, +} from 'features/controlLayers/store/regionalGuidanceSlice'; import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPlusBold } from 'react-icons/pi'; -import { assert } from 'tsafe'; + type AddPromptButtonProps = { - layerId: string; + id: string; }; -export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => { +export const AddPromptButtons = ({ id }: AddPromptButtonProps) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToIPALayer(layerId); + const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToRGLayer(id); const selectValidActions = useMemo( () => - createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = canvasV2.layers.find((l) => l.id === layerId); - assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); + createMemoizedSelector(selectRegionalGuidanceSlice, (regionalGuidanceState) => { + const rg = regionalGuidanceState.regions.find((rg) => rg.id === id); return { - canAddPositivePrompt: layer.positivePrompt === null, - canAddNegativePrompt: layer.negativePrompt === null, + canAddPositivePrompt: rg?.positivePrompt === null, + canAddNegativePrompt: rg?.negativePrompt === null, }; }), - [layerId] + [id] ); const validActions = useAppSelector(selectValidActions); const addPositivePrompt = useCallback(() => { - dispatch(regionalGuidancePositivePromptChanged({ layerId, prompt: '' })); - }, [dispatch, layerId]); + dispatch(rgPositivePromptChanged({ id, prompt: '' })); + }, [dispatch, id]); const addNegativePrompt = useCallback(() => { - dispatch(regionalGuidanceNegativePromptChanged({ layerId, prompt: '' })); - }, [dispatch, layerId]); + dispatch(rgNegativePromptChanged({ id, prompt: '' })); + }, [dispatch, id]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/BrushSize.tsx b/invokeai/frontend/web/src/features/controlLayers/components/BrushWidth.tsx similarity index 60% rename from invokeai/frontend/web/src/features/controlLayers/components/BrushSize.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/BrushWidth.tsx index 4c102ea0ff9..b1b813f6528 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/BrushSize.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/BrushWidth.tsx @@ -10,33 +10,33 @@ import { PopoverTrigger, } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { brushSizeChanged, initialControlLayersState } from 'features/controlLayers/store/controlLayersSlice'; +import { brushWidthChanged } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const marks = [0, 100, 200, 300]; const formatPx = (v: number | string) => `${v} px`; -export const BrushSize = memo(() => { +export const BrushWidth = memo(() => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const brushSize = useAppSelector((s) => s.canvasV2.brushSize); + const width = useAppSelector((s) => s.canvasV2.tool.brush.width); const onChange = useCallback( (v: number) => { - dispatch(brushSizeChanged(Math.round(v))); + dispatch(brushWidthChanged(Math.round(v))); }, [dispatch] ); return ( - {t('controlLayers.brushSize')} + {t('controlLayers.brushWidth')} { - + @@ -60,4 +53,4 @@ export const BrushSize = memo(() => { ); }); -BrushSize.displayName = 'BrushSize'; +BrushWidth.displayName = 'BrushSize'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx deleted file mode 100644 index 9d543446e91..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { CALayerControlAdapterWrapper } from 'features/controlLayers/components/CALayer/CALayerControlAdapterWrapper'; -import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; -import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; -import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; -import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; -import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; -import { layerSelected, selectLayerOrThrow } from 'features/controlLayers/store/controlLayersSlice'; -import { isControlAdapterLayer } from 'features/controlLayers/store/types'; -import { memo, useCallback } from 'react'; - -import CALayerOpacity from './CALayerOpacity'; - -type Props = { - layerId: string; -}; - -export const CALayer = memo(({ layerId }: Props) => { - const dispatch = useAppDispatch(); - const isSelected = useAppSelector( - (s) => selectLayerOrThrow(s.canvasV2, layerId, isControlAdapterLayer).isSelected - ); - const onClick = useCallback(() => { - dispatch(layerSelected(layerId)); - }, [dispatch, layerId]); - const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); - - return ( - - - - - - - - - - {isOpen && ( - - - - )} - - ); -}); - -CALayer.displayName = 'CALayer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx deleted file mode 100644 index f5ccd49cb2b..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { ControlAdapter } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapter'; -import { - caOrIPALayerBeginEndStepPctChanged, - caOrIPALayerWeightChanged, - controlAdapterControlModeChanged, - controlAdapterImageChanged, - controlAdapterModelChanged, - controlAdapterProcessedImageChanged, - controlAdapterProcessorConfigChanged, - selectLayerOrThrow, -} from 'features/controlLayers/store/controlLayersSlice'; -import { isControlAdapterLayer } from 'features/controlLayers/store/types'; -import type { ControlModeV2, ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; -import type { CALayerImageDropData } from 'features/dnd/types'; -import { memo, useCallback, useMemo } from 'react'; -import type { - CALayerImagePostUploadAction, - ControlNetModelConfig, - ImageDTO, - T2IAdapterModelConfig, -} from 'services/api/types'; - -type Props = { - layerId: string; -}; - -export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => { - const dispatch = useAppDispatch(); - const controlAdapter = useAppSelector( - (s) => selectLayerOrThrow(s.canvasV2, layerId, isControlAdapterLayer).controlAdapter - ); - - const onChangeBeginEndStepPct = useCallback( - (beginEndStepPct: [number, number]) => { - dispatch( - caOrIPALayerBeginEndStepPctChanged({ - layerId, - beginEndStepPct, - }) - ); - }, - [dispatch, layerId] - ); - - const onChangeControlMode = useCallback( - (controlMode: ControlModeV2) => { - dispatch( - controlAdapterControlModeChanged({ - layerId, - controlMode, - }) - ); - }, - [dispatch, layerId] - ); - - const onChangeWeight = useCallback( - (weight: number) => { - dispatch(caOrIPALayerWeightChanged({ layerId, weight })); - }, - [dispatch, layerId] - ); - - const onChangeProcessorConfig = useCallback( - (processorConfig: ProcessorConfig | null) => { - dispatch(controlAdapterProcessorConfigChanged({ layerId, processorConfig })); - }, - [dispatch, layerId] - ); - - const onChangeModel = useCallback( - (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => { - dispatch( - controlAdapterModelChanged({ - layerId, - modelConfig, - }) - ); - }, - [dispatch, layerId] - ); - - const onChangeImage = useCallback( - (imageDTO: ImageDTO | null) => { - dispatch(controlAdapterImageChanged({ layerId, imageDTO })); - }, - [dispatch, layerId] - ); - - const onErrorLoadingImage = useCallback(() => { - dispatch(controlAdapterImageChanged({ layerId, imageDTO: null })); - }, [dispatch, layerId]); - - const onErrorLoadingProcessedImage = useCallback(() => { - dispatch(controlAdapterProcessedImageChanged({ layerId, imageDTO: null })); - }, [dispatch, layerId]); - - const droppableData = useMemo( - () => ({ - actionType: 'SET_CA_LAYER_IMAGE', - context: { - layerId, - }, - id: layerId, - }), - [layerId] - ); - - const postUploadAction = useMemo( - () => ({ - layerId, - type: 'SET_CA_LAYER_IMAGE', - }), - [layerId] - ); - - return ( - - ); -}); - -CALayerControlAdapterWrapper.displayName = 'CALayerControlAdapterWrapper'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterBeginEndStepPct.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Common/BeginEndStepPct.tsx similarity index 87% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterBeginEndStepPct.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/Common/BeginEndStepPct.tsx index 9da9ce50a06..4f75c0bb977 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterBeginEndStepPct.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Common/BeginEndStepPct.tsx @@ -11,7 +11,7 @@ type Props = { const formatPct = (v: number) => `${Math.round(v * 100)}%`; const ariaLabel = ['Begin Step %', 'End Step %']; -export const ControlAdapterBeginEndStepPct = memo(({ beginEndStepPct, onChange }: Props) => { +export const BeginEndStepPct = memo(({ beginEndStepPct, onChange }: Props) => { const { t } = useTranslation(); const onReset = useCallback(() => { onChange([0, 1]); @@ -40,4 +40,4 @@ export const ControlAdapterBeginEndStepPct = memo(({ beginEndStepPct, onChange } ); }); -ControlAdapterBeginEndStepPct.displayName = 'ControlAdapterBeginEndStepPct'; +BeginEndStepPct.displayName = 'BeginEndStepPct'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterWeight.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Common/Weight.tsx similarity index 93% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterWeight.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/Common/Weight.tsx index 4bb7bb39114..d9f841fa26a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterWeight.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Common/Weight.tsx @@ -12,7 +12,7 @@ type Props = { const formatValue = (v: number) => v.toFixed(2); const marks = [0, 1, 2]; -export const ControlAdapterWeight = memo(({ weight, onChange }: Props) => { +export const Weight = memo(({ weight, onChange }: Props) => { const { t } = useTranslation(); const initial = useAppSelector((s) => s.config.sd.ca.weight.initial); const sliderMin = useAppSelector((s) => s.config.sd.ca.weight.sliderMin); @@ -52,4 +52,4 @@ export const ControlAdapterWeight = memo(({ weight, onChange }: Props) => { ); }); -ControlAdapterWeight.displayName = 'ControlAdapterWeight'; +Weight.displayName = 'Weight'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterControlModeSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAControlModeSelect.tsx similarity index 82% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterControlModeSelect.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAControlModeSelect.tsx index 2c35ce51b60..9c8ecb896f1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterControlModeSelect.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAControlModeSelect.tsx @@ -1,8 +1,8 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import type { ControlModeV2 } from 'features/controlLayers/util/controlAdapters'; -import { isControlModeV2 } from 'features/controlLayers/util/controlAdapters'; +import type { ControlModeV2} from 'features/controlLayers/store/types'; +import { isControlModeV2 } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { assert } from 'tsafe'; @@ -12,7 +12,7 @@ type Props = { onChange: (controlMode: ControlModeV2) => void; }; -export const ControlAdapterControlModeSelect = memo(({ controlMode, onChange }: Props) => { +export const CAControlModeSelect = memo(({ controlMode, onChange }: Props) => { const { t } = useTranslation(); const CONTROL_MODE_DATA = useMemo( () => [ @@ -57,4 +57,4 @@ export const ControlAdapterControlModeSelect = memo(({ controlMode, onChange }: ); }); -ControlAdapterControlModeSelect.displayName = 'ControlAdapterControlModeSelect'; +CAControlModeSelect.displayName = 'CAControlModeSelect'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntity.tsx new file mode 100644 index 00000000000..2840bd39625 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntity.tsx @@ -0,0 +1,35 @@ +import { Flex, useDisclosure } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { CAHeaderItems } from 'features/controlLayers/components/ControlAdapter/CAHeaderItems'; +import { CASettings } from 'features/controlLayers/components/ControlAdapter/CASettings'; +import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; +import { entitySelected } from 'features/controlLayers/store/controlLayersSlice'; +import { memo, useCallback } from 'react'; + +type Props = { + id: string; +}; + +export const CAEntity = memo(({ id }: Props) => { + const dispatch = useAppDispatch(); + const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id); + const disclosure = useDisclosure({ defaultIsOpen: true }); + const onClick = useCallback(() => { + dispatch(entitySelected({ id, type: 'control_adapter' })); + }, [dispatch, id]); + + return ( + + + + + {disclosure.isOpen && ( + + + + )} + + ); +}); + +CAEntity.displayName = 'CAEntity'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAHeaderItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAHeaderItems.tsx new file mode 100644 index 00000000000..67b2c539d53 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAHeaderItems.tsx @@ -0,0 +1,109 @@ +import { Menu, MenuItem, MenuList, Spacer } from '@invoke-ai/ui-library'; +import { createAppSelector } from 'app/store/createMemoizedSelector'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { CAOpacityAndFilter } from 'features/controlLayers/components/ControlAdapter/CAOpacityAndFilter'; +import { EntityDeleteButton } from 'features/controlLayers/components/LayerCommon/EntityDeleteButton'; +import { EntityEnabledToggle } from 'features/controlLayers/components/LayerCommon/EntityEnabledToggle'; +import { EntityMenuButton } from 'features/controlLayers/components/LayerCommon/EntityMenuButton'; +import { EntityTitle } from 'features/controlLayers/components/LayerCommon/EntityTitle'; +import { + caDeleted, + caIsEnabledToggled, + caMovedBackwardOne, + caMovedForwardOne, + caMovedToBack, + caMovedToFront, + selectCA, + selectControlAdaptersV2Slice, +} from 'features/controlLayers/store/controlAdaptersSlice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + PiArrowDownBold, + PiArrowLineDownBold, + PiArrowLineUpBold, + PiArrowUpBold, + PiTrashSimpleBold, +} from 'react-icons/pi'; +import { assert } from 'tsafe'; + +type Props = { + id: string; +}; + +const selectValidActions = createAppSelector( + [selectControlAdaptersV2Slice, (caState, id: string) => id], + (caState, id) => { + const ca = selectCA(caState, id); + assert(ca, `CA with id ${id} not found`); + const caIndex = caState.controlAdapters.indexOf(ca); + const caCount = caState.controlAdapters.length; + return { + canMoveForward: caIndex < caCount - 1, + canMoveBackward: caIndex > 0, + canMoveToFront: caIndex < caCount - 1, + canMoveToBack: caIndex > 0, + }; + } +); + +export const CAHeaderItems = memo(({ id }: Props) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const validActions = useAppSelector((s) => selectValidActions(s, id)); + const isEnabled = useAppSelector((s) => { + const ca = selectCA(s.controlAdaptersV2, id); + assert(ca, `CA with id ${id} not found`); + return ca.isEnabled; + }); + const onToggle = useCallback(() => { + dispatch(caIsEnabledToggled({ id })); + }, [dispatch, id]); + const onDelete = useCallback(() => { + dispatch(caDeleted({ id })); + }, [dispatch, id]); + const moveForwardOne = useCallback(() => { + dispatch(caMovedForwardOne({ id })); + }, [dispatch, id]); + const moveToFront = useCallback(() => { + dispatch(caMovedToFront({ id })); + }, [dispatch, id]); + const moveBackwardOne = useCallback(() => { + dispatch(caMovedBackwardOne({ id })); + }, [dispatch, id]); + const moveToBack = useCallback(() => { + dispatch(caMovedToBack({ id })); + }, [dispatch, id]); + + return ( + <> + + + + + + + + }> + {t('controlLayers.moveToFront')} + + }> + {t('controlLayers.moveForward')} + + }> + {t('controlLayers.moveBackward')} + + }> + {t('controlLayers.moveToBack')} + + } color="error.300"> + {t('common.delete')} + + + + + + ); +}); + +CAHeaderItems.displayName = 'CAHeaderItems'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx similarity index 83% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx index e840885050f..34baca75fe5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx @@ -3,13 +3,11 @@ import { skipToken } from '@reduxjs/toolkit/query'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; -import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; -import type { ControlNetConfigV2, T2IAdapterConfigV2 } from 'features/controlLayers/util/controlAdapters'; +import type { ControlAdapterData } from 'features/controlLayers/store/types'; import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowCounterClockwiseBold, PiFloppyDiskBold, PiRulerBold } from 'react-icons/pi'; @@ -22,7 +20,7 @@ import { import type { ImageDTO, PostUploadAction } from 'services/api/types'; type Props = { - controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2; + controlAdapter: ControlAdapterData; onChangeImage: (imageDTO: ImageDTO | null) => void; droppableData: TypesafeDroppableData; postUploadAction: PostUploadAction; @@ -30,7 +28,7 @@ type Props = { onErrorLoadingProcessedImage: () => void; }; -export const ControlAdapterImagePreview = memo( +export const CAImagePreview = memo( ({ controlAdapter, onChangeImage, @@ -43,7 +41,6 @@ export const ControlAdapterImagePreview = memo( const dispatch = useAppDispatch(); const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId); const isConnected = useAppSelector((s) => s.system.isConnected); - const activeTabName = useAppSelector(activeTabNameSelector); const optimalDimension = useAppSelector(selectOptimalDimension); const shift = useShiftModifier(); @@ -88,27 +85,21 @@ export const ControlAdapterImagePreview = memo( return; } - if (activeTabName === 'canvas') { - dispatch( - setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension) - ); + const options = { updateAspectRatio: true, clamp: true }; + + if (shift) { + const { width, height } = controlImage; + dispatch(widthChanged({ width, ...options })); + dispatch(heightChanged({ height, ...options })); } else { - const options = { updateAspectRatio: true, clamp: true }; - - if (shift) { - const { width, height } = controlImage; - dispatch(widthChanged({ width, ...options })); - dispatch(heightChanged({ height, ...options })); - } else { - const { width, height } = calculateNewSize( - controlImage.width / controlImage.height, - optimalDimension * optimalDimension - ); - dispatch(widthChanged({ width, ...options })); - dispatch(heightChanged({ height, ...options })); - } + const { width, height } = calculateNewSize( + controlImage.width / controlImage.height, + optimalDimension * optimalDimension + ); + dispatch(widthChanged({ width, ...options })); + dispatch(heightChanged({ height, ...options })); } - }, [controlImage, activeTabName, dispatch, optimalDimension, shift]); + }, [controlImage, dispatch, optimalDimension, shift]); const handleMouseEnter = useCallback(() => { setIsMouseOverImage(true); @@ -235,4 +226,4 @@ export const ControlAdapterImagePreview = memo( } ); -ControlAdapterImagePreview.displayName = 'ControlAdapterImagePreview'; +CAImagePreview.displayName = 'CAImagePreview'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAModelCombobox.tsx similarity index 92% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAModelCombobox.tsx index 535f3067a49..9b6a5ad9dbf 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAModelCombobox.tsx @@ -11,7 +11,7 @@ type Props = { onChange: (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => void; }; -export const ControlAdapterModelCombobox = memo(({ modelKey, onChange: onChangeModel }: Props) => { +export const CAModelCombobox = memo(({ modelKey, onChange: onChangeModel }: Props) => { const { t } = useTranslation(); const currentBaseModel = useAppSelector((s) => s.generation.model?.base); const [modelConfigs, { isLoading }] = useControlNetAndT2IAdapterModels(); @@ -60,4 +60,4 @@ export const ControlAdapterModelCombobox = memo(({ modelKey, onChange: onChangeM ); }); -ControlAdapterModelCombobox.displayName = 'ControlAdapterModelCombobox'; +CAModelCombobox.displayName = 'CAModelCombobox'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAOpacityAndFilter.tsx similarity index 83% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAOpacityAndFilter.tsx index 94f7cdf5fe1..7703754044a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAOpacityAndFilter.tsx @@ -15,34 +15,34 @@ import { import { useAppDispatch } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; import { useCALayerOpacity } from 'features/controlLayers/hooks/layerStateHooks'; -import { caLayerIsFilterEnabledChanged, layerOpacityChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { caFilterChanged, caOpacityChanged } from 'features/controlLayers/store/controlAdaptersSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiDropHalfFill } from 'react-icons/pi'; type Props = { - layerId: string; + id: string; }; const marks = [0, 25, 50, 75, 100]; const formatPct = (v: number | string) => `${v} %`; -const CALayerOpacity = ({ layerId }: Props) => { +export const CAOpacityAndFilter = memo(({ id }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const { opacity, isFilterEnabled } = useCALayerOpacity(layerId); + const { opacity, isFilterEnabled } = useCALayerOpacity(id); const onChangeOpacity = useCallback( (v: number) => { - dispatch(layerOpacityChanged({ layerId, opacity: v / 100 })); + dispatch(caOpacityChanged({ id, opacity: v / 100 })); }, - [dispatch, layerId] + [dispatch, id] ); const onChangeFilter = useCallback( (e: ChangeEvent) => { - dispatch(caLayerIsFilterEnabledChanged({ layerId, isFilterEnabled: e.target.checked })); + dispatch(caFilterChanged({ id, filter: e.target.checked ? 'LightnessToAlphaFilter' : 'none' })); }, - [dispatch, layerId] + [dispatch, id] ); return ( @@ -93,6 +93,6 @@ const CALayerOpacity = ({ layerId }: Props) => { ); -}; +}); -export default memo(CALayerOpacity); +CAOpacityAndFilter.displayName = 'CAOpacityAndFilter'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorConfig.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAProcessorConfig.tsx similarity index 55% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorConfig.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAProcessorConfig.tsx index 034dc5454e7..e2f009e48f6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorConfig.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAProcessorConfig.tsx @@ -1,24 +1,23 @@ -import type { ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import { CannyProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor'; +import { ColorMapProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/ColorMapProcessor'; +import { ContentShuffleProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/ContentShuffleProcessor'; +import { DepthAnythingProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/DepthAnythingProcessor'; +import { DWOpenposeProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/DWOpenposeProcessor'; +import { HedProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/HedProcessor'; +import { LineartProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/LineartProcessor'; +import { MediapipeFaceProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/MediapipeFaceProcessor'; +import { MidasDepthProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/MidasDepthProcessor'; +import { MlsdImageProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/MlsdImageProcessor'; +import { PidiProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/PidiProcessor'; +import type { ProcessorConfig } from 'features/controlLayers/store/types'; import { memo } from 'react'; -import { CannyProcessor } from './processors/CannyProcessor'; -import { ColorMapProcessor } from './processors/ColorMapProcessor'; -import { ContentShuffleProcessor } from './processors/ContentShuffleProcessor'; -import { DepthAnythingProcessor } from './processors/DepthAnythingProcessor'; -import { DWOpenposeProcessor } from './processors/DWOpenposeProcessor'; -import { HedProcessor } from './processors/HedProcessor'; -import { LineartProcessor } from './processors/LineartProcessor'; -import { MediapipeFaceProcessor } from './processors/MediapipeFaceProcessor'; -import { MidasDepthProcessor } from './processors/MidasDepthProcessor'; -import { MlsdImageProcessor } from './processors/MlsdImageProcessor'; -import { PidiProcessor } from './processors/PidiProcessor'; - type Props = { config: ProcessorConfig | null; onChange: (config: ProcessorConfig | null) => void; }; -export const ControlAdapterProcessorConfig = memo(({ config, onChange }: Props) => { +export const CAProcessorConfig = memo(({ config, onChange }: Props) => { if (!config) { return null; } @@ -82,4 +81,4 @@ export const ControlAdapterProcessorConfig = memo(({ config, onChange }: Props) } }); -ControlAdapterProcessorConfig.displayName = 'ControlAdapterProcessorConfig'; +CAProcessorConfig.displayName = 'CAProcessorConfig'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAProcessorTypeSelect.tsx similarity index 88% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAProcessorTypeSelect.tsx index 5598b81787a..70e9113c552 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAProcessorTypeSelect.tsx @@ -3,8 +3,8 @@ import { Combobox, Flex, FormControl, FormLabel, IconButton } from '@invoke-ai/u import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import type { ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; -import { CA_PROCESSOR_DATA, isProcessorTypeV2 } from 'features/controlLayers/util/controlAdapters'; +import type {ProcessorConfig } from 'features/controlLayers/store/types'; +import { CA_PROCESSOR_DATA, isProcessorTypeV2 } from 'features/controlLayers/store/types'; import { configSelector } from 'features/system/store/configSelectors'; import { includes, map } from 'lodash-es'; import { memo, useCallback, useMemo } from 'react'; @@ -22,7 +22,7 @@ const selectDisabledProcessors = createMemoizedSelector( (config) => config.sd.disabledControlNetProcessors ); -export const ControlAdapterProcessorTypeSelect = memo(({ config, onChange }: Props) => { +export const CAProcessorTypeSelect = memo(({ config, onChange }: Props) => { const { t } = useTranslation(); const disabledProcessors = useAppSelector(selectDisabledProcessors); const options = useMemo(() => { @@ -67,4 +67,4 @@ export const ControlAdapterProcessorTypeSelect = memo(({ config, onChange }: Pro ); }); -ControlAdapterProcessorTypeSelect.displayName = 'ControlAdapterProcessorTypeSelect'; +CAProcessorTypeSelect.displayName = 'CAProcessorTypeSelect'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx new file mode 100644 index 00000000000..d8920701581 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx @@ -0,0 +1,157 @@ +import { Box, Divider, Flex, Icon, IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { BeginEndStepPct } from 'features/controlLayers/components/Common/BeginEndStepPct'; +import { Weight } from 'features/controlLayers/components/Common/Weight'; +import { CAControlModeSelect } from 'features/controlLayers/components/ControlAdapter/CAControlModeSelect'; +import { CAImagePreview } from 'features/controlLayers/components/ControlAdapter/CAImagePreview'; +import { CAModelCombobox } from 'features/controlLayers/components/ControlAdapter/CAModelCombobox'; +import { CAProcessorConfig } from 'features/controlLayers/components/ControlAdapter/CAProcessorConfig'; +import { CAProcessorTypeSelect } from 'features/controlLayers/components/ControlAdapter/CAProcessorTypeSelect'; +import { + caBeginEndStepPctChanged, + caControlModeChanged, + caImageChanged, + caModelChanged, + caProcessedImageChanged, + caProcessorConfigChanged, + caWeightChanged, +} from 'features/controlLayers/store/controlAdaptersSlice'; +import type { ControlModeV2, ProcessorConfig } from 'features/controlLayers/store/types'; +import type { CAImageDropData } from 'features/dnd/types'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiCaretUpBold } from 'react-icons/pi'; +import { useToggle } from 'react-use'; +import type { + CAImagePostUploadAction, + ControlNetModelConfig, + ImageDTO, + T2IAdapterModelConfig, +} from 'services/api/types'; +import { assert } from 'tsafe'; + +type Props = { + id: string; +}; + +export const CASettings = memo(({ id }: Props) => { + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + const [isExpanded, toggleIsExpanded] = useToggle(false); + + const controlAdapter = useAppSelector((s) => { + const ca = s.controlAdaptersV2.controlAdapters.find((ca) => ca.id === id); + assert(ca, `ControlAdapter with id ${id} not found`); + return ca; + }); + + const onChangeBeginEndStepPct = useCallback( + (beginEndStepPct: [number, number]) => { + dispatch(caBeginEndStepPctChanged({ id, beginEndStepPct })); + }, + [dispatch, id] + ); + + const onChangeControlMode = useCallback( + (controlMode: ControlModeV2) => { + dispatch(caControlModeChanged({ id, controlMode })); + }, + [dispatch, id] + ); + + const onChangeWeight = useCallback( + (weight: number) => { + dispatch(caWeightChanged({ id, weight })); + }, + [dispatch, id] + ); + + const onChangeProcessorConfig = useCallback( + (processorConfig: ProcessorConfig | null) => { + dispatch(caProcessorConfigChanged({ id, processorConfig })); + }, + [dispatch, id] + ); + + const onChangeModel = useCallback( + (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => { + dispatch(caModelChanged({ id, modelConfig })); + }, + [dispatch, id] + ); + + const onChangeImage = useCallback( + (imageDTO: ImageDTO | null) => { + dispatch(caImageChanged({ id, imageDTO })); + }, + [dispatch, id] + ); + + const onErrorLoadingImage = useCallback(() => { + dispatch(caImageChanged({ id, imageDTO: null })); + }, [dispatch, id]); + + const onErrorLoadingProcessedImage = useCallback(() => { + dispatch(caProcessedImageChanged({ id, imageDTO: null })); + }, [dispatch, id]); + + const droppableData = useMemo(() => ({ actionType: 'SET_CA_IMAGE', context: { id }, id }), [id]); + const postUploadAction = useMemo(() => ({ id, type: 'SET_CA_IMAGE' }), [id]); + + return ( + + + + + + + + } + /> + + + + {controlAdapter.controlMode && ( + + )} + + + + + + + + {isExpanded && ( + <> + + + + + + + )} + + ); +}); + +CASettings.displayName = 'CASettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/CannyProcessor.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/CannyProcessor.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ColorMapProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ColorMapProcessor.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ColorMapProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ColorMapProcessor.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ContentShuffleProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ContentShuffleProcessor.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ContentShuffleProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ContentShuffleProcessor.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DWOpenposeProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DWOpenposeProcessor.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DWOpenposeProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DWOpenposeProcessor.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DepthAnythingProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DepthAnythingProcessor.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DepthAnythingProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DepthAnythingProcessor.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/HedProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/HedProcessor.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/HedProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/HedProcessor.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/LineartProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/LineartProcessor.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/LineartProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/LineartProcessor.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MediapipeFaceProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MediapipeFaceProcessor.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MediapipeFaceProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MediapipeFaceProcessor.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MidasDepthProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MidasDepthProcessor.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MidasDepthProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MidasDepthProcessor.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MlsdImageProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MlsdImageProcessor.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MlsdImageProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MlsdImageProcessor.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/PidiProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/PidiProcessor.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/PidiProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/PidiProcessor.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ProcessorWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ProcessorWrapper.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ProcessorWrapper.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ProcessorWrapper.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/types.ts b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/types.ts similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/types.ts rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/types.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx deleted file mode 100644 index 2a7b21352e5..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { Box, Divider, Flex, Icon, IconButton } from '@invoke-ai/ui-library'; -import { ControlAdapterModelCombobox } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox'; -import type { - ControlModeV2, - ControlNetConfigV2, - ProcessorConfig, - T2IAdapterConfigV2, -} from 'features/controlLayers/util/controlAdapters'; -import type { TypesafeDroppableData } from 'features/dnd/types'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiCaretUpBold } from 'react-icons/pi'; -import { useToggle } from 'react-use'; -import type { ControlNetModelConfig, ImageDTO, PostUploadAction, T2IAdapterModelConfig } from 'services/api/types'; - -import { ControlAdapterBeginEndStepPct } from './ControlAdapterBeginEndStepPct'; -import { ControlAdapterControlModeSelect } from './ControlAdapterControlModeSelect'; -import { ControlAdapterImagePreview } from './ControlAdapterImagePreview'; -import { ControlAdapterProcessorConfig } from './ControlAdapterProcessorConfig'; -import { ControlAdapterProcessorTypeSelect } from './ControlAdapterProcessorTypeSelect'; -import { ControlAdapterWeight } from './ControlAdapterWeight'; - -type Props = { - controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2; - onChangeBeginEndStepPct: (beginEndStepPct: [number, number]) => void; - onChangeControlMode: (controlMode: ControlModeV2) => void; - onChangeWeight: (weight: number) => void; - onChangeProcessorConfig: (processorConfig: ProcessorConfig | null) => void; - onChangeModel: (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => void; - onChangeImage: (imageDTO: ImageDTO | null) => void; - onErrorLoadingImage: () => void; - onErrorLoadingProcessedImage: () => void; - droppableData: TypesafeDroppableData; - postUploadAction: PostUploadAction; -}; - -export const ControlAdapter = memo( - ({ - controlAdapter, - onChangeBeginEndStepPct, - onChangeControlMode, - onChangeWeight, - onChangeProcessorConfig, - onChangeModel, - onChangeImage, - onErrorLoadingImage, - onErrorLoadingProcessedImage, - droppableData, - postUploadAction, - }: Props) => { - const { t } = useTranslation(); - const [isExpanded, toggleIsExpanded] = useToggle(false); - - return ( - - - - - - - - } - /> - - - - {controlAdapter.type === 'controlnet' && ( - - )} - - - - - - - - {isExpanded && ( - <> - - - - - - - )} - - ); - } -); - -ControlAdapter.displayName = 'ControlAdapter'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapter.tsx index 86ed77ce36c..75a1fa0c6b8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapter.tsx @@ -1,5 +1,5 @@ import { Box, Flex } from '@invoke-ai/ui-library'; -import { ControlAdapterBeginEndStepPct } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapterBeginEndStepPct'; +import { BeginEndStepPct } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapterBeginEndStepPct'; import { ControlAdapterWeight } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapterWeight'; import { IPAdapterImagePreview } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview'; import { IPAdapterMethod } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod'; @@ -49,7 +49,7 @@ export const IPAdapter = memo( - diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index 27ef318c1ad..50e39c43e3c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -8,7 +8,7 @@ import { AddLayerButton } from 'features/controlLayers/components/AddLayerButton import { CALayer } from 'features/controlLayers/components/CALayer/CALayer'; import { DeleteAllLayersButton } from 'features/controlLayers/components/DeleteAllLayersButton'; import { IILayer } from 'features/controlLayers/components/IILayer/IILayer'; -import { IPALayer } from 'features/controlLayers/components/IPALayer/IPALayer'; +import { IPAEntity } from 'features/controlLayers/components/IPALayer/IPALayer'; import { RasterLayer } from 'features/controlLayers/components/RasterLayer/RasterLayer'; import { RGLayer } from 'features/controlLayers/components/RGLayer/RGLayer'; import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; @@ -58,10 +58,10 @@ const LayerWrapper = memo(({ id, type }: LayerWrapperProps) => { return ; } if (type === 'control_adapter_layer') { - return ; + return ; } if (type === 'ip_adapter_layer') { - return ; + return ; } if (type === 'initial_image_layer') { return ; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index 7f140e2be63..15da0966804 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -2,7 +2,7 @@ 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 { BrushWidth } 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'; @@ -28,7 +28,7 @@ export const ControlLayersToolbar = memo(() => { - {withBrushSize && } + {withBrushSize && } {withBrushColor && } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/EraserWidth.tsx b/invokeai/frontend/web/src/features/controlLayers/components/EraserWidth.tsx new file mode 100644 index 00000000000..d976fa8470b --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/EraserWidth.tsx @@ -0,0 +1,56 @@ +import { + CompositeNumberInput, + CompositeSlider, + FormControl, + FormLabel, + Popover, + PopoverArrow, + PopoverBody, + PopoverContent, + PopoverTrigger, +} from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { eraserWidthChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +const marks = [0, 100, 200, 300]; +const formatPx = (v: number | string) => `${v} px`; + +export const EraserWidth = memo(() => { + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + const width = useAppSelector((s) => s.canvasV2.tool.eraser.width); + const onChange = useCallback( + (v: number) => { + dispatch(eraserWidthChanged(Math.round(v))); + }, + [dispatch] + ); + return ( + + {t('controlLayers.eraserWidth')} + + + + + + + + + + + + + ); +}); + +EraserWidth.displayName = 'EraserWidth'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/BrushColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/FillColorPicker.tsx similarity index 67% rename from invokeai/frontend/web/src/features/controlLayers/components/BrushColorPicker.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/FillColorPicker.tsx index ec8e535b40c..3550f67f139 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/BrushColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/FillColorPicker.tsx @@ -1,19 +1,19 @@ 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'; -import { brushColorChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { rgbaColorToString } from 'common/util/colorCodeTransformers'; +import { fillChanged } from 'features/controlLayers/store/controlLayersSlice'; import type { RgbaColor } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -export const BrushColorPicker = memo(() => { +export const FillColorPicker = memo(() => { const { t } = useTranslation(); - const brushColor = useAppSelector((s) => s.canvasV2.brushColor); + const fill = useAppSelector((s) => s.canvasV2.tool.fill); const dispatch = useAppDispatch(); const onChange = useCallback( (color: RgbaColor) => { - dispatch(brushColorChanged(color)); + dispatch(fillChanged(color)); }, [dispatch] ); @@ -25,7 +25,7 @@ export const BrushColorPicker = memo(() => { aria-label={t('controlLayers.brushColor')} borderRadius="full" borderWidth={1} - bg={rgbaColorToString(brushColor)} + bg={rgbaColorToString(fill)} w={8} h={8} cursor="pointer" @@ -34,11 +34,11 @@ export const BrushColorPicker = memo(() => { - + ); }); -BrushColorPicker.displayName = 'BrushColorPicker'; +FillColorPicker.displayName = 'BrushColorPicker'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx index 202b830e479..34ef2ce0ac6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx @@ -2,10 +2,10 @@ import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InitialImagePreview } from 'features/controlLayers/components/IILayer/InitialImagePreview'; import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; -import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; +import { EntityMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; import { LayerOpacity } from 'features/controlLayers/components/LayerCommon/LayerOpacity'; -import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; -import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; +import { EntityTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; +import { EntityEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; import { iiLayerDenoisingStrengthChanged, @@ -67,11 +67,11 @@ export const IILayer = memo(({ layerId }: Props) => { return ( - - + + - + {isOpen && ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx index d227f73045c..e4b89dfe216 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx @@ -2,41 +2,39 @@ import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { IPALayerIPAdapterWrapper } from 'features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper'; import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; -import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; -import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; +import { EntityTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; +import { EntityEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; -import { layerSelected, selectLayerOrThrow } from 'features/controlLayers/store/controlLayersSlice'; -import { isIPAdapterLayer } from 'features/controlLayers/store/types'; +import { entitySelected } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback } from 'react'; type Props = { - layerId: string; + id: string; }; -export const IPALayer = memo(({ layerId }: Props) => { +export const IPAEntity = memo(({ id }: Props) => { const dispatch = useAppDispatch(); - const isSelected = useAppSelector( - (s) => selectLayerOrThrow(s.canvasV2, layerId, isIPAdapterLayer).isSelected - ); + const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); const onClick = useCallback(() => { - dispatch(layerSelected(layerId)); - }, [dispatch, layerId]); + dispatch(entitySelected({ id, type: 'ip_adapter' })); + }, [dispatch, id]); + return ( - - + + - + {isOpen && ( - + )} ); }); -IPALayer.displayName = 'IPALayer'; +IPAEntity.displayName = 'IPAEntity'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx index 7d7ba7e1b50..7620fa57b66 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx @@ -11,7 +11,7 @@ import { } from 'features/controlLayers/store/controlLayersSlice'; import { isIPAdapterLayer } from 'features/controlLayers/store/types'; import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; -import type { IPALayerImageDropData } from 'features/dnd/types'; +import type { IPAImageDropData } from 'features/dnd/types'; import { memo, useCallback, useMemo } from 'react'; import type { ImageDTO, IPAdapterModelConfig, IPALayerImagePostUploadAction } from 'services/api/types'; @@ -72,7 +72,7 @@ export const IPALayerIPAdapterWrapper = memo(({ layerId }: Props) => { [dispatch, layerId] ); - const droppableData = useMemo( + const droppableData = useMemo( () => ({ actionType: 'SET_IPA_LAYER_IMAGE', context: { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerDeleteButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityDeleteButton.tsx similarity index 50% rename from invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerDeleteButton.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityDeleteButton.tsx index 0cd7d83dfe6..441c839587b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerDeleteButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityDeleteButton.tsx @@ -1,19 +1,13 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; -import { layerDeleted } from 'features/controlLayers/store/controlLayersSlice'; -import { memo, useCallback } from 'react'; +import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiTrashSimpleBold } from 'react-icons/pi'; -type Props = { layerId: string }; +type Props = { onDelete: () => void }; -export const LayerDeleteButton = memo(({ layerId }: Props) => { +export const EntityDeleteButton = memo(({ onDelete }: Props) => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const deleteLayer = useCallback(() => { - dispatch(layerDeleted(layerId)); - }, [dispatch, layerId]); return ( { aria-label={t('common.delete')} tooltip={t('common.delete')} icon={} - onClick={deleteLayer} + onClick={onDelete} onDoubleClick={stopPropagation} // double click expands the layer /> ); }); -LayerDeleteButton.displayName = 'LayerDeleteButton'; +EntityDeleteButton.displayName = 'EntityDeleteButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerVisibilityToggle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityEnabledToggle.tsx similarity index 50% rename from invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerVisibilityToggle.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityEnabledToggle.tsx index 227d74c35a6..ca4855a64fe 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerVisibilityToggle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityEnabledToggle.tsx @@ -1,23 +1,16 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; -import { useLayerIsEnabled } from 'features/controlLayers/hooks/layerStateHooks'; -import { layerIsEnabledToggled } from 'features/controlLayers/store/controlLayersSlice'; -import { memo, useCallback } from 'react'; +import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiCheckBold } from 'react-icons/pi'; type Props = { - layerId: string; + isEnabled: boolean; + onToggle: () => void; }; -export const LayerIsEnabledToggle = memo(({ layerId }: Props) => { +export const EntityEnabledToggle = memo(({ isEnabled, onToggle }: Props) => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const isEnabled = useLayerIsEnabled(layerId); - const onClick = useCallback(() => { - dispatch(layerIsEnabledToggled(layerId)); - }, [dispatch, layerId]); return ( { tooltip={t(isEnabled ? 'common.enabled' : 'common.disabled')} variant="outline" icon={isEnabled ? : undefined} - onClick={onClick} + onClick={onToggle} colorScheme="base" onDoubleClick={stopPropagation} // double click expands the layer /> ); }); -LayerIsEnabledToggle.displayName = 'LayerVisibilityToggle'; +EntityEnabledToggle.displayName = 'EntityEnabledToggle'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityMenu.tsx similarity index 96% rename from invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenu.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityMenu.tsx index aabad5ed63b..a10079f1608 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityMenu.tsx @@ -11,7 +11,7 @@ import { PiArrowCounterClockwiseBold, PiDotsThreeVerticalBold, PiTrashSimpleBold type Props = { layerId: string }; -export const LayerMenu = memo(({ layerId }: Props) => { +export const EntityMenu = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); const { t } = useTranslation(); const layerType = useLayerType(layerId); @@ -68,4 +68,4 @@ export const LayerMenu = memo(({ layerId }: Props) => { ); }); -LayerMenu.displayName = 'LayerMenu'; +EntityMenu.displayName = 'EntityMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityMenuButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityMenuButton.tsx new file mode 100644 index 00000000000..51887ed5e12 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityMenuButton.tsx @@ -0,0 +1,18 @@ +import { IconButton, MenuButton } from '@invoke-ai/ui-library'; +import { stopPropagation } from 'common/util/stopPropagation'; +import { memo } from 'react'; +import { PiDotsThreeVerticalBold } from 'react-icons/pi'; + +export const EntityMenuButton = memo(() => { + return ( + } + onDoubleClick={stopPropagation} // double click expands the layer + /> + ); +}); + +EntityMenuButton.displayName = 'EntityMenuButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityTitle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityTitle.tsx new file mode 100644 index 00000000000..31fd5902b88 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityTitle.tsx @@ -0,0 +1,16 @@ +import { Text } from '@invoke-ai/ui-library'; +import { memo } from 'react'; + +type Props = { + title: string; +}; + +export const EntityTitle = memo(({ title }: Props) => { + return ( + + {title} + + ); +}); + +EntityTitle.displayName = 'EntityTitle'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx index d04da5e4a18..335a0d32cb6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx @@ -1,7 +1,7 @@ import { MenuItem } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useAddIPAdapterToIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; +import { useAddIPAdapterToRGLayer } from 'features/controlLayers/hooks/addLayerHooks'; import { regionalGuidanceNegativePromptChanged, regionalGuidancePositivePromptChanged, @@ -18,7 +18,7 @@ type Props = { layerId: string }; export const LayerMenuRGActions = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToIPALayer(layerId); + const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToRGLayer(layerId); const selectValidActions = useMemo( () => createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx deleted file mode 100644 index 053fbd234e0..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Text } from '@invoke-ai/ui-library'; -import type { LayerData } from 'features/controlLayers/store/types'; -import { memo, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -type Props = { - type: LayerData['type']; -}; - -export const LayerTitle = memo(({ type }: Props) => { - const { t } = useTranslation(); - const title = useMemo(() => { - if (type === 'regional_guidance_layer') { - return t('controlLayers.regionalGuidance'); - } else if (type === 'control_adapter_layer') { - return t('controlLayers.globalControlAdapter'); - } else if (type === 'ip_adapter_layer') { - return t('controlLayers.globalIPAdapter'); - } else if (type === 'initial_image_layer') { - return t('controlLayers.globalInitialImage'); - } else if (type === 'raster_layer') { - return t('controlLayers.rasterLayer'); - } - }, [t, type]); - - return ( - - {title} - - ); -}); - -LayerTitle.displayName = 'LayerTitle'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx index 4df5670f0e3..5d5ce7fb02a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx @@ -4,9 +4,9 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { rgbColorToString } from 'features/canvas/util/colorToString'; import { AddPromptButtons } from 'features/controlLayers/components/AddPromptButtons'; import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; -import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; -import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; -import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; +import { EntityMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; +import { EntityTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; +import { EntityEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; import { layerSelected, selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; @@ -52,8 +52,8 @@ export const RGLayer = memo(({ layerId }: Props) => { return ( - - + + {autoNegative === 'invert' && ( @@ -62,12 +62,12 @@ export const RGLayer = memo(({ layerId }: Props) => { )} - + {isOpen && ( - {!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && } + {!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && } {hasPositivePrompt && } {hasNegativePrompt && } {hasIPAdapters && } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx index 802ad2bb3d8..2fa392e0d08 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx @@ -12,10 +12,10 @@ import { selectRGLayerIPAdapterOrThrow, } from 'features/controlLayers/store/controlLayersSlice'; import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; -import type { RGLayerIPAdapterImageDropData } from 'features/dnd/types'; +import type { RGIPAdapterImageDropData } from 'features/dnd/types'; import { memo, useCallback, useMemo } from 'react'; import { PiTrashSimpleBold } from 'react-icons/pi'; -import type { ImageDTO, IPAdapterModelConfig, RGLayerIPAdapterImagePostUploadAction } from 'services/api/types'; +import type { ImageDTO, IPAdapterModelConfig, RGIPAdapterImagePostUploadAction } from 'services/api/types'; type Props = { layerId: string; @@ -78,7 +78,7 @@ export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNu [dispatch, ipAdapterId, layerId] ); - const droppableData = useMemo( + const droppableData = useMemo( () => ({ actionType: 'SET_RG_LAYER_IP_ADAPTER_IMAGE', context: { @@ -90,7 +90,7 @@ export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNu [ipAdapterId, layerId] ); - const postUploadAction = useMemo( + const postUploadAction = useMemo( () => ({ type: 'SET_RG_LAYER_IP_ADAPTER_IMAGE', layerId, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx index 921122fb338..82851dbb0b5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx @@ -2,14 +2,14 @@ import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIDroppable from 'common/components/IAIDroppable'; import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; -import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; +import { EntityMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; import { LayerOpacity } from 'features/controlLayers/components/LayerCommon/LayerOpacity'; -import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; -import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; +import { EntityTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; +import { EntityEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; import { layerSelected, selectLayerOrThrow } from 'features/controlLayers/store/controlLayersSlice'; import { isRasterLayer } from 'features/controlLayers/store/types'; -import type { RasterLayerImageDropData } from 'features/dnd/types'; +import type { LayerImageDropData } from 'features/dnd/types'; import { memo, useCallback, useMemo } from 'react'; type Props = { @@ -27,7 +27,7 @@ export const RasterLayer = memo(({ layerId }: Props) => { const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); const droppableData = useMemo(() => { - const _droppableData: RasterLayerImageDropData = { + const _droppableData: LayerImageDropData = { id: layerId, actionType: 'ADD_RASTER_LAYER_IMAGE', context: { layerId }, @@ -38,11 +38,11 @@ export const RasterLayer = memo(({ layerId }: Props) => { return ( - - + + - + {isOpen && ( diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts index 5b9e799063b..c52944b88c1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts @@ -1,18 +1,14 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { - controlAdapterAdded, - iiLayerAdded, - ipAdapterAdded, - regionalGuidanceIPAdapterAdded, -} from 'features/controlLayers/store/controlLayersSlice'; -import { isInitialImageLayer } from 'features/controlLayers/store/types'; +import { caAdded } from 'features/controlLayers/store/controlAdaptersSlice'; +import { ipaAdded } from 'features/controlLayers/store/ipAdaptersSlice'; +import { rgIPAdapterAdded } from 'features/controlLayers/store/regionalGuidanceSlice'; import { buildControlNet, buildIPAdapter, buildT2IAdapter, CA_PROCESSOR_DATA, isProcessorTypeV2, -} from 'features/controlLayers/util/controlAdapters'; +} from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { useCallback, useMemo } from 'react'; import { useControlNetAndT2IAdapterModels, useIPAdapterModels } from 'services/api/hooks/modelsByType'; @@ -46,7 +42,7 @@ export const useAddCALayer = () => { processorConfig, }); - dispatch(controlAdapterAdded(controlAdapter)); + dispatch(caAdded(controlAdapter)); }, [dispatch, model, baseModel]); return [addCALayer, isDisabled] as const; @@ -70,13 +66,13 @@ export const useAddIPALayer = () => { const ipAdapter = buildIPAdapter(id, { model: zModelIdentifierField.parse(model), }); - dispatch(ipAdapterAdded(ipAdapter)); + dispatch(ipaAdded(ipAdapter)); }, [dispatch, model]); return [addIPALayer, isDisabled] as const; }; -export const useAddIPAdapterToIPALayer = (layerId: string) => { +export const useAddIPAdapterToRGLayer = (id: string) => { const dispatch = useAppDispatch(); const baseModel = useAppSelector((s) => s.generation.model?.base); const [modelConfigs] = useIPAdapterModels(); @@ -90,22 +86,11 @@ export const useAddIPAdapterToIPALayer = (layerId: string) => { if (!model) { return; } - const id = uuidv4(); - const ipAdapter = buildIPAdapter(id, { + const ipAdapter = buildIPAdapter(uuidv4(), { model: zModelIdentifierField.parse(model), }); - dispatch(regionalGuidanceIPAdapterAdded({ layerId, ipAdapter })); - }, [dispatch, model, layerId]); + dispatch(rgIPAdapterAdded({ id, ipAdapter: { ...ipAdapter, id: uuidv4(), type: 'ip_adapter', isEnabled: true } })); + }, [model, dispatch, id]); return [addIPAdapter, isDisabled] as const; }; - -export const useAddIILayer = () => { - const dispatch = useAppDispatch(); - const isDisabled = useAppSelector((s) => Boolean(s.canvasV2.layers.find(isInitialImageLayer))); - const addIILayer = useCallback(() => { - dispatch(iiLayerAdded(null)); - }, [dispatch]); - - return [addIILayer, isDisabled] as const; -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts index 7b85bcfce33..8301a97573e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts @@ -108,7 +108,7 @@ const updateCALayerImageAttrs = (stage: Konva.Stage, konvaImage: Konva.Image, ca scaleX: 1, scaleY: 1, visible: ca.isEnabled, - filters: ca.filter === LightnessToAlphaFilter.name ? [LightnessToAlphaFilter] : [], + filters: ca.filter === 'LightnessToAlphaFilter' ? [LightnessToAlphaFilter] : [], }); needsCache = true; } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts index f53de4eb5a9..6a98523bef5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts @@ -2,15 +2,14 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; -import type { ControlModeV2, ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; -import { buildControlAdapterProcessorV2, imageDTOToImageWithDims } from 'features/controlLayers/util/controlAdapters'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { IRect } from 'konva/lib/types'; import { isEqual } from 'lodash-es'; import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types'; import { v4 as uuidv4 } from 'uuid'; -import type { ControlAdapterConfig, ControlAdapterData, Filter } from './types'; +import type { ControlAdapterConfig, ControlAdapterData, ControlModeV2, Filter, ProcessorConfig } from './types'; +import { buildControlAdapterProcessorV2, imageDTOToImageWithDims } from './types'; type ControlAdaptersV2State = { _version: 1; @@ -22,7 +21,7 @@ const initialState: ControlAdaptersV2State = { controlAdapters: [], }; -const selectCa = (state: ControlAdaptersV2State, id: string) => state.controlAdapters.find((ca) => ca.id === id); +export const selectCA = (state: ControlAdaptersV2State, id: string) => state.controlAdapters.find((ca) => ca.id === id); export const controlAdaptersV2Slice = createSlice({ name: 'controlAdaptersV2', @@ -40,7 +39,7 @@ export const controlAdaptersV2Slice = createSlice({ bboxNeedsUpdate: false, isEnabled: true, opacity: 1, - filter: 'lightness_to_alpha', + filter: 'LightnessToAlphaFilter', processorPendingBatchId: null, ...config, }); @@ -52,17 +51,17 @@ export const controlAdaptersV2Slice = createSlice({ caRecalled: (state, action: PayloadAction<{ data: ControlAdapterData }>) => { state.controlAdapters.push(action.payload.data); }, - caIsEnabledChanged: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => { - const { id, isEnabled } = action.payload; - const ca = selectCa(state, id); + caIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const ca = selectCA(state, id); if (!ca) { return; } - ca.isEnabled = isEnabled; + ca.isEnabled = !ca.isEnabled; }, caTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => { const { id, x, y } = action.payload; - const ca = selectCa(state, id); + const ca = selectCA(state, id); if (!ca) { return; } @@ -71,7 +70,7 @@ export const controlAdaptersV2Slice = createSlice({ }, caBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { const { id, bbox } = action.payload; - const ca = selectCa(state, id); + const ca = selectCA(state, id); if (!ca) { return; } @@ -84,7 +83,7 @@ export const controlAdaptersV2Slice = createSlice({ }, caOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => { const { id, opacity } = action.payload; - const ca = selectCa(state, id); + const ca = selectCA(state, id); if (!ca) { return; } @@ -92,7 +91,7 @@ export const controlAdaptersV2Slice = createSlice({ }, caMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; - const ca = selectCa(state, id); + const ca = selectCA(state, id); if (!ca) { return; } @@ -100,7 +99,7 @@ export const controlAdaptersV2Slice = createSlice({ }, caMovedToFront: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; - const ca = selectCa(state, id); + const ca = selectCA(state, id); if (!ca) { return; } @@ -108,7 +107,7 @@ export const controlAdaptersV2Slice = createSlice({ }, caMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; - const ca = selectCa(state, id); + const ca = selectCA(state, id); if (!ca) { return; } @@ -116,7 +115,7 @@ export const controlAdaptersV2Slice = createSlice({ }, caMovedToBack: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; - const ca = selectCa(state, id); + const ca = selectCA(state, id); if (!ca) { return; } @@ -124,7 +123,7 @@ export const controlAdaptersV2Slice = createSlice({ }, caImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => { const { id, imageDTO } = action.payload; - const ca = selectCa(state, id); + const ca = selectCA(state, id); if (!ca) { return; } @@ -145,7 +144,7 @@ export const controlAdaptersV2Slice = createSlice({ }, caProcessedImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => { const { id, imageDTO } = action.payload; - const ca = selectCa(state, id); + const ca = selectCA(state, id); if (!ca) { return; } @@ -162,7 +161,7 @@ export const controlAdaptersV2Slice = createSlice({ }> ) => { const { id, modelConfig } = action.payload; - const ca = selectCa(state, id); + const ca = selectCA(state, id); if (!ca) { return; } @@ -189,7 +188,7 @@ export const controlAdaptersV2Slice = createSlice({ }, caControlModeChanged: (state, action: PayloadAction<{ id: string; controlMode: ControlModeV2 }>) => { const { id, controlMode } = action.payload; - const ca = selectCa(state, id); + const ca = selectCA(state, id); if (!ca) { return; } @@ -200,7 +199,7 @@ export const controlAdaptersV2Slice = createSlice({ action: PayloadAction<{ id: string; processorConfig: ProcessorConfig | null }> ) => { const { id, processorConfig } = action.payload; - const ca = selectCa(state, id); + const ca = selectCA(state, id); if (!ca) { return; } @@ -211,7 +210,7 @@ export const controlAdaptersV2Slice = createSlice({ }, caFilterChanged: (state, action: PayloadAction<{ id: string; filter: Filter }>) => { const { id, filter } = action.payload; - const ca = selectCa(state, id); + const ca = selectCA(state, id); if (!ca) { return; } @@ -219,7 +218,7 @@ export const controlAdaptersV2Slice = createSlice({ }, caProcessorPendingBatchIdChanged: (state, action: PayloadAction<{ id: string; batchId: string | null }>) => { const { id, batchId } = action.payload; - const ca = selectCa(state, id); + const ca = selectCA(state, id); if (!ca) { return; } @@ -227,7 +226,7 @@ export const controlAdaptersV2Slice = createSlice({ }, caWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { const { id, weight } = action.payload; - const ca = selectCa(state, id); + const ca = selectCA(state, id); if (!ca) { return; } @@ -235,7 +234,7 @@ export const controlAdaptersV2Slice = createSlice({ }, caBeginEndStepPctChanged: (state, action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }>) => { const { id, beginEndStepPct } = action.payload; - const ca = selectCa(state, id); + const ca = selectCA(state, id); if (!ca) { return; } @@ -248,7 +247,7 @@ export const { caAdded, caBboxChanged, caDeleted, - caIsEnabledChanged, + caIsEnabledToggled, caMovedBackwardOne, caMovedForwardOne, caMovedToBack, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index ee2072eaf62..0dd87536c7d 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 { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/ import type { IRect, Vector2d } from 'konva/lib/types'; import { atom } from 'nanostores'; -import type { CanvasEntity, CanvasV2State, RgbaColor, StageAttrs, Tool } from './types'; +import type { CanvasEntity, CanvasEntityIdentifier, CanvasV2State, RgbaColor, StageAttrs, Tool } from './types'; import { DEFAULT_RGBA_COLOR } from './types'; const initialState: CanvasV2State = { @@ -110,6 +110,9 @@ export const canvasV2Slice = createSlice({ toolBufferChanged: (state, action: PayloadAction) => { state.tool.selectedBuffer = action.payload; }, + entitySelected: (state, action: PayloadAction) => { + state.selectedEntityIdentifier = action.payload; + }, }, extraReducers(builder) { builder.addCase(modelChanged, (state, action) => { @@ -145,6 +148,7 @@ export const { invertScrollChanged, toolChanged, toolBufferChanged, + entitySelected, } = canvasV2Slice.actions; export const selectCanvasV2Slice = (state: RootState) => state.canvasV2; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts index 7d177fabbce..182693abcce 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts @@ -3,8 +3,8 @@ import { createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; -import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; -import { imageDTOToImageWithDims } from 'features/controlLayers/util/controlAdapters'; +import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types'; +import { imageDTOToImageWithDims } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas'; import type { IRect } from 'konva/lib/types'; diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.test.ts similarity index 99% rename from invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts rename to invokeai/frontend/web/src/features/controlLayers/store/types.test.ts index 22f54d622cd..6d95cac1217 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.test.ts @@ -24,7 +24,7 @@ import type { ProcessorConfig, ProcessorTypeV2, ZoeDepthProcessorConfig, -} from './controlAdapters'; +} from './types'; describe('Control Adapter Types', () => { test('ProcessorType', () => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index fc4b010067e..8388ed05304 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,13 +1,4 @@ -import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; -import { - zBeginEndStepPct, - zCLIPVisionModelV2, - zControlModeV2, - zId, - zImageWithDims, - zIPMethodV2, - zProcessorConfig, -} from 'features/controlLayers/util/controlAdapters'; +import { deepClone } from 'common/util/deepClone'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; import type { @@ -24,11 +15,442 @@ import { zParameterPositivePrompt, } from 'features/parameters/types/parameterSchemas'; import type { IRect } from 'konva/lib/types'; -import type { ImageDTO } from 'services/api/types'; +import { merge } from 'lodash-es'; +import type { + AnyInvocation, + BaseModelType, + ControlNetModelConfig, + ImageDTO, + T2IAdapterModelConfig, +} from 'services/api/types'; import { z } from 'zod'; +export const zId = z.string().min(1); + +export const zImageWithDims = z.object({ + name: z.string(), + width: z.number().int().positive(), + height: z.number().int().positive(), +}); +export type ImageWithDims = z.infer; + +export const zBeginEndStepPct = z + .tuple([z.number().gte(0).lte(1), z.number().gte(0).lte(1)]) + .refine(([begin, end]) => begin < end, { + message: 'Begin must be less than end', + }); + +export const zControlModeV2 = z.enum(['balanced', 'more_prompt', 'more_control', 'unbalanced']); +export type ControlModeV2 = z.infer; +export const isControlModeV2 = (v: unknown): v is ControlModeV2 => zControlModeV2.safeParse(v).success; + +export const zCLIPVisionModelV2 = z.enum(['ViT-H', 'ViT-G']); +export type CLIPVisionModelV2 = z.infer; +export const isCLIPVisionModelV2 = (v: unknown): v is CLIPVisionModelV2 => zCLIPVisionModelV2.safeParse(v).success; + +export const zIPMethodV2 = z.enum(['full', 'style', 'composition']); +export type IPMethodV2 = z.infer; +export const isIPMethodV2 = (v: unknown): v is IPMethodV2 => zIPMethodV2.safeParse(v).success; + +const zCannyProcessorConfig = z.object({ + id: zId, + type: z.literal('canny_image_processor'), + low_threshold: z.number().int().gte(0).lte(255), + high_threshold: z.number().int().gte(0).lte(255), +}); +export type CannyProcessorConfig = z.infer; + +const zColorMapProcessorConfig = z.object({ + id: zId, + type: z.literal('color_map_image_processor'), + color_map_tile_size: z.number().int().gte(1), +}); +export type ColorMapProcessorConfig = z.infer; + +const zContentShuffleProcessorConfig = z.object({ + id: zId, + type: z.literal('content_shuffle_image_processor'), + w: z.number().int().gte(0), + h: z.number().int().gte(0), + f: z.number().int().gte(0), +}); +export type ContentShuffleProcessorConfig = z.infer; + +const zDepthAnythingModelSize = z.enum(['large', 'base', 'small']); +export type DepthAnythingModelSize = z.infer; +export const isDepthAnythingModelSize = (v: unknown): v is DepthAnythingModelSize => + zDepthAnythingModelSize.safeParse(v).success; +const zDepthAnythingProcessorConfig = z.object({ + id: zId, + type: z.literal('depth_anything_image_processor'), + model_size: zDepthAnythingModelSize, +}); +export type DepthAnythingProcessorConfig = z.infer; + +const zHedProcessorConfig = z.object({ + id: zId, + type: z.literal('hed_image_processor'), + scribble: z.boolean(), +}); +export type HedProcessorConfig = z.infer; + +const zLineartAnimeProcessorConfig = z.object({ + id: zId, + type: z.literal('lineart_anime_image_processor'), +}); +export type LineartAnimeProcessorConfig = z.infer; + +const zLineartProcessorConfig = z.object({ + id: zId, + type: z.literal('lineart_image_processor'), + coarse: z.boolean(), +}); +export type LineartProcessorConfig = z.infer; + +const zMediapipeFaceProcessorConfig = z.object({ + id: zId, + type: z.literal('mediapipe_face_processor'), + max_faces: z.number().int().gte(1), + min_confidence: z.number().gte(0).lte(1), +}); +export type MediapipeFaceProcessorConfig = z.infer; + +const zMidasDepthProcessorConfig = z.object({ + id: zId, + type: z.literal('midas_depth_image_processor'), + a_mult: z.number().gte(0), + bg_th: z.number().gte(0), +}); +export type MidasDepthProcessorConfig = z.infer; + +const zMlsdProcessorConfig = z.object({ + id: zId, + type: z.literal('mlsd_image_processor'), + thr_v: z.number().gte(0), + thr_d: z.number().gte(0), +}); +export type MlsdProcessorConfig = z.infer; + +const zNormalbaeProcessorConfig = z.object({ + id: zId, + type: z.literal('normalbae_image_processor'), +}); +export type NormalbaeProcessorConfig = z.infer; + +const zDWOpenposeProcessorConfig = z.object({ + id: zId, + type: z.literal('dw_openpose_image_processor'), + draw_body: z.boolean(), + draw_face: z.boolean(), + draw_hands: z.boolean(), +}); +export type DWOpenposeProcessorConfig = z.infer; + +const zPidiProcessorConfig = z.object({ + id: zId, + type: z.literal('pidi_image_processor'), + safe: z.boolean(), + scribble: z.boolean(), +}); +export type PidiProcessorConfig = z.infer; + +const zZoeDepthProcessorConfig = z.object({ + id: zId, + type: z.literal('zoe_depth_image_processor'), +}); +export type ZoeDepthProcessorConfig = z.infer; + +export const zProcessorConfig = z.discriminatedUnion('type', [ + zCannyProcessorConfig, + zColorMapProcessorConfig, + zContentShuffleProcessorConfig, + zDepthAnythingProcessorConfig, + zHedProcessorConfig, + zLineartAnimeProcessorConfig, + zLineartProcessorConfig, + zMediapipeFaceProcessorConfig, + zMidasDepthProcessorConfig, + zMlsdProcessorConfig, + zNormalbaeProcessorConfig, + zDWOpenposeProcessorConfig, + zPidiProcessorConfig, + zZoeDepthProcessorConfig, +]); +export type ProcessorConfig = z.infer; + +const zProcessorTypeV2 = z.enum([ + 'canny_image_processor', + 'color_map_image_processor', + 'content_shuffle_image_processor', + 'depth_anything_image_processor', + 'hed_image_processor', + 'lineart_anime_image_processor', + 'lineart_image_processor', + 'mediapipe_face_processor', + 'midas_depth_image_processor', + 'mlsd_image_processor', + 'normalbae_image_processor', + 'dw_openpose_image_processor', + 'pidi_image_processor', + 'zoe_depth_image_processor', +]); +export type ProcessorTypeV2 = z.infer; +export const isProcessorTypeV2 = (v: unknown): v is ProcessorTypeV2 => zProcessorTypeV2.safeParse(v).success; + +type ProcessorData = { + type: T; + labelTKey: string; + descriptionTKey: string; + buildDefaults(baseModel?: BaseModelType): Extract; + buildNode(image: ImageWithDims, config: Extract): Extract; +}; + +const minDim = (image: ImageWithDims): number => Math.min(image.width, image.height); + +type CAProcessorsData = { + [key in ProcessorTypeV2]: ProcessorData; +}; +/** + * A dict of ControlNet processors, including: + * - label translation key + * - description translation key + * - a builder to create default values for the config + * - a builder to create the node for the config + * + * TODO: Generate from the OpenAPI schema + */ +export const CA_PROCESSOR_DATA: CAProcessorsData = { + canny_image_processor: { + type: 'canny_image_processor', + labelTKey: 'controlnet.canny', + descriptionTKey: 'controlnet.cannyDescription', + buildDefaults: () => ({ + id: 'canny_image_processor', + type: 'canny_image_processor', + low_threshold: 100, + high_threshold: 200, + }), + buildNode: (image, config) => ({ + ...config, + type: 'canny_image_processor', + image: { image_name: image.name }, + detect_resolution: minDim(image), + image_resolution: minDim(image), + }), + }, + color_map_image_processor: { + type: 'color_map_image_processor', + labelTKey: 'controlnet.colorMap', + descriptionTKey: 'controlnet.colorMapDescription', + buildDefaults: () => ({ + id: 'color_map_image_processor', + type: 'color_map_image_processor', + color_map_tile_size: 64, + }), + buildNode: (image, config) => ({ + ...config, + type: 'color_map_image_processor', + image: { image_name: image.name }, + }), + }, + content_shuffle_image_processor: { + type: 'content_shuffle_image_processor', + labelTKey: 'controlnet.contentShuffle', + descriptionTKey: 'controlnet.contentShuffleDescription', + buildDefaults: (baseModel) => ({ + id: 'content_shuffle_image_processor', + type: 'content_shuffle_image_processor', + h: baseModel === 'sdxl' ? 1024 : 512, + w: baseModel === 'sdxl' ? 1024 : 512, + f: baseModel === 'sdxl' ? 512 : 256, + }), + buildNode: (image, config) => ({ + ...config, + image: { image_name: image.name }, + detect_resolution: minDim(image), + image_resolution: minDim(image), + }), + }, + depth_anything_image_processor: { + type: 'depth_anything_image_processor', + labelTKey: 'controlnet.depthAnything', + descriptionTKey: 'controlnet.depthAnythingDescription', + buildDefaults: () => ({ + id: 'depth_anything_image_processor', + type: 'depth_anything_image_processor', + model_size: 'small', + }), + buildNode: (image, config) => ({ + ...config, + image: { image_name: image.name }, + resolution: minDim(image), + }), + }, + hed_image_processor: { + type: 'hed_image_processor', + labelTKey: 'controlnet.hed', + descriptionTKey: 'controlnet.hedDescription', + buildDefaults: () => ({ + id: 'hed_image_processor', + type: 'hed_image_processor', + scribble: false, + }), + buildNode: (image, config) => ({ + ...config, + image: { image_name: image.name }, + detect_resolution: minDim(image), + image_resolution: minDim(image), + }), + }, + lineart_anime_image_processor: { + type: 'lineart_anime_image_processor', + labelTKey: 'controlnet.lineartAnime', + descriptionTKey: 'controlnet.lineartAnimeDescription', + buildDefaults: () => ({ + id: 'lineart_anime_image_processor', + type: 'lineart_anime_image_processor', + }), + buildNode: (image, config) => ({ + ...config, + image: { image_name: image.name }, + detect_resolution: minDim(image), + image_resolution: minDim(image), + }), + }, + lineart_image_processor: { + type: 'lineart_image_processor', + labelTKey: 'controlnet.lineart', + descriptionTKey: 'controlnet.lineartDescription', + buildDefaults: () => ({ + id: 'lineart_image_processor', + type: 'lineart_image_processor', + coarse: false, + }), + buildNode: (image, config) => ({ + ...config, + image: { image_name: image.name }, + detect_resolution: minDim(image), + image_resolution: minDim(image), + }), + }, + mediapipe_face_processor: { + type: 'mediapipe_face_processor', + labelTKey: 'controlnet.mediapipeFace', + descriptionTKey: 'controlnet.mediapipeFaceDescription', + buildDefaults: () => ({ + id: 'mediapipe_face_processor', + type: 'mediapipe_face_processor', + max_faces: 1, + min_confidence: 0.5, + }), + buildNode: (image, config) => ({ + ...config, + image: { image_name: image.name }, + detect_resolution: minDim(image), + image_resolution: minDim(image), + }), + }, + midas_depth_image_processor: { + type: 'midas_depth_image_processor', + labelTKey: 'controlnet.depthMidas', + descriptionTKey: 'controlnet.depthMidasDescription', + buildDefaults: () => ({ + id: 'midas_depth_image_processor', + type: 'midas_depth_image_processor', + a_mult: 2, + bg_th: 0.1, + }), + buildNode: (image, config) => ({ + ...config, + image: { image_name: image.name }, + detect_resolution: minDim(image), + image_resolution: minDim(image), + }), + }, + mlsd_image_processor: { + type: 'mlsd_image_processor', + labelTKey: 'controlnet.mlsd', + descriptionTKey: 'controlnet.mlsdDescription', + buildDefaults: () => ({ + id: 'mlsd_image_processor', + type: 'mlsd_image_processor', + thr_d: 0.1, + thr_v: 0.1, + }), + buildNode: (image, config) => ({ + ...config, + image: { image_name: image.name }, + detect_resolution: minDim(image), + image_resolution: minDim(image), + }), + }, + normalbae_image_processor: { + type: 'normalbae_image_processor', + labelTKey: 'controlnet.normalBae', + descriptionTKey: 'controlnet.normalBaeDescription', + buildDefaults: () => ({ + id: 'normalbae_image_processor', + type: 'normalbae_image_processor', + }), + buildNode: (image, config) => ({ + ...config, + image: { image_name: image.name }, + detect_resolution: minDim(image), + image_resolution: minDim(image), + }), + }, + dw_openpose_image_processor: { + type: 'dw_openpose_image_processor', + labelTKey: 'controlnet.dwOpenpose', + descriptionTKey: 'controlnet.dwOpenposeDescription', + buildDefaults: () => ({ + id: 'dw_openpose_image_processor', + type: 'dw_openpose_image_processor', + draw_body: true, + draw_face: false, + draw_hands: false, + }), + buildNode: (image, config) => ({ + ...config, + image: { image_name: image.name }, + image_resolution: minDim(image), + }), + }, + pidi_image_processor: { + type: 'pidi_image_processor', + labelTKey: 'controlnet.pidi', + descriptionTKey: 'controlnet.pidiDescription', + buildDefaults: () => ({ + id: 'pidi_image_processor', + type: 'pidi_image_processor', + scribble: false, + safe: false, + }), + buildNode: (image, config) => ({ + ...config, + image: { image_name: image.name }, + detect_resolution: minDim(image), + image_resolution: minDim(image), + }), + }, + zoe_depth_image_processor: { + type: 'zoe_depth_image_processor', + labelTKey: 'controlnet.depthZoe', + descriptionTKey: 'controlnet.depthZoeDescription', + buildDefaults: () => ({ + id: 'zoe_depth_image_processor', + type: 'zoe_depth_image_processor', + }), + buildNode: (image, config) => ({ + ...config, + image: { image_name: image.name }, + }), + }, +}; + const zTool = z.enum(['brush', 'eraser', 'move', 'rect', 'view', 'bbox']); export type Tool = z.infer; + const zDrawingTool = zTool.extract(['brush', 'eraser']); const zPoints = z.array(z.number()).refine((points) => points.length % 2 === 0, { @@ -244,7 +666,7 @@ const zInpaintMaskData = z.object({ }); export type InpaintMaskData = z.infer; -const zFilter = z.enum(['none', LightnessToAlphaFilter.name]); +const zFilter = z.enum(['none', 'LightnessToAlphaFilter']); export type Filter = z.infer; const zControlAdapterData = z.object({ @@ -272,6 +694,64 @@ export type ControlAdapterConfig = Pick< 'weight' | 'image' | 'processedImage' | 'processorConfig' | 'beginEndStepPct' | 'model' | 'controlMode' >; +export const initialControlNetV2: ControlAdapterConfig = { + model: null, + weight: 1, + beginEndStepPct: [0, 1], + controlMode: 'balanced', + image: null, + processedImage: null, + processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(), +}; + +export const initialT2IAdapterV2: ControlAdapterConfig = { + model: null, + weight: 1, + beginEndStepPct: [0, 1], + controlMode: null, + image: null, + processedImage: null, + processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(), +}; + +export const initialIPAdapterV2: IPAdapterConfig = { + image: null, + model: null, + beginEndStepPct: [0, 1], + method: 'full', + clipVisionModel: 'ViT-H', + weight: 1, +}; + +export const buildControlNet = (id: string, overrides?: Partial): ControlAdapterConfig => { + return merge(deepClone(initialControlNetV2), { id, ...overrides }); +}; + +export const buildT2IAdapter = (id: string, overrides?: Partial): ControlAdapterConfig => { + return merge(deepClone(initialT2IAdapterV2), { id, ...overrides }); +}; + +export const buildIPAdapter = (id: string, overrides?: Partial): IPAdapterConfig => { + return merge(deepClone(initialIPAdapterV2), { id, ...overrides }); +}; + +export const buildControlAdapterProcessorV2 = ( + modelConfig: ControlNetModelConfig | T2IAdapterModelConfig +): ProcessorConfig | null => { + const defaultPreprocessor = modelConfig.default_settings?.preprocessor; + if (!isProcessorTypeV2(defaultPreprocessor)) { + return null; + } + const processorConfig = CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults(modelConfig.base); + return processorConfig; +}; + +export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO): ImageWithDims => ({ + name: image_name, + width, + height, +}); + export type CanvasEntity = LayerData | IPAdapterData | ControlAdapterData | RegionalGuidanceData | InpaintMaskData; export type CanvasEntityIdentifier = Pick; diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts deleted file mode 100644 index 892d2c5eaca..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts +++ /dev/null @@ -1,546 +0,0 @@ -import { deepClone } from 'common/util/deepClone'; -import { zModelIdentifierField } from 'features/nodes/types/common'; -import { merge, omit } from 'lodash-es'; -import type { - AnyInvocation, - BaseModelType, - ControlNetModelConfig, - ImageDTO, - T2IAdapterModelConfig, -} from 'services/api/types'; -import { z } from 'zod'; - -export const zId = z.string().min(1); - -const zCannyProcessorConfig = z.object({ - id: zId, - type: z.literal('canny_image_processor'), - low_threshold: z.number().int().gte(0).lte(255), - high_threshold: z.number().int().gte(0).lte(255), -}); -export type CannyProcessorConfig = z.infer; - -const zColorMapProcessorConfig = z.object({ - id: zId, - type: z.literal('color_map_image_processor'), - color_map_tile_size: z.number().int().gte(1), -}); -export type ColorMapProcessorConfig = z.infer; - -const zContentShuffleProcessorConfig = z.object({ - id: zId, - type: z.literal('content_shuffle_image_processor'), - w: z.number().int().gte(0), - h: z.number().int().gte(0), - f: z.number().int().gte(0), -}); -export type ContentShuffleProcessorConfig = z.infer; - -const zDepthAnythingModelSize = z.enum(['large', 'base', 'small', 'small_v2']); -export type DepthAnythingModelSize = z.infer; -export const isDepthAnythingModelSize = (v: unknown): v is DepthAnythingModelSize => - zDepthAnythingModelSize.safeParse(v).success; -const zDepthAnythingProcessorConfig = z.object({ - id: zId, - type: z.literal('depth_anything_image_processor'), - model_size: zDepthAnythingModelSize, -}); -export type DepthAnythingProcessorConfig = z.infer; - -const zHedProcessorConfig = z.object({ - id: zId, - type: z.literal('hed_image_processor'), - scribble: z.boolean(), -}); -export type HedProcessorConfig = z.infer; - -const zLineartAnimeProcessorConfig = z.object({ - id: zId, - type: z.literal('lineart_anime_image_processor'), -}); -export type LineartAnimeProcessorConfig = z.infer; - -const zLineartProcessorConfig = z.object({ - id: zId, - type: z.literal('lineart_image_processor'), - coarse: z.boolean(), -}); -export type LineartProcessorConfig = z.infer; - -const zMediapipeFaceProcessorConfig = z.object({ - id: zId, - type: z.literal('mediapipe_face_processor'), - max_faces: z.number().int().gte(1), - min_confidence: z.number().gte(0).lte(1), -}); -export type MediapipeFaceProcessorConfig = z.infer; - -const zMidasDepthProcessorConfig = z.object({ - id: zId, - type: z.literal('midas_depth_image_processor'), - a_mult: z.number().gte(0), - bg_th: z.number().gte(0), -}); -export type MidasDepthProcessorConfig = z.infer; - -const zMlsdProcessorConfig = z.object({ - id: zId, - type: z.literal('mlsd_image_processor'), - thr_v: z.number().gte(0), - thr_d: z.number().gte(0), -}); -export type MlsdProcessorConfig = z.infer; - -const zNormalbaeProcessorConfig = z.object({ - id: zId, - type: z.literal('normalbae_image_processor'), -}); -export type NormalbaeProcessorConfig = z.infer; - -const zDWOpenposeProcessorConfig = z.object({ - id: zId, - type: z.literal('dw_openpose_image_processor'), - draw_body: z.boolean(), - draw_face: z.boolean(), - draw_hands: z.boolean(), -}); -export type DWOpenposeProcessorConfig = z.infer; - -const zPidiProcessorConfig = z.object({ - id: zId, - type: z.literal('pidi_image_processor'), - safe: z.boolean(), - scribble: z.boolean(), -}); -export type PidiProcessorConfig = z.infer; - -const zZoeDepthProcessorConfig = z.object({ - id: zId, - type: z.literal('zoe_depth_image_processor'), -}); -export type ZoeDepthProcessorConfig = z.infer; - -export const zProcessorConfig = z.discriminatedUnion('type', [ - zCannyProcessorConfig, - zColorMapProcessorConfig, - zContentShuffleProcessorConfig, - zDepthAnythingProcessorConfig, - zHedProcessorConfig, - zLineartAnimeProcessorConfig, - zLineartProcessorConfig, - zMediapipeFaceProcessorConfig, - zMidasDepthProcessorConfig, - zMlsdProcessorConfig, - zNormalbaeProcessorConfig, - zDWOpenposeProcessorConfig, - zPidiProcessorConfig, - zZoeDepthProcessorConfig, -]); -export type ProcessorConfig = z.infer; - -export const zImageWithDims = z.object({ - name: z.string(), - width: z.number().int().positive(), - height: z.number().int().positive(), -}); -export type ImageWithDims = z.infer; - -export const zBeginEndStepPct = z - .tuple([z.number().gte(0).lte(1), z.number().gte(0).lte(1)]) - .refine(([begin, end]) => begin < end, { - message: 'Begin must be less than end', - }); - -const zControlAdapterBase = z.object({ - id: zId, - weight: z.number().gte(-1).lte(2), - image: zImageWithDims.nullable(), - processedImage: zImageWithDims.nullable(), - processorConfig: zProcessorConfig.nullable(), - processorPendingBatchId: z.string().nullable().default(null), - beginEndStepPct: zBeginEndStepPct, -}); - -export const zControlModeV2 = z.enum(['balanced', 'more_prompt', 'more_control', 'unbalanced']); -export type ControlModeV2 = z.infer; -export const isControlModeV2 = (v: unknown): v is ControlModeV2 => zControlModeV2.safeParse(v).success; - -export const zControlNetConfigV2 = zControlAdapterBase.extend({ - type: z.literal('controlnet'), - model: zModelIdentifierField.nullable(), - controlMode: zControlModeV2, -}); -export type ControlNetConfigV2 = z.infer; - -export const zT2IAdapterConfigV2 = zControlAdapterBase.extend({ - type: z.literal('t2i_adapter'), - model: zModelIdentifierField.nullable(), -}); -export type T2IAdapterConfigV2 = z.infer; - -export const zCLIPVisionModelV2 = z.enum(['ViT-H', 'ViT-G']); -export type CLIPVisionModelV2 = z.infer; -export const isCLIPVisionModelV2 = (v: unknown): v is CLIPVisionModelV2 => zCLIPVisionModelV2.safeParse(v).success; - -export const zIPMethodV2 = z.enum(['full', 'style', 'composition']); -export type IPMethodV2 = z.infer; -export const isIPMethodV2 = (v: unknown): v is IPMethodV2 => zIPMethodV2.safeParse(v).success; - -export const zIPAdapterConfigV2 = z.object({ - id: zId, - type: z.literal('ip_adapter'), - weight: z.number().gte(-1).lte(2), - method: zIPMethodV2, - image: zImageWithDims.nullable(), - model: zModelIdentifierField.nullable(), - clipVisionModel: zCLIPVisionModelV2, - beginEndStepPct: zBeginEndStepPct, -}); -export type IPAdapterConfigV2 = z.infer; - -const zProcessorTypeV2 = z.enum([ - 'canny_image_processor', - 'color_map_image_processor', - 'content_shuffle_image_processor', - 'depth_anything_image_processor', - 'hed_image_processor', - 'lineart_anime_image_processor', - 'lineart_image_processor', - 'mediapipe_face_processor', - 'midas_depth_image_processor', - 'mlsd_image_processor', - 'normalbae_image_processor', - 'dw_openpose_image_processor', - 'pidi_image_processor', - 'zoe_depth_image_processor', -]); -export type ProcessorTypeV2 = z.infer; -export const isProcessorTypeV2 = (v: unknown): v is ProcessorTypeV2 => zProcessorTypeV2.safeParse(v).success; - -type ProcessorData = { - type: T; - labelTKey: string; - descriptionTKey: string; - buildDefaults(baseModel?: BaseModelType): Extract; - buildNode(image: ImageWithDims, config: Extract): Extract; -}; - -const minDim = (image: ImageWithDims): number => Math.min(image.width, image.height); - -type CAProcessorsData = { - [key in ProcessorTypeV2]: ProcessorData; -}; -/** - * A dict of ControlNet processors, including: - * - label translation key - * - description translation key - * - a builder to create default values for the config - * - a builder to create the node for the config - * - * TODO: Generate from the OpenAPI schema - */ -export const CA_PROCESSOR_DATA: CAProcessorsData = { - canny_image_processor: { - type: 'canny_image_processor', - labelTKey: 'controlnet.canny', - descriptionTKey: 'controlnet.cannyDescription', - buildDefaults: () => ({ - id: 'canny_image_processor', - type: 'canny_image_processor', - low_threshold: 100, - high_threshold: 200, - }), - buildNode: (image, config) => ({ - ...config, - type: 'canny_image_processor', - image: { image_name: image.name }, - detect_resolution: minDim(image), - image_resolution: minDim(image), - }), - }, - color_map_image_processor: { - type: 'color_map_image_processor', - labelTKey: 'controlnet.colorMap', - descriptionTKey: 'controlnet.colorMapDescription', - buildDefaults: () => ({ - id: 'color_map_image_processor', - type: 'color_map_image_processor', - color_map_tile_size: 64, - }), - buildNode: (image, config) => ({ - ...config, - type: 'color_map_image_processor', - image: { image_name: image.name }, - }), - }, - content_shuffle_image_processor: { - type: 'content_shuffle_image_processor', - labelTKey: 'controlnet.contentShuffle', - descriptionTKey: 'controlnet.contentShuffleDescription', - buildDefaults: (baseModel) => ({ - id: 'content_shuffle_image_processor', - type: 'content_shuffle_image_processor', - h: baseModel === 'sdxl' ? 1024 : 512, - w: baseModel === 'sdxl' ? 1024 : 512, - f: baseModel === 'sdxl' ? 512 : 256, - }), - buildNode: (image, config) => ({ - ...config, - image: { image_name: image.name }, - detect_resolution: minDim(image), - image_resolution: minDim(image), - }), - }, - depth_anything_image_processor: { - type: 'depth_anything_image_processor', - labelTKey: 'controlnet.depthAnything', - descriptionTKey: 'controlnet.depthAnythingDescription', - buildDefaults: () => ({ - id: 'depth_anything_image_processor', - type: 'depth_anything_image_processor', - model_size: 'small_v2', - }), - buildNode: (image, config) => ({ - ...config, - image: { image_name: image.name }, - resolution: minDim(image), - }), - }, - hed_image_processor: { - type: 'hed_image_processor', - labelTKey: 'controlnet.hed', - descriptionTKey: 'controlnet.hedDescription', - buildDefaults: () => ({ - id: 'hed_image_processor', - type: 'hed_image_processor', - scribble: false, - }), - buildNode: (image, config) => ({ - ...config, - image: { image_name: image.name }, - detect_resolution: minDim(image), - image_resolution: minDim(image), - }), - }, - lineart_anime_image_processor: { - type: 'lineart_anime_image_processor', - labelTKey: 'controlnet.lineartAnime', - descriptionTKey: 'controlnet.lineartAnimeDescription', - buildDefaults: () => ({ - id: 'lineart_anime_image_processor', - type: 'lineart_anime_image_processor', - }), - buildNode: (image, config) => ({ - ...config, - image: { image_name: image.name }, - detect_resolution: minDim(image), - image_resolution: minDim(image), - }), - }, - lineart_image_processor: { - type: 'lineart_image_processor', - labelTKey: 'controlnet.lineart', - descriptionTKey: 'controlnet.lineartDescription', - buildDefaults: () => ({ - id: 'lineart_image_processor', - type: 'lineart_image_processor', - coarse: false, - }), - buildNode: (image, config) => ({ - ...config, - image: { image_name: image.name }, - detect_resolution: minDim(image), - image_resolution: minDim(image), - }), - }, - mediapipe_face_processor: { - type: 'mediapipe_face_processor', - labelTKey: 'controlnet.mediapipeFace', - descriptionTKey: 'controlnet.mediapipeFaceDescription', - buildDefaults: () => ({ - id: 'mediapipe_face_processor', - type: 'mediapipe_face_processor', - max_faces: 1, - min_confidence: 0.5, - }), - buildNode: (image, config) => ({ - ...config, - image: { image_name: image.name }, - detect_resolution: minDim(image), - image_resolution: minDim(image), - }), - }, - midas_depth_image_processor: { - type: 'midas_depth_image_processor', - labelTKey: 'controlnet.depthMidas', - descriptionTKey: 'controlnet.depthMidasDescription', - buildDefaults: () => ({ - id: 'midas_depth_image_processor', - type: 'midas_depth_image_processor', - a_mult: 2, - bg_th: 0.1, - }), - buildNode: (image, config) => ({ - ...config, - image: { image_name: image.name }, - detect_resolution: minDim(image), - image_resolution: minDim(image), - }), - }, - mlsd_image_processor: { - type: 'mlsd_image_processor', - labelTKey: 'controlnet.mlsd', - descriptionTKey: 'controlnet.mlsdDescription', - buildDefaults: () => ({ - id: 'mlsd_image_processor', - type: 'mlsd_image_processor', - thr_d: 0.1, - thr_v: 0.1, - }), - buildNode: (image, config) => ({ - ...config, - image: { image_name: image.name }, - detect_resolution: minDim(image), - image_resolution: minDim(image), - }), - }, - normalbae_image_processor: { - type: 'normalbae_image_processor', - labelTKey: 'controlnet.normalBae', - descriptionTKey: 'controlnet.normalBaeDescription', - buildDefaults: () => ({ - id: 'normalbae_image_processor', - type: 'normalbae_image_processor', - }), - buildNode: (image, config) => ({ - ...config, - image: { image_name: image.name }, - detect_resolution: minDim(image), - image_resolution: minDim(image), - }), - }, - dw_openpose_image_processor: { - type: 'dw_openpose_image_processor', - labelTKey: 'controlnet.dwOpenpose', - descriptionTKey: 'controlnet.dwOpenposeDescription', - buildDefaults: () => ({ - id: 'dw_openpose_image_processor', - type: 'dw_openpose_image_processor', - draw_body: true, - draw_face: false, - draw_hands: false, - }), - buildNode: (image, config) => ({ - ...config, - image: { image_name: image.name }, - image_resolution: minDim(image), - }), - }, - pidi_image_processor: { - type: 'pidi_image_processor', - labelTKey: 'controlnet.pidi', - descriptionTKey: 'controlnet.pidiDescription', - buildDefaults: () => ({ - id: 'pidi_image_processor', - type: 'pidi_image_processor', - scribble: false, - safe: false, - }), - buildNode: (image, config) => ({ - ...config, - image: { image_name: image.name }, - detect_resolution: minDim(image), - image_resolution: minDim(image), - }), - }, - zoe_depth_image_processor: { - type: 'zoe_depth_image_processor', - labelTKey: 'controlnet.depthZoe', - descriptionTKey: 'controlnet.depthZoeDescription', - buildDefaults: () => ({ - id: 'zoe_depth_image_processor', - type: 'zoe_depth_image_processor', - }), - buildNode: (image, config) => ({ - ...config, - image: { image_name: image.name }, - }), - }, -}; - -export const initialControlNetV2: Omit = { - type: 'controlnet', - model: null, - weight: 1, - beginEndStepPct: [0, 1], - controlMode: 'balanced', - image: null, - processedImage: null, - processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(), - processorPendingBatchId: null, -}; - -export const initialT2IAdapterV2: Omit = { - type: 't2i_adapter', - model: null, - weight: 1, - beginEndStepPct: [0, 1], - image: null, - processedImage: null, - processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(), - processorPendingBatchId: null, -}; - -export const initialIPAdapterV2: Omit = { - type: 'ip_adapter', - image: null, - model: null, - beginEndStepPct: [0, 1], - method: 'full', - clipVisionModel: 'ViT-H', - weight: 1, -}; - -export const buildControlNet = (id: string, overrides?: Partial): ControlNetConfigV2 => { - return merge(deepClone(initialControlNetV2), { id, ...overrides }); -}; - -export const buildT2IAdapter = (id: string, overrides?: Partial): T2IAdapterConfigV2 => { - return merge(deepClone(initialT2IAdapterV2), { id, ...overrides }); -}; - -export const buildIPAdapter = (id: string, overrides?: Partial): IPAdapterConfigV2 => { - return merge(deepClone(initialIPAdapterV2), { id, ...overrides }); -}; - -export const buildControlAdapterProcessorV2 = ( - modelConfig: ControlNetModelConfig | T2IAdapterModelConfig -): ProcessorConfig | null => { - const defaultPreprocessor = modelConfig.default_settings?.preprocessor; - if (!isProcessorTypeV2(defaultPreprocessor)) { - return null; - } - const processorConfig = CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults(modelConfig.base); - return processorConfig; -}; - -export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO): ImageWithDims => ({ - name: image_name, - width, - height, -}); - -export const t2iAdapterToControlNet = (t2iAdapter: T2IAdapterConfigV2): ControlNetConfigV2 => { - return { - ...deepClone(t2iAdapter), - type: 'controlnet', - controlMode: initialControlNetV2.controlMode, - }; -}; - -export const controlNetToT2IAdapter = (controlNet: ControlNetConfigV2): T2IAdapterConfigV2 => { - return { - ...omit(deepClone(controlNet), 'controlMode'), - type: 't2i_adapter', - }; -}; diff --git a/invokeai/frontend/web/src/features/dnd/types/index.ts b/invokeai/frontend/web/src/features/dnd/types/index.ts index 1d99f4a4156..692454ea50a 100644 --- a/invokeai/frontend/web/src/features/dnd/types/index.ts +++ b/invokeai/frontend/web/src/features/dnd/types/index.ts @@ -22,39 +22,32 @@ export type CurrentImageDropData = BaseDropData & { actionType: 'SET_CURRENT_IMAGE'; }; -type ControlAdapterDropData = BaseDropData & { - actionType: 'SET_CONTROL_ADAPTER_IMAGE'; +export type CAImageDropData = BaseDropData & { + actionType: 'SET_CA_IMAGE'; context: { id: string; }; }; -export type CALayerImageDropData = BaseDropData & { - actionType: 'SET_CA_LAYER_IMAGE'; +export type IPAImageDropData = BaseDropData & { + actionType: 'SET_IPA_IMAGE'; context: { - layerId: string; - }; -}; - -export type IPALayerImageDropData = BaseDropData & { - actionType: 'SET_IPA_LAYER_IMAGE'; - context: { - layerId: string; + id: string; }; }; -export type RGLayerIPAdapterImageDropData = BaseDropData & { - actionType: 'SET_RG_LAYER_IP_ADAPTER_IMAGE'; +export type RGIPAdapterImageDropData = BaseDropData & { + actionType: 'SET_RG_IP_ADAPTER_IMAGE'; context: { - layerId: string; + id: string; ipAdapterId: string; }; }; -export type IILayerImageDropData = BaseDropData & { - actionType: 'SET_II_LAYER_IMAGE'; +export type LayerImageDropData = BaseDropData & { + actionType: 'ADD_LAYER_IMAGE'; context: { - layerId: string; + id: string; }; }; @@ -100,16 +93,14 @@ export type SelectForCompareDropData = BaseDropData & { export type TypesafeDroppableData = | CurrentImageDropData - | ControlAdapterDropData - | CanvasInitialImageDropData | NodesImageDropData | AddToBoardDropData | RemoveFromBoardDropData - | CALayerImageDropData - | IPALayerImageDropData - | RGLayerIPAdapterImageDropData - | IILayerImageDropData + | CAImageDropData + | IPAImageDropData + | RGIPAdapterImageDropData | SelectForCompareDropData + | RasterLayerImageDropData | UpscaleInitialImageDropData; type BaseDragData = { diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion.stories.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion.stories.tsx deleted file mode 100644 index 8909a826c32..00000000000 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion.stories.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; - -import { ControlSettingsAccordion } from './ControlSettingsAccordion'; - -const meta: Meta = { - title: 'Feature/ControlSettingsAccordion', - tags: ['autodocs'], - component: ControlSettingsAccordion, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - render: () => , -}; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion.tsx deleted file mode 100644 index eef997a11b7..00000000000 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { Button, ButtonGroup, Divider, Flex, StandaloneAccordion } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import ControlAdapterConfig from 'features/controlAdapters/components/ControlAdapterConfig'; -import { useAddControlAdapter } from 'features/controlAdapters/hooks/useAddControlAdapter'; -import { - selectAllControlNets, - selectAllIPAdapters, - selectAllT2IAdapters, - selectControlAdapterIds, - selectControlAdaptersSlice, - selectValidControlNets, - selectValidIPAdapters, - selectValidT2IAdapters, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle'; -import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; -import { Fragment, memo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiPlusBold } from 'react-icons/pi'; - -const selector = createMemoizedSelector([selectControlAdaptersSlice], (controlAdapters) => { - const badges: string[] = []; - let isError = false; - - const enabledNonRegionalIPAdapterCount = selectAllIPAdapters(controlAdapters).filter((ca) => ca.isEnabled).length; - - const validIPAdapterCount = selectValidIPAdapters(controlAdapters).length; - if (enabledNonRegionalIPAdapterCount > 0) { - badges.push(`${enabledNonRegionalIPAdapterCount} IP`); - } - if (enabledNonRegionalIPAdapterCount > validIPAdapterCount) { - isError = true; - } - - const enabledControlNetCount = selectAllControlNets(controlAdapters).filter((ca) => ca.isEnabled).length; - const validControlNetCount = selectValidControlNets(controlAdapters).length; - if (enabledControlNetCount > 0) { - badges.push(`${enabledControlNetCount} ControlNet`); - } - if (enabledControlNetCount > validControlNetCount) { - isError = true; - } - - const enabledT2IAdapterCount = selectAllT2IAdapters(controlAdapters).filter((ca) => ca.isEnabled).length; - const validT2IAdapterCount = selectValidT2IAdapters(controlAdapters).length; - if (enabledT2IAdapterCount > 0) { - badges.push(`${enabledT2IAdapterCount} T2I`); - } - if (enabledT2IAdapterCount > validT2IAdapterCount) { - isError = true; - } - - const controlAdapterIds = selectControlAdapterIds(controlAdapters); - - return { - controlAdapterIds, - badges, - isError, // TODO: Add some visual indicator that the control adapters are in an error state - }; -}); - -export const ControlSettingsAccordion: React.FC = memo(() => { - const { t } = useTranslation(); - const { controlAdapterIds, badges } = useAppSelector(selector); - const isControlNetEnabled = useFeatureStatus('controlNet'); - const { isOpen, onToggle } = useStandaloneAccordionToggle({ - id: 'control-settings', - defaultIsOpen: true, - }); - const [addControlNet, isAddControlNetDisabled] = useAddControlAdapter('controlnet'); - const [addIPAdapter, isAddIPAdapterDisabled] = useAddControlAdapter('ip_adapter'); - const [addT2IAdapter, isAddT2IAdapterDisabled] = useAddControlAdapter('t2i_adapter'); - - if (!isControlNetEnabled) { - return null; - } - - return ( - - - - - - - - {controlAdapterIds.map((id, i) => ( - - - - - ))} - - - ); -}); - -ControlSettingsAccordion.displayName = 'ControlAdaptersSettingsAccordion'; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx index 61453be0bad..499a0a307cd 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx @@ -2,7 +2,6 @@ import type { FormLabelProps } from '@invoke-ai/ui-library'; import { Expander, Flex, FormControlGroup, StandaloneAccordion } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; import { HrfSettings } from 'features/hrf/components/HrfSettings'; import { selectHrfSlice } from 'features/hrf/store/hrfSlice'; @@ -20,34 +19,22 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -import { ImageSizeCanvas } from './ImageSizeCanvas'; import { ImageSizeLinear } from './ImageSizeLinear'; const selector = createMemoizedSelector( - [selectGenerationSlice, selectCanvasSlice, selectHrfSlice, selectCanvasV2Slice, activeTabNameSelector], - (generation, canvas, hrf, controlLayers, activeTabName) => { + [selectGenerationSlice, selectHrfSlice, selectCanvasV2Slice, activeTabNameSelector], + (generation, hrf, canvasV2, activeTabName) => { const { shouldRandomizeSeed, model } = generation; const { hrfEnabled } = hrf; const badges: string[] = []; const isSDXL = model?.base === 'sdxl'; - if (activeTabName === 'canvas') { - const { - aspectRatio, - boundingBoxDimensions: { width, height }, - } = canvas; - badges.push(`${width}×${height}`); - badges.push(aspectRatio.id); - if (aspectRatio.isLocked) { - badges.push('locked'); - } - } else { - const { aspectRatio, width, height } = canvasV2.size; - badges.push(`${width}×${height}`); - badges.push(aspectRatio.id); - if (aspectRatio.isLocked) { - badges.push('locked'); - } + const { aspectRatio, width, height } = canvasV2.size; + badges.push(`${width}×${height}`); + badges.push(aspectRatio.id); + + if (aspectRatio.isLocked) { + badges.push('locked'); } if (!shouldRandomizeSeed) { @@ -86,8 +73,8 @@ export const ImageSettingsAccordion = memo(() => { > - {activeTabName === 'canvas' ? : } - {activeTabName === 'canvas' && } + + diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeCanvas.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeCanvas.tsx deleted file mode 100644 index 1dc8f49b783..00000000000 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeCanvas.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { aspectRatioChanged, setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; -import ParamBoundingBoxHeight from 'features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxHeight'; -import ParamBoundingBoxWidth from 'features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxWidth'; -import { AspectRatioIconPreview } from 'features/parameters/components/ImageSize/AspectRatioIconPreview'; -import { ImageSize } from 'features/parameters/components/ImageSize/ImageSize'; -import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; -import { memo, useCallback } from 'react'; - -export const ImageSizeCanvas = memo(() => { - const dispatch = useAppDispatch(); - const { width, height } = useAppSelector((s) => s.canvas.boundingBoxDimensions); - const aspectRatioState = useAppSelector((s) => s.canvas.aspectRatio); - const optimalDimension = useAppSelector(selectOptimalDimension); - - const onChangeWidth = useCallback( - (width: number) => { - if (width === 0) { - return; - } - dispatch(setBoundingBoxDimensions({ width }, optimalDimension)); - }, - [dispatch, optimalDimension] - ); - - const onChangeHeight = useCallback( - (height: number) => { - if (height === 0) { - return; - } - dispatch(setBoundingBoxDimensions({ height }, optimalDimension)); - }, - [dispatch, optimalDimension] - ); - - const onChangeAspectRatioState = useCallback( - (aspectRatioState: AspectRatioState) => { - dispatch(aspectRatioChanged(aspectRatioState)); - }, - [dispatch] - ); - - return ( - } - widthComponent={} - previewComponent={} - onChangeAspectRatioState={onChangeAspectRatioState} - onChangeWidth={onChangeWidth} - onChangeHeight={onChangeHeight} - /> - ); -}); - -ImageSizeCanvas.displayName = 'ImageSizeCanvas'; diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/useClearIntermediates.ts b/invokeai/frontend/web/src/features/system/components/SettingsModal/useClearIntermediates.ts index f392acb5218..6302a16ba55 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/useClearIntermediates.ts +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/useClearIntermediates.ts @@ -1,6 +1,4 @@ import { useAppDispatch } from 'app/store/storeHooks'; -import { resetCanvas } from 'features/canvas/store/canvasSlice'; -import { controlAdaptersReset } from 'features/controlAdapters/store/controlAdaptersSlice'; import { toast } from 'features/toast/toast'; import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -40,8 +38,8 @@ export const useClearIntermediates = (shouldShowClearIntermediates: boolean): Us _clearIntermediates() .unwrap() .then((clearedCount) => { - dispatch(controlAdaptersReset()); - dispatch(resetCanvas()); + // dispatch(controlAdaptersReset()); + // dispatch(resetCanvas()); toast({ id: 'INTERMEDIATES_CLEARED', title: t('settings.intermediatesCleared', { count: clearedCount }), diff --git a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx index b98d713b80d..c5bef7f6db4 100644 --- a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx +++ b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx @@ -16,7 +16,6 @@ import ModelManagerTab from 'features/ui/components/tabs/ModelManagerTab'; import NodesTab from 'features/ui/components/tabs/NodesTab'; import QueueTab from 'features/ui/components/tabs/QueueTab'; import TextToImageTab from 'features/ui/components/tabs/TextToImageTab'; -import UnifiedCanvasTab from 'features/ui/components/tabs/UnifiedCanvasTab'; import type { UsePanelOptions } from 'features/ui/hooks/usePanel'; import { usePanel } from 'features/ui/hooks/usePanel'; import { usePanelStorage } from 'features/ui/hooks/usePanelStorage'; @@ -30,11 +29,10 @@ import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { MdZoomOutMap } from 'react-icons/md'; import { PiFlowArrowBold } from 'react-icons/pi'; -import { RiBox2Line, RiBrushLine, RiInputMethodLine, RiPlayList2Fill } from 'react-icons/ri'; +import { RiBox2Line, RiInputMethodLine, RiPlayList2Fill } from 'react-icons/ri'; import type { ImperativePanelGroupHandle } from 'react-resizable-panels'; import { Panel, PanelGroup } from 'react-resizable-panels'; -import ParametersPanelCanvas from './ParametersPanels/ParametersPanelCanvas'; import ParametersPanelUpscale from './ParametersPanels/ParametersPanelUpscale'; import ResizeHandle from './tabs/ResizeHandle'; import UpscalingTab from './tabs/UpscalingTab'; @@ -55,13 +53,6 @@ const TAB_DATA: Record = { content: , parametersPanel: , }, - canvas: { - id: 'canvas', - translationKey: 'ui.tabs.canvas', - icon: , - content: , - parametersPanel: , - }, upscaling: { id: 'upscaling', translationKey: 'ui.tabs.upscaling', diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx deleted file mode 100644 index d98a736e3bd..00000000000 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { Box, Flex } from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; -import { useAppSelector } from 'app/store/storeHooks'; -import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants'; -import { Prompts } from 'features/parameters/components/Prompts/Prompts'; -import QueueControls from 'features/queue/components/QueueControls'; -import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion'; -import { CompositingSettingsAccordion } from 'features/settingsAccordions/components/CompositingSettingsAccordion/CompositingSettingsAccordion'; -import { ControlSettingsAccordion } from 'features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion'; -import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion'; -import { ImageSettingsAccordion } from 'features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion'; -import { RefinerSettingsAccordion } from 'features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion'; -import { StylePresetMenu } from 'features/stylePresets/components/StylePresetMenu'; -import { StylePresetMenuTrigger } from 'features/stylePresets/components/StylePresetMenuTrigger'; -import { $isMenuOpen } from 'features/stylePresets/store/isMenuOpen'; -import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; -import type { CSSProperties } from 'react'; -import { memo } from 'react'; - -const overlayScrollbarsStyles: CSSProperties = { - height: '100%', - width: '100%', -}; - -const ParametersPanelCanvas = () => { - const isSDXL = useAppSelector((s) => s.generation.model?.base === 'sdxl'); - const isMenuOpen = useStore($isMenuOpen); - - return ( - - - - - - {isMenuOpen && ( - - - - - - )} - - - - - - - - {isSDXL && } - - - - - - - ); -}; - -export default memo(ParametersPanelCanvas); diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx index 6a98798f90b..bf2b915090b 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx @@ -10,7 +10,6 @@ import { Prompts } from 'features/parameters/components/Prompts/Prompts'; import QueueControls from 'features/queue/components/QueueControls'; import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion'; import { CompositingSettingsAccordion } from 'features/settingsAccordions/components/CompositingSettingsAccordion/CompositingSettingsAccordion'; -import { ControlSettingsAccordion } from 'features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion'; import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion'; import { ImageSettingsAccordion } from 'features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion'; import { RefinerSettingsAccordion } from 'features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion'; @@ -44,7 +43,7 @@ const ParametersPanelTextToImage = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const activeTabName = useAppSelector(activeTabNameSelector); - const controlLayersCount = useAppSelector((s) => s.canvasV2.layers.length); + const controlLayersCount = useAppSelector((s) => s.layers.layers.length); const controlLayersTitle = useMemo(() => { if (controlLayersCount === 0) { return t('controlLayers.controlLayers'); @@ -108,8 +107,7 @@ const ParametersPanelTextToImage = () => { - {activeTabName !== 'generation' && } - {activeTabName === 'canvas' && } + {isSDXL && } diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvasTab.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvasTab.tsx deleted file mode 100644 index db2156fbde8..00000000000 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvasTab.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { Flex } from '@invoke-ai/ui-library'; -import IAIDropOverlay from 'common/components/IAIDropOverlay'; -import IAICanvas from 'features/canvas/components/IAICanvas'; -import IAICanvasToolbar from 'features/canvas/components/IAICanvasToolbar/IAICanvasToolbar'; -import { CANVAS_TAB_TESTID } from 'features/canvas/store/constants'; -import { useDroppableTypesafe } from 'features/dnd/hooks/typesafeHooks'; -import type { CanvasInitialImageDropData } from 'features/dnd/types'; -import { isValidDrop } from 'features/dnd/util/isValidDrop'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; - -const droppableData: CanvasInitialImageDropData = { - id: 'canvas-intial-image', - actionType: 'SET_CANVAS_INITIAL_IMAGE', -}; - -const UnifiedCanvasTab = () => { - const { t } = useTranslation(); - const { - isOver, - setNodeRef: setDroppableRef, - active, - } = useDroppableTypesafe({ - id: 'unifiedCanvas', - data: droppableData, - }); - - return ( - - - - {isValidDrop(droppableData, active?.data.current) && ( - - )} - - ); -}; - -export default memo(UnifiedCanvasTab); diff --git a/invokeai/frontend/web/src/features/ui/store/tabMap.tsx b/invokeai/frontend/web/src/features/ui/store/tabMap.tsx index 5cf97b2d3e2..b41c3e1414d 100644 --- a/invokeai/frontend/web/src/features/ui/store/tabMap.tsx +++ b/invokeai/frontend/web/src/features/ui/store/tabMap.tsx @@ -1,3 +1,3 @@ -export const TAB_NUMBER_MAP = ['generation', 'canvas', 'upscaling', 'workflows', 'models', 'queue'] as const; +export const TAB_NUMBER_MAP = ['generation', 'upscaling', 'workflows', 'models', 'queue'] as const; export type InvokeTabName = (typeof TAB_NUMBER_MAP)[number]; diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index d702cf298a5..4b9af802eda 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -195,44 +195,28 @@ export type OutputFields = Extract< // Node Outputs export type ImageOutput = S['ImageOutput']; -// Post-image upload actions, controls workflows when images are uploaded - -type ControlAdapterAction = { - type: 'SET_CONTROL_ADAPTER_IMAGE'; +export type CAImagePostUploadAction = { + type: 'SET_CA_IMAGE'; id: string; }; -export type CALayerImagePostUploadAction = { - type: 'SET_CA_LAYER_IMAGE'; - layerId: string; -}; - export type IPALayerImagePostUploadAction = { - type: 'SET_IPA_LAYER_IMAGE'; - layerId: string; + type: 'SET_IPA_IMAGE'; + id: string; }; -export type RGLayerIPAdapterImagePostUploadAction = { - type: 'SET_RG_LAYER_IP_ADAPTER_IMAGE'; - layerId: string; +export type RGIPAdapterImagePostUploadAction = { + type: 'SET_RG_IP_ADAPTER_IMAGE'; + id: string; ipAdapterId: string; }; -export type IILayerImagePostUploadAction = { - type: 'SET_II_LAYER_IMAGE'; - layerId: string; -}; - type NodesAction = { type: 'SET_NODES_IMAGE'; nodeId: string; fieldName: string; }; -type CanvasInitialImageAction = { - type: 'SET_CANVAS_INITIAL_IMAGE'; -}; - type UpscaleInitialImageAction = { type: 'SET_UPSCALE_INITIAL_IMAGE'; }; @@ -247,13 +231,10 @@ type AddToBatchAction = { }; export type PostUploadAction = - | ControlAdapterAction | NodesAction - | CanvasInitialImageAction | ToastAction | AddToBatchAction - | CALayerImagePostUploadAction + | CAImagePostUploadAction | IPALayerImagePostUploadAction - | RGLayerIPAdapterImagePostUploadAction - | IILayerImagePostUploadAction + | RGIPAdapterImagePostUploadAction | UpscaleInitialImageAction; From 254f4ba5749be60fb082c18766226fa1fc1ca5aa Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 14 Jun 2024 21:59:20 +1000 Subject: [PATCH 036/678] refactor(ui): canvas v2 (wip) --- .../components/ControlAdapter/CAEntity.tsx | 6 +- .../ControlAdapter/CAHeaderItems.tsx | 12 +- .../ControlAdapter/CAProcessorConfig.tsx | 22 +-- .../components/ControlAdapter/CASettings.tsx | 12 +- .../processors/CannyProcessor.tsx | 5 +- .../processors/ColorMapProcessor.tsx | 5 +- .../processors/ContentShuffleProcessor.tsx | 6 +- .../processors/DWOpenposeProcessor.tsx | 6 +- .../processors/DepthAnythingProcessor.tsx | 6 +- .../processors/HedProcessor.tsx | 4 +- .../processors/LineartProcessor.tsx | 4 +- .../processors/MediapipeFaceProcessor.tsx | 5 +- .../processors/MidasDepthProcessor.tsx | 6 +- .../processors/MlsdImageProcessor.tsx | 6 +- .../processors/PidiProcessor.tsx | 4 +- .../ControlAdapter/processors/types.ts | 2 +- .../ControlAndIPAdapter/IPAdapter.tsx | 72 ---------- .../IPAdapterImagePreview.tsx | 113 --------------- .../IPAdapterModelSelect.tsx | 100 -------------- .../components/HeadsUpDisplay.tsx | 2 - .../IPALayer/IPALayerIPAdapterWrapper.tsx | 109 --------------- .../IPALayer.tsx => IPAdapter/IPAEntity.tsx} | 15 +- .../components/IPAdapter/IPAHeaderItems.tsx | 39 ++++++ .../components/IPAdapter/IPAImagePreview.tsx | 100 ++++++++++++++ .../IPAMethod.tsx} | 8 +- .../components/IPAdapter/IPAModelCombobox.tsx | 98 +++++++++++++ .../components/IPAdapter/IPASettings.tsx | 122 ++++++++++++++++ .../controlLayers/components/ToolChooser.tsx | 130 +++++++++++------- .../{Common => common}/BeginEndStepPct.tsx | 0 .../components/{Common => common}/Weight.tsx | 0 .../store/controlAdaptersSlice.ts | 6 + .../controlLayers/store/ipAdaptersSlice.ts | 35 +++-- .../store/regionalGuidanceSlice.ts | 12 ++ .../src/features/ui/components/InvokeTabs.tsx | 11 ++ 34 files changed, 552 insertions(+), 531 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapter.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{IPALayer/IPALayer.tsx => IPAdapter/IPAEntity.tsx} (61%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeaderItems.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter/IPAdapterMethod.tsx => IPAdapter/IPAMethod.tsx} (83%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAModelCombobox.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{Common => common}/BeginEndStepPct.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{Common => common}/Weight.tsx (100%) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntity.tsx index 2840bd39625..f58ba6770c7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntity.tsx @@ -13,17 +13,17 @@ type Props = { export const CAEntity = memo(({ id }: Props) => { const dispatch = useAppDispatch(); const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id); - const disclosure = useDisclosure({ defaultIsOpen: true }); + const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); const onClick = useCallback(() => { dispatch(entitySelected({ id, type: 'control_adapter' })); }, [dispatch, id]); return ( - + - {disclosure.isOpen && ( + {isOpen && ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAHeaderItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAHeaderItems.tsx index 67b2c539d53..3987787af7f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAHeaderItems.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAHeaderItems.tsx @@ -13,7 +13,7 @@ import { caMovedForwardOne, caMovedToBack, caMovedToFront, - selectCA, + selectCAOrThrow, selectControlAdaptersV2Slice, } from 'features/controlLayers/store/controlAdaptersSlice'; import { memo, useCallback } from 'react'; @@ -25,7 +25,6 @@ import { PiArrowUpBold, PiTrashSimpleBold, } from 'react-icons/pi'; -import { assert } from 'tsafe'; type Props = { id: string; @@ -34,8 +33,7 @@ type Props = { const selectValidActions = createAppSelector( [selectControlAdaptersV2Slice, (caState, id: string) => id], (caState, id) => { - const ca = selectCA(caState, id); - assert(ca, `CA with id ${id} not found`); + const ca = selectCAOrThrow(caState, id); const caIndex = caState.controlAdapters.indexOf(ca); const caCount = caState.controlAdapters.length; return { @@ -51,11 +49,7 @@ export const CAHeaderItems = memo(({ id }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const validActions = useAppSelector((s) => selectValidActions(s, id)); - const isEnabled = useAppSelector((s) => { - const ca = selectCA(s.controlAdaptersV2, id); - assert(ca, `CA with id ${id} not found`); - return ca.isEnabled; - }); + const isEnabled = useAppSelector((s) => selectCAOrThrow(s.controlAdaptersV2, id).isEnabled); const onToggle = useCallback(() => { dispatch(caIsEnabledToggled({ id })); }, [dispatch, id]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAProcessorConfig.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAProcessorConfig.tsx index e2f009e48f6..a668c90a81f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAProcessorConfig.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAProcessorConfig.tsx @@ -1,14 +1,14 @@ -import { CannyProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor'; -import { ColorMapProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/ColorMapProcessor'; -import { ContentShuffleProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/ContentShuffleProcessor'; -import { DepthAnythingProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/DepthAnythingProcessor'; -import { DWOpenposeProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/DWOpenposeProcessor'; -import { HedProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/HedProcessor'; -import { LineartProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/LineartProcessor'; -import { MediapipeFaceProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/MediapipeFaceProcessor'; -import { MidasDepthProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/MidasDepthProcessor'; -import { MlsdImageProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/MlsdImageProcessor'; -import { PidiProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/PidiProcessor'; +import { CannyProcessor } from 'features/controlLayers/components/ControlAdapter/processors/CannyProcessor'; +import { ColorMapProcessor } from 'features/controlLayers/components/ControlAdapter/processors/ColorMapProcessor'; +import { ContentShuffleProcessor } from 'features/controlLayers/components/ControlAdapter/processors/ContentShuffleProcessor'; +import { DepthAnythingProcessor } from 'features/controlLayers/components/ControlAdapter/processors/DepthAnythingProcessor'; +import { DWOpenposeProcessor } from 'features/controlLayers/components/ControlAdapter/processors/DWOpenposeProcessor'; +import { HedProcessor } from 'features/controlLayers/components/ControlAdapter/processors/HedProcessor'; +import { LineartProcessor } from 'features/controlLayers/components/ControlAdapter/processors/LineartProcessor'; +import { MediapipeFaceProcessor } from 'features/controlLayers/components/ControlAdapter/processors/MediapipeFaceProcessor'; +import { MidasDepthProcessor } from 'features/controlLayers/components/ControlAdapter/processors/MidasDepthProcessor'; +import { MlsdImageProcessor } from 'features/controlLayers/components/ControlAdapter/processors/MlsdImageProcessor'; +import { PidiProcessor } from 'features/controlLayers/components/ControlAdapter/processors/PidiProcessor'; import type { ProcessorConfig } from 'features/controlLayers/store/types'; import { memo } from 'react'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx index d8920701581..e20b79b8d2f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx @@ -1,7 +1,7 @@ import { Box, Divider, Flex, Icon, IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { BeginEndStepPct } from 'features/controlLayers/components/Common/BeginEndStepPct'; -import { Weight } from 'features/controlLayers/components/Common/Weight'; +import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct'; +import { Weight } from 'features/controlLayers/components/common/Weight'; import { CAControlModeSelect } from 'features/controlLayers/components/ControlAdapter/CAControlModeSelect'; import { CAImagePreview } from 'features/controlLayers/components/ControlAdapter/CAImagePreview'; import { CAModelCombobox } from 'features/controlLayers/components/ControlAdapter/CAModelCombobox'; @@ -15,6 +15,7 @@ import { caProcessedImageChanged, caProcessorConfigChanged, caWeightChanged, + selectCAOrThrow, } from 'features/controlLayers/store/controlAdaptersSlice'; import type { ControlModeV2, ProcessorConfig } from 'features/controlLayers/store/types'; import type { CAImageDropData } from 'features/dnd/types'; @@ -28,7 +29,6 @@ import type { ImageDTO, T2IAdapterModelConfig, } from 'services/api/types'; -import { assert } from 'tsafe'; type Props = { id: string; @@ -39,11 +39,7 @@ export const CASettings = memo(({ id }: Props) => { const { t } = useTranslation(); const [isExpanded, toggleIsExpanded] = useToggle(false); - const controlAdapter = useAppSelector((s) => { - const ca = s.controlAdaptersV2.controlAdapters.find((ca) => ca.id === id); - assert(ca, `ControlAdapter with id ${id} not found`); - return ca; - }); + const controlAdapter = useAppSelector((s) => selectCAOrThrow(s.controlAdaptersV2, id)); const onChangeBeginEndStepPct = useCallback( (beginEndStepPct: [number, number]) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/CannyProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/CannyProcessor.tsx index ef6e4160d6b..d05d1897127 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/CannyProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/CannyProcessor.tsx @@ -1,6 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import { CA_PROCESSOR_DATA, type CannyProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; +import type { CannyProcessorConfig } from 'features/controlLayers/store/types'; +import { CA_PROCESSOR_DATA } from 'features/controlLayers/store/types'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ColorMapProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ColorMapProcessor.tsx index 6faa00dd141..951e4c36dbf 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ColorMapProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ColorMapProcessor.tsx @@ -1,6 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import { CA_PROCESSOR_DATA, type ColorMapProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; +import type { ColorMapProcessorConfig } from 'features/controlLayers/store/types'; +import { CA_PROCESSOR_DATA } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ContentShuffleProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ContentShuffleProcessor.tsx index c03efd27c61..1b7b173287e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ContentShuffleProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ContentShuffleProcessor.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import type { ContentShuffleProcessorConfig } from 'features/controlLayers/util/controlAdapters'; -import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; +import type { ContentShuffleProcessorConfig } from 'features/controlLayers/store/types'; +import { CA_PROCESSOR_DATA } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DWOpenposeProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DWOpenposeProcessor.tsx index 3bbe813dccb..1e157adb2aa 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DWOpenposeProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DWOpenposeProcessor.tsx @@ -1,7 +1,7 @@ import { Flex, FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import type { DWOpenposeProcessorConfig } from 'features/controlLayers/util/controlAdapters'; -import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; +import type { DWOpenposeProcessorConfig } from 'features/controlLayers/store/types'; +import { CA_PROCESSOR_DATA } from 'features/controlLayers/store/types'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DepthAnythingProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DepthAnythingProcessor.tsx index 3cf61581ea4..b4aa76ed6e6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DepthAnythingProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DepthAnythingProcessor.tsx @@ -1,8 +1,8 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import type { DepthAnythingModelSize, DepthAnythingProcessorConfig } from 'features/controlLayers/util/controlAdapters'; -import { isDepthAnythingModelSize } from 'features/controlLayers/util/controlAdapters'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; +import type { DepthAnythingModelSize, DepthAnythingProcessorConfig } from 'features/controlLayers/store/types'; +import { isDepthAnythingModelSize } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/HedProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/HedProcessor.tsx index 1ca75eae2f6..6c27e386c55 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/HedProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/HedProcessor.tsx @@ -1,6 +1,6 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import type { HedProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; +import type { HedProcessorConfig } from 'features/controlLayers/store/types'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/LineartProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/LineartProcessor.tsx index aeb4121a365..4abf7e920c6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/LineartProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/LineartProcessor.tsx @@ -1,6 +1,6 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import type { LineartProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; +import type { LineartProcessorConfig } from 'features/controlLayers/store/types'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MediapipeFaceProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MediapipeFaceProcessor.tsx index 0f45d83ef03..2cde63791e5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MediapipeFaceProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MediapipeFaceProcessor.tsx @@ -1,6 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import { CA_PROCESSOR_DATA, type MediapipeFaceProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; +import type { MediapipeFaceProcessorConfig } from 'features/controlLayers/store/types'; +import { CA_PROCESSOR_DATA } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MidasDepthProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MidasDepthProcessor.tsx index 1ce728984c6..4f66f31a7f3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MidasDepthProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MidasDepthProcessor.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import type { MidasDepthProcessorConfig } from 'features/controlLayers/util/controlAdapters'; -import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; +import type { MidasDepthProcessorConfig } from 'features/controlLayers/store/types'; +import { CA_PROCESSOR_DATA } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MlsdImageProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MlsdImageProcessor.tsx index b6eef311ef6..d578fc8ef32 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MlsdImageProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MlsdImageProcessor.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import type { MlsdProcessorConfig } from 'features/controlLayers/util/controlAdapters'; -import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; +import type { MlsdProcessorConfig } from 'features/controlLayers/store/types'; +import { CA_PROCESSOR_DATA } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/PidiProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/PidiProcessor.tsx index e7d559a1b44..6605baaadf3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/PidiProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/PidiProcessor.tsx @@ -1,6 +1,6 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import type { PidiProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; +import type { PidiProcessorConfig } from 'features/controlLayers/store/types'; import type { ChangeEvent } from 'react'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/types.ts b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/types.ts index 48a0942678d..a9667437a49 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/types.ts @@ -1,4 +1,4 @@ -import type { ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import type { ProcessorConfig } from 'features/controlLayers/store/types'; export type ProcessorComponentProps = { onChange: (config: T) => void; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapter.tsx deleted file mode 100644 index 75a1fa0c6b8..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapter.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Box, Flex } from '@invoke-ai/ui-library'; -import { BeginEndStepPct } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapterBeginEndStepPct'; -import { ControlAdapterWeight } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapterWeight'; -import { IPAdapterImagePreview } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview'; -import { IPAdapterMethod } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod'; -import { IPAdapterModelSelect } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect'; -import type { CLIPVisionModelV2, IPAdapterConfigV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; -import type { TypesafeDroppableData } from 'features/dnd/types'; -import { memo } from 'react'; -import type { ImageDTO, IPAdapterModelConfig, PostUploadAction } from 'services/api/types'; - -type Props = { - ipAdapter: IPAdapterConfigV2; - onChangeBeginEndStepPct: (beginEndStepPct: [number, number]) => void; - onChangeWeight: (weight: number) => void; - onChangeIPMethod: (method: IPMethodV2) => void; - onChangeModel: (modelConfig: IPAdapterModelConfig) => void; - onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModelV2) => void; - onChangeImage: (imageDTO: ImageDTO | null) => void; - droppableData: TypesafeDroppableData; - postUploadAction: PostUploadAction; -}; - -export const IPAdapter = memo( - ({ - ipAdapter, - onChangeBeginEndStepPct, - onChangeWeight, - onChangeIPMethod, - onChangeModel, - onChangeCLIPVisionModel, - onChangeImage, - droppableData, - postUploadAction, - }: Props) => { - return ( - - - - - - - - - - - - - - - - - - ); - } -); - -IPAdapter.displayName = 'IPAdapter'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx deleted file mode 100644 index ed60951513f..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { Flex, useShiftModifier } from '@invoke-ai/ui-library'; -import { skipToken } from '@reduxjs/toolkit/query'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIDndImage from 'common/components/IAIDndImage'; -import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; -import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; -import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; -import type { ImageWithDims } from 'features/controlLayers/util/controlAdapters'; -import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types'; -import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { memo, useCallback, useEffect, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi'; -import { useGetImageDTOQuery } from 'services/api/endpoints/images'; -import type { ImageDTO, PostUploadAction } from 'services/api/types'; - -type Props = { - image: ImageWithDims | null; - onChangeImage: (imageDTO: ImageDTO | null) => void; - ipAdapterId: string; // required for the dnd/upload interactions - droppableData: TypesafeDroppableData; - postUploadAction: PostUploadAction; -}; - -export const IPAdapterImagePreview = memo( - ({ image, onChangeImage, ipAdapterId, droppableData, postUploadAction }: Props) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const isConnected = useAppSelector((s) => s.system.isConnected); - const activeTabName = useAppSelector(activeTabNameSelector); - const optimalDimension = useAppSelector(selectOptimalDimension); - const shift = useShiftModifier(); - - const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(image?.name ?? skipToken); - const handleResetControlImage = useCallback(() => { - onChangeImage(null); - }, [onChangeImage]); - - const handleSetControlImageToDimensions = useCallback(() => { - if (!controlImage) { - return; - } - - if (activeTabName === 'canvas') { - dispatch( - setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension) - ); - } else { - const options = { updateAspectRatio: true, clamp: true }; - if (shift) { - const { width, height } = controlImage; - dispatch(widthChanged({ width, ...options })); - dispatch(heightChanged({ height, ...options })); - } else { - const { width, height } = calculateNewSize( - controlImage.width / controlImage.height, - optimalDimension * optimalDimension - ); - dispatch(widthChanged({ width, ...options })); - dispatch(heightChanged({ height, ...options })); - } - } - }, [controlImage, activeTabName, dispatch, optimalDimension, shift]); - - const draggableData = useMemo(() => { - if (controlImage) { - return { - id: ipAdapterId, - payloadType: 'IMAGE_DTO', - payload: { imageDTO: controlImage }, - }; - } - }, [controlImage, ipAdapterId]); - - useEffect(() => { - if (isConnected && isErrorControlImage) { - handleResetControlImage(); - } - }, [handleResetControlImage, isConnected, isErrorControlImage]); - - return ( - - - - {controlImage && ( - - } - tooltip={t('controlnet.resetControlImage')} - /> - } - tooltip={ - shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions') - } - /> - - )} - - ); - } -); - -IPAdapterImagePreview.displayName = 'IPAdapterImagePreview'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect.tsx deleted file mode 100644 index b0541dca2c5..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import type { ComboboxOnChange } from '@invoke-ai/ui-library'; -import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; -import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; -import type { CLIPVisionModelV2 } from 'features/controlLayers/util/controlAdapters'; -import { isCLIPVisionModelV2 } from 'features/controlLayers/util/controlAdapters'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useIPAdapterModels } from 'services/api/hooks/modelsByType'; -import type { AnyModelConfig, IPAdapterModelConfig } from 'services/api/types'; -import { assert } from 'tsafe'; - -const CLIP_VISION_OPTIONS = [ - { label: 'ViT-H', value: 'ViT-H' }, - { label: 'ViT-G', value: 'ViT-G' }, -]; - -type Props = { - modelKey: string | null; - onChangeModel: (modelConfig: IPAdapterModelConfig) => void; - clipVisionModel: CLIPVisionModelV2; - onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModelV2) => void; -}; - -export const IPAdapterModelSelect = memo( - ({ modelKey, onChangeModel, clipVisionModel, onChangeCLIPVisionModel }: Props) => { - const { t } = useTranslation(); - const currentBaseModel = useAppSelector((s) => s.generation.model?.base); - const [modelConfigs, { isLoading }] = useIPAdapterModels(); - const selectedModel = useMemo(() => modelConfigs.find((m) => m.key === modelKey), [modelConfigs, modelKey]); - - const _onChangeModel = useCallback( - (modelConfig: IPAdapterModelConfig | null) => { - if (!modelConfig) { - return; - } - onChangeModel(modelConfig); - }, - [onChangeModel] - ); - - const _onChangeCLIPVisionModel = useCallback( - (v) => { - assert(isCLIPVisionModelV2(v?.value)); - onChangeCLIPVisionModel(v.value); - }, - [onChangeCLIPVisionModel] - ); - - const getIsDisabled = useCallback( - (model: AnyModelConfig): boolean => { - const isCompatible = currentBaseModel === model.base; - const hasMainModel = Boolean(currentBaseModel); - return !hasMainModel || !isCompatible; - }, - [currentBaseModel] - ); - - const { options, value, onChange, noOptionsMessage } = useGroupedModelCombobox({ - modelConfigs, - onChange: _onChangeModel, - selectedModel, - getIsDisabled, - isLoading, - }); - - const clipVisionModelValue = useMemo( - () => CLIP_VISION_OPTIONS.find((o) => o.value === clipVisionModel), - [clipVisionModel] - ); - - return ( - - - - - - - {selectedModel?.format === 'checkpoint' && ( - - - - )} - - ); - } -); - -IPAdapterModelSelect.displayName = 'IPAdapterModelSelect'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx index 2aea3a422cd..602b29914d6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx @@ -7,7 +7,6 @@ import { memo } from 'react'; export const HeadsUpDisplay = memo(() => { const stageAttrs = useStore($stageAttrs); - const layerCount = useAppSelector((s) => s.canvasV2.layers.length); const bbox = useAppSelector((s) => s.canvasV2.bbox); return ( @@ -15,7 +14,6 @@ export const HeadsUpDisplay = memo(() => { - diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx deleted file mode 100644 index 7620fa57b66..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { IPAdapter } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapter'; -import { - caOrIPALayerBeginEndStepPctChanged, - caOrIPALayerWeightChanged, - ipAdapterCLIPVisionModelChanged, - ipAdapterImageChanged, - ipAdapterMethodChanged, - ipAdapterModelChanged, - selectLayerOrThrow, -} from 'features/controlLayers/store/controlLayersSlice'; -import { isIPAdapterLayer } from 'features/controlLayers/store/types'; -import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; -import type { IPAImageDropData } from 'features/dnd/types'; -import { memo, useCallback, useMemo } from 'react'; -import type { ImageDTO, IPAdapterModelConfig, IPALayerImagePostUploadAction } from 'services/api/types'; - -type Props = { - layerId: string; -}; - -export const IPALayerIPAdapterWrapper = memo(({ layerId }: Props) => { - const dispatch = useAppDispatch(); - const ipAdapter = useAppSelector( - (s) => selectLayerOrThrow(s.canvasV2, layerId, isIPAdapterLayer).ipAdapter - ); - - const onChangeBeginEndStepPct = useCallback( - (beginEndStepPct: [number, number]) => { - dispatch( - caOrIPALayerBeginEndStepPctChanged({ - layerId, - beginEndStepPct, - }) - ); - }, - [dispatch, layerId] - ); - - const onChangeWeight = useCallback( - (weight: number) => { - dispatch(caOrIPALayerWeightChanged({ layerId, weight })); - }, - [dispatch, layerId] - ); - - const onChangeIPMethod = useCallback( - (method: IPMethodV2) => { - dispatch(ipAdapterMethodChanged({ layerId, method })); - }, - [dispatch, layerId] - ); - - const onChangeModel = useCallback( - (modelConfig: IPAdapterModelConfig) => { - dispatch(ipAdapterModelChanged({ layerId, modelConfig })); - }, - [dispatch, layerId] - ); - - const onChangeCLIPVisionModel = useCallback( - (clipVisionModel: CLIPVisionModelV2) => { - dispatch(ipAdapterCLIPVisionModelChanged({ layerId, clipVisionModel })); - }, - [dispatch, layerId] - ); - - const onChangeImage = useCallback( - (imageDTO: ImageDTO | null) => { - dispatch(ipAdapterImageChanged({ layerId, imageDTO })); - }, - [dispatch, layerId] - ); - - const droppableData = useMemo( - () => ({ - actionType: 'SET_IPA_LAYER_IMAGE', - context: { - layerId, - }, - id: layerId, - }), - [layerId] - ); - - const postUploadAction = useMemo( - () => ({ - type: 'SET_IPA_LAYER_IMAGE', - layerId, - }), - [layerId] - ); - - return ( - - ); -}); - -IPALayerIPAdapterWrapper.displayName = 'IPALayerIPAdapterWrapper'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAEntity.tsx similarity index 61% rename from invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAEntity.tsx index e4b89dfe216..f000045179d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAEntity.tsx @@ -1,9 +1,7 @@ -import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; +import { Flex, useDisclosure } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { IPALayerIPAdapterWrapper } from 'features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper'; -import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; -import { EntityTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; -import { EntityEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; +import { IPAHeaderItems } from 'features/controlLayers/components/IPAdapter/IPAHeaderItems'; +import { IPASettings } from 'features/controlLayers/components/IPAdapter/IPASettings'; import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; import { entitySelected } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback } from 'react'; @@ -23,14 +21,11 @@ export const IPAEntity = memo(({ id }: Props) => { return ( - - - - + {isOpen && ( - + )} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeaderItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeaderItems.tsx new file mode 100644 index 00000000000..d4de4f7f6f6 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeaderItems.tsx @@ -0,0 +1,39 @@ +import { Spacer } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { EntityDeleteButton } from 'features/controlLayers/components/LayerCommon/EntityDeleteButton'; +import { EntityEnabledToggle } from 'features/controlLayers/components/LayerCommon/EntityEnabledToggle'; +import { EntityTitle } from 'features/controlLayers/components/LayerCommon/EntityTitle'; +import { + ipaDeleted, + ipaIsEnabledToggled, + selectIPAOrThrow, +} from 'features/controlLayers/store/ipAdaptersSlice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +type Props = { + id: string; +}; + +export const IPAHeaderItems = memo(({ id }: Props) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isEnabled = useAppSelector((s) => selectIPAOrThrow(s.ipAdapters, id).isEnabled); + const onToggle = useCallback(() => { + dispatch(ipaIsEnabledToggled({ id })); + }, [dispatch, id]); + const onDelete = useCallback(() => { + dispatch(ipaDeleted({ id })); + }, [dispatch, id]); + + return ( + <> + + + + + + ); +}); + +IPAHeaderItems.displayName = 'IPAHeaderItems'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx new file mode 100644 index 00000000000..47fd0f5c92e --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx @@ -0,0 +1,100 @@ +import { Flex, useShiftModifier } from '@invoke-ai/ui-library'; +import { skipToken } from '@reduxjs/toolkit/query'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import IAIDndImage from 'common/components/IAIDndImage'; +import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; +import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; +import type { ImageWithDims } from 'features/controlLayers/store/types'; +import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types'; +import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; +import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; +import { memo, useCallback, useEffect, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi'; +import { useGetImageDTOQuery } from 'services/api/endpoints/images'; +import type { ImageDTO, PostUploadAction } from 'services/api/types'; + +type Props = { + image: ImageWithDims | null; + onChangeImage: (imageDTO: ImageDTO | null) => void; + ipAdapterId: string; // required for the dnd/upload interactions + droppableData: TypesafeDroppableData; + postUploadAction: PostUploadAction; +}; + +export const IPAImagePreview = memo(({ image, onChangeImage, ipAdapterId, droppableData, postUploadAction }: Props) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isConnected = useAppSelector((s) => s.system.isConnected); + const optimalDimension = useAppSelector(selectOptimalDimension); + const shift = useShiftModifier(); + + const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(image?.name ?? skipToken); + const handleResetControlImage = useCallback(() => { + onChangeImage(null); + }, [onChangeImage]); + + const handleSetControlImageToDimensions = useCallback(() => { + if (!controlImage) { + return; + } + + const options = { updateAspectRatio: true, clamp: true }; + if (shift) { + const { width, height } = controlImage; + dispatch(widthChanged({ width, ...options })); + dispatch(heightChanged({ height, ...options })); + } else { + const { width, height } = calculateNewSize( + controlImage.width / controlImage.height, + optimalDimension * optimalDimension + ); + dispatch(widthChanged({ width, ...options })); + dispatch(heightChanged({ height, ...options })); + } + }, [controlImage, dispatch, optimalDimension, shift]); + + const draggableData = useMemo(() => { + if (controlImage) { + return { + id: ipAdapterId, + payloadType: 'IMAGE_DTO', + payload: { imageDTO: controlImage }, + }; + } + }, [controlImage, ipAdapterId]); + + useEffect(() => { + if (isConnected && isErrorControlImage) { + handleResetControlImage(); + } + }, [handleResetControlImage, isConnected, isErrorControlImage]); + + return ( + + + + {controlImage && ( + + } + tooltip={t('controlnet.resetControlImage')} + /> + } + tooltip={shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')} + /> + + )} + + ); +}); + +IPAImagePreview.displayName = 'IPAImagePreview'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAMethod.tsx similarity index 83% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAMethod.tsx index 4f6a468fc3c..55c99fa6f7a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAMethod.tsx @@ -1,8 +1,8 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import type { IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; -import { isIPMethodV2 } from 'features/controlLayers/util/controlAdapters'; +import type { IPMethodV2} from 'features/controlLayers/store/types'; +import { isIPMethodV2 } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { assert } from 'tsafe'; @@ -12,7 +12,7 @@ type Props = { onChange: (method: IPMethodV2) => void; }; -export const IPAdapterMethod = memo(({ method, onChange }: Props) => { +export const IPAMethod = memo(({ method, onChange }: Props) => { const { t } = useTranslation(); const options: { label: string; value: IPMethodV2 }[] = useMemo( () => [ @@ -41,4 +41,4 @@ export const IPAdapterMethod = memo(({ method, onChange }: Props) => { ); }); -IPAdapterMethod.displayName = 'IPAdapterMethod'; +IPAMethod.displayName = 'IPAMethod'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAModelCombobox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAModelCombobox.tsx new file mode 100644 index 00000000000..08c3faeb2db --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAModelCombobox.tsx @@ -0,0 +1,98 @@ +import type { ComboboxOnChange } from '@invoke-ai/ui-library'; +import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; +import type { CLIPVisionModelV2} from 'features/controlLayers/store/types'; +import { isCLIPVisionModelV2 } from 'features/controlLayers/store/types'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useIPAdapterModels } from 'services/api/hooks/modelsByType'; +import type { AnyModelConfig, IPAdapterModelConfig } from 'services/api/types'; +import { assert } from 'tsafe'; + +const CLIP_VISION_OPTIONS = [ + { label: 'ViT-H', value: 'ViT-H' }, + { label: 'ViT-G', value: 'ViT-G' }, +]; + +type Props = { + modelKey: string | null; + onChangeModel: (modelConfig: IPAdapterModelConfig) => void; + clipVisionModel: CLIPVisionModelV2; + onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModelV2) => void; +}; + +export const IPAModelCombobox = memo(({ modelKey, onChangeModel, clipVisionModel, onChangeCLIPVisionModel }: Props) => { + const { t } = useTranslation(); + const currentBaseModel = useAppSelector((s) => s.generation.model?.base); + const [modelConfigs, { isLoading }] = useIPAdapterModels(); + const selectedModel = useMemo(() => modelConfigs.find((m) => m.key === modelKey), [modelConfigs, modelKey]); + + const _onChangeModel = useCallback( + (modelConfig: IPAdapterModelConfig | null) => { + if (!modelConfig) { + return; + } + onChangeModel(modelConfig); + }, + [onChangeModel] + ); + + const _onChangeCLIPVisionModel = useCallback( + (v) => { + assert(isCLIPVisionModelV2(v?.value)); + onChangeCLIPVisionModel(v.value); + }, + [onChangeCLIPVisionModel] + ); + + const getIsDisabled = useCallback( + (model: AnyModelConfig): boolean => { + const isCompatible = currentBaseModel === model.base; + const hasMainModel = Boolean(currentBaseModel); + return !hasMainModel || !isCompatible; + }, + [currentBaseModel] + ); + + const { options, value, onChange, noOptionsMessage } = useGroupedModelCombobox({ + modelConfigs, + onChange: _onChangeModel, + selectedModel, + getIsDisabled, + isLoading, + }); + + const clipVisionModelValue = useMemo( + () => CLIP_VISION_OPTIONS.find((o) => o.value === clipVisionModel), + [clipVisionModel] + ); + + return ( + + + + + + + {selectedModel?.format === 'checkpoint' && ( + + + + )} + + ); +}); + +IPAModelCombobox.displayName = 'IPAModelCombobox'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx new file mode 100644 index 00000000000..1daacb941fd --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx @@ -0,0 +1,122 @@ +import { Box, Flex } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct'; +import { Weight } from 'features/controlLayers/components/common/Weight'; +import { IPAMethod } from 'features/controlLayers/components/IPAdapter/IPAMethod'; +import { + ipaBeginEndStepPctChanged, + ipaCLIPVisionModelChanged, + ipaImageChanged, + ipaMethodChanged, + ipaModelChanged, + ipaWeightChanged, + selectIPAOrThrow, +} from 'features/controlLayers/store/ipAdaptersSlice'; +import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types'; +import type { IPAImageDropData } from 'features/dnd/types'; +import { memo, useCallback, useMemo } from 'react'; +import type { ImageDTO, IPAdapterModelConfig, IPALayerImagePostUploadAction } from 'services/api/types'; + +import { IPAImagePreview } from './IPAImagePreview'; +import { IPAModelCombobox } from './IPAModelCombobox'; + +type Props = { + id: string; +}; + +export const IPASettings = memo(({ id }: Props) => { + const dispatch = useAppDispatch(); + const ipAdapter = useAppSelector((s) => selectIPAOrThrow(s.ipAdapters, id)); + + const onChangeBeginEndStepPct = useCallback( + (beginEndStepPct: [number, number]) => { + dispatch(ipaBeginEndStepPctChanged({ id, beginEndStepPct })); + }, + [dispatch, id] + ); + + const onChangeWeight = useCallback( + (weight: number) => { + dispatch(ipaWeightChanged({ id, weight })); + }, + [dispatch, id] + ); + + const onChangeIPMethod = useCallback( + (method: IPMethodV2) => { + dispatch(ipaMethodChanged({ id, method })); + }, + [dispatch, id] + ); + + const onChangeModel = useCallback( + (modelConfig: IPAdapterModelConfig) => { + dispatch(ipaModelChanged({ id, modelConfig })); + }, + [dispatch, id] + ); + + const onChangeCLIPVisionModel = useCallback( + (clipVisionModel: CLIPVisionModelV2) => { + dispatch(ipaCLIPVisionModelChanged({ id, clipVisionModel })); + }, + [dispatch, id] + ); + + const onChangeImage = useCallback( + (imageDTO: ImageDTO | null) => { + dispatch(ipaImageChanged({ id, imageDTO })); + }, + [dispatch, id] + ); + + const droppableData = useMemo( + () => ({ + actionType: 'SET_IPA_IMAGE', + context: { id }, + id, + }), + [id] + ); + + const postUploadAction = useMemo( + () => ({ + type: 'SET_IPA_IMAGE', + id, + }), + [id] + ); + + return ( + + + + + + + + + + + + + + + + + + ); +}); + +IPASettings.displayName = 'IPASettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx index d9eadd1a31e..6434ef402d4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx @@ -1,14 +1,13 @@ import { ButtonGroup, IconButton } from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; -import { createSelector } from '@reduxjs/toolkit'; +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { - $tool, - layerReset, - selectCanvasV2Slice, - selectedLayerDeleted, -} from 'features/controlLayers/store/controlLayersSlice'; -import { useCallback } from 'react'; +import { caDeleted } from 'features/controlLayers/store/controlAdaptersSlice'; +import { selectCanvasV2Slice, toolChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { ipaDeleted } from 'features/controlLayers/store/ipAdaptersSlice'; +import { layerDeleted, layerReset } from 'features/controlLayers/store/layersSlice'; +import { rgDeleted, rgReset } from 'features/controlLayers/store/regionalGuidanceSlice'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { useCallback, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { @@ -20,55 +19,94 @@ import { PiRectangleBold, } from 'react-icons/pi'; -const selectIsDisabled = createSelector(selectCanvasV2Slice, (controlLayers) => { - const selectedLayer = canvasV2.layers.find((l) => l.id === canvasV2.selectedLayerId); - return selectedLayer?.type !== 'regional_guidance_layer' && selectedLayer?.type !== 'raster_layer'; -}); +const DRAWING_TOOL_TYPES = ['layer', 'regional_guidance', 'inpaint_mask']; + +const getIsDrawingToolEnabled = (entityIdentifier: CanvasEntityIdentifier | null) => { + if (!entityIdentifier) { + return false; + } + return DRAWING_TOOL_TYPES.includes(entityIdentifier.type); +}; + +const selectSelectedEntityIdentifier = createMemoizedSelector( + selectCanvasV2Slice, + (canvasV2State) => canvasV2State.selectedEntityIdentifier +); export const ToolChooser: React.FC = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const isDisabled = useAppSelector(selectIsDisabled); - const selectedLayerId = useAppSelector((s) => s.canvasV2.selectedLayerId); - const tool = useStore($tool); + const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); + const isDrawingToolDisabled = useMemo( + () => !getIsDrawingToolEnabled(selectedEntityIdentifier), + [selectedEntityIdentifier] + ); + const isMoveToolDisabled = useMemo(() => selectedEntityIdentifier === null, [selectedEntityIdentifier]); + const tool = useAppSelector((s) => s.canvasV2.tool.selected); const setToolToBrush = useCallback(() => { - $tool.set('brush'); - }, []); - useHotkeys('b', setToolToBrush, { enabled: !isDisabled }, [isDisabled]); + dispatch(toolChanged('brush')); + }, [dispatch]); + useHotkeys('b', setToolToBrush, { enabled: !isDrawingToolDisabled }, [isDrawingToolDisabled, setToolToBrush]); const setToolToEraser = useCallback(() => { - $tool.set('eraser'); - }, []); - useHotkeys('e', setToolToEraser, { enabled: !isDisabled }, [isDisabled]); + dispatch(toolChanged('eraser')); + }, [dispatch]); + useHotkeys('e', setToolToEraser, { enabled: !isDrawingToolDisabled }, [isDrawingToolDisabled, setToolToEraser]); const setToolToRect = useCallback(() => { - $tool.set('rect'); - }, []); - useHotkeys('u', setToolToRect, { enabled: !isDisabled }, [isDisabled]); + dispatch(toolChanged('rect')); + }, [dispatch]); + useHotkeys('u', setToolToRect, { enabled: !isDrawingToolDisabled }, [isDrawingToolDisabled, setToolToRect]); const setToolToMove = useCallback(() => { - $tool.set('move'); - }, []); - useHotkeys('v', setToolToMove, { enabled: !isDisabled }, [isDisabled]); + dispatch(toolChanged('move')); + }, [dispatch]); + useHotkeys('v', setToolToMove, { enabled: !isMoveToolDisabled }, [isMoveToolDisabled, setToolToMove]); const setToolToView = useCallback(() => { - $tool.set('view'); - }, []); - useHotkeys('h', setToolToView, { enabled: !isDisabled }, [isDisabled]); + dispatch(toolChanged('view')); + }, [dispatch]); + useHotkeys('h', setToolToView, [setToolToView]); const setToolToBbox = useCallback(() => { - $tool.set('bbox'); - }, []); - useHotkeys('q', setToolToBbox, { enabled: !isDisabled }, [isDisabled]); + dispatch(toolChanged('bbox')); + }, [dispatch]); + useHotkeys('q', setToolToBbox, [setToolToBbox]); const resetSelectedLayer = useCallback(() => { - if (selectedLayerId === null) { + if (selectedEntityIdentifier === null) { return; } - dispatch(layerReset(selectedLayerId)); - }, [dispatch, selectedLayerId]); - useHotkeys('shift+c', resetSelectedLayer); + const { type, id } = selectedEntityIdentifier; + if (type === 'layer') { + dispatch(layerReset({ id })); + } + if (type === 'regional_guidance') { + dispatch(rgReset({ id })); + } + }, [dispatch, selectedEntityIdentifier]); + const isResetEnabled = useMemo( + () => selectedEntityIdentifier?.type === 'layer' || selectedEntityIdentifier?.type === 'regional_guidance', + [selectedEntityIdentifier] + ); + useHotkeys('shift+c', resetSelectedLayer, { enabled: isResetEnabled }, [isResetEnabled, resetSelectedLayer]); const deleteSelectedLayer = useCallback(() => { - dispatch(selectedLayerDeleted()); - }, [dispatch]); - useHotkeys('shift+d', deleteSelectedLayer); + if (selectedEntityIdentifier === null) { + return; + } + const { type, id } = selectedEntityIdentifier; + if (type === 'layer') { + dispatch(layerDeleted({ id })); + } + if (type === 'regional_guidance') { + dispatch(rgDeleted({ id })); + } + if (type === 'control_adapter') { + dispatch(caDeleted({ id })); + } + if (type === 'ip_adapter') { + dispatch(ipaDeleted({ id })); + } + }, [dispatch, selectedEntityIdentifier]); + const isDeleteEnabled = useMemo(() => selectedEntityIdentifier !== null, [selectedEntityIdentifier]); + useHotkeys('shift+d', deleteSelectedLayer, { enabled: isDeleteEnabled }, [isDeleteEnabled, deleteSelectedLayer]); return ( @@ -78,7 +116,7 @@ export const ToolChooser: React.FC = () => { icon={} variant={tool === 'brush' ? 'solid' : 'outline'} onClick={setToolToBrush} - isDisabled={isDisabled} + isDisabled={isDrawingToolDisabled} /> { icon={} variant={tool === 'eraser' ? 'solid' : 'outline'} onClick={setToolToEraser} - isDisabled={isDisabled} + isDisabled={isDrawingToolDisabled} /> { icon={} variant={tool === 'rect' ? 'solid' : 'outline'} onClick={setToolToRect} - isDisabled={isDisabled} + isDisabled={isDrawingToolDisabled} /> { icon={} variant={tool === 'move' ? 'solid' : 'outline'} onClick={setToolToMove} - isDisabled={isDisabled} + isDisabled={isMoveToolDisabled} /> { icon={} variant={tool === 'view' ? 'solid' : 'outline'} onClick={setToolToView} - isDisabled={isDisabled} /> { icon={} variant={tool === 'bbox' ? 'solid' : 'outline'} onClick={setToolToBbox} - isDisabled={isDisabled} /> ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Common/BeginEndStepPct.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/BeginEndStepPct.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/Common/BeginEndStepPct.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/common/BeginEndStepPct.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Common/Weight.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/Weight.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/Common/Weight.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/common/Weight.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts index 6a98523bef5..8faa0a900b5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts @@ -6,6 +6,7 @@ import { zModelIdentifierField } from 'features/nodes/types/common'; import type { IRect } from 'konva/lib/types'; import { isEqual } from 'lodash-es'; import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types'; +import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; import type { ControlAdapterConfig, ControlAdapterData, ControlModeV2, Filter, ProcessorConfig } from './types'; @@ -22,6 +23,11 @@ const initialState: ControlAdaptersV2State = { }; export const selectCA = (state: ControlAdaptersV2State, id: string) => state.controlAdapters.find((ca) => ca.id === id); +export const selectCAOrThrow = (state: ControlAdaptersV2State, id: string) => { + const ca = selectCA(state, id); + assert(ca, `Control Adapter with id ${id} not found`); + return ca; +}; export const controlAdaptersV2Slice = createSlice({ name: 'controlAdaptersV2', diff --git a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersSlice.ts index 7424d41903f..9e9a9541aee 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersSlice.ts @@ -1,13 +1,13 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; -import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; -import { imageDTOToImageWithDims } from 'features/controlLayers/util/controlAdapters'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; +import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; -import type { IPAdapterConfig, IPAdapterData } from './types'; +import type { CLIPVisionModelV2, IPAdapterConfig, IPAdapterData, IPMethodV2 } from './types'; +import { imageDTOToImageWithDims } from './types'; type IPAdaptersState = { _version: 1; @@ -19,7 +19,12 @@ const initialState: IPAdaptersState = { ipAdapters: [], }; -const selectIpa = (state: IPAdaptersState, id: string) => state.ipAdapters.find((ipa) => ipa.id === id); +export const selectIPA = (state: IPAdaptersState, id: string) => state.ipAdapters.find((ipa) => ipa.id === id); +export const selectIPAOrThrow = (state: IPAdaptersState, id: string) => { + const ipa = selectIPA(state, id); + assert(ipa, `IP Adapter with id ${id} not found`); + return ipa; +}; export const ipAdaptersSlice = createSlice({ name: 'ipAdapters', @@ -41,11 +46,11 @@ export const ipAdaptersSlice = createSlice({ ipaRecalled: (state, action: PayloadAction<{ data: IPAdapterData }>) => { state.ipAdapters.push(action.payload.data); }, - ipaIsEnabledChanged: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => { - const { id, isEnabled } = action.payload; - const ipa = selectIpa(state, id); + ipaIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const ipa = selectIPA(state, id); if (ipa) { - ipa.isEnabled = isEnabled; + ipa.isEnabled = !ipa.isEnabled; } }, ipaDeleted: (state, action: PayloadAction<{ id: string }>) => { @@ -53,7 +58,7 @@ export const ipAdaptersSlice = createSlice({ }, ipaImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => { const { id, imageDTO } = action.payload; - const ipa = selectIpa(state, id); + const ipa = selectIPA(state, id); if (!ipa) { return; } @@ -61,7 +66,7 @@ export const ipAdaptersSlice = createSlice({ }, ipaMethodChanged: (state, action: PayloadAction<{ id: string; method: IPMethodV2 }>) => { const { id, method } = action.payload; - const ipa = selectIpa(state, id); + const ipa = selectIPA(state, id); if (!ipa) { return; } @@ -75,7 +80,7 @@ export const ipAdaptersSlice = createSlice({ }> ) => { const { id, modelConfig } = action.payload; - const ipa = selectIpa(state, id); + const ipa = selectIPA(state, id); if (!ipa) { return; } @@ -87,7 +92,7 @@ export const ipAdaptersSlice = createSlice({ }, ipaCLIPVisionModelChanged: (state, action: PayloadAction<{ id: string; clipVisionModel: CLIPVisionModelV2 }>) => { const { id, clipVisionModel } = action.payload; - const ipa = selectIpa(state, id); + const ipa = selectIPA(state, id); if (!ipa) { return; } @@ -95,7 +100,7 @@ export const ipAdaptersSlice = createSlice({ }, ipaWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { const { id, weight } = action.payload; - const ipa = selectIpa(state, id); + const ipa = selectIPA(state, id); if (!ipa) { return; } @@ -103,7 +108,7 @@ export const ipAdaptersSlice = createSlice({ }, ipaBeginEndStepPctChanged: (state, action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }>) => { const { id, beginEndStepPct } = action.payload; - const ipa = selectIpa(state, id); + const ipa = selectIPA(state, id); if (!ipa) { return; } @@ -115,7 +120,7 @@ export const ipAdaptersSlice = createSlice({ export const { ipaAdded, ipaRecalled, - ipaIsEnabledChanged, + ipaIsEnabledToggled, ipaDeleted, ipaImageChanged, ipaMethodChanged, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts index 182693abcce..cb41306b7e8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts @@ -87,6 +87,17 @@ export const regionalGuidanceSlice = createSlice({ }, prepare: () => ({ payload: { id: uuidv4() } }), }, + rgReset: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + rg.objects = []; + rg.bbox = null; + rg.bboxNeedsUpdate = false; + rg.imageCache = null; + }, rgRecalled: (state, action: PayloadAction<{ data: RegionalGuidanceData }>) => { const { data } = action.payload; state.regions.push(data); @@ -388,6 +399,7 @@ export const regionalGuidanceSlice = createSlice({ export const { rgAdded, rgRecalled, + rgReset, rgIsEnabledToggled, rgTranslated, rgBboxChanged, diff --git a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx index c5bef7f6db4..b78b643731e 100644 --- a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx +++ b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx @@ -290,3 +290,14 @@ const InvokeTabs = () => { }; export default memo(InvokeTabs); + +const ParametersPanelComponent = memo(() => { + const activeTabName = useAppSelector(activeTabNameSelector); + + if (activeTabName === 'workflows') { + return ; + } else { + return ; + } +}); +ParametersPanelComponent.displayName = 'ParametersPanelComponent'; From f72845a1b48793385bac96ab6226e1d783bc4aad Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 15 Jun 2024 11:38:24 +1000 Subject: [PATCH 037/678] refactor(ui): canvas v2 (wip) Redo all UI components for different canvas entity types --- .../src/app/store/createMemoizedSelector.ts | 1 + .../components/ControlAdapter/CA.tsx | 29 ++++ .../ControlAdapter/CAActionsMenu.tsx | 87 +++++++++++ .../components/ControlAdapter/CAEntity.tsx | 35 ----- .../ControlAdapter/CAEntityHeader.tsx | 41 ++++++ .../ControlAdapter/CAHeaderItems.tsx | 103 ------------- .../components/ControlAdapter/CASettings.tsx | 99 +++++++------ .../components/ControlLayersPanelContent.tsx | 4 +- .../ControlLayersSettingsPopover.tsx | 13 +- .../components/IILayer/IILayer.tsx | 92 ------------ .../IILayer/InitialImagePreview.tsx | 111 -------------- .../components/IPAdapter/IPA.tsx | 29 ++++ .../components/IPAdapter/IPAEntity.tsx | 35 ----- .../components/IPAdapter/IPAHeader.tsx | 37 +++++ .../components/IPAdapter/IPAHeaderItems.tsx | 39 ----- .../components/IPAdapter/IPASettings.tsx | 71 ++++----- .../controlLayers/components/Layer/Layer.tsx | 29 ++++ .../components/Layer/LayerActionsMenu.tsx | 87 +++++++++++ .../components/Layer/LayerHeader.tsx | 42 ++++++ .../{LayerCommon => Layer}/LayerOpacity.tsx | 28 +--- .../components/Layer/LayerSettings.tsx | 24 +++ .../components/LayerCommon/EntityMenu.tsx | 71 --------- .../LayerCommon/LayerMenuArrangeActions.tsx | 69 --------- .../LayerCommon/LayerMenuRGActions.tsx | 56 ------- .../components/LayerCommon/LayerWrapper.tsx | 31 ---- ...skLayerOpacity.tsx => RGGlobalOpacity.tsx} | 23 ++- .../components/RGLayer/RGLayer.tsx | 80 ---------- .../RGLayer/RGLayerAutoNegativeCheckbox.tsx | 48 ------ .../components/RGLayer/RGLayerColorPicker.tsx | 66 --------- .../RGLayer/RGLayerIPAdapterList.tsx | 46 ------ .../RGLayer/RGLayerIPAdapterWrapper.tsx | 131 ----------------- .../RGLayer/RGLayerPromptDeleteButton.tsx | 38 ----- .../RGLayer/RGLayerSettingsPopover.tsx | 55 ------- .../components/RasterLayer/RasterLayer.tsx | 58 -------- .../components/RegionalGuidance/RG.tsx | 31 ++++ .../RegionalGuidance/RGActionsMenu.tsx | 119 +++++++++++++++ .../RegionalGuidance/RGDeletePromptButton.tsx | 24 +++ .../components/RegionalGuidance/RGHeader.tsx | 50 +++++++ .../RegionalGuidance/RGIPAdapterSettings.tsx | 139 ++++++++++++++++++ .../RegionalGuidance/RGIPAdapters.tsx | 34 +++++ .../RGMaskFillColorPicker.tsx | 50 +++++++ .../RGNegativePrompt.tsx} | 24 +-- .../RGPositivePrompt.tsx} | 24 +-- .../RegionalGuidance/RGSettings.tsx | 30 ++++ .../RegionalGuidance/RGSettingsPopover.tsx | 64 ++++++++ .../common/CanvasEntityContainer.tsx | 38 +++++ .../CanvasEntityDeleteButton.tsx} | 4 +- .../CanvasEntityEnabledToggle.tsx} | 4 +- .../components/common/CanvasEntityHeader.tsx | 15 ++ .../CanvasEntityMenuButton.tsx} | 4 +- .../common/CanvasEntitySettings.tsx | 13 ++ .../CanvasEntityTitle.tsx} | 4 +- .../controlLayers/store/layersSlice.ts | 16 +- .../store/regionalGuidanceSlice.ts | 63 ++++---- 54 files changed, 1196 insertions(+), 1362 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntity.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityHeader.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAHeaderItems.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IILayer/InitialImagePreview.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAEntity.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeader.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeaderItems.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{LayerCommon => Layer}/LayerOpacity.tsx (71%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerSettings.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityMenu.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuArrangeActions.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerWrapper.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{GlobalMaskLayerOpacity.tsx => RGGlobalOpacity.tsx} (61%) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerSettingsPopover.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGDeletePromptButton.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGHeader.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapterSettings.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapters.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{RGLayer/RGLayerNegativePrompt.tsx => RegionalGuidance/RGNegativePrompt.tsx} (65%) rename invokeai/frontend/web/src/features/controlLayers/components/{RGLayer/RGLayerPositivePrompt.tsx => RegionalGuidance/RGPositivePrompt.tsx} (65%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettings.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettingsPopover.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{LayerCommon/EntityDeleteButton.tsx => common/CanvasEntityDeleteButton.tsx} (81%) rename invokeai/frontend/web/src/features/controlLayers/components/{LayerCommon/EntityEnabledToggle.tsx => common/CanvasEntityEnabledToggle.tsx} (82%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{LayerCommon/EntityMenuButton.tsx => common/CanvasEntityMenuButton.tsx} (79%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntitySettings.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{LayerCommon/EntityTitle.tsx => common/CanvasEntityTitle.tsx} (67%) diff --git a/invokeai/frontend/web/src/app/store/createMemoizedSelector.ts b/invokeai/frontend/web/src/app/store/createMemoizedSelector.ts index eb096418458..fb49d796e10 100644 --- a/invokeai/frontend/web/src/app/store/createMemoizedSelector.ts +++ b/invokeai/frontend/web/src/app/store/createMemoizedSelector.ts @@ -22,3 +22,4 @@ export const getSelectorsOptions: GetSelectorsOptions = { }; export const createAppSelector = createSelector.withTypes(); +export const createMemoizedAppSelector = createMemoizedSelector.withTypes(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx new file mode 100644 index 00000000000..cd636b71ca1 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx @@ -0,0 +1,29 @@ +import { useDisclosure } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; +import { CAHeader } from 'features/controlLayers/components/ControlAdapter/CAEntityHeader'; +import { CASettings } from 'features/controlLayers/components/ControlAdapter/CASettings'; +import { entitySelected } from 'features/controlLayers/store/controlLayersSlice'; +import { memo, useCallback } from 'react'; + +type Props = { + id: string; +}; + +export const CA = memo(({ id }: Props) => { + const dispatch = useAppDispatch(); + const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id); + const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); + const onSelect = useCallback(() => { + dispatch(entitySelected({ id, type: 'control_adapter' })); + }, [dispatch, id]); + + return ( + + + {isOpen && } + + ); +}); + +CA.displayName = 'CA'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx new file mode 100644 index 00000000000..75b94719427 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx @@ -0,0 +1,87 @@ +import { Menu, MenuItem, MenuList } from '@invoke-ai/ui-library'; +import { createAppSelector } from 'app/store/createMemoizedSelector'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; +import { + caDeleted, + caMovedBackwardOne, + caMovedForwardOne, + caMovedToBack, + caMovedToFront, + selectCAOrThrow, + selectControlAdaptersV2Slice, +} from 'features/controlLayers/store/controlAdaptersSlice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + PiArrowDownBold, + PiArrowLineDownBold, + PiArrowLineUpBold, + PiArrowUpBold, + PiTrashSimpleBold, +} from 'react-icons/pi'; + +type Props = { + id: string; +}; + +const selectValidActions = createAppSelector( + [selectControlAdaptersV2Slice, (caState, id: string) => id], + (caState, id) => { + const ca = selectCAOrThrow(caState, id); + const caIndex = caState.controlAdapters.indexOf(ca); + const caCount = caState.controlAdapters.length; + return { + canMoveForward: caIndex < caCount - 1, + canMoveBackward: caIndex > 0, + canMoveToFront: caIndex < caCount - 1, + canMoveToBack: caIndex > 0, + }; + } +); + +export const CAActionsMenu = memo(({ id }: Props) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const validActions = useAppSelector((s) => selectValidActions(s, id)); + const onDelete = useCallback(() => { + dispatch(caDeleted({ id })); + }, [dispatch, id]); + const moveForwardOne = useCallback(() => { + dispatch(caMovedForwardOne({ id })); + }, [dispatch, id]); + const moveToFront = useCallback(() => { + dispatch(caMovedToFront({ id })); + }, [dispatch, id]); + const moveBackwardOne = useCallback(() => { + dispatch(caMovedBackwardOne({ id })); + }, [dispatch, id]); + const moveToBack = useCallback(() => { + dispatch(caMovedToBack({ id })); + }, [dispatch, id]); + + return ( + + + + }> + {t('controlLayers.moveToFront')} + + }> + {t('controlLayers.moveForward')} + + }> + {t('controlLayers.moveBackward')} + + }> + {t('controlLayers.moveToBack')} + + } color="error.300"> + {t('common.delete')} + + + + ); +}); + +CAActionsMenu.displayName = 'CAActionsMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntity.tsx deleted file mode 100644 index f58ba6770c7..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntity.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Flex, useDisclosure } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { CAHeaderItems } from 'features/controlLayers/components/ControlAdapter/CAHeaderItems'; -import { CASettings } from 'features/controlLayers/components/ControlAdapter/CASettings'; -import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; -import { entitySelected } from 'features/controlLayers/store/controlLayersSlice'; -import { memo, useCallback } from 'react'; - -type Props = { - id: string; -}; - -export const CAEntity = memo(({ id }: Props) => { - const dispatch = useAppDispatch(); - const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id); - const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); - const onClick = useCallback(() => { - dispatch(entitySelected({ id, type: 'control_adapter' })); - }, [dispatch, id]); - - return ( - - - - - {isOpen && ( - - - - )} - - ); -}); - -CAEntity.displayName = 'CAEntity'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityHeader.tsx new file mode 100644 index 00000000000..1aeaacca8f0 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityHeader.tsx @@ -0,0 +1,41 @@ +import { Spacer } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; +import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; +import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; +import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; +import { CAActionsMenu } from 'features/controlLayers/components/ControlAdapter/CAActionsMenu'; +import { CAOpacityAndFilter } from 'features/controlLayers/components/ControlAdapter/CAOpacityAndFilter'; +import { caDeleted, caIsEnabledToggled, selectCAOrThrow } from 'features/controlLayers/store/controlAdaptersSlice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +type Props = { + id: string; + onToggleVisibility: () => void; +}; + +export const CAHeader = memo(({ id, onToggleVisibility }: Props) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isEnabled = useAppSelector((s) => selectCAOrThrow(s.controlAdaptersV2, id).isEnabled); + const onToggleIsEnabled = useCallback(() => { + dispatch(caIsEnabledToggled({ id })); + }, [dispatch, id]); + const onDelete = useCallback(() => { + dispatch(caDeleted({ id })); + }, [dispatch, id]); + + return ( + + + + + + + + + ); +}); + +CAHeader.displayName = 'CAEntityHeader'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAHeaderItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAHeaderItems.tsx deleted file mode 100644 index 3987787af7f..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAHeaderItems.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { Menu, MenuItem, MenuList, Spacer } from '@invoke-ai/ui-library'; -import { createAppSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { CAOpacityAndFilter } from 'features/controlLayers/components/ControlAdapter/CAOpacityAndFilter'; -import { EntityDeleteButton } from 'features/controlLayers/components/LayerCommon/EntityDeleteButton'; -import { EntityEnabledToggle } from 'features/controlLayers/components/LayerCommon/EntityEnabledToggle'; -import { EntityMenuButton } from 'features/controlLayers/components/LayerCommon/EntityMenuButton'; -import { EntityTitle } from 'features/controlLayers/components/LayerCommon/EntityTitle'; -import { - caDeleted, - caIsEnabledToggled, - caMovedBackwardOne, - caMovedForwardOne, - caMovedToBack, - caMovedToFront, - selectCAOrThrow, - selectControlAdaptersV2Slice, -} from 'features/controlLayers/store/controlAdaptersSlice'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { - PiArrowDownBold, - PiArrowLineDownBold, - PiArrowLineUpBold, - PiArrowUpBold, - PiTrashSimpleBold, -} from 'react-icons/pi'; - -type Props = { - id: string; -}; - -const selectValidActions = createAppSelector( - [selectControlAdaptersV2Slice, (caState, id: string) => id], - (caState, id) => { - const ca = selectCAOrThrow(caState, id); - const caIndex = caState.controlAdapters.indexOf(ca); - const caCount = caState.controlAdapters.length; - return { - canMoveForward: caIndex < caCount - 1, - canMoveBackward: caIndex > 0, - canMoveToFront: caIndex < caCount - 1, - canMoveToBack: caIndex > 0, - }; - } -); - -export const CAHeaderItems = memo(({ id }: Props) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const validActions = useAppSelector((s) => selectValidActions(s, id)); - const isEnabled = useAppSelector((s) => selectCAOrThrow(s.controlAdaptersV2, id).isEnabled); - const onToggle = useCallback(() => { - dispatch(caIsEnabledToggled({ id })); - }, [dispatch, id]); - const onDelete = useCallback(() => { - dispatch(caDeleted({ id })); - }, [dispatch, id]); - const moveForwardOne = useCallback(() => { - dispatch(caMovedForwardOne({ id })); - }, [dispatch, id]); - const moveToFront = useCallback(() => { - dispatch(caMovedToFront({ id })); - }, [dispatch, id]); - const moveBackwardOne = useCallback(() => { - dispatch(caMovedBackwardOne({ id })); - }, [dispatch, id]); - const moveToBack = useCallback(() => { - dispatch(caMovedToBack({ id })); - }, [dispatch, id]); - - return ( - <> - - - - - - - - }> - {t('controlLayers.moveToFront')} - - }> - {t('controlLayers.moveForward')} - - }> - {t('controlLayers.moveBackward')} - - }> - {t('controlLayers.moveToBack')} - - } color="error.300"> - {t('common.delete')} - - - - - - ); -}); - -CAHeaderItems.displayName = 'CAHeaderItems'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx index e20b79b8d2f..01f7edcc797 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx @@ -1,6 +1,7 @@ import { Box, Divider, Flex, Icon, IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct'; +import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; import { Weight } from 'features/controlLayers/components/common/Weight'; import { CAControlModeSelect } from 'features/controlLayers/components/ControlAdapter/CAControlModeSelect'; import { CAImagePreview } from 'features/controlLayers/components/ControlAdapter/CAImagePreview'; @@ -95,58 +96,60 @@ export const CASettings = memo(({ id }: Props) => { const postUploadAction = useMemo(() => ({ id, type: 'SET_CA_IMAGE' }), [id]); return ( - - - - - + + + + + + - - } - /> - - - - {controlAdapter.controlMode && ( - - )} - - - - - + } /> - - {isExpanded && ( - <> - - - - + + + {controlAdapter.controlMode && ( + + )} + + - - )} - + + + + + {isExpanded && ( + <> + + + + + + + )} + + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index 50e39c43e3c..2dfd08c1b02 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -9,7 +9,7 @@ import { CALayer } from 'features/controlLayers/components/CALayer/CALayer'; import { DeleteAllLayersButton } from 'features/controlLayers/components/DeleteAllLayersButton'; import { IILayer } from 'features/controlLayers/components/IILayer/IILayer'; import { IPAEntity } from 'features/controlLayers/components/IPALayer/IPALayer'; -import { RasterLayer } from 'features/controlLayers/components/RasterLayer/RasterLayer'; +import { Layer } from 'features/controlLayers/components/RasterLayer/RasterLayer'; import { RGLayer } from 'features/controlLayers/components/RGLayer/RGLayer'; import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; import type { LayerData } from 'features/controlLayers/store/types'; @@ -67,7 +67,7 @@ const LayerWrapper = memo(({ id, type }: LayerWrapperProps) => { return ; } if (type === 'raster_layer') { - return ; + return ; } }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx index 1f3307bdf1f..9b9f957dfc8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx @@ -11,7 +11,7 @@ import { } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { setShouldInvertBrushSizeScrollDirection } from 'features/canvas/store/canvasSlice'; -import { GlobalMaskLayerOpacity } from 'features/controlLayers/components/GlobalMaskLayerOpacity'; +import { RGGlobalOpacity } from 'features/controlLayers/components/RGGlobalOpacity'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -20,8 +20,8 @@ import { RiSettings4Fill } from 'react-icons/ri'; const ControlLayersSettingsPopover = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const shouldInvertBrushSizeScrollDirection = useAppSelector((s) => s.canvas.shouldInvertBrushSizeScrollDirection); - const handleChangeShouldInvertBrushSizeScrollDirection = useCallback( + const invertScroll = useAppSelector((s) => s.canvasV2.tool.invertScroll); + const onChangeInvertScroll = useCallback( (e: ChangeEvent) => dispatch(setShouldInvertBrushSizeScrollDirection(e.target.checked)), [dispatch] ); @@ -33,13 +33,10 @@ const ControlLayersSettingsPopover = () => { - + {t('unifiedCanvas.invertBrushSizeScrollDirection')} - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx deleted file mode 100644 index 34ef2ce0ac6..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { InitialImagePreview } from 'features/controlLayers/components/IILayer/InitialImagePreview'; -import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; -import { EntityMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; -import { LayerOpacity } from 'features/controlLayers/components/LayerCommon/LayerOpacity'; -import { EntityTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; -import { EntityEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; -import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; -import { - iiLayerDenoisingStrengthChanged, - iiLayerImageChanged, - layerSelected, - selectLayerOrThrow, -} from 'features/controlLayers/store/controlLayersSlice'; -import { isInitialImageLayer } from 'features/controlLayers/store/types'; -import type { IILayerImageDropData } from 'features/dnd/types'; -import ImageToImageStrength from 'features/parameters/components/ImageToImage/ImageToImageStrength'; -import { memo, useCallback, useMemo } from 'react'; -import type { IILayerImagePostUploadAction, ImageDTO } from 'services/api/types'; - -type Props = { - layerId: string; -}; - -export const IILayer = memo(({ layerId }: Props) => { - const dispatch = useAppDispatch(); - const layer = useAppSelector((s) => selectLayerOrThrow(s.canvasV2, layerId, isInitialImageLayer)); - const onClick = useCallback(() => { - dispatch(layerSelected(layerId)); - }, [dispatch, layerId]); - const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); - - const onChangeImage = useCallback( - (imageDTO: ImageDTO | null) => { - dispatch(iiLayerImageChanged({ layerId, imageDTO })); - }, - [dispatch, layerId] - ); - - const onChangeDenoisingStrength = useCallback( - (denoisingStrength: number) => { - dispatch(iiLayerDenoisingStrengthChanged({ layerId, denoisingStrength })); - }, - [dispatch, layerId] - ); - - const droppableData = useMemo( - () => ({ - actionType: 'SET_II_LAYER_IMAGE', - context: { - layerId, - }, - id: layerId, - }), - [layerId] - ); - - const postUploadAction = useMemo( - () => ({ - layerId, - type: 'SET_II_LAYER_IMAGE', - }), - [layerId] - ); - - return ( - - - - - - - - - - {isOpen && ( - - - - - )} - - ); -}); - -IILayer.displayName = 'IILayer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/InitialImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/InitialImagePreview.tsx deleted file mode 100644 index 0a9157068da..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/InitialImagePreview.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { Flex, useShiftModifier } from '@invoke-ai/ui-library'; -import { skipToken } from '@reduxjs/toolkit/query'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIDndImage from 'common/components/IAIDndImage'; -import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; -import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; -import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; -import type { ImageWithDims } from 'features/controlLayers/util/controlAdapters'; -import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types'; -import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { memo, useCallback, useEffect, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi'; -import { useGetImageDTOQuery } from 'services/api/endpoints/images'; -import type { ImageDTO, PostUploadAction } from 'services/api/types'; - -type Props = { - image: ImageWithDims | null; - onChangeImage: (imageDTO: ImageDTO | null) => void; - droppableData: TypesafeDroppableData; - postUploadAction: PostUploadAction; -}; - -export const InitialImagePreview = memo(({ image, onChangeImage, droppableData, postUploadAction }: Props) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const isConnected = useAppSelector((s) => s.system.isConnected); - const activeTabName = useAppSelector(activeTabNameSelector); - const optimalDimension = useAppSelector(selectOptimalDimension); - const shift = useShiftModifier(); - - const { currentData: imageDTO, isError: isErrorControlImage } = useGetImageDTOQuery(image?.name ?? skipToken); - - const onReset = useCallback(() => { - onChangeImage(null); - }, [onChangeImage]); - - const onUseSize = useCallback(() => { - if (!imageDTO) { - return; - } - - if (activeTabName === 'canvas') { - dispatch(setBoundingBoxDimensions({ width: imageDTO.width, height: imageDTO.height }, optimalDimension)); - } else { - const options = { updateAspectRatio: true, clamp: true }; - if (shift) { - const { width, height } = imageDTO; - dispatch(widthChanged({ width, ...options })); - dispatch(heightChanged({ height, ...options })); - } else { - const { width, height } = calculateNewSize( - imageDTO.width / imageDTO.height, - optimalDimension * optimalDimension - ); - dispatch(widthChanged({ width, ...options })); - dispatch(heightChanged({ height, ...options })); - } - } - }, [imageDTO, activeTabName, dispatch, optimalDimension, shift]); - - const draggableData = useMemo(() => { - if (imageDTO) { - return { - id: 'initial_image_layer', - payloadType: 'IMAGE_DTO', - payload: { imageDTO: imageDTO }, - }; - } - }, [imageDTO]); - - useEffect(() => { - if (isConnected && isErrorControlImage) { - onReset(); - } - }, [onReset, isConnected, isErrorControlImage]); - - return ( - - - - - {imageDTO && ( - - } - tooltip={t('controlnet.resetControlImage')} - /> - } - tooltip={ - shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions') - } - /> - - )} - - - ); -}); - -InitialImagePreview.displayName = 'InitialImagePreview'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx new file mode 100644 index 00000000000..f94afe4da31 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx @@ -0,0 +1,29 @@ +import { useDisclosure } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; +import { IPAHeader } from 'features/controlLayers/components/IPAdapter/IPAHeader'; +import { IPASettings } from 'features/controlLayers/components/IPAdapter/IPASettings'; +import { entitySelected } from 'features/controlLayers/store/controlLayersSlice'; +import { memo, useCallback } from 'react'; + +type Props = { + id: string; +}; + +export const IPA = memo(({ id }: Props) => { + const dispatch = useAppDispatch(); + const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id); + const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); + const onSelect = useCallback(() => { + dispatch(entitySelected({ id, type: 'ip_adapter' })); + }, [dispatch, id]); + + return ( + + + {isOpen && } + + ); +}); + +IPA.displayName = 'IPA'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAEntity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAEntity.tsx deleted file mode 100644 index f000045179d..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAEntity.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Flex, useDisclosure } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { IPAHeaderItems } from 'features/controlLayers/components/IPAdapter/IPAHeaderItems'; -import { IPASettings } from 'features/controlLayers/components/IPAdapter/IPASettings'; -import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; -import { entitySelected } from 'features/controlLayers/store/controlLayersSlice'; -import { memo, useCallback } from 'react'; - -type Props = { - id: string; -}; - -export const IPAEntity = memo(({ id }: Props) => { - const dispatch = useAppDispatch(); - const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id); - const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); - const onClick = useCallback(() => { - dispatch(entitySelected({ id, type: 'ip_adapter' })); - }, [dispatch, id]); - - return ( - - - - - {isOpen && ( - - - - )} - - ); -}); - -IPAEntity.displayName = 'IPAEntity'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeader.tsx new file mode 100644 index 00000000000..9604a3283f1 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeader.tsx @@ -0,0 +1,37 @@ +import { Spacer } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; +import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; +import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; +import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; +import { ipaDeleted, ipaIsEnabledToggled, selectIPAOrThrow } from 'features/controlLayers/store/ipAdaptersSlice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +type Props = { + id: string; + onToggleVisibility: () => void; +}; + +export const IPAHeader = memo(({ id, onToggleVisibility }: Props) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isEnabled = useAppSelector((s) => selectIPAOrThrow(s.ipAdapters, id).isEnabled); + const onToggleIsEnabled = useCallback(() => { + dispatch(ipaIsEnabledToggled({ id })); + }, [dispatch, id]); + const onDelete = useCallback(() => { + dispatch(ipaDeleted({ id })); + }, [dispatch, id]); + + return ( + + + + + + + ); +}); + +IPAHeader.displayName = 'IPAHeader'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeaderItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeaderItems.tsx deleted file mode 100644 index d4de4f7f6f6..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeaderItems.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Spacer } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { EntityDeleteButton } from 'features/controlLayers/components/LayerCommon/EntityDeleteButton'; -import { EntityEnabledToggle } from 'features/controlLayers/components/LayerCommon/EntityEnabledToggle'; -import { EntityTitle } from 'features/controlLayers/components/LayerCommon/EntityTitle'; -import { - ipaDeleted, - ipaIsEnabledToggled, - selectIPAOrThrow, -} from 'features/controlLayers/store/ipAdaptersSlice'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -type Props = { - id: string; -}; - -export const IPAHeaderItems = memo(({ id }: Props) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const isEnabled = useAppSelector((s) => selectIPAOrThrow(s.ipAdapters, id).isEnabled); - const onToggle = useCallback(() => { - dispatch(ipaIsEnabledToggled({ id })); - }, [dispatch, id]); - const onDelete = useCallback(() => { - dispatch(ipaDeleted({ id })); - }, [dispatch, id]); - - return ( - <> - - - - - - ); -}); - -IPAHeaderItems.displayName = 'IPAHeaderItems'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx index 1daacb941fd..dfe26b41ed0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx @@ -1,6 +1,7 @@ import { Box, Flex } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct'; +import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; import { Weight } from 'features/controlLayers/components/common/Weight'; import { IPAMethod } from 'features/controlLayers/components/IPAdapter/IPAMethod'; import { @@ -70,52 +71,40 @@ export const IPASettings = memo(({ id }: Props) => { [dispatch, id] ); - const droppableData = useMemo( - () => ({ - actionType: 'SET_IPA_IMAGE', - context: { id }, - id, - }), - [id] - ); - - const postUploadAction = useMemo( - () => ({ - type: 'SET_IPA_IMAGE', - id, - }), - [id] - ); + const droppableData = useMemo(() => ({ actionType: 'SET_IPA_IMAGE', context: { id }, id }), [id]); + const postUploadAction = useMemo(() => ({ type: 'SET_IPA_IMAGE', id }), [id]); return ( - - - - - - - - - - - + + + + + + - - + + + + + + + + + - + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx new file mode 100644 index 00000000000..214f4e825cf --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx @@ -0,0 +1,29 @@ +import { useDisclosure } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; +import { LayerHeader } from 'features/controlLayers/components/RasterLayer/LayerHeader'; +import { LayerSettings } from 'features/controlLayers/components/RasterLayer/LayerSettings'; +import { entitySelected } from 'features/controlLayers/store/controlLayersSlice'; +import { memo, useCallback } from 'react'; + +type Props = { + id: string; +}; + +export const Layer = memo(({ id }: Props) => { + const dispatch = useAppDispatch(); + const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id); + const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); + const onSelect = useCallback(() => { + dispatch(entitySelected({ id, type: 'layer' })); + }, [dispatch, id]); + + return ( + + + {isOpen && } + + ); +}); + +Layer.displayName = 'Layer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx new file mode 100644 index 00000000000..03b4e8953dc --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx @@ -0,0 +1,87 @@ +import { Menu, MenuItem, MenuList } from '@invoke-ai/ui-library'; +import { createAppSelector } from 'app/store/createMemoizedSelector'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; +import { + layerDeleted, + layerMovedBackwardOne, + layerMovedForwardOne, + layerMovedToBack, + layerMovedToFront, + selectLayerOrThrow, + selectLayersSlice, +} from 'features/controlLayers/store/layersSlice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + PiArrowDownBold, + PiArrowLineDownBold, + PiArrowLineUpBold, + PiArrowUpBold, + PiTrashSimpleBold, +} from 'react-icons/pi'; + +type Props = { + id: string; +}; + +const selectValidActions = createAppSelector( + [selectLayersSlice, (layersState, id: string) => id], + (layersState, id) => { + const layer = selectLayerOrThrow(layersState, id); + const layerIndex = layersState.layers.indexOf(layer); + const layerCount = layersState.layers.length; + return { + canMoveForward: layerIndex < layerCount - 1, + canMoveBackward: layerIndex > 0, + canMoveToFront: layerIndex < layerCount - 1, + canMoveToBack: layerIndex > 0, + }; + } +); + +export const LayerActionsMenu = memo(({ id }: Props) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const validActions = useAppSelector((s) => selectValidActions(s, id)); + const onDelete = useCallback(() => { + dispatch(layerDeleted({ id })); + }, [dispatch, id]); + const moveForwardOne = useCallback(() => { + dispatch(layerMovedForwardOne({ id })); + }, [dispatch, id]); + const moveToFront = useCallback(() => { + dispatch(layerMovedToFront({ id })); + }, [dispatch, id]); + const moveBackwardOne = useCallback(() => { + dispatch(layerMovedBackwardOne({ id })); + }, [dispatch, id]); + const moveToBack = useCallback(() => { + dispatch(layerMovedToBack({ id })); + }, [dispatch, id]); + + return ( + + + + }> + {t('controlLayers.moveToFront')} + + }> + {t('controlLayers.moveForward')} + + }> + {t('controlLayers.moveBackward')} + + }> + {t('controlLayers.moveToBack')} + + } color="error.300"> + {t('common.delete')} + + + + ); +}); + +LayerActionsMenu.displayName = 'LayerActionsMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx new file mode 100644 index 00000000000..cd8bc2d5f99 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx @@ -0,0 +1,42 @@ +import { Spacer } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; +import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; +import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; +import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; +import { LayerActionsMenu } from 'features/controlLayers/components/Layer/LayerActionsMenu'; +import { layerDeleted, layerIsEnabledToggled, selectLayerOrThrow } from 'features/controlLayers/store/layersSlice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { LayerOpacity } from './LayerOpacity'; + +type Props = { + id: string; + onToggleVisibility: () => void; +}; + +export const LayerHeader = memo(({ id, onToggleVisibility }: Props) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isEnabled = useAppSelector((s) => selectLayerOrThrow(s.layers, id).isEnabled); + const onToggleIsEnabled = useCallback(() => { + dispatch(layerIsEnabledToggled({ id })); + }, [dispatch, id]); + const onDelete = useCallback(() => { + dispatch(layerDeleted({ id })); + }, [dispatch, id]); + + return ( + + + + + + + + + ); +}); + +LayerHeader.displayName = 'LayerHeader'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerOpacity.tsx similarity index 71% rename from invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerOpacity.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerOpacity.tsx index b92ccc41f91..00de65b7130 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerOpacity.tsx @@ -11,43 +11,29 @@ import { PopoverContent, PopoverTrigger, } from '@invoke-ai/ui-library'; -import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; -import { - layerOpacityChanged, - selectCanvasV2Slice, - selectLayerOrThrow, -} from 'features/controlLayers/store/controlLayersSlice'; -import { isLayerWithOpacity } from 'features/controlLayers/store/types'; -import { memo, useCallback, useMemo } from 'react'; +import { layerOpacityChanged, selectLayerOrThrow } from 'features/controlLayers/store/layersSlice'; +import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiDropHalfFill } from 'react-icons/pi'; type Props = { - layerId: string; + id: string; }; const marks = [0, 25, 50, 75, 100]; const formatPct = (v: number | string) => `${v} %`; -export const LayerOpacity = memo(({ layerId }: Props) => { +export const LayerOpacity = memo(({ id }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const selectOpacity = useMemo( - () => - createSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = selectLayerOrThrow(canvasV2, layerId, isLayerWithOpacity); - return Math.round(layer.opacity * 100); - }), - [layerId] - ); - const opacity = useAppSelector(selectOpacity); + const opacity = useAppSelector((s) => Math.round(selectLayerOrThrow(s.layers, id).opacity * 100)); const onChangeOpacity = useCallback( (v: number) => { - dispatch(layerOpacityChanged({ layerId, opacity: v / 100 })); + dispatch(layerOpacityChanged({ id, opacity: v / 100 })); }, - [dispatch, layerId] + [dispatch, id] ); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerSettings.tsx new file mode 100644 index 00000000000..0ef45dc2234 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerSettings.tsx @@ -0,0 +1,24 @@ +import IAIDroppable from 'common/components/IAIDroppable'; +import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; +import type { LayerImageDropData } from 'features/dnd/types'; +import { memo, useMemo } from 'react'; + +type Props = { + id: string; +}; + +export const LayerSettings = memo(({ id }: Props) => { + const droppableData = useMemo( + () => ({ id, actionType: 'ADD_LAYER_IMAGE', context: { id } }), + [id] + ); + + return ( + + PLACEHOLDER + + + ); +}); + +LayerSettings.displayName = 'LayerSettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityMenu.tsx deleted file mode 100644 index a10079f1608..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityMenu.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuList } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { stopPropagation } from 'common/util/stopPropagation'; -import { LayerMenuArrangeActions } from 'features/controlLayers/components/LayerCommon/LayerMenuArrangeActions'; -import { LayerMenuRGActions } from 'features/controlLayers/components/LayerCommon/LayerMenuRGActions'; -import { useLayerType } from 'features/controlLayers/hooks/layerStateHooks'; -import { layerDeleted, layerReset } from 'features/controlLayers/store/controlLayersSlice'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiArrowCounterClockwiseBold, PiDotsThreeVerticalBold, PiTrashSimpleBold } from 'react-icons/pi'; - -type Props = { layerId: string }; - -export const EntityMenu = memo(({ layerId }: Props) => { - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - const layerType = useLayerType(layerId); - const resetLayer = useCallback(() => { - dispatch(layerReset(layerId)); - }, [dispatch, layerId]); - const deleteLayer = useCallback(() => { - dispatch(layerDeleted(layerId)); - }, [dispatch, layerId]); - const shouldShowArrangeActions = useMemo(() => { - return ( - layerType === 'regional_guidance_layer' || - layerType === 'control_adapter_layer' || - layerType === 'initial_image_layer' || - layerType === 'raster_layer' - ); - }, [layerType]); - const shouldShowResetAction = useMemo(() => { - return layerType === 'regional_guidance_layer' || layerType === 'raster_layer'; - }, [layerType]); - - return ( - - } - onDoubleClick={stopPropagation} // double click expands the layer - /> - - {layerType === 'regional_guidance_layer' && ( - <> - - - - )} - {shouldShowArrangeActions && ( - <> - - - - )} - {shouldShowResetAction && ( - }> - {t('accessibility.reset')} - - )} - } color="error.300"> - {t('common.delete')} - - - - ); -}); - -EntityMenu.displayName = 'EntityMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuArrangeActions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuArrangeActions.tsx deleted file mode 100644 index 4c86a98c36c..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuArrangeActions.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { MenuItem } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { - layerMovedBackward, - layerMovedForward, - layerMovedToBack, - layerMovedToFront, - selectCanvasV2Slice, -} from 'features/controlLayers/store/controlLayersSlice'; -import { isRenderableLayer } from 'features/controlLayers/store/types'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiArrowDownBold, PiArrowLineDownBold, PiArrowLineUpBold, PiArrowUpBold } from 'react-icons/pi'; -import { assert } from 'tsafe'; - -type Props = { layerId: string }; - -export const LayerMenuArrangeActions = memo(({ layerId }: Props) => { - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - const selectValidActions = useMemo( - () => - createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = canvasV2.layers.find((l) => l.id === layerId); - assert(isRenderableLayer(layer), `Layer ${layerId} not found or not an RP layer`); - const layerIndex = canvasV2.layers.findIndex((l) => l.id === layerId); - const layerCount = canvasV2.layers.length; - return { - canMoveForward: layerIndex < layerCount - 1, - canMoveBackward: layerIndex > 0, - canMoveToFront: layerIndex < layerCount - 1, - canMoveToBack: layerIndex > 0, - }; - }), - [layerId] - ); - const validActions = useAppSelector(selectValidActions); - const moveForward = useCallback(() => { - dispatch(layerMovedForward(layerId)); - }, [dispatch, layerId]); - const moveToFront = useCallback(() => { - dispatch(layerMovedToFront(layerId)); - }, [dispatch, layerId]); - const moveBackward = useCallback(() => { - dispatch(layerMovedBackward(layerId)); - }, [dispatch, layerId]); - const moveToBack = useCallback(() => { - dispatch(layerMovedToBack(layerId)); - }, [dispatch, layerId]); - return ( - <> - }> - {t('controlLayers.moveToFront')} - - }> - {t('controlLayers.moveForward')} - - }> - {t('controlLayers.moveBackward')} - - }> - {t('controlLayers.moveToBack')} - - - ); -}); - -LayerMenuArrangeActions.displayName = 'LayerMenuArrangeActions'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx deleted file mode 100644 index 335a0d32cb6..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { MenuItem } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useAddIPAdapterToRGLayer } from 'features/controlLayers/hooks/addLayerHooks'; -import { - regionalGuidanceNegativePromptChanged, - regionalGuidancePositivePromptChanged, - selectCanvasV2Slice, -} from 'features/controlLayers/store/controlLayersSlice'; -import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiPlusBold } from 'react-icons/pi'; -import { assert } from 'tsafe'; - -type Props = { layerId: string }; - -export const LayerMenuRGActions = memo(({ layerId }: Props) => { - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToRGLayer(layerId); - const selectValidActions = useMemo( - () => - createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = canvasV2.layers.find((l) => l.id === layerId); - assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); - return { - canAddPositivePrompt: layer.positivePrompt === null, - canAddNegativePrompt: layer.negativePrompt === null, - }; - }), - [layerId] - ); - const validActions = useAppSelector(selectValidActions); - const addPositivePrompt = useCallback(() => { - dispatch(regionalGuidancePositivePromptChanged({ layerId, prompt: '' })); - }, [dispatch, layerId]); - const addNegativePrompt = useCallback(() => { - dispatch(regionalGuidanceNegativePromptChanged({ layerId, prompt: '' })); - }, [dispatch, layerId]); - return ( - <> - }> - {t('controlLayers.addPositivePrompt')} - - }> - {t('controlLayers.addNegativePrompt')} - - } isDisabled={isAddIPAdapterDisabled}> - {t('controlLayers.addIPAdapter')} - - - ); -}); - -LayerMenuRGActions.displayName = 'LayerMenuRGActions'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerWrapper.tsx deleted file mode 100644 index 804ae40070f..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerWrapper.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import type { ChakraProps } from '@invoke-ai/ui-library'; -import { Flex } from '@invoke-ai/ui-library'; -import type { PropsWithChildren } from 'react'; -import { memo } from 'react'; - -type Props = PropsWithChildren<{ - onClick?: () => void; - borderColor: ChakraProps['bg']; -}>; - -export const LayerWrapper = memo(({ onClick, borderColor, children }: Props) => { - return ( - - - {children} - - - ); -}); - -LayerWrapper.displayName = 'LayerWrapper'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/GlobalMaskLayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGGlobalOpacity.tsx similarity index 61% rename from invokeai/frontend/web/src/features/controlLayers/components/GlobalMaskLayerOpacity.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RGGlobalOpacity.tsx index 23720f4c227..db56cdd5582 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/GlobalMaskLayerOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGGlobalOpacity.tsx @@ -1,24 +1,19 @@ import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { - globalMaskLayerOpacityChanged, - initialControlLayersState, -} from 'features/controlLayers/store/controlLayersSlice'; +import { rgGlobalOpacityChanged } from 'features/controlLayers/store/regionalGuidanceSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const marks = [0, 25, 50, 75, 100]; const formatPct = (v: number | string) => `${v} %`; -export const GlobalMaskLayerOpacity = memo(() => { +export const RGGlobalOpacity = memo(() => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const globalMaskLayerOpacity = useAppSelector((s) => - Math.round(s.canvasV2.globalMaskLayerOpacity * 100) - ); + const opacity = useAppSelector((s) => Math.round(s.regionalGuidance.opacity * 100)); const onChange = useCallback( (v: number) => { - dispatch(globalMaskLayerOpacityChanged(v / 100)); + dispatch(rgGlobalOpacityChanged({ opacity: v / 100 })); }, [dispatch] ); @@ -30,8 +25,8 @@ export const GlobalMaskLayerOpacity = memo(() => { min={0} max={100} step={1} - value={globalMaskLayerOpacity} - defaultValue={initialControlLayersState.globalMaskLayerOpacity * 100} + value={opacity} + defaultValue={0.3} onChange={onChange} marks={marks} minW={48} @@ -40,8 +35,8 @@ export const GlobalMaskLayerOpacity = memo(() => { min={0} max={100} step={1} - value={globalMaskLayerOpacity} - defaultValue={initialControlLayersState.globalMaskLayerOpacity * 100} + value={opacity} + defaultValue={0.3} onChange={onChange} w={28} format={formatPct} @@ -51,4 +46,4 @@ export const GlobalMaskLayerOpacity = memo(() => { ); }); -GlobalMaskLayerOpacity.displayName = 'GlobalMaskLayerOpacity'; +RGGlobalOpacity.displayName = 'RGGlobalOpacity'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx deleted file mode 100644 index 5d5ce7fb02a..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Badge, Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { rgbColorToString } from 'features/canvas/util/colorToString'; -import { AddPromptButtons } from 'features/controlLayers/components/AddPromptButtons'; -import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; -import { EntityMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; -import { EntityTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; -import { EntityEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; -import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; -import { layerSelected, selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; -import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { assert } from 'tsafe'; - -import { RGLayerColorPicker } from './RGLayerColorPicker'; -import { RGLayerIPAdapterList } from './RGLayerIPAdapterList'; -import { RGLayerNegativePrompt } from './RGLayerNegativePrompt'; -import { RGLayerPositivePrompt } from './RGLayerPositivePrompt'; -import RGLayerSettingsPopover from './RGLayerSettingsPopover'; - -type Props = { - layerId: string; -}; - -export const RGLayer = memo(({ layerId }: Props) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const selector = useMemo( - () => - createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = canvasV2.layers.find((l) => l.id === layerId); - assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); - return { - color: rgbColorToString(layer.previewColor), - hasPositivePrompt: layer.positivePrompt !== null, - hasNegativePrompt: layer.negativePrompt !== null, - hasIPAdapters: layer.ipAdapters.length > 0, - isSelected: layerId === canvasV2.selectedLayerId, - autoNegative: layer.autoNegative, - }; - }), - [layerId] - ); - const { autoNegative, color, hasPositivePrompt, hasNegativePrompt, hasIPAdapters, isSelected } = - useAppSelector(selector); - const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); - const onClick = useCallback(() => { - dispatch(layerSelected(layerId)); - }, [dispatch, layerId]); - return ( - - - - - - {autoNegative === 'invert' && ( - - {t('controlLayers.autoNegative')} - - )} - - - - - - {isOpen && ( - - {!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && } - {hasPositivePrompt && } - {hasNegativePrompt && } - {hasIPAdapters && } - - )} - - ); -}); - -RGLayer.displayName = 'RGLayer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx deleted file mode 100644 index 998682d4ccd..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { createSelector } from '@reduxjs/toolkit'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { regionalGuidanceAutoNegativeChanged, selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; -import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; -import type { ChangeEvent } from 'react'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { assert } from 'tsafe'; - -type Props = { - layerId: string; -}; - -const useAutoNegative = (layerId: string) => { - const selectAutoNegative = useMemo( - () => - createSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = canvasV2.layers.find((l) => l.id === layerId); - assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); - return layer.autoNegative; - }), - [layerId] - ); - const autoNegative = useAppSelector(selectAutoNegative); - return autoNegative; -}; - -export const RGLayerAutoNegativeCheckbox = memo(({ layerId }: Props) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const autoNegative = useAutoNegative(layerId); - const onChange = useCallback( - (e: ChangeEvent) => { - dispatch(regionalGuidanceAutoNegativeChanged({ layerId, autoNegative: e.target.checked ? 'invert' : 'off' })); - }, - [dispatch, layerId] - ); - - return ( - - {t('controlLayers.autoNegative')} - - - ); -}); - -RGLayerAutoNegativeCheckbox.displayName = 'RGLayerAutoNegativeCheckbox'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx deleted file mode 100644 index 7ce002817ac..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger, Tooltip } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import RgbColorPicker from 'common/components/RgbColorPicker'; -import { stopPropagation } from 'common/util/stopPropagation'; -import { rgbColorToString } from 'features/canvas/util/colorToString'; -import { rgFillChanged, selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; -import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; -import { memo, useCallback, useMemo } from 'react'; -import type { RgbColor } from 'react-colorful'; -import { useTranslation } from 'react-i18next'; -import { assert } from 'tsafe'; - -type Props = { - layerId: string; -}; - -export const RGLayerColorPicker = memo(({ layerId }: Props) => { - const { t } = useTranslation(); - const selectColor = useMemo( - () => - createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = canvasV2.layers.find((l) => l.id === layerId); - assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an vector mask layer`); - return layer.previewColor; - }), - [layerId] - ); - const color = useAppSelector(selectColor); - const dispatch = useAppDispatch(); - const onColorChange = useCallback( - (color: RgbColor) => { - dispatch(rgFillChanged({ layerId, color })); - }, - [dispatch, layerId] - ); - return ( - - - - - - - - - - - - - - - ); -}); - -RGLayerColorPicker.displayName = 'RGLayerColorPicker'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx deleted file mode 100644 index 5b6be683d18..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Divider, Flex } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { RGLayerIPAdapterWrapper } from 'features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; -import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; -import { memo, useMemo } from 'react'; -import { assert } from 'tsafe'; - -type Props = { - layerId: string; -}; - -export const RGLayerIPAdapterList = memo(({ layerId }: Props) => { - const selectIPAdapterIds = useMemo( - () => - createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = canvasV2.layers.filter(isRegionalGuidanceLayer).find((l) => l.id === layerId); - assert(layer, `Layer ${layerId} not found`); - return layer.ipAdapters; - }), - [layerId] - ); - const ipAdapters = useAppSelector(selectIPAdapterIds); - - if (ipAdapters.length === 0) { - return null; - } - - return ( - <> - {ipAdapters.map(({ id }, index) => ( - - {index > 0 && ( - - - - )} - - - ))} - - ); -}); - -RGLayerIPAdapterList.displayName = 'RGLayerIPAdapterList'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx deleted file mode 100644 index 2fa392e0d08..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { IPAdapter } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapter'; -import { - regionalGuidanceIPAdapterBeginEndStepPctChanged, - regionalGuidanceIPAdapterCLIPVisionModelChanged, - regionalGuidanceIPAdapterDeleted, - regionalGuidanceIPAdapterImageChanged, - regionalGuidanceIPAdapterMethodChanged, - regionalGuidanceIPAdapterModelChanged, - regionalGuidanceIPAdapterWeightChanged, - selectRGLayerIPAdapterOrThrow, -} from 'features/controlLayers/store/controlLayersSlice'; -import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; -import type { RGIPAdapterImageDropData } from 'features/dnd/types'; -import { memo, useCallback, useMemo } from 'react'; -import { PiTrashSimpleBold } from 'react-icons/pi'; -import type { ImageDTO, IPAdapterModelConfig, RGIPAdapterImagePostUploadAction } from 'services/api/types'; - -type Props = { - layerId: string; - ipAdapterId: string; - ipAdapterNumber: number; -}; - -export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNumber }: Props) => { - const dispatch = useAppDispatch(); - const onDeleteIPAdapter = useCallback(() => { - dispatch(regionalGuidanceIPAdapterDeleted({ layerId, ipAdapterId })); - }, [dispatch, ipAdapterId, layerId]); - const ipAdapter = useAppSelector((s) => selectRGLayerIPAdapterOrThrow(s.canvasV2, layerId, ipAdapterId)); - - const onChangeBeginEndStepPct = useCallback( - (beginEndStepPct: [number, number]) => { - dispatch( - regionalGuidanceIPAdapterBeginEndStepPctChanged({ - layerId, - ipAdapterId, - beginEndStepPct, - }) - ); - }, - [dispatch, ipAdapterId, layerId] - ); - - const onChangeWeight = useCallback( - (weight: number) => { - dispatch(regionalGuidanceIPAdapterWeightChanged({ layerId, ipAdapterId, weight })); - }, - [dispatch, ipAdapterId, layerId] - ); - - const onChangeIPMethod = useCallback( - (method: IPMethodV2) => { - dispatch(regionalGuidanceIPAdapterMethodChanged({ layerId, ipAdapterId, method })); - }, - [dispatch, ipAdapterId, layerId] - ); - - const onChangeModel = useCallback( - (modelConfig: IPAdapterModelConfig) => { - dispatch(regionalGuidanceIPAdapterModelChanged({ layerId, ipAdapterId, modelConfig })); - }, - [dispatch, ipAdapterId, layerId] - ); - - const onChangeCLIPVisionModel = useCallback( - (clipVisionModel: CLIPVisionModelV2) => { - dispatch(regionalGuidanceIPAdapterCLIPVisionModelChanged({ layerId, ipAdapterId, clipVisionModel })); - }, - [dispatch, ipAdapterId, layerId] - ); - - const onChangeImage = useCallback( - (imageDTO: ImageDTO | null) => { - dispatch(regionalGuidanceIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO })); - }, - [dispatch, ipAdapterId, layerId] - ); - - const droppableData = useMemo( - () => ({ - actionType: 'SET_RG_LAYER_IP_ADAPTER_IMAGE', - context: { - layerId, - ipAdapterId, - }, - id: layerId, - }), - [ipAdapterId, layerId] - ); - - const postUploadAction = useMemo( - () => ({ - type: 'SET_RG_LAYER_IP_ADAPTER_IMAGE', - layerId, - ipAdapterId, - }), - [ipAdapterId, layerId] - ); - - return ( - - - {`IP Adapter ${ipAdapterNumber}`} - - } - aria-label="Delete IP Adapter" - onClick={onDeleteIPAdapter} - variant="ghost" - colorScheme="error" - /> - - - - ); -}); - -RGLayerIPAdapterWrapper.displayName = 'RGLayerIPAdapterWrapper'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton.tsx deleted file mode 100644 index cbc99e70ded..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { IconButton, Tooltip } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { - regionalGuidanceNegativePromptChanged, - regionalGuidancePositivePromptChanged, -} from 'features/controlLayers/store/controlLayersSlice'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiTrashSimpleBold } from 'react-icons/pi'; - -type Props = { - layerId: string; - polarity: 'positive' | 'negative'; -}; - -export const RGLayerPromptDeleteButton = memo(({ layerId, polarity }: Props) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const onClick = useCallback(() => { - if (polarity === 'positive') { - dispatch(regionalGuidancePositivePromptChanged({ layerId, prompt: null })); - } else { - dispatch(regionalGuidanceNegativePromptChanged({ layerId, prompt: null })); - } - }, [dispatch, layerId, polarity]); - return ( - - } - onClick={onClick} - /> - - ); -}); - -RGLayerPromptDeleteButton.displayName = 'RGLayerPromptDeleteButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerSettingsPopover.tsx deleted file mode 100644 index 9203069b3c2..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerSettingsPopover.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import type { FormLabelProps } from '@invoke-ai/ui-library'; -import { - Flex, - FormControlGroup, - IconButton, - Popover, - PopoverArrow, - PopoverBody, - PopoverContent, - PopoverTrigger, -} from '@invoke-ai/ui-library'; -import { stopPropagation } from 'common/util/stopPropagation'; -import { RGLayerAutoNegativeCheckbox } from 'features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiGearSixBold } from 'react-icons/pi'; - -type Props = { - layerId: string; -}; - -const formLabelProps: FormLabelProps = { - flexGrow: 1, - minW: 32, -}; - -const RGLayerSettingsPopover = ({ layerId }: Props) => { - const { t } = useTranslation(); - - return ( - - - } - onDoubleClick={stopPropagation} // double click expands the layer - /> - - - - - - - - - - - - - ); -}; - -export default memo(RGLayerSettingsPopover); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx deleted file mode 100644 index 82851dbb0b5..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIDroppable from 'common/components/IAIDroppable'; -import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; -import { EntityMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; -import { LayerOpacity } from 'features/controlLayers/components/LayerCommon/LayerOpacity'; -import { EntityTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; -import { EntityEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; -import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; -import { layerSelected, selectLayerOrThrow } from 'features/controlLayers/store/controlLayersSlice'; -import { isRasterLayer } from 'features/controlLayers/store/types'; -import type { LayerImageDropData } from 'features/dnd/types'; -import { memo, useCallback, useMemo } from 'react'; - -type Props = { - layerId: string; -}; - -export const RasterLayer = memo(({ layerId }: Props) => { - const dispatch = useAppDispatch(); - const isSelected = useAppSelector( - (s) => selectLayerOrThrow(s.canvasV2, layerId, isRasterLayer).isSelected - ); - const onClick = useCallback(() => { - dispatch(layerSelected(layerId)); - }, [dispatch, layerId]); - const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); - - const droppableData = useMemo(() => { - const _droppableData: LayerImageDropData = { - id: layerId, - actionType: 'ADD_RASTER_LAYER_IMAGE', - context: { layerId }, - }; - return _droppableData; - }, [layerId]); - - return ( - - - - - - - - - - {isOpen && ( - - PLACEHOLDER - - )} - - - ); -}); - -RasterLayer.displayName = 'RasterLayer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx new file mode 100644 index 00000000000..6c503f98e1c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx @@ -0,0 +1,31 @@ +import { useDisclosure } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { rgbColorToString } from 'features/canvas/util/colorToString'; +import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; +import { RGHeader } from 'features/controlLayers/components/RegionalGuidance/RGHeader'; +import { RGSettings } from 'features/controlLayers/components/RegionalGuidance/RGSettings'; +import { entitySelected } from 'features/controlLayers/store/controlLayersSlice'; +import { selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice'; +import { memo, useCallback } from 'react'; + +type Props = { + id: string; +}; + +export const RG = memo(({ id }: Props) => { + const dispatch = useAppDispatch(); + const selectedBorderColor = useAppSelector((s) => rgbColorToString(selectRGOrThrow(s.regionalGuidance, id).fill)); + const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id); + const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); + const onSelect = useCallback(() => { + dispatch(entitySelected({ id, type: 'regional_guidance' })); + }, [dispatch, id]); + return ( + + + {isOpen && } + + ); +}); + +RG.displayName = 'RG'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx new file mode 100644 index 00000000000..db1cb078bc2 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx @@ -0,0 +1,119 @@ +import { Menu, MenuDivider, MenuItem, MenuList } from '@invoke-ai/ui-library'; +import { createMemoizedAppSelector } from 'app/store/createMemoizedSelector'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; +import { useAddIPAdapterToRGLayer } from 'features/controlLayers/hooks/addLayerHooks'; +import { + rgDeleted, + rgMovedBackwardOne, + rgMovedForwardOne, + rgMovedToBack, + rgMovedToFront, + rgNegativePromptChanged, + rgPositivePromptChanged, + rgReset, + selectRegionalGuidanceSlice, + selectRGOrThrow, +} from 'features/controlLayers/store/regionalGuidanceSlice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + PiArrowCounterClockwiseBold, + PiArrowDownBold, + PiArrowLineDownBold, + PiArrowLineUpBold, + PiArrowUpBold, + PiPlusBold, + PiTrashSimpleBold, +} from 'react-icons/pi'; + +type Props = { + id: string; +}; + +const selectActionsValidity = createMemoizedAppSelector( + [selectRegionalGuidanceSlice, (rgState, id: string) => id], + (rgState, id) => { + const rg = selectRGOrThrow(rgState, id); + const rgIndex = rgState.regions.indexOf(rg); + const rgCount = rgState.regions.length; + return { + isMoveForwardOneDisabled: rgIndex < rgCount - 1, + isMoveBackardOneDisabled: rgIndex > 0, + isMoveToFrontDisabled: rgIndex < rgCount - 1, + isMoveToBackDisabled: rgIndex > 0, + isAddPositivePromptDisabled: rg.positivePrompt === null, + isAddNegativePromptDisabled: rg.negativePrompt === null, + }; + } +); + +export const RGActionsMenu = memo(({ id }: Props) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const [onAddIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToRGLayer(id); + const actions = useAppSelector((s) => selectActionsValidity(s, id)); + const onDelete = useCallback(() => { + dispatch(rgDeleted({ id })); + }, [dispatch, id]); + const onReset = useCallback(() => { + dispatch(rgReset({ id })); + }, [dispatch, id]); + const onMoveForwardOne = useCallback(() => { + dispatch(rgMovedForwardOne({ id })); + }, [dispatch, id]); + const onMoveToFront = useCallback(() => { + dispatch(rgMovedToFront({ id })); + }, [dispatch, id]); + const onMoveBackwardOne = useCallback(() => { + dispatch(rgMovedBackwardOne({ id })); + }, [dispatch, id]); + const onMoveToBack = useCallback(() => { + dispatch(rgMovedToBack({ id })); + }, [dispatch, id]); + const onAddPositivePrompt = useCallback(() => { + dispatch(rgPositivePromptChanged({ id, prompt: '' })); + }, [dispatch, id]); + const onAddNegativePrompt = useCallback(() => { + dispatch(rgNegativePromptChanged({ id, prompt: '' })); + }, [dispatch, id]); + + return ( + + + + }> + {t('controlLayers.addPositivePrompt')} + + }> + {t('controlLayers.addNegativePrompt')} + + } isDisabled={isAddIPAdapterDisabled}> + {t('controlLayers.addIPAdapter')} + + + }> + {t('controlLayers.moveToFront')} + + }> + {t('controlLayers.moveForward')} + + }> + {t('controlLayers.moveBackward')} + + }> + {t('controlLayers.moveToBack')} + + + }> + {t('accessibility.reset')} + + } color="error.300"> + {t('common.delete')} + + + + ); +}); + +RGActionsMenu.displayName = 'RGActionsMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGDeletePromptButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGDeletePromptButton.tsx new file mode 100644 index 00000000000..4e994b43ec1 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGDeletePromptButton.tsx @@ -0,0 +1,24 @@ +import { IconButton, Tooltip } from '@invoke-ai/ui-library'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiTrashSimpleBold } from 'react-icons/pi'; + +type Props = { + onDelete: () => void; +}; + +export const RGDeletePromptButton = memo(({ onDelete }: Props) => { + const { t } = useTranslation(); + return ( + + } + onClick={onDelete} + /> + + ); +}); + +RGDeletePromptButton.displayName = 'RGDeletePromptButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGHeader.tsx new file mode 100644 index 00000000000..50f854e856c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGHeader.tsx @@ -0,0 +1,50 @@ +import { Badge, Spacer } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; +import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; +import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; +import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; +import { RGActionsMenu } from 'features/controlLayers/components/RegionalGuidance/RGActionsMenu'; +import { rgDeleted, rgIsEnabledToggled, selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { RGMaskFillColorPicker } from './RGMaskFillColorPicker'; +import { RGSettingsPopover } from './RGSettingsPopover'; + +type Props = { + id: string; + onToggleVisibility: () => void; +}; + +export const RGHeader = memo(({ id, onToggleVisibility }: Props) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isEnabled = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).isEnabled); + const autoNegative = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).autoNegative); + const onToggleIsEnabled = useCallback(() => { + dispatch(rgIsEnabledToggled({ id })); + }, [dispatch, id]); + const onDelete = useCallback(() => { + dispatch(rgDeleted({ id })); + }, [dispatch, id]); + + return ( + + + + + {autoNegative === 'invert' && ( + + {t('controlLayers.autoNegative')} + + )} + + + + + + ); +}); + +RGHeader.displayName = 'RGHeader'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapterSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapterSettings.tsx new file mode 100644 index 00000000000..678b70ee90c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapterSettings.tsx @@ -0,0 +1,139 @@ +import { Box, Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct'; +import { Weight } from 'features/controlLayers/components/common/Weight'; +import { IPAImagePreview } from 'features/controlLayers/components/IPAdapter/IPAImagePreview'; +import { IPAMethod } from 'features/controlLayers/components/IPAdapter/IPAMethod'; +import { IPAModelCombobox } from 'features/controlLayers/components/IPAdapter/IPAModelCombobox'; +import { + rgIPAdapterBeginEndStepPctChanged, + rgIPAdapterCLIPVisionModelChanged, + rgIPAdapterDeleted, + rgIPAdapterImageChanged, + rgIPAdapterMethodChanged, + rgIPAdapterModelChanged, + rgIPAdapterWeightChanged, + selectRGOrThrow, +} from 'features/controlLayers/store/regionalGuidanceSlice'; +import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types'; +import type { RGIPAdapterImageDropData } from 'features/dnd/types'; +import { memo, useCallback, useMemo } from 'react'; +import { PiTrashSimpleBold } from 'react-icons/pi'; +import type { ImageDTO, IPAdapterModelConfig, RGIPAdapterImagePostUploadAction } from 'services/api/types'; +import { assert } from 'tsafe'; + +type Props = { + id: string; + ipAdapterId: string; + ipAdapterNumber: number; +}; + +export const RGIPAdapterSettings = memo(({ id, ipAdapterId, ipAdapterNumber }: Props) => { + const dispatch = useAppDispatch(); + const onDeleteIPAdapter = useCallback(() => { + dispatch(rgIPAdapterDeleted({ id, ipAdapterId })); + }, [dispatch, ipAdapterId, id]); + const ipAdapter = useAppSelector((s) => { + const ipa = selectRGOrThrow(s.regionalGuidance, id).ipAdapters.find((ipa) => ipa.id === ipAdapterId); + assert(ipa, `Regional GuidanceIP Adapter with id ${ipAdapterId} not found`); + return ipa; + }); + + const onChangeBeginEndStepPct = useCallback( + (beginEndStepPct: [number, number]) => { + dispatch(rgIPAdapterBeginEndStepPctChanged({ id, ipAdapterId, beginEndStepPct })); + }, + [dispatch, ipAdapterId, id] + ); + + const onChangeWeight = useCallback( + (weight: number) => { + dispatch(rgIPAdapterWeightChanged({ id, ipAdapterId, weight })); + }, + [dispatch, ipAdapterId, id] + ); + + const onChangeIPMethod = useCallback( + (method: IPMethodV2) => { + dispatch(rgIPAdapterMethodChanged({ id, ipAdapterId, method })); + }, + [dispatch, ipAdapterId, id] + ); + + const onChangeModel = useCallback( + (modelConfig: IPAdapterModelConfig) => { + dispatch(rgIPAdapterModelChanged({ id, ipAdapterId, modelConfig })); + }, + [dispatch, ipAdapterId, id] + ); + + const onChangeCLIPVisionModel = useCallback( + (clipVisionModel: CLIPVisionModelV2) => { + dispatch(rgIPAdapterCLIPVisionModelChanged({ id, ipAdapterId, clipVisionModel })); + }, + [dispatch, ipAdapterId, id] + ); + + const onChangeImage = useCallback( + (imageDTO: ImageDTO | null) => { + dispatch(rgIPAdapterImageChanged({ id, ipAdapterId, imageDTO })); + }, + [dispatch, ipAdapterId, id] + ); + + const droppableData = useMemo( + () => ({ actionType: 'SET_RG_IP_ADAPTER_IMAGE', context: { id, ipAdapterId }, id }), + [ipAdapterId, id] + ); + const postUploadAction = useMemo( + () => ({ type: 'SET_RG_IP_ADAPTER_IMAGE', id, ipAdapterId }), + [ipAdapterId, id] + ); + + return ( + + + {`IP Adapter ${ipAdapterNumber}`} + + } + aria-label="Delete IP Adapter" + onClick={onDeleteIPAdapter} + variant="ghost" + colorScheme="error" + /> + + + + + + + + + + + + + + + + + + + + ); +}); + +RGIPAdapterSettings.displayName = 'RGIPAdapterSettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapters.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapters.tsx new file mode 100644 index 00000000000..5d787ffdda2 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapters.tsx @@ -0,0 +1,34 @@ +import { Divider, Flex } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { RGIPAdapterSettings } from 'features/controlLayers/components/RegionalGuidance/RGIPAdapterSettings'; +import { selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice'; +import { memo } from 'react'; + +type Props = { + id: string; +}; + +export const RGIPAdapters = memo(({ id }: Props) => { + const ipAdapterIds = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).ipAdapters.map(({ id }) => id)); + + if (ipAdapterIds.length === 0) { + return null; + } + + return ( + <> + {ipAdapterIds.map((id, index) => ( + + {index > 0 && ( + + + + )} + + + ))} + + ); +}); + +RGIPAdapters.displayName = 'RGLayerIPAdapterList'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx new file mode 100644 index 00000000000..e325b6d9654 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx @@ -0,0 +1,50 @@ +import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import RgbColorPicker from 'common/components/RgbColorPicker'; +import { stopPropagation } from 'common/util/stopPropagation'; +import { rgbColorToString } from 'features/canvas/util/colorToString'; +import { rgFillChanged, selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice'; +import { memo, useCallback } from 'react'; +import type { RgbColor } from 'react-colorful'; +import { useTranslation } from 'react-i18next'; + +type Props = { + id: string; +}; + +export const RGMaskFillColorPicker = memo(({ id }: Props) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const fill = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).fill); + const onChange = useCallback( + (fill: RgbColor) => { + dispatch(rgFillChanged({ id, fill })); + }, + [dispatch, id] + ); + return ( + + + + + + + + + + + ); +}); + +RGMaskFillColorPicker.displayName = 'RGMaskFillColorPicker'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerNegativePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGNegativePrompt.tsx similarity index 65% rename from invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerNegativePrompt.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGNegativePrompt.tsx index 92ae46a1319..e42f2728aa5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerNegativePrompt.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGNegativePrompt.tsx @@ -1,8 +1,7 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { RGLayerPromptDeleteButton } from 'features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton'; -import { useLayerNegativePrompt } from 'features/controlLayers/hooks/layerStateHooks'; -import { regionalGuidanceNegativePromptChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { RGDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RGDeletePromptButton'; +import { rgNegativePromptChanged, selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; @@ -11,20 +10,23 @@ import { memo, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; type Props = { - layerId: string; + id: string; }; -export const RGLayerNegativePrompt = memo(({ layerId }: Props) => { - const prompt = useLayerNegativePrompt(layerId); +export const RGNegativePrompt = memo(({ id }: Props) => { + const prompt = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).negativePrompt ?? ''); const dispatch = useAppDispatch(); const textareaRef = useRef(null); const { t } = useTranslation(); const _onChange = useCallback( (v: string) => { - dispatch(regionalGuidanceNegativePromptChanged({ layerId, prompt: v })); + dispatch(rgNegativePromptChanged({ id, prompt: v })); }, - [dispatch, layerId] + [dispatch, id] ); + const onDeletePrompt = useCallback(() => { + dispatch(rgNegativePromptChanged({ id, prompt: null })); + }, [dispatch, id]); const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown } = usePrompt({ prompt, textareaRef, @@ -47,7 +49,7 @@ export const RGLayerNegativePrompt = memo(({ layerId }: Props) => { fontSize="sm" /> - + @@ -55,4 +57,4 @@ export const RGLayerNegativePrompt = memo(({ layerId }: Props) => { ); }); -RGLayerNegativePrompt.displayName = 'RGLayerNegativePrompt'; +RGNegativePrompt.displayName = 'RGNegativePrompt'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPositivePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGPositivePrompt.tsx similarity index 65% rename from invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPositivePrompt.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGPositivePrompt.tsx index 34c4366dfc7..0bc82526b22 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPositivePrompt.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGPositivePrompt.tsx @@ -1,8 +1,7 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { RGLayerPromptDeleteButton } from 'features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton'; -import { useLayerPositivePrompt } from 'features/controlLayers/hooks/layerStateHooks'; -import { regionalGuidancePositivePromptChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { RGDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RGDeletePromptButton'; +import { rgPositivePromptChanged, selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; @@ -11,20 +10,23 @@ import { memo, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; type Props = { - layerId: string; + id: string; }; -export const RGLayerPositivePrompt = memo(({ layerId }: Props) => { - const prompt = useLayerPositivePrompt(layerId); +export const RGPositivePrompt = memo(({ id }: Props) => { + const prompt = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).positivePrompt ?? ''); const dispatch = useAppDispatch(); const textareaRef = useRef(null); const { t } = useTranslation(); const _onChange = useCallback( (v: string) => { - dispatch(regionalGuidancePositivePromptChanged({ layerId, prompt: v })); + dispatch(rgPositivePromptChanged({ id, prompt: v })); }, - [dispatch, layerId] + [dispatch, id] ); + const onDeletePrompt = useCallback(() => { + dispatch(rgPositivePromptChanged({ id, prompt: null })); + }, [dispatch, id]); const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown } = usePrompt({ prompt, textareaRef, @@ -47,7 +49,7 @@ export const RGLayerPositivePrompt = memo(({ layerId }: Props) => { minH={28} /> - + @@ -55,4 +57,4 @@ export const RGLayerPositivePrompt = memo(({ layerId }: Props) => { ); }); -RGLayerPositivePrompt.displayName = 'RGLayerPositivePrompt'; +RGPositivePrompt.displayName = 'RGPositivePrompt'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettings.tsx new file mode 100644 index 00000000000..c626facd01a --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettings.tsx @@ -0,0 +1,30 @@ +import { useAppSelector } from 'app/store/storeHooks'; +import { AddPromptButtons } from 'features/controlLayers/components/AddPromptButtons'; +import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; +import { selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice'; +import { memo } from 'react'; + +import { RGIPAdapters } from './RGIPAdapters'; +import { RGNegativePrompt } from './RGNegativePrompt'; +import { RGPositivePrompt } from './RGPositivePrompt'; + +type Props = { + id: string; +}; + +export const RGSettings = memo(({ id }: Props) => { + const hasPositivePrompt = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).positivePrompt !== null); + const hasNegativePrompt = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).negativePrompt !== null); + const hasIPAdapters = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).ipAdapters.length > 0); + + return ( + + {!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && } + {hasPositivePrompt && } + {hasNegativePrompt && } + {hasIPAdapters && } + + ); +}); + +RGSettings.displayName = 'RGSettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettingsPopover.tsx new file mode 100644 index 00000000000..a74f0039faa --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettingsPopover.tsx @@ -0,0 +1,64 @@ +import { + Checkbox, + Flex, + FormControl, + FormLabel, + IconButton, + Popover, + PopoverArrow, + PopoverBody, + PopoverContent, + PopoverTrigger, +} from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { stopPropagation } from 'common/util/stopPropagation'; +import { rgAutoNegativeChanged, selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice'; +import type { ChangeEvent } from 'react'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiGearSixBold } from 'react-icons/pi'; + +type Props = { + id: string; +}; + +export const RGSettingsPopover = memo(({ id }: Props) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const autoNegative = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).autoNegative); + const onChange = useCallback( + (e: ChangeEvent) => { + dispatch(rgAutoNegativeChanged({ id, autoNegative: e.target.checked ? 'invert' : 'off' })); + }, + [dispatch, id] + ); + + return ( + + + } + onDoubleClick={stopPropagation} // double click expands the layer + /> + + + + + + + + {t('controlLayers.autoNegative')} + + + + + + + + ); +}); + +RGSettingsPopover.displayName = 'RGSettingsPopover'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx new file mode 100644 index 00000000000..700c1669bf6 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx @@ -0,0 +1,38 @@ +import type { ChakraProps } from '@invoke-ai/ui-library'; +import { Flex } from '@invoke-ai/ui-library'; +import type { PropsWithChildren } from 'react'; +import { memo, useMemo } from 'react'; + +type Props = PropsWithChildren<{ + isSelected: boolean; + onSelect: () => void; + selectedBorderColor?: ChakraProps['bg']; +}>; + +export const CanvasEntityContainer = memo(({ isSelected, onSelect, selectedBorderColor, children }: Props) => { + const bg = useMemo(() => { + if (isSelected) { + return selectedBorderColor ?? 'base.400'; + } + return 'base.800'; + }, [isSelected, selectedBorderColor]); + return ( + + + {children} + + + ); +}); + +CanvasEntityContainer.displayName = 'CanvasEntityContainer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityDeleteButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityDeleteButton.tsx similarity index 81% rename from invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityDeleteButton.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityDeleteButton.tsx index 441c839587b..1cbb0fa29a8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityDeleteButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityDeleteButton.tsx @@ -6,7 +6,7 @@ import { PiTrashSimpleBold } from 'react-icons/pi'; type Props = { onDelete: () => void }; -export const EntityDeleteButton = memo(({ onDelete }: Props) => { +export const CanvasEntityDeleteButton = memo(({ onDelete }: Props) => { const { t } = useTranslation(); return ( { ); }); -EntityDeleteButton.displayName = 'EntityDeleteButton'; +CanvasEntityDeleteButton.displayName = 'CanvasEntityDeleteButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityEnabledToggle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx similarity index 82% rename from invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityEnabledToggle.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx index ca4855a64fe..eaa41fcfe9e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityEnabledToggle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx @@ -9,7 +9,7 @@ type Props = { onToggle: () => void; }; -export const EntityEnabledToggle = memo(({ isEnabled, onToggle }: Props) => { +export const CanvasEntityEnabledToggle = memo(({ isEnabled, onToggle }: Props) => { const { t } = useTranslation(); return ( @@ -26,4 +26,4 @@ export const EntityEnabledToggle = memo(({ isEnabled, onToggle }: Props) => { ); }); -EntityEnabledToggle.displayName = 'EntityEnabledToggle'; +CanvasEntityEnabledToggle.displayName = 'CanvasEntityEnabledToggle'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx new file mode 100644 index 00000000000..31977b61afb --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx @@ -0,0 +1,15 @@ +import { Flex } from '@invoke-ai/ui-library'; +import type { PropsWithChildren } from 'react'; +import { memo } from 'react'; + +type Props = PropsWithChildren<{ onToggle: () => void }>; + +export const CanvasEntityHeader = memo(({ children, onToggle }: Props) => { + return ( + + {children} + + ); +}); + +CanvasEntityHeader.displayName = 'CanvasEntityHeader'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityMenuButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuButton.tsx similarity index 79% rename from invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityMenuButton.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuButton.tsx index 51887ed5e12..2f358f902d1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityMenuButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuButton.tsx @@ -3,7 +3,7 @@ import { stopPropagation } from 'common/util/stopPropagation'; import { memo } from 'react'; import { PiDotsThreeVerticalBold } from 'react-icons/pi'; -export const EntityMenuButton = memo(() => { +export const CanvasEntityMenuButton = memo(() => { return ( { ); }); -EntityMenuButton.displayName = 'EntityMenuButton'; +CanvasEntityMenuButton.displayName = 'CanvasEntityMenuButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntitySettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntitySettings.tsx new file mode 100644 index 00000000000..d9665c9f0a2 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntitySettings.tsx @@ -0,0 +1,13 @@ +import { Flex } from '@invoke-ai/ui-library'; +import type { PropsWithChildren } from 'react'; +import { memo } from 'react'; + +export const CanvasEntitySettings = memo(({ children }: PropsWithChildren) => { + return ( + + {children} + + ); +}); + +CanvasEntitySettings.displayName = 'CanvasEntitySettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityTitle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitle.tsx similarity index 67% rename from invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityTitle.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitle.tsx index 31fd5902b88..ce72f2f0025 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityTitle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitle.tsx @@ -5,7 +5,7 @@ type Props = { title: string; }; -export const EntityTitle = memo(({ title }: Props) => { +export const CanvasEntityTitle = memo(({ title }: Props) => { return ( {title} @@ -13,4 +13,4 @@ export const EntityTitle = memo(({ title }: Props) => { ); }); -EntityTitle.displayName = 'EntityTitle'; +CanvasEntityTitle.displayName = 'CanvasEntityTitle'; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts index 7c6705ae9b9..b25a700f3f6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts @@ -4,6 +4,7 @@ import type { PersistConfig, RootState } from 'app/store/store'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { getBrushLineId, getEraserLineId, getImageObjectId, getRectShapeId } from 'features/controlLayers/konva/naming'; import type { IRect } from 'konva/lib/types'; +import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; import type { @@ -22,7 +23,12 @@ type LayersState = { }; const initialState: LayersState = { _version: 1, layers: [] }; -const selectLayer = (state: LayersState, id: string) => state.layers.find((layer) => layer.id === id); +export const selectLayer = (state: LayersState, id: string) => state.layers.find((layer) => layer.id === id); +export const selectLayerOrThrow = (state: LayersState, id: string) => { + const layer = selectLayer(state, id); + assert(layer, `Layer with id ${id} not found`); + return layer; +}; export const layersSlice = createSlice({ name: 'layers', @@ -48,13 +54,13 @@ export const layersSlice = createSlice({ layerRecalled: (state, action: PayloadAction<{ data: LayerData }>) => { state.layers.push(action.payload.data); }, - layerIsEnabledChanged: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => { - const { id, isEnabled } = action.payload; + layerIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; const layer = selectLayer(state, id); if (!layer) { return; } - layer.isEnabled = isEnabled; + layer.isEnabled = !layer.isEnabled; }, layerTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => { const { id, x, y } = action.payload; @@ -239,7 +245,7 @@ export const { layerMovedToFront, layerMovedBackwardOne, layerMovedToBack, - layerIsEnabledChanged, + layerIsEnabledToggled, layerOpacityChanged, layerTranslated, layerBboxChanged, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts index cb41306b7e8..2d5c16d4217 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts @@ -36,7 +36,12 @@ const initialState: RegionalGuidanceState = { opacity: 0.3, }; -const selectRg = (state: RegionalGuidanceState, id: string) => state.regions.find((rg) => rg.id === id); +export const selectRG = (state: RegionalGuidanceState, id: string) => state.regions.find((rg) => rg.id === id); +export const selectRGOrThrow = (state: RegionalGuidanceState, id: string) => { + const rg = selectRG(state, id); + assert(rg, `Region with id ${id} not found`); + return rg; +}; const DEFAULT_MASK_COLORS: RgbColor[] = [ { r: 121, g: 157, b: 219 }, // rgb(121, 157, 219) @@ -89,7 +94,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgReset: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -102,16 +107,16 @@ export const regionalGuidanceSlice = createSlice({ const { data } = action.payload; state.regions.push(data); }, - rgIsEnabledToggled: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => { - const { id, isEnabled } = action.payload; - const rg = selectRg(state, id); + rgIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const rg = selectRG(state, id); if (rg) { - rg.isEnabled = isEnabled; + rg.isEnabled = !rg.isEnabled; } }, rgTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => { const { id, x, y } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (rg) { rg.x = x; rg.y = y; @@ -119,7 +124,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { const { id, bbox } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (rg) { rg.bbox = bbox; rg.bboxNeedsUpdate = false; @@ -135,7 +140,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -143,7 +148,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgMovedToFront: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -151,7 +156,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -159,7 +164,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgMovedToBack: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -167,7 +172,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgPositivePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => { const { id, prompt } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -175,7 +180,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgNegativePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => { const { id, prompt } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -183,7 +188,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgFillChanged: (state, action: PayloadAction<{ id: string; fill: RgbColor }>) => { const { id, fill } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -191,7 +196,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgMaskImageUploaded: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO }>) => { const { id, imageDTO } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -199,7 +204,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgAutoNegativeChanged: (state, action: PayloadAction<{ id: string; autoNegative: ParameterAutoNegative }>) => { const { id, autoNegative } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -207,7 +212,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgIPAdapterAdded: (state, action: PayloadAction<{ id: string; ipAdapter: IPAdapterData }>) => { const { id, ipAdapter } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -215,7 +220,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgIPAdapterDeleted: (state, action: PayloadAction<{ id: string; ipAdapterId: string }>) => { const { id, ipAdapterId } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -226,7 +231,7 @@ export const regionalGuidanceSlice = createSlice({ action: PayloadAction<{ id: string; ipAdapterId: string; imageDTO: ImageDTO | null }> ) => { const { id, ipAdapterId, imageDTO } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -238,7 +243,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgIPAdapterWeightChanged: (state, action: PayloadAction<{ id: string; ipAdapterId: string; weight: number }>) => { const { id, ipAdapterId, weight } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -253,7 +258,7 @@ export const regionalGuidanceSlice = createSlice({ action: PayloadAction<{ id: string; ipAdapterId: string; beginEndStepPct: [number, number] }> ) => { const { id, ipAdapterId, beginEndStepPct } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -268,7 +273,7 @@ export const regionalGuidanceSlice = createSlice({ action: PayloadAction<{ id: string; ipAdapterId: string; method: IPMethodV2 }> ) => { const { id, ipAdapterId, method } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -287,7 +292,7 @@ export const regionalGuidanceSlice = createSlice({ }> ) => { const { id, ipAdapterId, modelConfig } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -306,7 +311,7 @@ export const regionalGuidanceSlice = createSlice({ action: PayloadAction<{ id: string; ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }> ) => { const { id, ipAdapterId, clipVisionModel } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -319,7 +324,7 @@ export const regionalGuidanceSlice = createSlice({ rgBrushLineAdded: { reducer: (state, action: PayloadAction) => { const { id, points, lineId, color, width } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -340,7 +345,7 @@ export const regionalGuidanceSlice = createSlice({ rgEraserLineAdded: { reducer: (state, action: PayloadAction) => { const { id, points, lineId, width } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -359,7 +364,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgLinePointAdded: (state, action: PayloadAction) => { const { id, point } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -378,7 +383,7 @@ export const regionalGuidanceSlice = createSlice({ // Ignore zero-area rectangles return; } - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } From 959a433e6152b24592590c52d33ae3631d5e79bb Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 15 Jun 2024 11:39:02 +1000 Subject: [PATCH 038/678] refactor(ui): canvas v2 (wip) missed a spot --- .../web/src/features/controlLayers/components/Layer/Layer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx index 214f4e825cf..06235f59de0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx @@ -1,8 +1,8 @@ import { useDisclosure } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; -import { LayerHeader } from 'features/controlLayers/components/RasterLayer/LayerHeader'; -import { LayerSettings } from 'features/controlLayers/components/RasterLayer/LayerSettings'; +import { LayerHeader } from 'features/controlLayers/components/Layer/LayerHeader'; +import { LayerSettings } from 'features/controlLayers/components/Layer/LayerSettings'; import { entitySelected } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback } from 'react'; From 8c4f98131bb1b9f5599975136339afb52dd93feb Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 15 Jun 2024 18:54:26 +1000 Subject: [PATCH 039/678] refactor(ui): canvas v2 (wip) Fix a few more components --- .../components/ControlLayersPanelContent.tsx | 89 ++++++++++--------- .../components/ControlLayersToolbar.tsx | 23 ++--- .../components/DeleteAllLayersButton.tsx | 15 +++- .../store/controlAdaptersSlice.ts | 4 + .../controlLayers/store/ipAdaptersSlice.ts | 4 + .../controlLayers/store/layersSlice.ts | 4 + .../store/regionalGuidanceSlice.ts | 4 + .../features/controlLayers/store/selectors.ts | 17 ++++ 8 files changed, 99 insertions(+), 61 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/selectors.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index 2dfd08c1b02..c1308d98ec9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -5,70 +5,73 @@ import { useAppSelector } from 'app/store/storeHooks'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import { AddLayerButton } from 'features/controlLayers/components/AddLayerButton'; -import { CALayer } from 'features/controlLayers/components/CALayer/CALayer'; +import { CA } from 'features/controlLayers/components/ControlAdapter/CA'; import { DeleteAllLayersButton } from 'features/controlLayers/components/DeleteAllLayersButton'; -import { IILayer } from 'features/controlLayers/components/IILayer/IILayer'; -import { IPAEntity } from 'features/controlLayers/components/IPALayer/IPALayer'; -import { Layer } from 'features/controlLayers/components/RasterLayer/RasterLayer'; -import { RGLayer } from 'features/controlLayers/components/RGLayer/RGLayer'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; -import type { LayerData } from 'features/controlLayers/store/types'; -import { isRenderableLayer } from 'features/controlLayers/store/types'; -import { partition } from 'lodash-es'; -import { memo } from 'react'; +import { IPA } from 'features/controlLayers/components/IPAdapter/IPA'; +import { Layer } from 'features/controlLayers/components/Layer/Layer'; +import { RG } from 'features/controlLayers/components/RegionalGuidance/RG'; +import { mapId } from 'features/controlLayers/konva/util'; +import { selectControlAdaptersV2Slice } from 'features/controlLayers/store/controlAdaptersSlice'; +import { selectIPAdaptersSlice } from 'features/controlLayers/store/ipAdaptersSlice'; +import { selectLayersSlice } from 'features/controlLayers/store/layersSlice'; +import { selectRegionalGuidanceSlice } from 'features/controlLayers/store/regionalGuidanceSlice'; +import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -const selectLayerIdTypePairs = createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { - const [renderableLayers, ipAdapterLayers] = partition(canvasV2.layers, isRenderableLayer); - return [...ipAdapterLayers, ...renderableLayers].map((l) => ({ id: l.id, type: l.type })).reverse(); +const selectRGIds = createMemoizedSelector(selectRegionalGuidanceSlice, (rgState) => { + return rgState.regions.map(mapId).reverse(); +}); + +const selectCAIds = createMemoizedSelector(selectControlAdaptersV2Slice, (caState) => { + return caState.controlAdapters.map(mapId).reverse(); +}); + +const selectIPAIds = createMemoizedSelector(selectIPAdaptersSlice, (ipaState) => { + return ipaState.ipAdapters.map(mapId).reverse(); +}); + +const selectLayerIds = createMemoizedSelector(selectLayersSlice, (layersState) => { + return layersState.layers.map(mapId).reverse(); }); export const ControlLayersPanelContent = memo(() => { const { t } = useTranslation(); - const layerIdTypePairs = useAppSelector(selectLayerIdTypePairs); + const rgIds = useAppSelector(selectRGIds); + const caIds = useAppSelector(selectCAIds); + const ipaIds = useAppSelector(selectIPAIds); + const layerIds = useAppSelector(selectLayerIds); + const entityCount = useMemo( + () => rgIds.length + caIds.length + ipaIds.length + layerIds.length, + [rgIds.length, caIds.length, ipaIds.length, layerIds.length] + ); + return ( - {layerIdTypePairs.length > 0 && ( + {entityCount > 0 && ( - {layerIdTypePairs.map(({ id, type }) => ( - + {rgIds.map((id) => ( + + ))} + {caIds.map((id) => ( + + ))} + {ipaIds.map((id) => ( + + ))} + {layerIds.map((id) => ( + ))} )} - {layerIdTypePairs.length === 0 && } + {entityCount === 0 && } ); }); ControlLayersPanelContent.displayName = 'ControlLayersPanelContent'; - -type LayerWrapperProps = { - id: string; - type: LayerData['type']; -}; - -const LayerWrapper = memo(({ id, type }: LayerWrapperProps) => { - if (type === 'regional_guidance_layer') { - return ; - } - if (type === 'control_adapter_layer') { - return ; - } - if (type === 'ip_adapter_layer') { - return ; - } - if (type === 'initial_image_layer') { - return ; - } - if (type === 'raster_layer') { - return ; - } -}); - -LayerWrapper.displayName = 'LayerWrapper'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index 15da0966804..df2e911c506 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -1,24 +1,18 @@ /* 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 { BrushWidth } from 'features/controlLayers/components/BrushSize'; +import { useAppSelector } from 'app/store/storeHooks'; +import { BrushWidth } from 'features/controlLayers/components/BrushWidth'; import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover'; +import { EraserWidth } from 'features/controlLayers/components/EraserWidth'; +import { FillColorPicker } from 'features/controlLayers/components/FillColorPicker'; 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, useMemo } from 'react'; +import { memo } 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 === 'rect'; - }, [tool]); + const tool = useAppSelector((s) => s.canvasV2.tool.selected); return ( @@ -28,8 +22,9 @@ export const ControlLayersToolbar = memo(() => { - {withBrushSize && } - {withBrushColor && } + {tool === 'brush' && } + {tool === 'eraser' && } + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx index e69b83fa791..f43ffc77258 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx @@ -1,6 +1,10 @@ import { Button } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { allLayersDeleted } from 'features/controlLayers/store/controlLayersSlice'; +import { caAllDeleted } from 'features/controlLayers/store/controlAdaptersSlice'; +import { ipaAllDeleted } from 'features/controlLayers/store/ipAdaptersSlice'; +import { layerAllDeleted } from 'features/controlLayers/store/layersSlice'; +import { rgAllDeleted } from 'features/controlLayers/store/regionalGuidanceSlice'; +import { selectEntityCount } from 'features/controlLayers/store/selectors'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiTrashSimpleBold } from 'react-icons/pi'; @@ -8,9 +12,12 @@ import { PiTrashSimpleBold } from 'react-icons/pi'; export const DeleteAllLayersButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const isDisabled = useAppSelector((s) => s.canvasV2.layers.length === 0); + const entityCount = useAppSelector(selectEntityCount); const onClick = useCallback(() => { - dispatch(allLayersDeleted()); + dispatch(caAllDeleted()); + dispatch(rgAllDeleted()); + dispatch(ipaAllDeleted()); + dispatch(layerAllDeleted()); }, [dispatch]); return ( @@ -19,7 +26,7 @@ export const DeleteAllLayersButton = memo(() => { leftIcon={} variant="ghost" colorScheme="error" - isDisabled={isDisabled} + isDisabled={entityCount > 0} data-testid="control-layers-delete-all-layers-button" > {t('controlLayers.deleteAll')} diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts index 8faa0a900b5..07438575e2c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts @@ -246,6 +246,9 @@ export const controlAdaptersV2Slice = createSlice({ } ca.beginEndStepPct = beginEndStepPct; }, + caAllDeleted: (state) => { + state.controlAdapters = []; + }, }, }); @@ -270,6 +273,7 @@ export const { caProcessorPendingBatchIdChanged, caWeightChanged, caBeginEndStepPctChanged, + caAllDeleted, } = controlAdaptersV2Slice.actions; export const selectControlAdaptersV2Slice = (state: RootState) => state.controlAdaptersV2; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersSlice.ts index 9e9a9541aee..cdde6078c80 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersSlice.ts @@ -114,6 +114,9 @@ export const ipAdaptersSlice = createSlice({ } ipa.beginEndStepPct = beginEndStepPct; }, + ipaAllDeleted: (state) => { + state.ipAdapters = []; + }, }, }); @@ -128,6 +131,7 @@ export const { ipaCLIPVisionModelChanged, ipaWeightChanged, ipaBeginEndStepPctChanged, + ipaAllDeleted, } = ipAdaptersSlice.actions; export const selectIPAdaptersSlice = (state: RootState) => state.ipAdapters; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts index b25a700f3f6..66567dc47cb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts @@ -234,6 +234,9 @@ export const layersSlice = createSlice({ }, prepare: (payload: ImageObjectAddedArg) => ({ payload: { ...payload, imageId: uuidv4() } }), }, + layerAllDeleted: (state) => { + state.layers = []; + }, }, }); @@ -254,6 +257,7 @@ export const { layerLinePointAdded, layerRectAdded, layerImageAdded, + layerAllDeleted, } = layersSlice.actions; export const selectLayersSlice = (state: RootState) => state.layers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts index 2d5c16d4217..0b341d9399a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts @@ -398,6 +398,9 @@ export const regionalGuidanceSlice = createSlice({ }, prepare: (payload: RectShapeAddedArg) => ({ payload: { ...payload, rectId: uuidv4() } }), }, + rgAllDeleted: (state) => { + state.regions = []; + }, }, }); @@ -431,6 +434,7 @@ export const { rgEraserLineAdded, rgLinePointAdded, rgRectAdded, + rgAllDeleted, } = regionalGuidanceSlice.actions; export const selectRegionalGuidanceSlice = (state: RootState) => state.regionalGuidance; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts new file mode 100644 index 00000000000..44ac9f32b8a --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts @@ -0,0 +1,17 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { selectControlAdaptersV2Slice } from 'features/controlLayers/store/controlAdaptersSlice'; +import { selectIPAdaptersSlice } from 'features/controlLayers/store/ipAdaptersSlice'; +import { selectLayersSlice } from 'features/controlLayers/store/layersSlice'; +import { selectRegionalGuidanceSlice } from 'features/controlLayers/store/regionalGuidanceSlice'; + +export const selectEntityCount = createSelector( + selectRegionalGuidanceSlice, + selectControlAdaptersV2Slice, + selectIPAdaptersSlice, + selectLayersSlice, + (rgState, caState, ipaState, layersState) => { + return ( + rgState.regions.length + caState.controlAdapters.length + ipaState.ipAdapters.length + layersState.layers.length + ); + } +); From 5760d3180e19ca1d906088d1761580e44c997a99 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 15 Jun 2024 20:22:07 +1000 Subject: [PATCH 040/678] refactor(ui): canvas v2 (wip) merge all canvas state reducers into one big slice (but with the logic split across files so it's not hell) --- .../listeners/boardAndImagesDeleted.ts | 2 +- .../listeners/controlAdapterPreprocessor.ts | 2 +- .../listeners/imageDeletionListeners.ts | 2 +- .../listeners/imageDropped.ts | 2 +- .../listeners/imageUploaded.ts | 68 +-- .../listeners/modelsLoaded.ts | 2 +- .../listeners/promptChanged.ts | 2 +- .../listeners/setDefaultSettings.ts | 2 +- invokeai/frontend/web/src/app/store/store.ts | 20 +- .../src/common/hooks/useIsReadyToEnqueue.ts | 2 +- .../components/ControlAdapterImagePreview.tsx | 2 +- .../components/AddLayerButton.tsx | 3 +- .../components/AddPromptButtons.tsx | 8 +- .../controlLayers/components/BrushWidth.tsx | 2 +- .../components/ControlAdapter/CA.tsx | 2 +- .../ControlAdapter/CAActionsMenu.tsx | 31 +- .../ControlAdapter/CAEntityHeader.tsx | 5 +- .../ControlAdapter/CAImagePreview.tsx | 2 +- .../ControlAdapter/CAOpacityAndFilter.tsx | 2 +- .../components/ControlAdapter/CASettings.tsx | 6 +- .../components/ControlLayersPanelContent.tsx | 37 +- .../components/DeleteAllLayersButton.tsx | 20 +- .../controlLayers/components/EraserWidth.tsx | 2 +- .../components/FillColorPicker.tsx | 2 +- .../components/HeadsUpDisplay.tsx | 2 +- .../components/IPAdapter/IPA.tsx | 2 +- .../components/IPAdapter/IPAHeader.tsx | 5 +- .../components/IPAdapter/IPAImagePreview.tsx | 2 +- .../components/IPAdapter/IPASettings.tsx | 6 +- .../controlLayers/components/Layer/Layer.tsx | 2 +- .../components/Layer/LayerActionsMenu.tsx | 31 +- .../components/Layer/LayerHeader.tsx | 5 +- .../components/Layer/LayerOpacity.tsx | 5 +- .../components/RGGlobalOpacity.tsx | 4 +- .../components/RegionalGuidance/RG.tsx | 6 +- .../RegionalGuidance/RGActionsMenu.tsx | 16 +- .../components/RegionalGuidance/RGHeader.tsx | 7 +- .../RegionalGuidance/RGIPAdapterSettings.tsx | 6 +- .../RegionalGuidance/RGIPAdapters.tsx | 4 +- .../RGMaskFillColorPicker.tsx | 5 +- .../RegionalGuidance/RGNegativePrompt.tsx | 5 +- .../RegionalGuidance/RGPositivePrompt.tsx | 5 +- .../RegionalGuidance/RGSettings.tsx | 8 +- .../RegionalGuidance/RGSettingsPopover.tsx | 5 +- .../components/StageComponent.tsx | 86 ++-- .../controlLayers/components/ToolChooser.tsx | 15 +- .../components/UndoRedoButtonGroup.tsx | 2 +- .../controlLayers/hooks/addLayerHooks.ts | 4 +- .../controlLayers/hooks/layerStateHooks.ts | 2 +- ...controlLayersSlice.ts => canvasV2Slice.ts} | 103 ++++ .../store/controlAdaptersReducers.ts | 238 +++++++++ .../store/controlAdaptersSlice.ts | 291 ----------- .../controlLayers/store/ipAdaptersReducers.ts | 102 ++++ .../controlLayers/store/ipAdaptersSlice.ts | 149 ------ .../controlLayers/store/layersReducers.ts | 227 +++++++++ .../controlLayers/store/layersSlice.ts | 275 ----------- .../store/regionalGuidanceSlice.ts | 452 ------------------ .../controlLayers/store/regionsReducers.ts | 381 +++++++++++++++ .../src/features/controlLayers/store/test.ts | 57 +++ .../src/features/controlLayers/store/types.ts | 5 + .../components/DeleteImageModal.tsx | 2 +- .../deleteImageModal/store/selectors.ts | 2 +- .../components/Boards/DeleteBoardModal.tsx | 2 +- .../SingleSelectionMenuItems.tsx | 2 +- .../ImageViewer/CurrentImageButtons.tsx | 2 +- .../src/features/metadata/util/handlers.ts | 2 +- .../src/features/metadata/util/recallers.ts | 2 +- .../util/graph/generation/addControlLayers.ts | 2 +- .../components/Core/ParamNegativePrompt.tsx | 2 +- .../components/Core/ParamPositivePrompt.tsx | 2 +- .../ImageSize/AspectRatioCanvasPreview.tsx | 2 +- .../parameters/hooks/usePreselectedImage.ts | 2 +- .../queue/components/QueueButtonTooltip.tsx | 2 +- .../ParamSDXLNegativeStylePrompt.tsx | 2 +- .../ParamSDXLPositiveStylePrompt.tsx | 2 +- .../SDXLPrompts/SDXLConcatButton.tsx | 2 +- .../ImageSettingsAccordion.tsx | 2 +- .../ImageSizeLinear.tsx | 2 +- .../ParametersPanelTextToImage.tsx | 2 +- 79 files changed, 1313 insertions(+), 1473 deletions(-) rename invokeai/frontend/web/src/features/controlLayers/store/{controlLayersSlice.ts => canvasV2Slice.ts} (72%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts delete mode 100644 invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts delete mode 100644 invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersSlice.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts delete mode 100644 invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts delete mode 100644 invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/test.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts index fb4a23912a7..eb7a793d3a4 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts @@ -1,7 +1,7 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { resetCanvas } from 'features/canvas/store/canvasSlice'; import { controlAdaptersReset } from 'features/controlAdapters/store/controlAdaptersSlice'; -import { allLayersDeleted } from 'features/controlLayers/store/controlLayersSlice'; +import { allLayersDeleted } from 'features/controlLayers/store/canvasV2Slice'; import { getImageUsage } from 'features/deleteImageModal/store/selectors'; import { nodeEditorReset } from 'features/nodes/store/nodesSlice'; import { imagesApi } from 'services/api/endpoints/images'; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts index bdd72764f2a..4e5fb3cb007 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts @@ -10,7 +10,7 @@ import { controlAdapterProcessorConfigChanged, controlAdapterProcessorPendingBatchIdChanged, controlAdapterRecalled, -} from 'features/controlLayers/store/controlLayersSlice'; +} from 'features/controlLayers/store/canvasV2Slice'; import { isControlAdapterLayer } from 'features/controlLayers/store/types'; import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; import { toast } from 'features/toast/toast'; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts index 218b0be8ee7..cb4e9ec8c8d 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts @@ -8,7 +8,7 @@ import { selectControlAdapterAll, } from 'features/controlAdapters/store/controlAdaptersSlice'; import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; -import { layerDeleted } from 'features/controlLayers/store/controlLayersSlice'; +import { layerDeleted } from 'features/controlLayers/store/canvasV2Slice'; import { isControlAdapterLayer, isInitialImageLayer, diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index 663a8f7d193..f5d04ccd020 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -13,7 +13,7 @@ import { layerImageAdded, ipAdapterImageChanged, regionalGuidanceIPAdapterImageChanged, -} from 'features/controlLayers/store/controlLayersSlice'; +} from 'features/controlLayers/store/canvasV2Slice'; import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import { isValidDrop } from 'features/dnd/util/isValidDrop'; import { diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts index 3c9059d9c95..e63d0aa8c39 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts @@ -1,19 +1,8 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; -import { - controlAdapterImageChanged, - controlAdapterIsEnabledChanged, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { - controlAdapterImageChanged, - iiLayerImageChanged, - ipAdapterImageChanged, - regionalGuidanceIPAdapterImageChanged, -} from 'features/controlLayers/store/controlLayersSlice'; +import { caImageChanged, ipaImageChanged, rgIPAdapterImageChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectListBoardsQueryArgs } from 'features/gallery/store/gallerySelectors'; import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; @@ -81,15 +70,6 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis return; } - if (postUploadAction?.type === 'SET_CANVAS_INITIAL_IMAGE') { - dispatch(setInitialCanvasImage(imageDTO, selectOptimalDimension(state))); - toast({ - ...DEFAULT_UPLOADED_TOAST, - description: t('toast.setAsCanvasInitialImage'), - }); - return; - } - if (postUploadAction?.type === 'SET_UPSCALE_INITIAL_IMAGE') { dispatch(upscaleInitialImageChanged(imageDTO)); toast({ @@ -99,57 +79,27 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis return; } - if (postUploadAction?.type === 'SET_CONTROL_ADAPTER_IMAGE') { + if (postUploadAction?.type === 'SET_CA_IMAGE') { const { id } = postUploadAction; - dispatch( - controlAdapterIsEnabledChanged({ - id, - isEnabled: true, - }) - ); - dispatch( - controlAdapterImageChanged({ - id, - controlImage: imageDTO.image_name, - }) - ); - toast({ - ...DEFAULT_UPLOADED_TOAST, - description: t('toast.setControlImage'), - }); - return; - } - - if (postUploadAction?.type === 'SET_CA_LAYER_IMAGE') { - const { layerId } = postUploadAction; - dispatch(controlAdapterImageChanged({ layerId, imageDTO })); - toast({ - ...DEFAULT_UPLOADED_TOAST, - description: t('toast.setControlImage'), - }); - } - - if (postUploadAction?.type === 'SET_IPA_LAYER_IMAGE') { - const { layerId } = postUploadAction; - dispatch(ipAdapterImageChanged({ layerId, imageDTO })); + dispatch(caImageChanged({ id, imageDTO })); toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage'), }); } - if (postUploadAction?.type === 'SET_RG_LAYER_IP_ADAPTER_IMAGE') { - const { layerId, ipAdapterId } = postUploadAction; - dispatch(regionalGuidanceIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO })); + if (postUploadAction?.type === 'SET_IPA_IMAGE') { + const { id } = postUploadAction; + dispatch(ipaImageChanged({ id, imageDTO })); toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage'), }); } - if (postUploadAction?.type === 'SET_II_LAYER_IMAGE') { - const { layerId } = postUploadAction; - dispatch(iiLayerImageChanged({ layerId, imageDTO })); + if (postUploadAction?.type === 'SET_RG_IP_ADAPTER_IMAGE') { + const { id, ipAdapterId } = postUploadAction; + dispatch(rgIPAdapterImageChanged({ id, ipAdapterId, imageDTO })); toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage'), diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts index 30f558086e9..5645567613d 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts @@ -6,7 +6,7 @@ import { controlAdapterModelCleared, selectControlAdapterAll, } from 'features/controlAdapters/store/controlAdaptersSlice'; -import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { heightChanged, widthChanged } from 'features/controlLayers/store/canvasV2Slice'; import { loraRemoved } from 'features/lora/store/loraSlice'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; import { modelChanged, vaeSelected } from 'features/parameters/store/generationSlice'; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts index 2b4da169eba..aba3e8ecc3d 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts @@ -1,6 +1,6 @@ import { isAnyOf } from '@reduxjs/toolkit'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { positivePromptChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { positivePromptChanged } from 'features/controlLayers/store/canvasV2Slice'; import { combinatorialToggled, isErrorChanged, diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts index 415c359d70c..c706b55c7da 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts @@ -1,5 +1,5 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { heightChanged, widthChanged } from 'features/controlLayers/store/canvasV2Slice'; import { setDefaultSettings } from 'features/parameters/store/actions'; import { setCfgRescaleMultiplier, diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 17a4205725d..3cedd0db26f 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -5,17 +5,7 @@ import { idbKeyValDriver } from 'app/store/enhancers/reduxRemember/driver'; import { errorHandler } from 'app/store/enhancers/reduxRemember/errors'; import type { JSONObject } from 'common/types'; import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice'; -import { - controlAdaptersV2PersistConfig, - controlAdaptersV2Slice, -} from 'features/controlLayers/store/controlAdaptersSlice'; -import { canvasV2PersistConfig, canvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; -import { ipAdaptersPersistConfig, ipAdaptersSlice } from 'features/controlLayers/store/ipAdaptersSlice'; -import { layersPersistConfig, layersSlice } from 'features/controlLayers/store/layersSlice'; -import { - regionalGuidancePersistConfig, - regionalGuidanceSlice, -} from 'features/controlLayers/store/regionalGuidanceSlice'; +import { canvasV2PersistConfig, canvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { deleteImageModalSlice } from 'features/deleteImageModal/store/slice'; import { dynamicPromptsPersistConfig, dynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/gallerySlice'; @@ -70,10 +60,6 @@ const allReducers = { [workflowSettingsSlice.name]: workflowSettingsSlice.reducer, [upscaleSlice.name]: upscaleSlice.reducer, [stylePresetSlice.name]: stylePresetSlice.reducer, - [layersSlice.name]: layersSlice.reducer, - [controlAdaptersV2Slice.name]: controlAdaptersV2Slice.reducer, - [ipAdaptersSlice.name]: ipAdaptersSlice.reducer, - [regionalGuidanceSlice.name]: regionalGuidanceSlice.reducer, }; const rootReducer = combineReducers(allReducers); @@ -118,10 +104,6 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = { [workflowSettingsPersistConfig.name]: workflowSettingsPersistConfig, [upscalePersistConfig.name]: upscalePersistConfig, [stylePresetPersistConfig.name]: stylePresetPersistConfig, - [layersPersistConfig.name]: layersPersistConfig, - [controlAdaptersV2PersistConfig.name]: controlAdaptersV2PersistConfig, - [ipAdaptersPersistConfig.name]: ipAdaptersPersistConfig, - [regionalGuidancePersistConfig.name]: regionalGuidancePersistConfig, }; const unserialize: UnserializeFunction = (data, key) => { diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index c47a285c54a..2f2847f058c 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { selectControlAdaptersV2Slice } from 'features/controlLayers/store/controlAdaptersSlice'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { selectIPAdaptersSlice } from 'features/controlLayers/store/ipAdaptersSlice'; import { selectLayersSlice } from 'features/controlLayers/store/layersSlice'; import { selectRegionalGuidanceSlice } from 'features/controlLayers/store/regionalGuidanceSlice'; diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx index 6caf46d0b47..c8670470b29 100644 --- a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx @@ -12,7 +12,7 @@ import { controlAdapterImageChanged, selectControlAdaptersSlice, } from 'features/controlAdapters/store/controlAdaptersSlice'; -import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { heightChanged, widthChanged } from 'features/controlLayers/store/canvasV2Slice'; import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx index 591f4a41a18..aee648be933 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx @@ -1,8 +1,7 @@ import { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { useAddCALayer, useAddIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; -import { layerAdded } from 'features/controlLayers/store/layersSlice'; -import { rgAdded } from 'features/controlLayers/store/regionalGuidanceSlice'; +import { layerAdded, rgAdded } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPlusBold } from 'react-icons/pi'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx index 8f312aba1d9..3f4222b2028 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx @@ -5,8 +5,8 @@ import { useAddIPAdapterToRGLayer } from 'features/controlLayers/hooks/addLayerH import { rgNegativePromptChanged, rgPositivePromptChanged, - selectRegionalGuidanceSlice, -} from 'features/controlLayers/store/regionalGuidanceSlice'; + selectCanvasV2Slice, +} from 'features/controlLayers/store/canvasV2Slice'; import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPlusBold } from 'react-icons/pi'; @@ -21,8 +21,8 @@ export const AddPromptButtons = ({ id }: AddPromptButtonProps) => { const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToRGLayer(id); const selectValidActions = useMemo( () => - createMemoizedSelector(selectRegionalGuidanceSlice, (regionalGuidanceState) => { - const rg = regionalGuidanceState.regions.find((rg) => rg.id === id); + createMemoizedSelector(selectCanvasV2Slice, (caState) => { + const rg = caState.regions.find((rg) => rg.id === id); return { canAddPositivePrompt: rg?.positivePrompt === null, canAddNegativePrompt: rg?.negativePrompt === null, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/BrushWidth.tsx b/invokeai/frontend/web/src/features/controlLayers/components/BrushWidth.tsx index b1b813f6528..fdb7b33d84e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/BrushWidth.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/BrushWidth.tsx @@ -10,7 +10,7 @@ import { PopoverTrigger, } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { brushWidthChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { brushWidthChanged } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx index cd636b71ca1..f58e37b275d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx @@ -3,7 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CAHeader } from 'features/controlLayers/components/ControlAdapter/CAEntityHeader'; import { CASettings } from 'features/controlLayers/components/ControlAdapter/CASettings'; -import { entitySelected } from 'features/controlLayers/store/controlLayersSlice'; +import { entitySelected } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; type Props = { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx index 75b94719427..1219a1d226e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx @@ -8,9 +8,9 @@ import { caMovedForwardOne, caMovedToBack, caMovedToFront, - selectCAOrThrow, - selectControlAdaptersV2Slice, -} from 'features/controlLayers/store/controlAdaptersSlice'; + selectCanvasV2Slice, +} from 'features/controlLayers/store/canvasV2Slice'; +import { selectCAOrThrow } from 'features/controlLayers/store/controlAdaptersReducers'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { @@ -25,20 +25,17 @@ type Props = { id: string; }; -const selectValidActions = createAppSelector( - [selectControlAdaptersV2Slice, (caState, id: string) => id], - (caState, id) => { - const ca = selectCAOrThrow(caState, id); - const caIndex = caState.controlAdapters.indexOf(ca); - const caCount = caState.controlAdapters.length; - return { - canMoveForward: caIndex < caCount - 1, - canMoveBackward: caIndex > 0, - canMoveToFront: caIndex < caCount - 1, - canMoveToBack: caIndex > 0, - }; - } -); +const selectValidActions = createAppSelector([selectCanvasV2Slice, (canvasV2, id: string) => id], (canvasV2, id) => { + const ca = selectCAOrThrow(canvasV2, id); + const caIndex = canvasV2.controlAdapters.indexOf(ca); + const caCount = canvasV2.controlAdapters.length; + return { + canMoveForward: caIndex < caCount - 1, + canMoveBackward: caIndex > 0, + canMoveToFront: caIndex < caCount - 1, + canMoveToBack: caIndex > 0, + }; +}); export const CAActionsMenu = memo(({ id }: Props) => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityHeader.tsx index 1aeaacca8f0..fe49d9953b9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityHeader.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityHeader.tsx @@ -6,7 +6,8 @@ import { CanvasEntityHeader } from 'features/controlLayers/components/common/Can import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; import { CAActionsMenu } from 'features/controlLayers/components/ControlAdapter/CAActionsMenu'; import { CAOpacityAndFilter } from 'features/controlLayers/components/ControlAdapter/CAOpacityAndFilter'; -import { caDeleted, caIsEnabledToggled, selectCAOrThrow } from 'features/controlLayers/store/controlAdaptersSlice'; +import { caDeleted, caIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCAOrThrow } from 'features/controlLayers/store/controlAdaptersReducers'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -18,7 +19,7 @@ type Props = { export const CAHeader = memo(({ id, onToggleVisibility }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const isEnabled = useAppSelector((s) => selectCAOrThrow(s.controlAdaptersV2, id).isEnabled); + const isEnabled = useAppSelector((s) => selectCAOrThrow(s.canvasV2, id).isEnabled); const onToggleIsEnabled = useCallback(() => { dispatch(caIsEnabledToggled({ id })); }, [dispatch, id]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx index 34baca75fe5..fc574fbb045 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx @@ -3,7 +3,7 @@ import { skipToken } from '@reduxjs/toolkit/query'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; -import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { heightChanged, widthChanged } from 'features/controlLayers/store/canvasV2Slice'; import type { ControlAdapterData } from 'features/controlLayers/store/types'; import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAOpacityAndFilter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAOpacityAndFilter.tsx index 7703754044a..ce4e918f529 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAOpacityAndFilter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAOpacityAndFilter.tsx @@ -15,7 +15,7 @@ import { import { useAppDispatch } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; import { useCALayerOpacity } from 'features/controlLayers/hooks/layerStateHooks'; -import { caFilterChanged, caOpacityChanged } from 'features/controlLayers/store/controlAdaptersSlice'; +import { caFilterChanged, caOpacityChanged } from 'features/controlLayers/store/canvasV2Slice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx index 01f7edcc797..292e4565842 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx @@ -16,8 +16,8 @@ import { caProcessedImageChanged, caProcessorConfigChanged, caWeightChanged, - selectCAOrThrow, -} from 'features/controlLayers/store/controlAdaptersSlice'; +} from 'features/controlLayers/store/canvasV2Slice'; +import { selectCAOrThrow } from 'features/controlLayers/store/controlAdaptersReducers'; import type { ControlModeV2, ProcessorConfig } from 'features/controlLayers/store/types'; import type { CAImageDropData } from 'features/dnd/types'; import { memo, useCallback, useMemo } from 'react'; @@ -40,7 +40,7 @@ export const CASettings = memo(({ id }: Props) => { const { t } = useTranslation(); const [isExpanded, toggleIsExpanded] = useToggle(false); - const controlAdapter = useAppSelector((s) => selectCAOrThrow(s.controlAdaptersV2, id)); + const controlAdapter = useAppSelector((s) => selectCAOrThrow(s.canvasV2, id)); const onChangeBeginEndStepPct = useCallback( (beginEndStepPct: [number, number]) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index c1308d98ec9..c4620483353 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -11,39 +11,22 @@ import { IPA } from 'features/controlLayers/components/IPAdapter/IPA'; import { Layer } from 'features/controlLayers/components/Layer/Layer'; import { RG } from 'features/controlLayers/components/RegionalGuidance/RG'; import { mapId } from 'features/controlLayers/konva/util'; -import { selectControlAdaptersV2Slice } from 'features/controlLayers/store/controlAdaptersSlice'; -import { selectIPAdaptersSlice } from 'features/controlLayers/store/ipAdaptersSlice'; -import { selectLayersSlice } from 'features/controlLayers/store/layersSlice'; -import { selectRegionalGuidanceSlice } from 'features/controlLayers/store/regionalGuidanceSlice'; -import { memo, useMemo } from 'react'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -const selectRGIds = createMemoizedSelector(selectRegionalGuidanceSlice, (rgState) => { - return rgState.regions.map(mapId).reverse(); -}); - -const selectCAIds = createMemoizedSelector(selectControlAdaptersV2Slice, (caState) => { - return caState.controlAdapters.map(mapId).reverse(); -}); - -const selectIPAIds = createMemoizedSelector(selectIPAdaptersSlice, (ipaState) => { - return ipaState.ipAdapters.map(mapId).reverse(); -}); - -const selectLayerIds = createMemoizedSelector(selectLayersSlice, (layersState) => { - return layersState.layers.map(mapId).reverse(); +const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2State) => { + const rgIds = canvasV2State.regions.map(mapId).reverse(); + const caIds = canvasV2State.controlAdapters.map(mapId).reverse(); + const ipaIds = canvasV2State.ipAdapters.map(mapId).reverse(); + const layerIds = canvasV2State.layers.map(mapId).reverse(); + const entityCount = rgIds.length + caIds.length + ipaIds.length + layerIds.length; + return { rgIds, caIds, ipaIds, layerIds, entityCount }; }); export const ControlLayersPanelContent = memo(() => { const { t } = useTranslation(); - const rgIds = useAppSelector(selectRGIds); - const caIds = useAppSelector(selectCAIds); - const ipaIds = useAppSelector(selectIPAIds); - const layerIds = useAppSelector(selectLayerIds); - const entityCount = useMemo( - () => rgIds.length + caIds.length + ipaIds.length + layerIds.length, - [rgIds.length, caIds.length, ipaIds.length, layerIds.length] - ); + const { rgIds, caIds, ipaIds, layerIds, entityCount } = useAppSelector(selectEntityIds); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx index f43ffc77258..647a8fba2c8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx @@ -1,10 +1,6 @@ import { Button } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { caAllDeleted } from 'features/controlLayers/store/controlAdaptersSlice'; -import { ipaAllDeleted } from 'features/controlLayers/store/ipAdaptersSlice'; -import { layerAllDeleted } from 'features/controlLayers/store/layersSlice'; -import { rgAllDeleted } from 'features/controlLayers/store/regionalGuidanceSlice'; -import { selectEntityCount } from 'features/controlLayers/store/selectors'; +import { allEntitiesDeleted } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiTrashSimpleBold } from 'react-icons/pi'; @@ -12,12 +8,16 @@ import { PiTrashSimpleBold } from 'react-icons/pi'; export const DeleteAllLayersButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const entityCount = useAppSelector(selectEntityCount); + const entityCount = useAppSelector((s) => { + return ( + s.canvasV2.regions.length + + s.canvasV2.controlAdapters.length + + s.canvasV2.ipAdapters.length + + s.canvasV2.layers.length + ); + }); const onClick = useCallback(() => { - dispatch(caAllDeleted()); - dispatch(rgAllDeleted()); - dispatch(ipaAllDeleted()); - dispatch(layerAllDeleted()); + dispatch(allEntitiesDeleted()); }, [dispatch]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/EraserWidth.tsx b/invokeai/frontend/web/src/features/controlLayers/components/EraserWidth.tsx index d976fa8470b..0903763f2d2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/EraserWidth.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/EraserWidth.tsx @@ -10,7 +10,7 @@ import { PopoverTrigger, } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { eraserWidthChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { eraserWidthChanged } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/FillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/FillColorPicker.tsx index 3550f67f139..3d71523c628 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/FillColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/FillColorPicker.tsx @@ -2,7 +2,7 @@ import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@inv import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIColorPicker from 'common/components/IAIColorPicker'; import { rgbaColorToString } from 'common/util/colorCodeTransformers'; -import { fillChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { fillChanged } from 'features/controlLayers/store/canvasV2Slice'; import type { RgbaColor } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx index 602b29914d6..f5ab9d1ba5c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx @@ -1,7 +1,7 @@ import { Box, Flex, Text } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; -import { $stageAttrs } from 'features/controlLayers/store/controlLayersSlice'; +import { $stageAttrs } from 'features/controlLayers/store/canvasV2Slice'; import { round } from 'lodash-es'; import { memo } from 'react'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx index f94afe4da31..69e88c9d8c4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx @@ -3,7 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { IPAHeader } from 'features/controlLayers/components/IPAdapter/IPAHeader'; import { IPASettings } from 'features/controlLayers/components/IPAdapter/IPASettings'; -import { entitySelected } from 'features/controlLayers/store/controlLayersSlice'; +import { entitySelected } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; type Props = { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeader.tsx index 9604a3283f1..ec56c81b911 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeader.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeader.tsx @@ -4,7 +4,8 @@ import { CanvasEntityDeleteButton } from 'features/controlLayers/components/comm import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; -import { ipaDeleted, ipaIsEnabledToggled, selectIPAOrThrow } from 'features/controlLayers/store/ipAdaptersSlice'; +import { ipaDeleted, ipaIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice'; +import { selectIPAOrThrow } from 'features/controlLayers/store/ipAdaptersReducers'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -16,7 +17,7 @@ type Props = { export const IPAHeader = memo(({ id, onToggleVisibility }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const isEnabled = useAppSelector((s) => selectIPAOrThrow(s.ipAdapters, id).isEnabled); + const isEnabled = useAppSelector((s) => selectIPAOrThrow(s.canvasV2, id).isEnabled); const onToggleIsEnabled = useCallback(() => { dispatch(ipaIsEnabledToggled({ id })); }, [dispatch, id]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx index 47fd0f5c92e..71018b5cc65 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx @@ -3,7 +3,7 @@ import { skipToken } from '@reduxjs/toolkit/query'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; -import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { heightChanged, widthChanged } from 'features/controlLayers/store/canvasV2Slice'; import type { ImageWithDims } from 'features/controlLayers/store/types'; import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx index dfe26b41ed0..f8911818eee 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx @@ -11,8 +11,8 @@ import { ipaMethodChanged, ipaModelChanged, ipaWeightChanged, - selectIPAOrThrow, -} from 'features/controlLayers/store/ipAdaptersSlice'; +} from 'features/controlLayers/store/canvasV2Slice'; +import { selectIPAOrThrow } from 'features/controlLayers/store/ipAdaptersReducers'; import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types'; import type { IPAImageDropData } from 'features/dnd/types'; import { memo, useCallback, useMemo } from 'react'; @@ -27,7 +27,7 @@ type Props = { export const IPASettings = memo(({ id }: Props) => { const dispatch = useAppDispatch(); - const ipAdapter = useAppSelector((s) => selectIPAOrThrow(s.ipAdapters, id)); + const ipAdapter = useAppSelector((s) => selectIPAOrThrow(s.canvasV2, id)); const onChangeBeginEndStepPct = useCallback( (beginEndStepPct: [number, number]) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx index 06235f59de0..e49d76ebbee 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx @@ -3,7 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { LayerHeader } from 'features/controlLayers/components/Layer/LayerHeader'; import { LayerSettings } from 'features/controlLayers/components/Layer/LayerSettings'; -import { entitySelected } from 'features/controlLayers/store/controlLayersSlice'; +import { entitySelected } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; type Props = { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx index 03b4e8953dc..a9ebc599bef 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx @@ -8,9 +8,9 @@ import { layerMovedForwardOne, layerMovedToBack, layerMovedToFront, - selectLayerOrThrow, - selectLayersSlice, -} from 'features/controlLayers/store/layersSlice'; + selectCanvasV2Slice, +} from 'features/controlLayers/store/canvasV2Slice'; +import { selectLayerOrThrow } from 'features/controlLayers/store/layersReducers'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { @@ -25,20 +25,17 @@ type Props = { id: string; }; -const selectValidActions = createAppSelector( - [selectLayersSlice, (layersState, id: string) => id], - (layersState, id) => { - const layer = selectLayerOrThrow(layersState, id); - const layerIndex = layersState.layers.indexOf(layer); - const layerCount = layersState.layers.length; - return { - canMoveForward: layerIndex < layerCount - 1, - canMoveBackward: layerIndex > 0, - canMoveToFront: layerIndex < layerCount - 1, - canMoveToBack: layerIndex > 0, - }; - } -); +const selectValidActions = createAppSelector([selectCanvasV2Slice, (canvasV2, id: string) => id], (canvasV2, id) => { + const layer = selectLayerOrThrow(canvasV2, id); + const layerIndex = canvasV2.layers.indexOf(layer); + const layerCount = canvasV2.layers.length; + return { + canMoveForward: layerIndex < layerCount - 1, + canMoveBackward: layerIndex > 0, + canMoveToFront: layerIndex < layerCount - 1, + canMoveToBack: layerIndex > 0, + }; +}); export const LayerActionsMenu = memo(({ id }: Props) => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx index cd8bc2d5f99..c1f5606ebae 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx @@ -5,7 +5,8 @@ import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/com import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; import { LayerActionsMenu } from 'features/controlLayers/components/Layer/LayerActionsMenu'; -import { layerDeleted, layerIsEnabledToggled, selectLayerOrThrow } from 'features/controlLayers/store/layersSlice'; +import { layerDeleted, layerIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice'; +import { selectLayerOrThrow } from 'features/controlLayers/store/layersReducers'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -19,7 +20,7 @@ type Props = { export const LayerHeader = memo(({ id, onToggleVisibility }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const isEnabled = useAppSelector((s) => selectLayerOrThrow(s.layers, id).isEnabled); + const isEnabled = useAppSelector((s) => selectLayerOrThrow(s.canvasV2, id).isEnabled); const onToggleIsEnabled = useCallback(() => { dispatch(layerIsEnabledToggled({ id })); }, [dispatch, id]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerOpacity.tsx index 00de65b7130..da1582310ef 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerOpacity.tsx @@ -13,7 +13,8 @@ import { } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; -import { layerOpacityChanged, selectLayerOrThrow } from 'features/controlLayers/store/layersSlice'; +import { layerOpacityChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { selectLayerOrThrow } from 'features/controlLayers/store/layersReducers'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiDropHalfFill } from 'react-icons/pi'; @@ -28,7 +29,7 @@ const formatPct = (v: number | string) => `${v} %`; export const LayerOpacity = memo(({ id }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const opacity = useAppSelector((s) => Math.round(selectLayerOrThrow(s.layers, id).opacity * 100)); + const opacity = useAppSelector((s) => Math.round(selectLayerOrThrow(s.canvasV2, id).opacity * 100)); const onChangeOpacity = useCallback( (v: number) => { dispatch(layerOpacityChanged({ id, opacity: v / 100 })); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGGlobalOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGGlobalOpacity.tsx index db56cdd5582..e20026f3ddb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGGlobalOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGGlobalOpacity.tsx @@ -1,6 +1,6 @@ import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { rgGlobalOpacityChanged } from 'features/controlLayers/store/regionalGuidanceSlice'; +import { rgGlobalOpacityChanged } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -10,7 +10,7 @@ const formatPct = (v: number | string) => `${v} %`; export const RGGlobalOpacity = memo(() => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const opacity = useAppSelector((s) => Math.round(s.regionalGuidance.opacity * 100)); + const opacity = useAppSelector((s) => Math.round(s.canvasV2.maskFillOpacity * 100)); const onChange = useCallback( (v: number) => { dispatch(rgGlobalOpacityChanged({ opacity: v / 100 })); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx index 6c503f98e1c..26b5cf624c9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx @@ -4,8 +4,8 @@ import { rgbColorToString } from 'features/canvas/util/colorToString'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { RGHeader } from 'features/controlLayers/components/RegionalGuidance/RGHeader'; import { RGSettings } from 'features/controlLayers/components/RegionalGuidance/RGSettings'; -import { entitySelected } from 'features/controlLayers/store/controlLayersSlice'; -import { selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice'; +import { entitySelected } from 'features/controlLayers/store/canvasV2Slice'; +import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import { memo, useCallback } from 'react'; type Props = { @@ -14,7 +14,7 @@ type Props = { export const RG = memo(({ id }: Props) => { const dispatch = useAppDispatch(); - const selectedBorderColor = useAppSelector((s) => rgbColorToString(selectRGOrThrow(s.regionalGuidance, id).fill)); + const selectedBorderColor = useAppSelector((s) => rgbColorToString(selectRGOrThrow(s.canvasV2, id).fill)); const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); const onSelect = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx index db1cb078bc2..94107c9c0fa 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx @@ -12,9 +12,9 @@ import { rgNegativePromptChanged, rgPositivePromptChanged, rgReset, - selectRegionalGuidanceSlice, - selectRGOrThrow, -} from 'features/controlLayers/store/regionalGuidanceSlice'; + selectCanvasV2Slice, +} from 'features/controlLayers/store/canvasV2Slice'; +import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { @@ -32,11 +32,11 @@ type Props = { }; const selectActionsValidity = createMemoizedAppSelector( - [selectRegionalGuidanceSlice, (rgState, id: string) => id], - (rgState, id) => { - const rg = selectRGOrThrow(rgState, id); - const rgIndex = rgState.regions.indexOf(rg); - const rgCount = rgState.regions.length; + [selectCanvasV2Slice, (canvasV2, id: string) => id], + (canvasV2, id) => { + const rg = selectRGOrThrow(canvasV2, id); + const rgIndex = canvasV2.regions.indexOf(rg); + const rgCount = canvasV2.regions.length; return { isMoveForwardOneDisabled: rgIndex < rgCount - 1, isMoveBackardOneDisabled: rgIndex > 0, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGHeader.tsx index 50f854e856c..ec4cc36b664 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGHeader.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGHeader.tsx @@ -5,7 +5,8 @@ import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/com import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; import { RGActionsMenu } from 'features/controlLayers/components/RegionalGuidance/RGActionsMenu'; -import { rgDeleted, rgIsEnabledToggled, selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice'; +import { rgDeleted, rgIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice'; +import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -20,8 +21,8 @@ type Props = { export const RGHeader = memo(({ id, onToggleVisibility }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const isEnabled = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).isEnabled); - const autoNegative = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).autoNegative); + const isEnabled = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).isEnabled); + const autoNegative = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).autoNegative); const onToggleIsEnabled = useCallback(() => { dispatch(rgIsEnabledToggled({ id })); }, [dispatch, id]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapterSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapterSettings.tsx index 678b70ee90c..6e35a910b77 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapterSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapterSettings.tsx @@ -13,8 +13,8 @@ import { rgIPAdapterMethodChanged, rgIPAdapterModelChanged, rgIPAdapterWeightChanged, - selectRGOrThrow, -} from 'features/controlLayers/store/regionalGuidanceSlice'; +} from 'features/controlLayers/store/canvasV2Slice'; +import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types'; import type { RGIPAdapterImageDropData } from 'features/dnd/types'; import { memo, useCallback, useMemo } from 'react'; @@ -34,7 +34,7 @@ export const RGIPAdapterSettings = memo(({ id, ipAdapterId, ipAdapterNumber }: P dispatch(rgIPAdapterDeleted({ id, ipAdapterId })); }, [dispatch, ipAdapterId, id]); const ipAdapter = useAppSelector((s) => { - const ipa = selectRGOrThrow(s.regionalGuidance, id).ipAdapters.find((ipa) => ipa.id === ipAdapterId); + const ipa = selectRGOrThrow(s.canvasV2, id).ipAdapters.find((ipa) => ipa.id === ipAdapterId); assert(ipa, `Regional GuidanceIP Adapter with id ${ipAdapterId} not found`); return ipa; }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapters.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapters.tsx index 5d787ffdda2..a6df88f1bc7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapters.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapters.tsx @@ -1,7 +1,7 @@ import { Divider, Flex } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { RGIPAdapterSettings } from 'features/controlLayers/components/RegionalGuidance/RGIPAdapterSettings'; -import { selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice'; +import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import { memo } from 'react'; type Props = { @@ -9,7 +9,7 @@ type Props = { }; export const RGIPAdapters = memo(({ id }: Props) => { - const ipAdapterIds = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).ipAdapters.map(({ id }) => id)); + const ipAdapterIds = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).ipAdapters.map(({ id }) => id)); if (ipAdapterIds.length === 0) { return null; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx index e325b6d9654..24df9dc5586 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx @@ -3,7 +3,8 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import RgbColorPicker from 'common/components/RgbColorPicker'; import { stopPropagation } from 'common/util/stopPropagation'; import { rgbColorToString } from 'features/canvas/util/colorToString'; -import { rgFillChanged, selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice'; +import { rgFillChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import { memo, useCallback } from 'react'; import type { RgbColor } from 'react-colorful'; import { useTranslation } from 'react-i18next'; @@ -15,7 +16,7 @@ type Props = { export const RGMaskFillColorPicker = memo(({ id }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const fill = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).fill); + const fill = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).fill); const onChange = useCallback( (fill: RgbColor) => { dispatch(rgFillChanged({ id, fill })); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGNegativePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGNegativePrompt.tsx index e42f2728aa5..db879be4f04 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGNegativePrompt.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGNegativePrompt.tsx @@ -1,7 +1,8 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { RGDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RGDeletePromptButton'; -import { rgNegativePromptChanged, selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice'; +import { rgNegativePromptChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; @@ -14,7 +15,7 @@ type Props = { }; export const RGNegativePrompt = memo(({ id }: Props) => { - const prompt = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).negativePrompt ?? ''); + const prompt = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).negativePrompt ?? ''); const dispatch = useAppDispatch(); const textareaRef = useRef(null); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGPositivePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGPositivePrompt.tsx index 0bc82526b22..49bca080da5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGPositivePrompt.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGPositivePrompt.tsx @@ -1,7 +1,8 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { RGDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RGDeletePromptButton'; -import { rgPositivePromptChanged, selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice'; +import { rgPositivePromptChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; @@ -14,7 +15,7 @@ type Props = { }; export const RGPositivePrompt = memo(({ id }: Props) => { - const prompt = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).positivePrompt ?? ''); + const prompt = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).positivePrompt ?? ''); const dispatch = useAppDispatch(); const textareaRef = useRef(null); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettings.tsx index c626facd01a..7ba5ec84a38 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettings.tsx @@ -1,7 +1,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { AddPromptButtons } from 'features/controlLayers/components/AddPromptButtons'; import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; -import { selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice'; +import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import { memo } from 'react'; import { RGIPAdapters } from './RGIPAdapters'; @@ -13,9 +13,9 @@ type Props = { }; export const RGSettings = memo(({ id }: Props) => { - const hasPositivePrompt = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).positivePrompt !== null); - const hasNegativePrompt = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).negativePrompt !== null); - const hasIPAdapters = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).ipAdapters.length > 0); + const hasPositivePrompt = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).positivePrompt !== null); + const hasNegativePrompt = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).negativePrompt !== null); + const hasIPAdapters = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).ipAdapters.length > 0); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettingsPopover.tsx index a74f0039faa..cf7c1574602 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettingsPopover.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettingsPopover.tsx @@ -12,7 +12,8 @@ import { } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; -import { rgAutoNegativeChanged, selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice'; +import { rgAutoNegativeChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -25,7 +26,7 @@ type Props = { export const RGSettingsPopover = memo(({ id }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const autoNegative = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).autoNegative); + const autoNegative = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).autoNegative); const onChange = useCallback( (e: ChangeEvent) => { dispatch(rgAutoNegativeChanged({ id, autoNegative: e.target.checked ? 'invert' : 'off' })); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index a0b65d3a4e8..3dd7b1df35a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -1,14 +1,11 @@ import { $alt, $ctrl, $meta, $shift, Box, Flex, Heading } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; -import { createSelector } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { rgbaColorToString } from 'features/canvas/util/colorToString'; import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay'; import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants'; import { setStageEventHandlers } from 'features/controlLayers/konva/events'; import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers/layers'; -import { caBboxChanged, caTranslated } from 'features/controlLayers/store/controlAdaptersSlice'; import { $bbox, $currentFill, @@ -23,29 +20,26 @@ import { $toolState, bboxChanged, brushWidthChanged, + caBboxChanged, + caTranslated, eraserWidthChanged, - selectCanvasV2Slice, - toolBufferChanged, - toolChanged, -} from 'features/controlLayers/store/controlLayersSlice'; -import { layerBboxChanged, layerBrushLineAdded, layerEraserLineAdded, layerLinePointAdded, layerRectAdded, layerTranslated, - selectLayersSlice, -} from 'features/controlLayers/store/layersSlice'; -import { rgBboxChanged, rgBrushLineAdded, rgEraserLineAdded, rgLinePointAdded, rgRectAdded, rgTranslated, - selectRegionalGuidanceSlice, -} from 'features/controlLayers/store/regionalGuidanceSlice'; + selectCanvasV2Slice, + toolBufferChanged, + toolChanged, +} from 'features/controlLayers/store/canvasV2Slice'; +import { selectEntityCount } from 'features/controlLayers/store/selectors'; import type { BboxChangedArg, BrushLineAddedArg, @@ -69,62 +63,42 @@ Konva.showWarnings = false; const log = logger('controlLayers'); -const selectBrushFill = createSelector( - selectCanvasV2Slice, - selectLayersSlice, - selectRegionalGuidanceSlice, - (canvas, layers, regionalGuidance) => { - const rg = regionalGuidance.regions.find((i) => i.id === canvas.selectedEntityIdentifier?.id); - - if (rg) { - return rgbaColorToString({ ...rg.fill, a: regionalGuidance.opacity }); - } - - return rgbaColorToString(canvas.tool.fill); - } -); - const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, asPreview: boolean) => { const dispatch = useAppDispatch(); const canvasV2State = useAppSelector(selectCanvasV2Slice); - const layersState = useAppSelector((s) => s.layers); - const controlAdaptersState = useAppSelector((s) => s.controlAdaptersV2); - const ipAdaptersState = useAppSelector((s) => s.ipAdapters); - const regionalGuidanceState = useAppSelector((s) => s.regionalGuidance); const lastCursorPos = useStore($lastCursorPos); const lastMouseDownPos = useStore($lastMouseDownPos); const isMouseDown = useStore($isMouseDown); const isDrawing = useStore($isDrawing); - const brushColor = useAppSelector(selectBrushFill); const selectedEntity = useMemo(() => { const identifier = canvasV2State.selectedEntityIdentifier; if (!identifier) { return null; } else if (identifier.type === 'layer') { - return layersState.layers.find((i) => i.id === identifier.id) ?? null; + return canvasV2State.layers.find((i) => i.id === identifier.id) ?? null; } else if (identifier.type === 'control_adapter') { - return controlAdaptersState.controlAdapters.find((i) => i.id === identifier.id) ?? null; + return canvasV2State.controlAdapters.find((i) => i.id === identifier.id) ?? null; } else if (identifier.type === 'ip_adapter') { - return ipAdaptersState.ipAdapters.find((i) => i.id === identifier.id) ?? null; + return canvasV2State.ipAdapters.find((i) => i.id === identifier.id) ?? null; } else if (identifier.type === 'regional_guidance') { - return regionalGuidanceState.regions.find((i) => i.id === identifier.id) ?? null; + return canvasV2State.regions.find((i) => i.id === identifier.id) ?? null; } else { return null; } }, [ + canvasV2State.controlAdapters, + canvasV2State.ipAdapters, + canvasV2State.layers, + canvasV2State.regions, canvasV2State.selectedEntityIdentifier, - controlAdaptersState.controlAdapters, - ipAdaptersState.ipAdapters, - layersState.layers, - regionalGuidanceState.regions, ]); const currentFill = useMemo(() => { if (selectedEntity && selectedEntity.type === 'regional_guidance') { - return { ...selectedEntity.fill, a: regionalGuidanceState.opacity }; + return { ...selectedEntity.fill, a: canvasV2State.maskFillOpacity }; } return canvasV2State.tool.fill; - }, [canvasV2State.tool.fill, regionalGuidanceState.opacity, selectedEntity]); + }, [canvasV2State.maskFillOpacity, canvasV2State.tool.fill, selectedEntity]); const renderers = useMemo(() => (asPreview ? debouncedRenderers : normalRenderers), [asPreview]); const dpr = useDevicePixelRatio({ round: false }); @@ -341,7 +315,6 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, ); }, [ asPreview, - brushColor, canvasV2State.tool, currentFill, isDrawing, @@ -376,10 +349,10 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, log.trace('Rendering layers'); renderers.renderLayers( stage, - layersState.layers, - controlAdaptersState.controlAdapters, - regionalGuidanceState.regions, - regionalGuidanceState.opacity, + canvasV2State.layers, + canvasV2State.controlAdapters, + canvasV2State.regions, + canvasV2State.maskFillOpacity, canvasV2State.tool.selected, selectedEntity, getImageDTO, @@ -388,13 +361,13 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, }, [ stage, renderers, - layersState.layers, - controlAdaptersState.controlAdapters, - regionalGuidanceState.regions, - regionalGuidanceState.opacity, onPosChanged, canvasV2State.tool.selected, selectedEntity, + canvasV2State.layers, + canvasV2State.controlAdapters, + canvasV2State.regions, + canvasV2State.maskFillOpacity, ]); // useLayoutEffect(() => { @@ -450,7 +423,7 @@ export const StageComponent = memo(({ asPreview = false }: Props) => { backgroundRepeat="repeat" opacity={0.2} /> - {!asPreview && } + {!asPreview && } { StageComponent.displayName = 'StageComponent'; -const NoLayersFallback = () => { +const NoEntitiesFallback = () => { const { t } = useTranslation(); - const layerCount = useAppSelector((s) => s.layers.layers.length); - if (layerCount) { + const entityCount = useAppSelector(selectEntityCount); + + if (entityCount) { return null; } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx index 6434ef402d4..863a56c1bd7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx @@ -1,11 +1,16 @@ import { ButtonGroup, IconButton } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { caDeleted } from 'features/controlLayers/store/controlAdaptersSlice'; -import { selectCanvasV2Slice, toolChanged } from 'features/controlLayers/store/controlLayersSlice'; -import { ipaDeleted } from 'features/controlLayers/store/ipAdaptersSlice'; -import { layerDeleted, layerReset } from 'features/controlLayers/store/layersSlice'; -import { rgDeleted, rgReset } from 'features/controlLayers/store/regionalGuidanceSlice'; +import { + caDeleted, + ipaDeleted, + layerDeleted, + layerReset, + rgDeleted, + rgReset, + selectCanvasV2Slice, + toolChanged, +} from 'features/controlLayers/store/canvasV2Slice'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { useCallback, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/UndoRedoButtonGroup.tsx b/invokeai/frontend/web/src/features/controlLayers/components/UndoRedoButtonGroup.tsx index 8babae7fcc3..eae2bc65cf4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/UndoRedoButtonGroup.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/UndoRedoButtonGroup.tsx @@ -1,7 +1,7 @@ /* eslint-disable i18next/no-literal-string */ import { ButtonGroup, IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { redo, undo } from 'features/controlLayers/store/controlLayersSlice'; +import { redo, undo } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts index c52944b88c1..8d3def70b1a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts @@ -1,7 +1,5 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { caAdded } from 'features/controlLayers/store/controlAdaptersSlice'; -import { ipaAdded } from 'features/controlLayers/store/ipAdaptersSlice'; -import { rgIPAdapterAdded } from 'features/controlLayers/store/regionalGuidanceSlice'; +import { caAdded, ipaAdded, rgIPAdapterAdded } from 'features/controlLayers/store/canvasV2Slice'; import { buildControlNet, buildIPAdapter, diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts index 03f47bcdda3..28f6af7ea49 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts @@ -1,7 +1,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { isControlAdapterLayer, isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; import { assert } from 'tsafe'; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts similarity index 72% rename from invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts rename to invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 0dd87536c7d..206ac47da6d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -3,6 +3,10 @@ import { createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { deepClone } from 'common/util/deepClone'; import { roundDownToMultiple } from 'common/util/roundDownToMultiple'; +import { controlAdaptersReducers } from 'features/controlLayers/store/controlAdaptersReducers'; +import { ipAdaptersReducers } from 'features/controlLayers/store/ipAdaptersReducers'; +import { layersReducers } from 'features/controlLayers/store/layersReducers'; +import { regionsReducers } from 'features/controlLayers/store/regionsReducers'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; @@ -47,12 +51,21 @@ const initialState: CanvasV2State = { width: 512, height: 512, }, + controlAdapters: [], + ipAdapters: [], + regions: [], + layers: [], + maskFillOpacity: 0.3, }; export const canvasV2Slice = createSlice({ name: 'canvasV2', initialState, reducers: { + ...layersReducers, + ...ipAdaptersReducers, + ...controlAdaptersReducers, + ...regionsReducers, positivePromptChanged: (state, action: PayloadAction) => { state.prompts.positivePrompt = action.payload; }, @@ -110,9 +123,18 @@ export const canvasV2Slice = createSlice({ toolBufferChanged: (state, action: PayloadAction) => { state.tool.selectedBuffer = action.payload; }, + maskFillOpacityChanged: (state, action: PayloadAction) => { + state.maskFillOpacity = action.payload; + }, entitySelected: (state, action: PayloadAction) => { state.selectedEntityIdentifier = action.payload; }, + allEntitiesDeleted: (state) => { + state.regions = []; + state.layers = []; + state.ipAdapters = []; + state.controlAdapters = []; + }, }, extraReducers(builder) { builder.addCase(modelChanged, (state, action) => { @@ -148,7 +170,88 @@ export const { invertScrollChanged, toolChanged, toolBufferChanged, + maskFillOpacityChanged, entitySelected, + allEntitiesDeleted, + // layers + layerAdded, + layerDeleted, + layerReset, + layerMovedForwardOne, + layerMovedToFront, + layerMovedBackwardOne, + layerMovedToBack, + layerIsEnabledToggled, + layerOpacityChanged, + layerTranslated, + layerBboxChanged, + layerBrushLineAdded, + layerEraserLineAdded, + layerLinePointAdded, + layerRectAdded, + layerImageAdded, + // IP Adapters + ipaAdded, + ipaRecalled, + ipaIsEnabledToggled, + ipaDeleted, + ipaImageChanged, + ipaMethodChanged, + ipaModelChanged, + ipaCLIPVisionModelChanged, + ipaWeightChanged, + ipaBeginEndStepPctChanged, + // Control Adapters + caAdded, + caBboxChanged, + caDeleted, + caIsEnabledToggled, + caMovedBackwardOne, + caMovedForwardOne, + caMovedToBack, + caMovedToFront, + caOpacityChanged, + caTranslated, + caRecalled, + caImageChanged, + caProcessedImageChanged, + caModelChanged, + caControlModeChanged, + caProcessorConfigChanged, + caFilterChanged, + caProcessorPendingBatchIdChanged, + caWeightChanged, + caBeginEndStepPctChanged, + // Regions + rgAdded, + rgRecalled, + rgReset, + rgIsEnabledToggled, + rgTranslated, + rgBboxChanged, + rgDeleted, + rgGlobalOpacityChanged, + rgMovedForwardOne, + rgMovedToFront, + rgMovedBackwardOne, + rgMovedToBack, + rgPositivePromptChanged, + rgNegativePromptChanged, + rgFillChanged, + rgMaskImageUploaded, + rgAutoNegativeChanged, + rgIPAdapterAdded, + rgIPAdapterDeleted, + rgIPAdapterImageChanged, + rgIPAdapterWeightChanged, + rgIPAdapterBeginEndStepPctChanged, + rgIPAdapterMethodChanged, + rgIPAdapterModelChanged, + rgIPAdapterCLIPVisionModelChanged, + rgBrushLineAdded, + rgEraserLineAdded, + rgLinePointAdded, + rgRectAdded, } = canvasV2Slice.actions; export const selectCanvasV2Slice = (state: RootState) => state.canvasV2; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts new file mode 100644 index 00000000000..6cda64871f5 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts @@ -0,0 +1,238 @@ +import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; +import { zModelIdentifierField } from 'features/nodes/types/common'; +import type { IRect } from 'konva/lib/types'; +import { isEqual } from 'lodash-es'; +import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types'; +import { assert } from 'tsafe'; +import { v4 as uuidv4 } from 'uuid'; + +import type { + CanvasV2State, + ControlAdapterConfig, + ControlAdapterData, + ControlModeV2, + Filter, + ProcessorConfig, +} from './types'; +import { buildControlAdapterProcessorV2, imageDTOToImageWithDims } from './types'; + +export const selectCA = (state: CanvasV2State, id: string) => state.controlAdapters.find((ca) => ca.id === id); +export const selectCAOrThrow = (state: CanvasV2State, id: string) => { + const ca = selectCA(state, id); + assert(ca, `Control Adapter with id ${id} not found`); + return ca; +}; + +export const controlAdaptersReducers = { + caAdded: { + reducer: (state, action: PayloadAction<{ id: string; config: ControlAdapterConfig }>) => { + const { id, config } = action.payload; + state.controlAdapters.push({ + id, + type: 'control_adapter', + x: 0, + y: 0, + bbox: null, + bboxNeedsUpdate: false, + isEnabled: true, + opacity: 1, + filter: 'LightnessToAlphaFilter', + processorPendingBatchId: null, + ...config, + }); + }, + prepare: (config: ControlAdapterConfig) => ({ + payload: { id: uuidv4(), config }, + }), + }, + caRecalled: (state, action: PayloadAction<{ data: ControlAdapterData }>) => { + state.controlAdapters.push(action.payload.data); + }, + caIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const ca = selectCA(state, id); + if (!ca) { + return; + } + ca.isEnabled = !ca.isEnabled; + }, + caTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => { + const { id, x, y } = action.payload; + const ca = selectCA(state, id); + if (!ca) { + return; + } + ca.x = x; + ca.y = y; + }, + caBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { + const { id, bbox } = action.payload; + const ca = selectCA(state, id); + if (!ca) { + return; + } + ca.bbox = bbox; + ca.bboxNeedsUpdate = false; + }, + caDeleted: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + state.controlAdapters = state.controlAdapters.filter((ca) => ca.id !== id); + }, + caOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => { + const { id, opacity } = action.payload; + const ca = selectCA(state, id); + if (!ca) { + return; + } + ca.opacity = opacity; + }, + caMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const ca = selectCA(state, id); + if (!ca) { + return; + } + moveOneToEnd(state.controlAdapters, ca); + }, + caMovedToFront: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const ca = selectCA(state, id); + if (!ca) { + return; + } + moveToEnd(state.controlAdapters, ca); + }, + caMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const ca = selectCA(state, id); + if (!ca) { + return; + } + moveOneToStart(state.controlAdapters, ca); + }, + caMovedToBack: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const ca = selectCA(state, id); + if (!ca) { + return; + } + moveToStart(state.controlAdapters, ca); + }, + caImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => { + const { id, imageDTO } = action.payload; + const ca = selectCA(state, id); + if (!ca) { + return; + } + ca.bbox = null; + ca.bboxNeedsUpdate = true; + ca.isEnabled = true; + if (imageDTO) { + const newImage = imageDTOToImageWithDims(imageDTO); + if (isEqual(newImage, ca.image)) { + return; + } + ca.image = newImage; + ca.processedImage = null; + } else { + ca.image = null; + ca.processedImage = null; + } + }, + caProcessedImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => { + const { id, imageDTO } = action.payload; + const ca = selectCA(state, id); + if (!ca) { + return; + } + ca.bbox = null; + ca.bboxNeedsUpdate = true; + ca.isEnabled = true; + ca.processedImage = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; + }, + caModelChanged: ( + state, + action: PayloadAction<{ + id: string; + modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null; + }> + ) => { + const { id, modelConfig } = action.payload; + const ca = selectCA(state, id); + if (!ca) { + return; + } + if (!modelConfig) { + ca.model = null; + return; + } + ca.model = zModelIdentifierField.parse(modelConfig); + + // We may need to convert the CA to match the model + if (!ca.controlMode && ca.model.type === 'controlnet') { + ca.controlMode = 'balanced'; + } else if (ca.controlMode && ca.model.type === 't2i_adapter') { + ca.controlMode = null; + } + + const candidateProcessorConfig = buildControlAdapterProcessorV2(modelConfig); + if (candidateProcessorConfig?.type !== ca.processorConfig?.type) { + // The processor has changed. For example, the previous model was a Canny model and the new model is a Depth + // model. We need to use the new processor. + ca.processedImage = null; + ca.processorConfig = candidateProcessorConfig; + } + }, + caControlModeChanged: (state, action: PayloadAction<{ id: string; controlMode: ControlModeV2 }>) => { + const { id, controlMode } = action.payload; + const ca = selectCA(state, id); + if (!ca) { + return; + } + ca.controlMode = controlMode; + }, + caProcessorConfigChanged: (state, action: PayloadAction<{ id: string; processorConfig: ProcessorConfig | null }>) => { + const { id, processorConfig } = action.payload; + const ca = selectCA(state, id); + if (!ca) { + return; + } + ca.processorConfig = processorConfig; + if (!processorConfig) { + ca.processedImage = null; + } + }, + caFilterChanged: (state, action: PayloadAction<{ id: string; filter: Filter }>) => { + const { id, filter } = action.payload; + const ca = selectCA(state, id); + if (!ca) { + return; + } + ca.filter = filter; + }, + caProcessorPendingBatchIdChanged: (state, action: PayloadAction<{ id: string; batchId: string | null }>) => { + const { id, batchId } = action.payload; + const ca = selectCA(state, id); + if (!ca) { + return; + } + ca.processorPendingBatchId = batchId; + }, + caWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { + const { id, weight } = action.payload; + const ca = selectCA(state, id); + if (!ca) { + return; + } + ca.weight = weight; + }, + caBeginEndStepPctChanged: (state, action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }>) => { + const { id, beginEndStepPct } = action.payload; + const ca = selectCA(state, id); + if (!ca) { + return; + } + ca.beginEndStepPct = beginEndStepPct; + }, +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts deleted file mode 100644 index 07438575e2c..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts +++ /dev/null @@ -1,291 +0,0 @@ -import type { PayloadAction } from '@reduxjs/toolkit'; -import { createSlice } from '@reduxjs/toolkit'; -import type { PersistConfig, RootState } from 'app/store/store'; -import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; -import { zModelIdentifierField } from 'features/nodes/types/common'; -import type { IRect } from 'konva/lib/types'; -import { isEqual } from 'lodash-es'; -import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types'; -import { assert } from 'tsafe'; -import { v4 as uuidv4 } from 'uuid'; - -import type { ControlAdapterConfig, ControlAdapterData, ControlModeV2, Filter, ProcessorConfig } from './types'; -import { buildControlAdapterProcessorV2, imageDTOToImageWithDims } from './types'; - -type ControlAdaptersV2State = { - _version: 1; - controlAdapters: ControlAdapterData[]; -}; - -const initialState: ControlAdaptersV2State = { - _version: 1, - controlAdapters: [], -}; - -export const selectCA = (state: ControlAdaptersV2State, id: string) => state.controlAdapters.find((ca) => ca.id === id); -export const selectCAOrThrow = (state: ControlAdaptersV2State, id: string) => { - const ca = selectCA(state, id); - assert(ca, `Control Adapter with id ${id} not found`); - return ca; -}; - -export const controlAdaptersV2Slice = createSlice({ - name: 'controlAdaptersV2', - initialState, - reducers: { - caAdded: { - reducer: (state, action: PayloadAction<{ id: string; config: ControlAdapterConfig }>) => { - const { id, config } = action.payload; - state.controlAdapters.push({ - id, - type: 'control_adapter', - x: 0, - y: 0, - bbox: null, - bboxNeedsUpdate: false, - isEnabled: true, - opacity: 1, - filter: 'LightnessToAlphaFilter', - processorPendingBatchId: null, - ...config, - }); - }, - prepare: (config: ControlAdapterConfig) => ({ - payload: { id: uuidv4(), config }, - }), - }, - caRecalled: (state, action: PayloadAction<{ data: ControlAdapterData }>) => { - state.controlAdapters.push(action.payload.data); - }, - caIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - ca.isEnabled = !ca.isEnabled; - }, - caTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => { - const { id, x, y } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - ca.x = x; - ca.y = y; - }, - caBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { - const { id, bbox } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - ca.bbox = bbox; - ca.bboxNeedsUpdate = false; - }, - caDeleted: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - state.controlAdapters = state.controlAdapters.filter((ca) => ca.id !== id); - }, - caOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => { - const { id, opacity } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - ca.opacity = opacity; - }, - caMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - moveOneToEnd(state.controlAdapters, ca); - }, - caMovedToFront: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - moveToEnd(state.controlAdapters, ca); - }, - caMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - moveOneToStart(state.controlAdapters, ca); - }, - caMovedToBack: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - moveToStart(state.controlAdapters, ca); - }, - caImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => { - const { id, imageDTO } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - ca.bbox = null; - ca.bboxNeedsUpdate = true; - ca.isEnabled = true; - if (imageDTO) { - const newImage = imageDTOToImageWithDims(imageDTO); - if (isEqual(newImage, ca.image)) { - return; - } - ca.image = newImage; - ca.processedImage = null; - } else { - ca.image = null; - ca.processedImage = null; - } - }, - caProcessedImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => { - const { id, imageDTO } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - ca.bbox = null; - ca.bboxNeedsUpdate = true; - ca.isEnabled = true; - ca.processedImage = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; - }, - caModelChanged: ( - state, - action: PayloadAction<{ - id: string; - modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null; - }> - ) => { - const { id, modelConfig } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - if (!modelConfig) { - ca.model = null; - return; - } - ca.model = zModelIdentifierField.parse(modelConfig); - - // We may need to convert the CA to match the model - if (!ca.controlMode && ca.model.type === 'controlnet') { - ca.controlMode = 'balanced'; - } else if (ca.controlMode && ca.model.type === 't2i_adapter') { - ca.controlMode = null; - } - - const candidateProcessorConfig = buildControlAdapterProcessorV2(modelConfig); - if (candidateProcessorConfig?.type !== ca.processorConfig?.type) { - // The processor has changed. For example, the previous model was a Canny model and the new model is a Depth - // model. We need to use the new processor. - ca.processedImage = null; - ca.processorConfig = candidateProcessorConfig; - } - }, - caControlModeChanged: (state, action: PayloadAction<{ id: string; controlMode: ControlModeV2 }>) => { - const { id, controlMode } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - ca.controlMode = controlMode; - }, - caProcessorConfigChanged: ( - state, - action: PayloadAction<{ id: string; processorConfig: ProcessorConfig | null }> - ) => { - const { id, processorConfig } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - ca.processorConfig = processorConfig; - if (!processorConfig) { - ca.processedImage = null; - } - }, - caFilterChanged: (state, action: PayloadAction<{ id: string; filter: Filter }>) => { - const { id, filter } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - ca.filter = filter; - }, - caProcessorPendingBatchIdChanged: (state, action: PayloadAction<{ id: string; batchId: string | null }>) => { - const { id, batchId } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - ca.processorPendingBatchId = batchId; - }, - caWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { - const { id, weight } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - ca.weight = weight; - }, - caBeginEndStepPctChanged: (state, action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }>) => { - const { id, beginEndStepPct } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - ca.beginEndStepPct = beginEndStepPct; - }, - caAllDeleted: (state) => { - state.controlAdapters = []; - }, - }, -}); - -export const { - caAdded, - caBboxChanged, - caDeleted, - caIsEnabledToggled, - caMovedBackwardOne, - caMovedForwardOne, - caMovedToBack, - caMovedToFront, - caOpacityChanged, - caTranslated, - caRecalled, - caImageChanged, - caProcessedImageChanged, - caModelChanged, - caControlModeChanged, - caProcessorConfigChanged, - caFilterChanged, - caProcessorPendingBatchIdChanged, - caWeightChanged, - caBeginEndStepPctChanged, - caAllDeleted, -} = controlAdaptersV2Slice.actions; - -export const selectControlAdaptersV2Slice = (state: RootState) => state.controlAdaptersV2; - -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrate = (state: any): any => { - return state; -}; - -export const controlAdaptersV2PersistConfig: PersistConfig = { - name: controlAdaptersV2Slice.name, - initialState, - migrate, - persistDenylist: [], -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts new file mode 100644 index 00000000000..16cf222b60c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts @@ -0,0 +1,102 @@ +import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import { zModelIdentifierField } from 'features/nodes/types/common'; +import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; +import { assert } from 'tsafe'; +import { v4 as uuidv4 } from 'uuid'; + +import type { CanvasV2State, CLIPVisionModelV2, IPAdapterConfig, IPAdapterData, IPMethodV2 } from './types'; +import { imageDTOToImageWithDims } from './types'; + +export const selectIPA = (state: CanvasV2State, id: string) => state.ipAdapters.find((ipa) => ipa.id === id); +export const selectIPAOrThrow = (state: CanvasV2State, id: string) => { + const ipa = selectIPA(state, id); + assert(ipa, `IP Adapter with id ${id} not found`); + return ipa; +}; + +export const ipAdaptersReducers = { + ipaAdded: { + reducer: (state, action: PayloadAction<{ id: string; config: IPAdapterConfig }>) => { + const { id, config } = action.payload; + const layer: IPAdapterData = { + id, + type: 'ip_adapter', + isEnabled: true, + ...config, + }; + state.ipAdapters.push(layer); + }, + prepare: (config: IPAdapterConfig) => ({ payload: { id: uuidv4(), config } }), + }, + ipaRecalled: (state, action: PayloadAction<{ data: IPAdapterData }>) => { + state.ipAdapters.push(action.payload.data); + }, + ipaIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const ipa = selectIPA(state, id); + if (ipa) { + ipa.isEnabled = !ipa.isEnabled; + } + }, + ipaDeleted: (state, action: PayloadAction<{ id: string }>) => { + state.ipAdapters = state.ipAdapters.filter((ipa) => ipa.id !== action.payload.id); + }, + ipaImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => { + const { id, imageDTO } = action.payload; + const ipa = selectIPA(state, id); + if (!ipa) { + return; + } + ipa.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; + }, + ipaMethodChanged: (state, action: PayloadAction<{ id: string; method: IPMethodV2 }>) => { + const { id, method } = action.payload; + const ipa = selectIPA(state, id); + if (!ipa) { + return; + } + ipa.method = method; + }, + ipaModelChanged: ( + state, + action: PayloadAction<{ + id: string; + modelConfig: IPAdapterModelConfig | null; + }> + ) => { + const { id, modelConfig } = action.payload; + const ipa = selectIPA(state, id); + if (!ipa) { + return; + } + if (modelConfig) { + ipa.model = zModelIdentifierField.parse(modelConfig); + } else { + ipa.model = null; + } + }, + ipaCLIPVisionModelChanged: (state, action: PayloadAction<{ id: string; clipVisionModel: CLIPVisionModelV2 }>) => { + const { id, clipVisionModel } = action.payload; + const ipa = selectIPA(state, id); + if (!ipa) { + return; + } + ipa.clipVisionModel = clipVisionModel; + }, + ipaWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { + const { id, weight } = action.payload; + const ipa = selectIPA(state, id); + if (!ipa) { + return; + } + ipa.weight = weight; + }, + ipaBeginEndStepPctChanged: (state, action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }>) => { + const { id, beginEndStepPct } = action.payload; + const ipa = selectIPA(state, id); + if (!ipa) { + return; + } + ipa.beginEndStepPct = beginEndStepPct; + }, +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersSlice.ts deleted file mode 100644 index cdde6078c80..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersSlice.ts +++ /dev/null @@ -1,149 +0,0 @@ -import type { PayloadAction } from '@reduxjs/toolkit'; -import { createSlice } from '@reduxjs/toolkit'; -import type { PersistConfig, RootState } from 'app/store/store'; -import { zModelIdentifierField } from 'features/nodes/types/common'; -import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; -import { assert } from 'tsafe'; -import { v4 as uuidv4 } from 'uuid'; - -import type { CLIPVisionModelV2, IPAdapterConfig, IPAdapterData, IPMethodV2 } from './types'; -import { imageDTOToImageWithDims } from './types'; - -type IPAdaptersState = { - _version: 1; - ipAdapters: IPAdapterData[]; -}; - -const initialState: IPAdaptersState = { - _version: 1, - ipAdapters: [], -}; - -export const selectIPA = (state: IPAdaptersState, id: string) => state.ipAdapters.find((ipa) => ipa.id === id); -export const selectIPAOrThrow = (state: IPAdaptersState, id: string) => { - const ipa = selectIPA(state, id); - assert(ipa, `IP Adapter with id ${id} not found`); - return ipa; -}; - -export const ipAdaptersSlice = createSlice({ - name: 'ipAdapters', - initialState, - reducers: { - ipaAdded: { - reducer: (state, action: PayloadAction<{ id: string; config: IPAdapterConfig }>) => { - const { id, config } = action.payload; - const layer: IPAdapterData = { - id, - type: 'ip_adapter', - isEnabled: true, - ...config, - }; - state.ipAdapters.push(layer); - }, - prepare: (config: IPAdapterConfig) => ({ payload: { id: uuidv4(), config } }), - }, - ipaRecalled: (state, action: PayloadAction<{ data: IPAdapterData }>) => { - state.ipAdapters.push(action.payload.data); - }, - ipaIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const ipa = selectIPA(state, id); - if (ipa) { - ipa.isEnabled = !ipa.isEnabled; - } - }, - ipaDeleted: (state, action: PayloadAction<{ id: string }>) => { - state.ipAdapters = state.ipAdapters.filter((ipa) => ipa.id !== action.payload.id); - }, - ipaImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => { - const { id, imageDTO } = action.payload; - const ipa = selectIPA(state, id); - if (!ipa) { - return; - } - ipa.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; - }, - ipaMethodChanged: (state, action: PayloadAction<{ id: string; method: IPMethodV2 }>) => { - const { id, method } = action.payload; - const ipa = selectIPA(state, id); - if (!ipa) { - return; - } - ipa.method = method; - }, - ipaModelChanged: ( - state, - action: PayloadAction<{ - id: string; - modelConfig: IPAdapterModelConfig | null; - }> - ) => { - const { id, modelConfig } = action.payload; - const ipa = selectIPA(state, id); - if (!ipa) { - return; - } - if (modelConfig) { - ipa.model = zModelIdentifierField.parse(modelConfig); - } else { - ipa.model = null; - } - }, - ipaCLIPVisionModelChanged: (state, action: PayloadAction<{ id: string; clipVisionModel: CLIPVisionModelV2 }>) => { - const { id, clipVisionModel } = action.payload; - const ipa = selectIPA(state, id); - if (!ipa) { - return; - } - ipa.clipVisionModel = clipVisionModel; - }, - ipaWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { - const { id, weight } = action.payload; - const ipa = selectIPA(state, id); - if (!ipa) { - return; - } - ipa.weight = weight; - }, - ipaBeginEndStepPctChanged: (state, action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }>) => { - const { id, beginEndStepPct } = action.payload; - const ipa = selectIPA(state, id); - if (!ipa) { - return; - } - ipa.beginEndStepPct = beginEndStepPct; - }, - ipaAllDeleted: (state) => { - state.ipAdapters = []; - }, - }, -}); - -export const { - ipaAdded, - ipaRecalled, - ipaIsEnabledToggled, - ipaDeleted, - ipaImageChanged, - ipaMethodChanged, - ipaModelChanged, - ipaCLIPVisionModelChanged, - ipaWeightChanged, - ipaBeginEndStepPctChanged, - ipaAllDeleted, -} = ipAdaptersSlice.actions; - -export const selectIPAdaptersSlice = (state: RootState) => state.ipAdapters; - -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrate = (state: any): any => { - return state; -}; - -export const ipAdaptersPersistConfig: PersistConfig = { - name: ipAdaptersSlice.name, - initialState, - migrate, - persistDenylist: [], -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts new file mode 100644 index 00000000000..d44585fd4fa --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -0,0 +1,227 @@ +import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; +import { getBrushLineId, getEraserLineId, getImageObjectId, getRectShapeId } from 'features/controlLayers/konva/naming'; +import type { IRect } from 'konva/lib/types'; +import { assert } from 'tsafe'; +import { v4 as uuidv4 } from 'uuid'; + +import type { + BrushLineAddedArg, + CanvasV2State, + EraserLineAddedArg, + ImageObjectAddedArg, + LayerData, + PointAddedToLineArg, + RectShapeAddedArg, +} from './types'; +import { isLine } from './types'; + +export const selectLayer = (state: CanvasV2State, id: string) => state.layers.find((layer) => layer.id === id); +export const selectLayerOrThrow = (state: CanvasV2State, id: string) => { + const layer = selectLayer(state, id); + assert(layer, `Layer with id ${id} not found`); + return layer; +}; + +export const layersReducers = { + layerAdded: { + reducer: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + state.layers.push({ + id, + type: 'layer', + isEnabled: true, + bbox: null, + bboxNeedsUpdate: false, + objects: [], + opacity: 1, + x: 0, + y: 0, + }); + }, + prepare: () => ({ payload: { id: uuidv4() } }), + }, + layerRecalled: (state, action: PayloadAction<{ data: LayerData }>) => { + state.layers.push(action.payload.data); + }, + layerIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + layer.isEnabled = !layer.isEnabled; + }, + layerTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => { + const { id, x, y } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + layer.x = x; + layer.y = y; + }, + layerBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { + const { id, bbox } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + layer.bbox = bbox; + layer.bboxNeedsUpdate = false; + if (bbox === null) { + layer.objects = []; + } + }, + layerReset: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + layer.isEnabled = true; + layer.objects = []; + layer.bbox = null; + layer.bboxNeedsUpdate = false; + }, + layerDeleted: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + state.layers = state.layers.filter((l) => l.id !== id); + }, + layerOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => { + const { id, opacity } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + layer.opacity = opacity; + }, + layerMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + moveOneToEnd(state.layers, layer); + }, + layerMovedToFront: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + moveToEnd(state.layers, layer); + }, + layerMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + moveOneToStart(state.layers, layer); + }, + layerMovedToBack: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + moveToStart(state.layers, layer); + }, + layerBrushLineAdded: { + reducer: (state, action: PayloadAction) => { + const { id, points, lineId, color, width } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + + layer.objects.push({ + id: getBrushLineId(id, lineId), + type: 'brush_line', + points, + strokeWidth: width, + color, + }); + layer.bboxNeedsUpdate = true; + }, + prepare: (payload: BrushLineAddedArg) => ({ + payload: { ...payload, lineId: uuidv4() }, + }), + }, + layerEraserLineAdded: { + reducer: (state, action: PayloadAction) => { + const { id, points, lineId, width } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + + layer.objects.push({ + id: getEraserLineId(id, lineId), + type: 'eraser_line', + points, + strokeWidth: width, + }); + layer.bboxNeedsUpdate = true; + }, + prepare: (payload: EraserLineAddedArg) => ({ + payload: { ...payload, lineId: uuidv4() }, + }), + }, + layerLinePointAdded: (state, action: PayloadAction) => { + const { id, point } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + const lastObject = layer.objects[layer.objects.length - 1]; + if (!lastObject || !isLine(lastObject)) { + return; + } + lastObject.points.push(...point); + layer.bboxNeedsUpdate = true; + }, + layerRectAdded: { + reducer: (state, action: PayloadAction) => { + const { id, rect, rectId, color } = action.payload; + if (rect.height === 0 || rect.width === 0) { + // Ignore zero-area rectangles + return; + } + const layer = selectLayer(state, id); + if (!layer) { + return; + } + layer.objects.push({ + type: 'rect_shape', + id: getRectShapeId(id, rectId), + ...rect, + color, + }); + layer.bboxNeedsUpdate = true; + }, + prepare: (payload: RectShapeAddedArg) => ({ payload: { ...payload, rectId: uuidv4() } }), + }, + layerImageAdded: { + reducer: (state, action: PayloadAction) => { + const { id, imageId, imageDTO } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + const { width, height, image_name: name } = imageDTO; + layer.objects.push({ + type: 'image', + id: getImageObjectId(id, imageId), + x: 0, + y: 0, + width, + height, + image: { width, height, name }, + }); + layer.bboxNeedsUpdate = true; + }, + prepare: (payload: ImageObjectAddedArg) => ({ payload: { ...payload, imageId: uuidv4() } }), + }, +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts deleted file mode 100644 index 66567dc47cb..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts +++ /dev/null @@ -1,275 +0,0 @@ -import type { PayloadAction } from '@reduxjs/toolkit'; -import { createSlice } from '@reduxjs/toolkit'; -import type { PersistConfig, RootState } from 'app/store/store'; -import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; -import { getBrushLineId, getEraserLineId, getImageObjectId, getRectShapeId } from 'features/controlLayers/konva/naming'; -import type { IRect } from 'konva/lib/types'; -import { assert } from 'tsafe'; -import { v4 as uuidv4 } from 'uuid'; - -import type { - BrushLineAddedArg, - EraserLineAddedArg, - ImageObjectAddedArg, - LayerData, - PointAddedToLineArg, - RectShapeAddedArg, -} from './types'; -import { isLine } from './types'; - -type LayersState = { - _version: 1; - layers: LayerData[]; -}; - -const initialState: LayersState = { _version: 1, layers: [] }; -export const selectLayer = (state: LayersState, id: string) => state.layers.find((layer) => layer.id === id); -export const selectLayerOrThrow = (state: LayersState, id: string) => { - const layer = selectLayer(state, id); - assert(layer, `Layer with id ${id} not found`); - return layer; -}; - -export const layersSlice = createSlice({ - name: 'layers', - initialState, - reducers: { - layerAdded: { - reducer: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - state.layers.push({ - id, - type: 'layer', - isEnabled: true, - bbox: null, - bboxNeedsUpdate: false, - objects: [], - opacity: 1, - x: 0, - y: 0, - }); - }, - prepare: () => ({ payload: { id: uuidv4() } }), - }, - layerRecalled: (state, action: PayloadAction<{ data: LayerData }>) => { - state.layers.push(action.payload.data); - }, - layerIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - layer.isEnabled = !layer.isEnabled; - }, - layerTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => { - const { id, x, y } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - layer.x = x; - layer.y = y; - }, - layerBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { - const { id, bbox } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - layer.bbox = bbox; - layer.bboxNeedsUpdate = false; - if (bbox === null) { - layer.objects = []; - } - }, - layerReset: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - layer.isEnabled = true; - layer.objects = []; - layer.bbox = null; - layer.bboxNeedsUpdate = false; - }, - layerDeleted: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - state.layers = state.layers.filter((l) => l.id !== id); - }, - layerOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => { - const { id, opacity } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - layer.opacity = opacity; - }, - layerMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - moveOneToEnd(state.layers, layer); - }, - layerMovedToFront: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - moveToEnd(state.layers, layer); - }, - layerMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - moveOneToStart(state.layers, layer); - }, - layerMovedToBack: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - moveToStart(state.layers, layer); - }, - layerBrushLineAdded: { - reducer: (state, action: PayloadAction) => { - const { id, points, lineId, color, width } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - - layer.objects.push({ - id: getBrushLineId(id, lineId), - type: 'brush_line', - points, - strokeWidth: width, - color, - }); - layer.bboxNeedsUpdate = true; - }, - prepare: (payload: BrushLineAddedArg) => ({ - payload: { ...payload, lineId: uuidv4() }, - }), - }, - layerEraserLineAdded: { - reducer: (state, action: PayloadAction) => { - const { id, points, lineId, width } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - - layer.objects.push({ - id: getEraserLineId(id, lineId), - type: 'eraser_line', - points, - strokeWidth: width, - }); - layer.bboxNeedsUpdate = true; - }, - prepare: (payload: EraserLineAddedArg) => ({ - payload: { ...payload, lineId: uuidv4() }, - }), - }, - layerLinePointAdded: (state, action: PayloadAction) => { - const { id, point } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - const lastObject = layer.objects[layer.objects.length - 1]; - if (!lastObject || !isLine(lastObject)) { - return; - } - lastObject.points.push(...point); - layer.bboxNeedsUpdate = true; - }, - layerRectAdded: { - reducer: (state, action: PayloadAction) => { - const { id, rect, rectId, color } = action.payload; - if (rect.height === 0 || rect.width === 0) { - // Ignore zero-area rectangles - return; - } - const layer = selectLayer(state, id); - if (!layer) { - return; - } - layer.objects.push({ - type: 'rect_shape', - id: getRectShapeId(id, rectId), - ...rect, - color, - }); - layer.bboxNeedsUpdate = true; - }, - prepare: (payload: RectShapeAddedArg) => ({ payload: { ...payload, rectId: uuidv4() } }), - }, - layerImageAdded: { - reducer: (state, action: PayloadAction) => { - const { id, imageId, imageDTO } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - const { width, height, image_name: name } = imageDTO; - layer.objects.push({ - type: 'image', - id: getImageObjectId(id, imageId), - x: 0, - y: 0, - width, - height, - image: { width, height, name }, - }); - layer.bboxNeedsUpdate = true; - }, - prepare: (payload: ImageObjectAddedArg) => ({ payload: { ...payload, imageId: uuidv4() } }), - }, - layerAllDeleted: (state) => { - state.layers = []; - }, - }, -}); - -export const { - layerAdded, - layerDeleted, - layerReset, - layerMovedForwardOne, - layerMovedToFront, - layerMovedBackwardOne, - layerMovedToBack, - layerIsEnabledToggled, - layerOpacityChanged, - layerTranslated, - layerBboxChanged, - layerBrushLineAdded, - layerEraserLineAdded, - layerLinePointAdded, - layerRectAdded, - layerImageAdded, - layerAllDeleted, -} = layersSlice.actions; - -export const selectLayersSlice = (state: RootState) => state.layers; - -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrate = (state: any): any => { - return state; -}; - -export const layersPersistConfig: PersistConfig = { - name: layersSlice.name, - initialState, - migrate, - persistDenylist: [], -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts deleted file mode 100644 index 0b341d9399a..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts +++ /dev/null @@ -1,452 +0,0 @@ -import type { PayloadAction } from '@reduxjs/toolkit'; -import { createSlice } from '@reduxjs/toolkit'; -import type { PersistConfig, RootState } from 'app/store/store'; -import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; -import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; -import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types'; -import { imageDTOToImageWithDims } from 'features/controlLayers/store/types'; -import { zModelIdentifierField } from 'features/nodes/types/common'; -import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas'; -import type { IRect } from 'konva/lib/types'; -import { isEqual } from 'lodash-es'; -import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; -import { assert } from 'tsafe'; -import { v4 as uuidv4 } from 'uuid'; - -import type { - BrushLineAddedArg, - EraserLineAddedArg, - IPAdapterData, - PointAddedToLineArg, - RectShapeAddedArg, - RegionalGuidanceData, - RgbColor, -} from './types'; -import { isLine } from './types'; - -type RegionalGuidanceState = { - _version: 1; - regions: RegionalGuidanceData[]; - opacity: number; -}; - -const initialState: RegionalGuidanceState = { - _version: 1, - regions: [], - opacity: 0.3, -}; - -export const selectRG = (state: RegionalGuidanceState, id: string) => state.regions.find((rg) => rg.id === id); -export const selectRGOrThrow = (state: RegionalGuidanceState, id: string) => { - const rg = selectRG(state, id); - assert(rg, `Region with id ${id} not found`); - return rg; -}; - -const DEFAULT_MASK_COLORS: RgbColor[] = [ - { r: 121, g: 157, b: 219 }, // rgb(121, 157, 219) - { r: 131, g: 214, b: 131 }, // rgb(131, 214, 131) - { r: 250, g: 225, b: 80 }, // rgb(250, 225, 80) - { r: 220, g: 144, b: 101 }, // rgb(220, 144, 101) - { r: 224, g: 117, b: 117 }, // rgb(224, 117, 117) - { r: 213, g: 139, b: 202 }, // rgb(213, 139, 202) - { r: 161, g: 120, b: 214 }, // rgb(161, 120, 214) -]; - -const getRGMaskFill = (state: RegionalGuidanceState): RgbColor => { - const lastFill = state.regions.slice(-1)[0]?.fill; - let i = DEFAULT_MASK_COLORS.findIndex((c) => isEqual(c, lastFill)); - if (i === -1) { - i = 0; - } - i = (i + 1) % DEFAULT_MASK_COLORS.length; - const fill = DEFAULT_MASK_COLORS[i]; - assert(fill, 'This should never happen'); - return fill; -}; - -export const regionalGuidanceSlice = createSlice({ - name: 'regionalGuidance', - initialState, - reducers: { - rgAdded: { - reducer: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const rg: RegionalGuidanceData = { - id, - type: 'regional_guidance', - isEnabled: true, - bbox: null, - bboxNeedsUpdate: false, - objects: [], - fill: getRGMaskFill(state), - x: 0, - y: 0, - autoNegative: 'invert', - positivePrompt: '', - negativePrompt: null, - ipAdapters: [], - imageCache: null, - }; - state.regions.push(rg); - }, - prepare: () => ({ payload: { id: uuidv4() } }), - }, - rgReset: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - rg.objects = []; - rg.bbox = null; - rg.bboxNeedsUpdate = false; - rg.imageCache = null; - }, - rgRecalled: (state, action: PayloadAction<{ data: RegionalGuidanceData }>) => { - const { data } = action.payload; - state.regions.push(data); - }, - rgIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const rg = selectRG(state, id); - if (rg) { - rg.isEnabled = !rg.isEnabled; - } - }, - rgTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => { - const { id, x, y } = action.payload; - const rg = selectRG(state, id); - if (rg) { - rg.x = x; - rg.y = y; - } - }, - rgBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { - const { id, bbox } = action.payload; - const rg = selectRG(state, id); - if (rg) { - rg.bbox = bbox; - rg.bboxNeedsUpdate = false; - } - }, - rgDeleted: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - state.regions = state.regions.filter((ca) => ca.id !== id); - }, - rgGlobalOpacityChanged: (state, action: PayloadAction<{ opacity: number }>) => { - const { opacity } = action.payload; - state.opacity = opacity; - }, - rgMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - moveOneToEnd(state.regions, rg); - }, - rgMovedToFront: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - moveToEnd(state.regions, rg); - }, - rgMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - moveOneToStart(state.regions, rg); - }, - rgMovedToBack: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - moveToStart(state.regions, rg); - }, - rgPositivePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => { - const { id, prompt } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - rg.positivePrompt = prompt; - }, - rgNegativePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => { - const { id, prompt } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - rg.negativePrompt = prompt; - }, - rgFillChanged: (state, action: PayloadAction<{ id: string; fill: RgbColor }>) => { - const { id, fill } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - rg.fill = fill; - }, - rgMaskImageUploaded: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO }>) => { - const { id, imageDTO } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - rg.imageCache = imageDTOToImageWithDims(imageDTO); - }, - rgAutoNegativeChanged: (state, action: PayloadAction<{ id: string; autoNegative: ParameterAutoNegative }>) => { - const { id, autoNegative } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - rg.autoNegative = autoNegative; - }, - rgIPAdapterAdded: (state, action: PayloadAction<{ id: string; ipAdapter: IPAdapterData }>) => { - const { id, ipAdapter } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - rg.ipAdapters.push(ipAdapter); - }, - rgIPAdapterDeleted: (state, action: PayloadAction<{ id: string; ipAdapterId: string }>) => { - const { id, ipAdapterId } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - rg.ipAdapters = rg.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId); - }, - rgIPAdapterImageChanged: ( - state, - action: PayloadAction<{ id: string; ipAdapterId: string; imageDTO: ImageDTO | null }> - ) => { - const { id, ipAdapterId, imageDTO } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); - if (!ipa) { - return; - } - ipa.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; - }, - rgIPAdapterWeightChanged: (state, action: PayloadAction<{ id: string; ipAdapterId: string; weight: number }>) => { - const { id, ipAdapterId, weight } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); - if (!ipa) { - return; - } - ipa.weight = weight; - }, - rgIPAdapterBeginEndStepPctChanged: ( - state, - action: PayloadAction<{ id: string; ipAdapterId: string; beginEndStepPct: [number, number] }> - ) => { - const { id, ipAdapterId, beginEndStepPct } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); - if (!ipa) { - return; - } - ipa.beginEndStepPct = beginEndStepPct; - }, - rgIPAdapterMethodChanged: ( - state, - action: PayloadAction<{ id: string; ipAdapterId: string; method: IPMethodV2 }> - ) => { - const { id, ipAdapterId, method } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); - if (!ipa) { - return; - } - ipa.method = method; - }, - rgIPAdapterModelChanged: ( - state, - action: PayloadAction<{ - id: string; - ipAdapterId: string; - modelConfig: IPAdapterModelConfig | null; - }> - ) => { - const { id, ipAdapterId, modelConfig } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); - if (!ipa) { - return; - } - if (modelConfig) { - ipa.model = zModelIdentifierField.parse(modelConfig); - } else { - ipa.model = null; - } - }, - rgIPAdapterCLIPVisionModelChanged: ( - state, - action: PayloadAction<{ id: string; ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }> - ) => { - const { id, ipAdapterId, clipVisionModel } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); - if (!ipa) { - return; - } - ipa.clipVisionModel = clipVisionModel; - }, - rgBrushLineAdded: { - reducer: (state, action: PayloadAction) => { - const { id, points, lineId, color, width } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - rg.objects.push({ - id: getBrushLineId(id, lineId), - type: 'brush_line', - points, - strokeWidth: width, - color, - }); - rg.bboxNeedsUpdate = true; - rg.imageCache = null; - }, - prepare: (payload: BrushLineAddedArg) => ({ - payload: { ...payload, lineId: uuidv4() }, - }), - }, - rgEraserLineAdded: { - reducer: (state, action: PayloadAction) => { - const { id, points, lineId, width } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - rg.objects.push({ - id: getEraserLineId(id, lineId), - type: 'eraser_line', - points, - strokeWidth: width, - }); - rg.bboxNeedsUpdate = true; - rg.imageCache = null; - }, - prepare: (payload: EraserLineAddedArg) => ({ - payload: { ...payload, lineId: uuidv4() }, - }), - }, - rgLinePointAdded: (state, action: PayloadAction) => { - const { id, point } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - const lastObject = rg.objects[rg.objects.length - 1]; - if (!lastObject || !isLine(lastObject)) { - return; - } - lastObject.points.push(...point); - rg.bboxNeedsUpdate = true; - rg.imageCache = null; - }, - rgRectAdded: { - reducer: (state, action: PayloadAction) => { - const { id, rect, rectId, color } = action.payload; - if (rect.height === 0 || rect.width === 0) { - // Ignore zero-area rectangles - return; - } - const rg = selectRG(state, id); - if (!rg) { - return; - } - rg.objects.push({ - type: 'rect_shape', - id: getRectShapeId(id, rectId), - ...rect, - color, - }); - rg.bboxNeedsUpdate = true; - rg.imageCache = null; - }, - prepare: (payload: RectShapeAddedArg) => ({ payload: { ...payload, rectId: uuidv4() } }), - }, - rgAllDeleted: (state) => { - state.regions = []; - }, - }, -}); - -export const { - rgAdded, - rgRecalled, - rgReset, - rgIsEnabledToggled, - rgTranslated, - rgBboxChanged, - rgDeleted, - rgGlobalOpacityChanged, - rgMovedForwardOne, - rgMovedToFront, - rgMovedBackwardOne, - rgMovedToBack, - rgPositivePromptChanged, - rgNegativePromptChanged, - rgFillChanged, - rgMaskImageUploaded, - rgAutoNegativeChanged, - rgIPAdapterAdded, - rgIPAdapterDeleted, - rgIPAdapterImageChanged, - rgIPAdapterWeightChanged, - rgIPAdapterBeginEndStepPctChanged, - rgIPAdapterMethodChanged, - rgIPAdapterModelChanged, - rgIPAdapterCLIPVisionModelChanged, - rgBrushLineAdded, - rgEraserLineAdded, - rgLinePointAdded, - rgRectAdded, - rgAllDeleted, -} = regionalGuidanceSlice.actions; - -export const selectRegionalGuidanceSlice = (state: RootState) => state.regionalGuidance; - -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrate = (state: any): any => { - return state; -}; - -export const regionalGuidancePersistConfig: PersistConfig = { - name: regionalGuidanceSlice.name, - initialState, - migrate, - persistDenylist: [], -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts new file mode 100644 index 00000000000..56f3b935d97 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -0,0 +1,381 @@ +import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; +import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; +import type { CanvasV2State, CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types'; +import { imageDTOToImageWithDims } from 'features/controlLayers/store/types'; +import { zModelIdentifierField } from 'features/nodes/types/common'; +import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas'; +import type { IRect } from 'konva/lib/types'; +import { isEqual } from 'lodash-es'; +import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; +import { assert } from 'tsafe'; +import { v4 as uuidv4 } from 'uuid'; + +import type { + BrushLineAddedArg, + EraserLineAddedArg, + IPAdapterData, + PointAddedToLineArg, + RectShapeAddedArg, + RegionalGuidanceData, + RgbColor, +} from './types'; +import { isLine } from './types'; + +export const selectRG = (state: CanvasV2State, id: string) => state.regions.find((rg) => rg.id === id); +export const selectRGOrThrow = (state: CanvasV2State, id: string) => { + const rg = selectRG(state, id); + assert(rg, `Region with id ${id} not found`); + return rg; +}; + +const DEFAULT_MASK_COLORS: RgbColor[] = [ + { r: 121, g: 157, b: 219 }, // rgb(121, 157, 219) + { r: 131, g: 214, b: 131 }, // rgb(131, 214, 131) + { r: 250, g: 225, b: 80 }, // rgb(250, 225, 80) + { r: 220, g: 144, b: 101 }, // rgb(220, 144, 101) + { r: 224, g: 117, b: 117 }, // rgb(224, 117, 117) + { r: 213, g: 139, b: 202 }, // rgb(213, 139, 202) + { r: 161, g: 120, b: 214 }, // rgb(161, 120, 214) +]; + +const getRGMaskFill = (state: CanvasV2State): RgbColor => { + const lastFill = state.regions.slice(-1)[0]?.fill; + let i = DEFAULT_MASK_COLORS.findIndex((c) => isEqual(c, lastFill)); + if (i === -1) { + i = 0; + } + i = (i + 1) % DEFAULT_MASK_COLORS.length; + const fill = DEFAULT_MASK_COLORS[i]; + assert(fill, 'This should never happen'); + return fill; +}; + +export const regionsReducers = { + rgAdded: { + reducer: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const rg: RegionalGuidanceData = { + id, + type: 'regional_guidance', + isEnabled: true, + bbox: null, + bboxNeedsUpdate: false, + objects: [], + fill: getRGMaskFill(state), + x: 0, + y: 0, + autoNegative: 'invert', + positivePrompt: '', + negativePrompt: null, + ipAdapters: [], + imageCache: null, + }; + state.regions.push(rg); + }, + prepare: () => ({ payload: { id: uuidv4() } }), + }, + rgReset: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const rg = selectRG(state, id); + if (!rg) { + return; + } + rg.objects = []; + rg.bbox = null; + rg.bboxNeedsUpdate = false; + rg.imageCache = null; + }, + rgRecalled: (state, action: PayloadAction<{ data: RegionalGuidanceData }>) => { + const { data } = action.payload; + state.regions.push(data); + }, + rgIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const rg = selectRG(state, id); + if (rg) { + rg.isEnabled = !rg.isEnabled; + } + }, + rgTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => { + const { id, x, y } = action.payload; + const rg = selectRG(state, id); + if (rg) { + rg.x = x; + rg.y = y; + } + }, + rgBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { + const { id, bbox } = action.payload; + const rg = selectRG(state, id); + if (rg) { + rg.bbox = bbox; + rg.bboxNeedsUpdate = false; + } + }, + rgDeleted: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + state.regions = state.regions.filter((ca) => ca.id !== id); + }, + rgGlobalOpacityChanged: (state, action: PayloadAction<{ opacity: number }>) => { + const { opacity } = action.payload; + state.maskFillOpacity = opacity; + }, + rgMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const rg = selectRG(state, id); + if (!rg) { + return; + } + moveOneToEnd(state.regions, rg); + }, + rgMovedToFront: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const rg = selectRG(state, id); + if (!rg) { + return; + } + moveToEnd(state.regions, rg); + }, + rgMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const rg = selectRG(state, id); + if (!rg) { + return; + } + moveOneToStart(state.regions, rg); + }, + rgMovedToBack: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const rg = selectRG(state, id); + if (!rg) { + return; + } + moveToStart(state.regions, rg); + }, + rgPositivePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => { + const { id, prompt } = action.payload; + const rg = selectRG(state, id); + if (!rg) { + return; + } + rg.positivePrompt = prompt; + }, + rgNegativePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => { + const { id, prompt } = action.payload; + const rg = selectRG(state, id); + if (!rg) { + return; + } + rg.negativePrompt = prompt; + }, + rgFillChanged: (state, action: PayloadAction<{ id: string; fill: RgbColor }>) => { + const { id, fill } = action.payload; + const rg = selectRG(state, id); + if (!rg) { + return; + } + rg.fill = fill; + }, + rgMaskImageUploaded: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO }>) => { + const { id, imageDTO } = action.payload; + const rg = selectRG(state, id); + if (!rg) { + return; + } + rg.imageCache = imageDTOToImageWithDims(imageDTO); + }, + rgAutoNegativeChanged: (state, action: PayloadAction<{ id: string; autoNegative: ParameterAutoNegative }>) => { + const { id, autoNegative } = action.payload; + const rg = selectRG(state, id); + if (!rg) { + return; + } + rg.autoNegative = autoNegative; + }, + rgIPAdapterAdded: (state, action: PayloadAction<{ id: string; ipAdapter: IPAdapterData }>) => { + const { id, ipAdapter } = action.payload; + const rg = selectRG(state, id); + if (!rg) { + return; + } + rg.ipAdapters.push(ipAdapter); + }, + rgIPAdapterDeleted: (state, action: PayloadAction<{ id: string; ipAdapterId: string }>) => { + const { id, ipAdapterId } = action.payload; + const rg = selectRG(state, id); + if (!rg) { + return; + } + rg.ipAdapters = rg.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId); + }, + rgIPAdapterImageChanged: ( + state, + action: PayloadAction<{ id: string; ipAdapterId: string; imageDTO: ImageDTO | null }> + ) => { + const { id, ipAdapterId, imageDTO } = action.payload; + const rg = selectRG(state, id); + if (!rg) { + return; + } + const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); + if (!ipa) { + return; + } + ipa.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; + }, + rgIPAdapterWeightChanged: (state, action: PayloadAction<{ id: string; ipAdapterId: string; weight: number }>) => { + const { id, ipAdapterId, weight } = action.payload; + const rg = selectRG(state, id); + if (!rg) { + return; + } + const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); + if (!ipa) { + return; + } + ipa.weight = weight; + }, + rgIPAdapterBeginEndStepPctChanged: ( + state, + action: PayloadAction<{ id: string; ipAdapterId: string; beginEndStepPct: [number, number] }> + ) => { + const { id, ipAdapterId, beginEndStepPct } = action.payload; + const rg = selectRG(state, id); + if (!rg) { + return; + } + const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); + if (!ipa) { + return; + } + ipa.beginEndStepPct = beginEndStepPct; + }, + rgIPAdapterMethodChanged: (state, action: PayloadAction<{ id: string; ipAdapterId: string; method: IPMethodV2 }>) => { + const { id, ipAdapterId, method } = action.payload; + const rg = selectRG(state, id); + if (!rg) { + return; + } + const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); + if (!ipa) { + return; + } + ipa.method = method; + }, + rgIPAdapterModelChanged: ( + state, + action: PayloadAction<{ + id: string; + ipAdapterId: string; + modelConfig: IPAdapterModelConfig | null; + }> + ) => { + const { id, ipAdapterId, modelConfig } = action.payload; + const rg = selectRG(state, id); + if (!rg) { + return; + } + const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); + if (!ipa) { + return; + } + if (modelConfig) { + ipa.model = zModelIdentifierField.parse(modelConfig); + } else { + ipa.model = null; + } + }, + rgIPAdapterCLIPVisionModelChanged: ( + state, + action: PayloadAction<{ id: string; ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }> + ) => { + const { id, ipAdapterId, clipVisionModel } = action.payload; + const rg = selectRG(state, id); + if (!rg) { + return; + } + const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); + if (!ipa) { + return; + } + ipa.clipVisionModel = clipVisionModel; + }, + rgBrushLineAdded: { + reducer: (state, action: PayloadAction) => { + const { id, points, lineId, color, width } = action.payload; + const rg = selectRG(state, id); + if (!rg) { + return; + } + rg.objects.push({ + id: getBrushLineId(id, lineId), + type: 'brush_line', + points, + strokeWidth: width, + color, + }); + rg.bboxNeedsUpdate = true; + rg.imageCache = null; + }, + prepare: (payload: BrushLineAddedArg) => ({ + payload: { ...payload, lineId: uuidv4() }, + }), + }, + rgEraserLineAdded: { + reducer: (state, action: PayloadAction) => { + const { id, points, lineId, width } = action.payload; + const rg = selectRG(state, id); + if (!rg) { + return; + } + rg.objects.push({ + id: getEraserLineId(id, lineId), + type: 'eraser_line', + points, + strokeWidth: width, + }); + rg.bboxNeedsUpdate = true; + rg.imageCache = null; + }, + prepare: (payload: EraserLineAddedArg) => ({ + payload: { ...payload, lineId: uuidv4() }, + }), + }, + rgLinePointAdded: (state, action: PayloadAction) => { + const { id, point } = action.payload; + const rg = selectRG(state, id); + if (!rg) { + return; + } + const lastObject = rg.objects[rg.objects.length - 1]; + if (!lastObject || !isLine(lastObject)) { + return; + } + lastObject.points.push(...point); + rg.bboxNeedsUpdate = true; + rg.imageCache = null; + }, + rgRectAdded: { + reducer: (state, action: PayloadAction) => { + const { id, rect, rectId, color } = action.payload; + if (rect.height === 0 || rect.width === 0) { + // Ignore zero-area rectangles + return; + } + const rg = selectRG(state, id); + if (!rg) { + return; + } + rg.objects.push({ + type: 'rect_shape', + id: getRectShapeId(id, rectId), + ...rect, + color, + }); + rg.bboxNeedsUpdate = true; + rg.imageCache = null; + }, + prepare: (payload: RectShapeAddedArg) => ({ payload: { ...payload, rectId: uuidv4() } }), + }, +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/test.ts b/invokeai/frontend/web/src/features/controlLayers/store/test.ts new file mode 100644 index 00000000000..2426f9af2ce --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/test.ts @@ -0,0 +1,57 @@ +import type { ActionReducerMapBuilder, PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; + +type MySlice = { + flavour: 'vanilla' | 'chocolate' | 'strawberry'; + sprinkles: boolean; + customers: { id: string; name: string }[]; +}; +const initialStateMySlice: MySlice = { flavour: 'vanilla', sprinkles: false, customers: [] }; + +const reducersInAnotherFile: SliceCaseReducers = { + sprinklesToggled: (state) => { + state.sprinkles = !state.sprinkles; + }, + customerAdded: { + reducer: (state, action: PayloadAction<{ id: string; name: string }>) => { + state.customers.push(action.payload); + }, + prepare: (name: string) => ({ payload: { name, id: crypto.randomUUID() } }), + }, +}; + +const extraReducersInAnotherFile = (builder: ActionReducerMapBuilder) => { + builder.addCase(otherSlice.actions.fooChanged, (state, action) => { + if (action.payload === 'bar') { + state.flavour = 'vanilla'; + } + }); +}; + +export const mySlice = createSlice({ + name: 'mySlice', + initialState: initialStateMySlice, + reducers: { + ...reducersInAnotherFile, + flavourChanged: (state, action: PayloadAction) => { + state.flavour = action.payload; + }, + }, + extraReducers: extraReducersInAnotherFile, +}); + +type OtherSlice = { + something: string; +}; + +const initialStateOtherSlice: OtherSlice = { something: 'foo' }; + +export const otherSlice = createSlice({ + name: 'otherSlice', + initialState: initialStateOtherSlice, + reducers: { + fooChanged: (state, action: PayloadAction) => { + state.something = action.payload; + }, + }, +}); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 8388ed05304..da1610559b9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -783,6 +783,11 @@ export type CanvasV2State = { aspectRatio: AspectRatioState; }; bbox: IRect; + layers: LayerData[]; + controlAdapters: ControlAdapterData[]; + ipAdapters: IPAdapterData[]; + regions: RegionalGuidanceData[]; + maskFillOpacity: number; }; export type StageAttrs = { x: number; y: number; width: number; height: number; scale: number }; diff --git a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx index 7ff8cb1b9ee..3d33999c717 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx +++ b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx @@ -3,7 +3,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions'; import { getImageUsage, selectImageUsage } from 'features/deleteImageModal/store/selectors'; import { diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts index 456872e23be..ce36e9080d3 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts @@ -7,7 +7,7 @@ import { } from 'features/controlAdapters/store/controlAdaptersSlice'; import type { ControlAdaptersState } from 'features/controlAdapters/store/types'; import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import type { CanvasV2State } from 'features/controlLayers/store/types'; import { isControlAdapterLayer, diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx index 87e78f22b47..fd9a52a69d7 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx @@ -15,7 +15,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import ImageUsageMessage from 'features/deleteImageModal/components/ImageUsageMessage'; import { getImageUsage } from 'features/deleteImageModal/store/selectors'; import type { ImageUsage } from 'features/deleteImageModal/store/types'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx index 1ae4b7b6bc3..b2b1f9eff0f 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx @@ -6,7 +6,7 @@ import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard'; import { useDownloadImage } from 'common/hooks/useDownloadImage'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; import { imagesToChangeSelected, isModalOpenChanged } from 'features/changeBoardModal/store/slice'; -import { iiLayerAdded } from 'features/controlLayers/store/controlLayersSlice'; +import { iiLayerAdded } from 'features/controlLayers/store/canvasV2Slice'; import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice'; import { useImageActions } from 'features/gallery/hooks/useImageActions'; import { sentImageToCanvas, sentImageToImg2Img } from 'features/gallery/store/actions'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx index d1f874271d6..d5c23ecb90a 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx @@ -4,7 +4,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { skipToken } from '@reduxjs/toolkit/query'; import { adHocPostProcessingRequested } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { iiLayerAdded } from 'features/controlLayers/store/controlLayersSlice'; +import { iiLayerAdded } from 'features/controlLayers/store/canvasV2Slice'; import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton'; import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice'; import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems'; diff --git a/invokeai/frontend/web/src/features/metadata/util/handlers.ts b/invokeai/frontend/web/src/features/metadata/util/handlers.ts index f55f085b7dd..d0f151118c5 100644 --- a/invokeai/frontend/web/src/features/metadata/util/handlers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/handlers.ts @@ -1,7 +1,7 @@ import { getStore } from 'app/store/nanostores/store'; import { deepClone } from 'common/util/deepClone'; import { objectKeys } from 'common/util/objectKeys'; -import { shouldConcatPromptsChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { shouldConcatPromptsChanged } from 'features/controlLayers/store/canvasV2Slice'; import type { LayerData } from 'features/controlLayers/store/types'; import type { LoRA } from 'features/lora/store/loraSlice'; import type { diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index 9e0ecf5b8bb..70027d0c975 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -19,7 +19,7 @@ import { positivePromptChanged, regionalGuidanceRecalled, widthChanged, -} from 'features/controlLayers/store/controlLayersSlice'; +} from 'features/controlLayers/store/canvasV2Slice'; import type { LayerData } from 'features/controlLayers/store/types'; import { setHrfEnabled, setHrfMethod, setHrfStrength } from 'features/hrf/store/hrfSlice'; import type { LoRA } from 'features/lora/store/loraSlice'; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts index 9ee41158b4e..78702fccfef 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts @@ -5,7 +5,7 @@ import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; import { blobToDataURL } from 'features/canvas/util/blobToDataURL'; import { RG_LAYER_NAME } from 'features/controlLayers/konva/naming'; import { renderers } from 'features/controlLayers/konva/renderers/layers'; -import { regionalGuidanceMaskImageUploaded } from 'features/controlLayers/store/controlLayersSlice'; +import { regionalGuidanceMaskImageUploaded } from 'features/controlLayers/store/canvasV2Slice'; import type { InitialImageLayer, LayerData, RegionalGuidanceLayer } from 'features/controlLayers/store/types'; import { isControlAdapterLayer, diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx index 73141cf3251..e415c2d5fa1 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx @@ -1,6 +1,6 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { negativePromptChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { negativePromptChanged } from 'features/controlLayers/store/canvasV2Slice'; import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { ViewModePrompt } from 'features/parameters/components/Prompts/ViewModePrompt'; diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx index 32c66231b92..f7c2c285ffb 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx @@ -1,6 +1,6 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { positivePromptChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { positivePromptChanged } from 'features/controlLayers/store/canvasV2Slice'; import { ShowDynamicPromptsPreviewButton } from 'features/dynamicPrompts/components/ShowDynamicPromptsPreviewButton'; import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioCanvasPreview.tsx b/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioCanvasPreview.tsx index 08b591f9b18..56b188c3bfa 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioCanvasPreview.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioCanvasPreview.tsx @@ -1,7 +1,7 @@ import { Flex } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { StageComponent } from 'features/controlLayers/components/StageComponent'; -import { $isPreviewVisible } from 'features/controlLayers/store/controlLayersSlice'; +import { $isPreviewVisible } from 'features/controlLayers/store/canvasV2Slice'; import { AspectRatioIconPreview } from 'features/parameters/components/ImageSize/AspectRatioIconPreview'; import { memo } from 'react'; diff --git a/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts b/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts index 683f5479f9a..74aef5f79ca 100644 --- a/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts +++ b/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts @@ -1,7 +1,7 @@ import { skipToken } from '@reduxjs/toolkit/query'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; -import { iiLayerAdded } from 'features/controlLayers/store/controlLayersSlice'; +import { iiLayerAdded } from 'features/controlLayers/store/canvasV2Slice'; import { parseAndRecallAllMetadata } from 'features/metadata/util/handlers'; import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { toast } from 'features/toast/toast'; diff --git a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx b/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx index 9c2d8ebebe6..834d66f0d0f 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx @@ -2,7 +2,7 @@ import { Divider, Flex, ListItem, Text, Tooltip, UnorderedList } from '@invoke-a import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { useIsReadyToEnqueue } from 'common/hooks/useIsReadyToEnqueue'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; import type { PropsWithChildren } from 'react'; diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt.tsx index 1f22c1b8459..f295ffd32f5 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt.tsx @@ -1,6 +1,6 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { negativePrompt2Changed } from 'features/controlLayers/store/controlLayersSlice'; +import { negativePrompt2Changed } from 'features/controlLayers/store/canvasV2Slice'; import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt.tsx index 0b86f3014d4..8e31185345f 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt.tsx @@ -1,6 +1,6 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { positivePrompt2Changed } from 'features/controlLayers/store/controlLayersSlice'; +import { positivePrompt2Changed } from 'features/controlLayers/store/canvasV2Slice'; import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/SDXLConcatButton.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/SDXLConcatButton.tsx index dc3b24402b8..9048437e3ab 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/SDXLConcatButton.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/SDXLConcatButton.tsx @@ -1,6 +1,6 @@ import { IconButton, Tooltip } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { shouldConcatPromptsChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { shouldConcatPromptsChanged } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiLinkSimpleBold, PiLinkSimpleBreakBold } from 'react-icons/pi'; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx index 499a0a307cd..52278cfd60e 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx @@ -2,7 +2,7 @@ import type { FormLabelProps } from '@invoke-ai/ui-library'; import { Expander, Flex, FormControlGroup, StandaloneAccordion } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { HrfSettings } from 'features/hrf/components/HrfSettings'; import { selectHrfSlice } from 'features/hrf/store/hrfSlice'; import ParamScaleBeforeProcessing from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing'; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx index 658f47196a9..13670c674f5 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx @@ -1,5 +1,5 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { aspectRatioChanged, heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { aspectRatioChanged, heightChanged, widthChanged } from 'features/controlLayers/store/canvasV2Slice'; import { ParamHeight } from 'features/parameters/components/Core/ParamHeight'; import { ParamWidth } from 'features/parameters/components/Core/ParamWidth'; import { AspectRatioCanvasPreview } from 'features/parameters/components/ImageSize/AspectRatioCanvasPreview'; diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx index bf2b915090b..0196336e206 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx @@ -4,7 +4,7 @@ import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants'; import { ControlLayersPanelContent } from 'features/controlLayers/components/ControlLayersPanelContent'; -import { $isPreviewVisible } from 'features/controlLayers/store/controlLayersSlice'; +import { $isPreviewVisible } from 'features/controlLayers/store/canvasV2Slice'; import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; import { Prompts } from 'features/parameters/components/Prompts/Prompts'; import QueueControls from 'features/queue/components/QueueControls'; From f636c0eb88fda4857fcd4278dbe1f0970020576d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 15 Jun 2024 20:24:00 +1000 Subject: [PATCH 041/678] refactor(ui): canvas v2 (wip) delete unused file --- .../ControlAdapter/CAOpacityAndFilter.tsx | 7 +- .../controlLayers/hooks/layerStateHooks.ts | 79 ------------------- 2 files changed, 4 insertions(+), 82 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAOpacityAndFilter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAOpacityAndFilter.tsx index ce4e918f529..f6bfbdf6f87 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAOpacityAndFilter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAOpacityAndFilter.tsx @@ -12,10 +12,10 @@ import { PopoverTrigger, Switch, } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; -import { useCALayerOpacity } from 'features/controlLayers/hooks/layerStateHooks'; import { caFilterChanged, caOpacityChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCAOrThrow } from 'features/controlLayers/store/controlAdaptersReducers'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -31,7 +31,8 @@ const formatPct = (v: number | string) => `${v} %`; export const CAOpacityAndFilter = memo(({ id }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const { opacity, isFilterEnabled } = useCALayerOpacity(id); + const opacity = useAppSelector((s) => Math.round(selectCAOrThrow(s.canvasV2, id).opacity * 100)); + const isFilterEnabled = useAppSelector((s) => selectCAOrThrow(s.canvasV2, id).filter === 'LightnessToAlphaFilter'); const onChangeOpacity = useCallback( (v: number) => { dispatch(caOpacityChanged({ id, opacity: v / 100 })); diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts deleted file mode 100644 index 28f6af7ea49..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; -import { isControlAdapterLayer, isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; -import { useMemo } from 'react'; -import { assert } from 'tsafe'; - -export const useLayerPositivePrompt = (layerId: string) => { - const selectLayer = useMemo( - () => - createSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = canvasV2.layers.find((l) => l.id === layerId); - assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); - assert(layer.positivePrompt !== null, `Layer ${layerId} does not have a positive prompt`); - return layer.positivePrompt; - }), - [layerId] - ); - const prompt = useAppSelector(selectLayer); - return prompt; -}; - -export const useLayerNegativePrompt = (layerId: string) => { - const selectLayer = useMemo( - () => - createSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = canvasV2.layers.find((l) => l.id === layerId); - assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); - assert(layer.negativePrompt !== null, `Layer ${layerId} does not have a negative prompt`); - return layer.negativePrompt; - }), - [layerId] - ); - const prompt = useAppSelector(selectLayer); - return prompt; -}; - -export const useLayerIsEnabled = (layerId: string) => { - const selectLayer = useMemo( - () => - createSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = canvasV2.layers.find((l) => l.id === layerId); - assert(layer, `Layer ${layerId} not found`); - return layer.isEnabled; - }), - [layerId] - ); - const isVisible = useAppSelector(selectLayer); - return isVisible; -}; - -export const useLayerType = (layerId: string) => { - const selectLayer = useMemo( - () => - createSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = canvasV2.layers.find((l) => l.id === layerId); - assert(layer, `Layer ${layerId} not found`); - return layer.type; - }), - [layerId] - ); - const type = useAppSelector(selectLayer); - return type; -}; - -export const useCALayerOpacity = (layerId: string) => { - const selectLayer = useMemo( - () => - createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = canvasV2.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId); - assert(layer, `Layer ${layerId} not found`); - return { opacity: Math.round(layer.opacity * 100), isFilterEnabled: layer.isFilterEnabled }; - }), - [layerId] - ); - const opacity = useAppSelector(selectLayer); - return opacity; -}; From d0cde66e9297d7ef6a4eba4d0d4e78f68a650e27 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 15 Jun 2024 20:25:07 +1000 Subject: [PATCH 042/678] refactor(ui): canvas v2 (wip) fix entity count select --- .../features/controlLayers/store/selectors.ts | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts index 44ac9f32b8a..2269f393f32 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts @@ -1,17 +1,8 @@ import { createSelector } from '@reduxjs/toolkit'; -import { selectControlAdaptersV2Slice } from 'features/controlLayers/store/controlAdaptersSlice'; -import { selectIPAdaptersSlice } from 'features/controlLayers/store/ipAdaptersSlice'; -import { selectLayersSlice } from 'features/controlLayers/store/layersSlice'; -import { selectRegionalGuidanceSlice } from 'features/controlLayers/store/regionalGuidanceSlice'; +import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -export const selectEntityCount = createSelector( - selectRegionalGuidanceSlice, - selectControlAdaptersV2Slice, - selectIPAdaptersSlice, - selectLayersSlice, - (rgState, caState, ipaState, layersState) => { - return ( - rgState.regions.length + caState.controlAdapters.length + ipaState.ipAdapters.length + layersState.layers.length - ); - } -); +export const selectEntityCount = createSelector(selectCanvasSlice, (canvasV2) => { + return ( + canvasV2.regions.length + canvasV2.controlAdapters.length + canvasV2.ipAdapters.length + canvasV2.layers.length + ); +}); From a4f55f6e5d11c0be66f13ee307c1ff73d9c6f092 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 15 Jun 2024 20:34:28 +1000 Subject: [PATCH 043/678] refactor(ui): rip out old control adapter implementation --- .../listeners/controlAdapterPreprocessor.ts | 70 ++- .../listeners/controlNetAutoProcess.ts | 85 ---- .../listeners/controlNetImageProcessed.ts | 118 ----- .../components/ControlAdapterConfig.tsx | 144 ------ .../components/ControlAdapterImagePreview.tsx | 227 --------- .../ControlAdapterProcessorComponent.tsx | 91 ---- .../ControlAdapterShouldAutoConfig.tsx | 38 -- .../hooks/useProcessorNodeChanged.ts | 20 - .../imports/ControlNetCanvasImageImports.tsx | 45 -- .../ParamControlAdapterBeginEnd.tsx | 89 ---- .../ParamControlAdapterControlMode.tsx | 66 --- .../ParamControlAdapterIPMethod.tsx | 63 --- .../parameters/ParamControlAdapterModel.tsx | 139 ------ .../ParamControlAdapterProcessorSelect.tsx | 70 --- .../ParamControlAdapterResizeMode.tsx | 64 --- .../parameters/ParamControlAdapterWeight.tsx | 74 --- .../components/processors/CannyProcessor.tsx | 129 ------ .../processors/ColorMapProcessor.tsx | 58 --- .../processors/ContentShuffleProcessor.tsx | 118 ----- .../processors/DWOpenposeProcessor.tsx | 93 ---- .../processors/DepthAnyThingProcessor.tsx | 102 ----- .../components/processors/HedProcessor.tsx | 95 ---- .../processors/LineartAnimeProcessor.tsx | 82 ---- .../processors/LineartProcessor.tsx | 94 ---- .../processors/MediapipeFaceProcessor.tsx | 134 ------ .../processors/MidasDepthProcessor.tsx | 136 ------ .../processors/MlsdImageProcessor.tsx | 137 ------ .../processors/NormalBaeProcessor.tsx | 82 ---- .../components/processors/PidiProcessor.tsx | 103 ----- .../processors/ZoeDepthProcessor.tsx | 15 - .../processors/common/ProcessorWrapper.tsx | 15 - .../hooks/useAddControlAdapter.ts | 61 --- .../hooks/useControlAdapterBeginEndStepPct.ts | 27 -- .../hooks/useControlAdapterCLIPVisionModel.ts | 24 - .../hooks/useControlAdapterControlImage.ts | 22 - .../hooks/useControlAdapterControlMode.ts | 26 -- .../hooks/useControlAdapterIPMethod.ts | 24 - .../hooks/useControlAdapterIsEnabled.ts | 22 - .../hooks/useControlAdapterModel.ts | 27 -- .../hooks/useControlAdapterModels.ts | 22 - .../useControlAdapterProcessedControlImage.ts | 24 - .../hooks/useControlAdapterProcessorNode.ts | 24 - .../hooks/useControlAdapterProcessorType.ts | 24 - .../hooks/useControlAdapterResizeMode.ts | 26 -- .../useControlAdapterShouldAutoConfig.ts | 26 -- .../hooks/useControlAdapterType.ts | 24 - .../hooks/useControlAdapterWeight.ts | 22 - .../useGetDefaultForControlnetProcessor.ts | 14 - .../features/controlAdapters/store/actions.ts | 5 - .../controlAdapters/store/constants.ts | 261 ----------- .../store/controlAdaptersSlice.ts | 433 ------------------ .../controlAdapters/store/types.test.ts | 10 - .../features/controlAdapters/store/types.ts | 274 ----------- .../util/buildControlAdapter.ts | 71 --- .../util/buildControlAdapterProcessor.ts | 11 - 55 files changed, 30 insertions(+), 4270 deletions(-) delete mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess.ts delete mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterConfig.tsx delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterProcessorComponent.tsx delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterShouldAutoConfig.tsx delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/components/hooks/useProcessorNodeChanged.ts delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/components/imports/ControlNetCanvasImageImports.tsx delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterBeginEnd.tsx delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterControlMode.tsx delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterIPMethod.tsx delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterModel.tsx delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterProcessorSelect.tsx delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterResizeMode.tsx delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterWeight.tsx delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/components/processors/CannyProcessor.tsx delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/components/processors/ColorMapProcessor.tsx delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/components/processors/ContentShuffleProcessor.tsx delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/components/processors/DWOpenposeProcessor.tsx delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/components/processors/DepthAnyThingProcessor.tsx delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/components/processors/HedProcessor.tsx delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/components/processors/LineartAnimeProcessor.tsx delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/components/processors/LineartProcessor.tsx delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/components/processors/MediapipeFaceProcessor.tsx delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/components/processors/MidasDepthProcessor.tsx delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/components/processors/MlsdImageProcessor.tsx delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/components/processors/NormalBaeProcessor.tsx delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/components/processors/PidiProcessor.tsx delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/components/processors/ZoeDepthProcessor.tsx delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/components/processors/common/ProcessorWrapper.tsx delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/hooks/useAddControlAdapter.ts delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterBeginEndStepPct.ts delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterCLIPVisionModel.ts delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterControlImage.ts delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterControlMode.ts delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterIPMethod.ts delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterIsEnabled.ts delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterModel.ts delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterModels.ts delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterProcessedControlImage.ts delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterProcessorNode.ts delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterProcessorType.ts delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterResizeMode.ts delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterShouldAutoConfig.ts delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterType.ts delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterWeight.ts delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/hooks/useGetDefaultForControlnetProcessor.ts delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/store/actions.ts delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/store/constants.ts delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/store/controlAdaptersSlice.ts delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/store/types.test.ts delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/store/types.ts delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/util/buildControlAdapter.ts delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/util/buildControlAdapterProcessor.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts index 4e5fb3cb007..6d77a533425 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts @@ -4,15 +4,15 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware' import type { AppDispatch } from 'app/store/store'; import { parseify } from 'common/util/serialize'; import { - controlAdapterImageChanged, - controlAdapterModelChanged, - controlAdapterProcessedImageChanged, - controlAdapterProcessorConfigChanged, - controlAdapterProcessorPendingBatchIdChanged, - controlAdapterRecalled, + caImageChanged, + caModelChanged, + caProcessedImageChanged, + caProcessorConfigChanged, + caProcessorPendingBatchIdChanged, + caRecalled, } from 'features/controlLayers/store/canvasV2Slice'; -import { isControlAdapterLayer } from 'features/controlLayers/store/types'; -import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; +import { selectCA } from 'features/controlLayers/store/controlAdaptersReducers'; +import { CA_PROCESSOR_DATA } from 'features/controlLayers/store/types'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { isEqual } from 'lodash-es'; @@ -22,13 +22,7 @@ import type { BatchConfig } from 'services/api/types'; import { socketInvocationComplete } from 'services/events/actions'; import { assert } from 'tsafe'; -const matcher = isAnyOf( - controlAdapterImageChanged, - controlAdapterProcessedImageChanged, - controlAdapterProcessorConfigChanged, - controlAdapterModelChanged, - controlAdapterRecalled -); +const matcher = isAnyOf(caImageChanged, caProcessedImageChanged, caProcessorConfigChanged, caModelChanged, caRecalled); const DEBOUNCE_MS = 300; const log = logger('session'); @@ -36,7 +30,7 @@ const log = logger('session'); /** * Simple helper to cancel a batch and reset the pending batch ID */ -const cancelProcessorBatch = async (dispatch: AppDispatch, layerId: string, batchId: string) => { +const cancelProcessorBatch = async (dispatch: AppDispatch, id: string, batchId: string) => { const req = dispatch(queueApi.endpoints.cancelByBatchIds.initiate({ batch_ids: [batchId] })); log.trace({ batchId }, 'Cancelling existing preprocessor batch'); try { @@ -46,7 +40,7 @@ const cancelProcessorBatch = async (dispatch: AppDispatch, layerId: string, batc } finally { req.reset(); // Always reset the pending batch ID - the cancel req could fail if the batch doesn't exist - dispatch(controlAdapterProcessorPendingBatchIdChanged({ layerId, batchId: null })); + dispatch(caProcessorPendingBatchIdChanged({ id, batchId: null })); } }; @@ -54,7 +48,7 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni startAppListening({ matcher, effect: async (action, { dispatch, getState, getOriginalState, cancelActiveListeners, delay, take, signal }) => { - const layerId = controlAdapterRecalled.match(action) ? action.payload.id : action.payload.layerId; + const id = caRecalled.match(action) ? action.payload.data.id : action.payload.id; const state = getState(); const originalState = getOriginalState(); @@ -65,22 +59,20 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni // Delay before starting actual work await delay(DEBOUNCE_MS); - const layer = state.canvasV2.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId); + const ca = selectCA(state.canvasV2, id); - if (!layer) { + if (!ca) { return; } // We should only process if the processor settings or image have changed - const originalLayer = originalState.canvasV2.layers - .filter(isControlAdapterLayer) - .find((l) => l.id === layerId); - const originalImage = originalLayer?.controlAdapter.image; - const originalConfig = originalLayer?.controlAdapter.processorConfig; + const originalCA = selectCA(originalState.canvasV2, id); + const originalImage = originalCA?.image; + const originalConfig = originalCA?.processorConfig; - const image = layer.controlAdapter.image; - const processedImage = layer.controlAdapter.processedImage; - const config = layer.controlAdapter.processorConfig; + const image = ca.image; + const processedImage = ca.processedImage; + const config = ca.processorConfig; if (isEqual(config, originalConfig) && isEqual(image, originalImage) && processedImage) { // Neither config nor image have changed, we can bail @@ -91,15 +83,15 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni // - If we have no image, we have nothing to process // - If we have no processor config, we have nothing to process // Clear the processed image and bail - dispatch(controlAdapterProcessedImageChanged({ layerId, imageDTO: null })); + dispatch(caProcessedImageChanged({ id, imageDTO: null })); return; } // At this point, the user has stopped fiddling with the processor settings and there is a processor selected. // If there is a pending processor batch, cancel it. - if (layer.controlAdapter.processorPendingBatchId) { - cancelProcessorBatch(dispatch, layerId, layer.controlAdapter.processorPendingBatchId); + if (ca.processorPendingBatchId) { + cancelProcessorBatch(dispatch, id, ca.processorPendingBatchId); } // TODO(psyche): I can't get TS to be happy, it thinkgs `config` is `never` but it should be inferred from the generic... I'll just cast it for now @@ -132,7 +124,7 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni const enqueueResult = await req.unwrap(); // TODO(psyche): Update the pydantic models, pretty sure we will _always_ have a batch_id here, but the model says it's optional assert(enqueueResult.batch.batch_id, 'Batch ID not returned from queue'); - dispatch(controlAdapterProcessorPendingBatchIdChanged({ layerId, batchId: enqueueResult.batch.batch_id })); + dispatch(caProcessorPendingBatchIdChanged({ id, batchId: enqueueResult.batch.batch_id })); log.debug({ enqueueResult: parseify(enqueueResult) }, t('queue.graphQueued')); // Wait for the processor node to complete @@ -154,17 +146,15 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni assert(imageDTO, "Failed to fetch processor output's image DTO"); // Whew! We made it. Update the layer with the processed image - log.debug({ layerId, imageDTO }, 'ControlNet image processed'); - dispatch(controlAdapterProcessedImageChanged({ layerId, imageDTO })); - dispatch(controlAdapterProcessorPendingBatchIdChanged({ layerId, batchId: null })); + log.debug({ id, imageDTO }, 'ControlNet image processed'); + dispatch(caProcessedImageChanged({ id, imageDTO })); + dispatch(caProcessorPendingBatchIdChanged({ id, batchId: null })); } catch (error) { if (signal.aborted) { // The listener was canceled - we need to cancel the pending processor batch, if there is one (could have changed by now). - const pendingBatchId = getState() - .canvasV2.layers.filter(isControlAdapterLayer) - .find((l) => l.id === layerId)?.controlAdapter.processorPendingBatchId; + const pendingBatchId = selectCA(getState().canvasV2, id)?.processorPendingBatchId; if (pendingBatchId) { - cancelProcessorBatch(dispatch, layerId, pendingBatchId); + cancelProcessorBatch(dispatch, id, pendingBatchId); } log.trace('Control Adapter preprocessor cancelled'); } else { @@ -174,7 +164,7 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni if (error instanceof Object) { if ('data' in error && 'status' in error) { if (error.status === 403) { - dispatch(controlAdapterImageChanged({ layerId, imageDTO: null })); + dispatch(caImageChanged({ id, imageDTO: null })); return; } } diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess.ts deleted file mode 100644 index e52df30681c..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess.ts +++ /dev/null @@ -1,85 +0,0 @@ -import type { AnyListenerPredicate } from '@reduxjs/toolkit'; -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import type { RootState } from 'app/store/store'; -import { controlAdapterImageProcessed } from 'features/controlAdapters/store/actions'; -import { - controlAdapterAutoConfigToggled, - controlAdapterImageChanged, - controlAdapterModelChanged, - controlAdapterProcessorParamsChanged, - controlAdapterProcessortTypeChanged, - selectControlAdapterById, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; - -type AnyControlAdapterParamChangeAction = - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType; - -const predicate: AnyListenerPredicate = (action, state, prevState) => { - const isActionMatched = - controlAdapterProcessorParamsChanged.match(action) || - controlAdapterModelChanged.match(action) || - controlAdapterImageChanged.match(action) || - controlAdapterProcessortTypeChanged.match(action) || - controlAdapterAutoConfigToggled.match(action); - - if (!isActionMatched) { - return false; - } - - const { id } = action.payload; - const prevCA = selectControlAdapterById(prevState.controlAdapters, id); - const ca = selectControlAdapterById(state.controlAdapters, id); - if (!prevCA || !isControlNetOrT2IAdapter(prevCA) || !ca || !isControlNetOrT2IAdapter(ca)) { - return false; - } - - if (controlAdapterAutoConfigToggled.match(action)) { - // do not process if the user just disabled auto-config - if (prevCA.shouldAutoConfig === true) { - return false; - } - } - - const { controlImage, processorType, shouldAutoConfig } = ca; - if (controlAdapterModelChanged.match(action) && !shouldAutoConfig) { - // do not process if the action is a model change but the processor settings are dirty - return false; - } - - const isProcessorSelected = processorType !== 'none'; - - const hasControlImage = Boolean(controlImage); - - return isProcessorSelected && hasControlImage; -}; - -const DEBOUNCE_MS = 300; - -/** - * Listener that automatically processes a ControlNet image when its processor parameters are changed. - * - * The network request is debounced. - */ -export const addControlNetAutoProcessListener = (startAppListening: AppStartListening) => { - startAppListening({ - predicate, - effect: async (action, { dispatch, cancelActiveListeners, delay }) => { - const log = logger('session'); - const { id } = (action as AnyControlAdapterParamChangeAction).payload; - - // Cancel any in-progress instances of this listener - cancelActiveListeners(); - log.trace('ControlNet auto-process triggered'); - // Delay before starting actual work - await delay(DEBOUNCE_MS); - - dispatch(controlAdapterImageProcessed({ id })); - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts deleted file mode 100644 index 574dad00eb6..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { parseify } from 'common/util/serialize'; -import { controlAdapterImageProcessed } from 'features/controlAdapters/store/actions'; -import { - controlAdapterImageChanged, - controlAdapterProcessedImageChanged, - pendingControlImagesCleared, - selectControlAdapterById, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; -import { toast } from 'features/toast/toast'; -import { t } from 'i18next'; -import { imagesApi } from 'services/api/endpoints/images'; -import { queueApi } from 'services/api/endpoints/queue'; -import type { BatchConfig, ImageDTO } from 'services/api/types'; -import { socketInvocationComplete } from 'services/events/actions'; - -export const addControlNetImageProcessedListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: controlAdapterImageProcessed, - effect: async (action, { dispatch, getState, take }) => { - const log = logger('session'); - const { id } = action.payload; - const ca = selectControlAdapterById(getState().controlAdapters, id); - - if (!ca?.controlImage || !isControlNetOrT2IAdapter(ca)) { - log.error('Unable to process ControlNet image'); - return; - } - - if (ca.processorType === 'none' || ca.processorNode.type === 'none') { - return; - } - - // ControlNet one-off procressing graph is just the processor node, no edges. - // Also we need to grab the image. - - const nodeId = ca.processorNode.id; - const enqueueBatchArg: BatchConfig = { - prepend: true, - batch: { - graph: { - nodes: { - [ca.processorNode.id]: { - ...ca.processorNode, - is_intermediate: true, - use_cache: false, - image: { image_name: ca.controlImage }, - }, - }, - edges: [], - }, - runs: 1, - }, - }; - - try { - const req = dispatch( - queueApi.endpoints.enqueueBatch.initiate(enqueueBatchArg, { - fixedCacheKey: 'enqueueBatch', - }) - ); - const enqueueResult = await req.unwrap(); - req.reset(); - log.debug({ enqueueResult: parseify(enqueueResult) }, t('queue.graphQueued')); - - const [invocationCompleteAction] = await take( - (action): action is ReturnType => - socketInvocationComplete.match(action) && - action.payload.data.batch_id === enqueueResult.batch.batch_id && - action.payload.data.invocation_source_id === nodeId - ); - - // We still have to check the output type - if (invocationCompleteAction.payload.data.result.type === 'image_output') { - const { image_name } = invocationCompleteAction.payload.data.result.image; - - // Wait for the ImageDTO to be received - const [{ payload }] = await take( - (action) => - imagesApi.endpoints.getImageDTO.matchFulfilled(action) && action.payload.image_name === image_name - ); - - const processedControlImage = payload as ImageDTO; - - log.debug({ controlNetId: action.payload, processedControlImage }, 'ControlNet image processed'); - - // Update the processed image in the store - dispatch( - controlAdapterProcessedImageChanged({ - id, - processedControlImage: processedControlImage.image_name, - }) - ); - } - } catch (error) { - log.error({ enqueueBatchArg: parseify(enqueueBatchArg) }, t('queue.graphFailedToQueue')); - - if (error instanceof Object) { - if ('data' in error && 'status' in error) { - if (error.status === 403) { - dispatch(pendingControlImagesCleared()); - dispatch(controlAdapterImageChanged({ id, controlImage: null })); - return; - } - } - } - - toast({ - id: 'GRAPH_QUEUE_FAILED', - title: t('queue.graphFailedToQueue'), - status: 'error', - }); - } - }, - }); -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterConfig.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterConfig.tsx deleted file mode 100644 index c13783cddd9..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterConfig.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { Box, Flex, FormControl, FormLabel, Icon, IconButton, Switch } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import ParamControlAdapterModel from 'features/controlAdapters/components/parameters/ParamControlAdapterModel'; -import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled'; -import { useControlAdapterType } from 'features/controlAdapters/hooks/useControlAdapterType'; -import { - controlAdapterDuplicated, - controlAdapterIsEnabledChanged, - controlAdapterRemoved, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import type { ChangeEvent } from 'react'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiCaretUpBold, PiCopyBold, PiTrashSimpleBold } from 'react-icons/pi'; -import { useToggle } from 'react-use'; - -import ControlAdapterImagePreview from './ControlAdapterImagePreview'; -import ControlAdapterProcessorComponent from './ControlAdapterProcessorComponent'; -import ControlAdapterShouldAutoConfig from './ControlAdapterShouldAutoConfig'; -import ControlNetCanvasImageImports from './imports/ControlNetCanvasImageImports'; -import { ParamControlAdapterBeginEnd } from './parameters/ParamControlAdapterBeginEnd'; -import ParamControlAdapterControlMode from './parameters/ParamControlAdapterControlMode'; -import ParamControlAdapterIPMethod from './parameters/ParamControlAdapterIPMethod'; -import ParamControlAdapterProcessorSelect from './parameters/ParamControlAdapterProcessorSelect'; -import ParamControlAdapterResizeMode from './parameters/ParamControlAdapterResizeMode'; -import ParamControlAdapterWeight from './parameters/ParamControlAdapterWeight'; - -const ControlAdapterConfig = (props: { id: string; number: number }) => { - const { id, number } = props; - const controlAdapterType = useControlAdapterType(id); - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - - const activeTabName = useAppSelector(activeTabNameSelector); - const isEnabled = useControlAdapterIsEnabled(id); - const [isExpanded, toggleIsExpanded] = useToggle(false); - - const handleDelete = useCallback(() => { - dispatch(controlAdapterRemoved({ id })); - }, [id, dispatch]); - - const handleDuplicate = useCallback(() => { - dispatch(controlAdapterDuplicated(id)); - }, [id, dispatch]); - - const handleToggleIsEnabled = useCallback( - (e: ChangeEvent) => { - dispatch( - controlAdapterIsEnabledChanged({ - id, - isEnabled: e.target.checked, - }) - ); - }, - [id, dispatch] - ); - - if (!controlAdapterType) { - return null; - } - - return ( - - - - {t(`controlnet.${controlAdapterType}`, { number })} - - - - - - - - {activeTabName === 'canvas' && } - } - /> - } - /> - - } - /> - - - - - - - - - - {!isExpanded && ( - - - - )} - - - - {isExpanded && ( - <> - - - - - - - - - - )} - - ); -}; - -export default memo(ControlAdapterConfig); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx deleted file mode 100644 index c8670470b29..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx +++ /dev/null @@ -1,227 +0,0 @@ -import { Box, Flex, Spinner } from '@invoke-ai/ui-library'; -import { skipToken } from '@reduxjs/toolkit/query'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIDndImage from 'common/components/IAIDndImage'; -import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; -import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; -import { useControlAdapterControlImage } from 'features/controlAdapters/hooks/useControlAdapterControlImage'; -import { useControlAdapterProcessedControlImage } from 'features/controlAdapters/hooks/useControlAdapterProcessedControlImage'; -import { useControlAdapterProcessorType } from 'features/controlAdapters/hooks/useControlAdapterProcessorType'; -import { - controlAdapterImageChanged, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { heightChanged, widthChanged } from 'features/controlLayers/store/canvasV2Slice'; -import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; -import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { memo, useCallback, useEffect, useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiArrowCounterClockwiseBold, PiFloppyDiskBold, PiRulerBold } from 'react-icons/pi'; -import { - useAddImageToBoardMutation, - useChangeImageIsIntermediateMutation, - useGetImageDTOQuery, - useRemoveImageFromBoardMutation, -} from 'services/api/endpoints/images'; -import type { PostUploadAction } from 'services/api/types'; - -type Props = { - id: string; - isSmall?: boolean; -}; - -const selectPendingControlImages = createMemoizedSelector( - selectControlAdaptersSlice, - (controlAdapters) => controlAdapters.pendingControlImages -); - -const ControlAdapterImagePreview = ({ isSmall, id }: Props) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const controlImageName = useControlAdapterControlImage(id); - const processedControlImageName = useControlAdapterProcessedControlImage(id); - const processorType = useControlAdapterProcessorType(id); - const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId); - const isConnected = useAppSelector((s) => s.system.isConnected); - const activeTabName = useAppSelector(activeTabNameSelector); - const optimalDimension = useAppSelector(selectOptimalDimension); - const pendingControlImages = useAppSelector(selectPendingControlImages); - - const [isMouseOverImage, setIsMouseOverImage] = useState(false); - - const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery( - controlImageName ?? skipToken - ); - - const { currentData: processedControlImage, isError: isErrorProcessedControlImage } = useGetImageDTOQuery( - processedControlImageName ?? skipToken - ); - - const [changeIsIntermediate] = useChangeImageIsIntermediateMutation(); - const [addToBoard] = useAddImageToBoardMutation(); - const [removeFromBoard] = useRemoveImageFromBoardMutation(); - const handleResetControlImage = useCallback(() => { - dispatch(controlAdapterImageChanged({ id, controlImage: null })); - }, [id, dispatch]); - - const handleSaveControlImage = useCallback(async () => { - if (!processedControlImage) { - return; - } - - await changeIsIntermediate({ - imageDTO: processedControlImage, - is_intermediate: false, - }).unwrap(); - - if (autoAddBoardId !== 'none') { - addToBoard({ - imageDTO: processedControlImage, - board_id: autoAddBoardId, - }); - } else { - removeFromBoard({ imageDTO: processedControlImage }); - } - }, [processedControlImage, changeIsIntermediate, autoAddBoardId, addToBoard, removeFromBoard]); - - const handleSetControlImageToDimensions = useCallback(() => { - if (!controlImage) { - return; - } - - if (activeTabName === 'canvas') { - dispatch(setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension)); - } else { - const options = { updateAspectRatio: true, clamp: true }; - const { width, height } = calculateNewSize( - controlImage.width / controlImage.height, - optimalDimension * optimalDimension - ); - dispatch(widthChanged({ width, ...options })); - dispatch(heightChanged({ height, ...options })); - } - }, [controlImage, activeTabName, dispatch, optimalDimension]); - - const handleMouseEnter = useCallback(() => { - setIsMouseOverImage(true); - }, []); - - const handleMouseLeave = useCallback(() => { - setIsMouseOverImage(false); - }, []); - - const draggableData = useMemo(() => { - if (controlImage) { - return { - id, - payloadType: 'IMAGE_DTO', - payload: { imageDTO: controlImage }, - }; - } - }, [controlImage, id]); - - const droppableData = useMemo( - () => ({ - id, - actionType: 'SET_CONTROL_ADAPTER_IMAGE', - context: { id }, - }), - [id] - ); - - const postUploadAction = useMemo(() => ({ type: 'SET_CONTROL_ADAPTER_IMAGE', id }), [id]); - - const shouldShowProcessedImage = - controlImage && - processedControlImage && - !isMouseOverImage && - !pendingControlImages.includes(id) && - processorType !== 'none'; - - useEffect(() => { - if (isConnected && (isErrorControlImage || isErrorProcessedControlImage)) { - handleResetControlImage(); - } - }, [handleResetControlImage, isConnected, isErrorControlImage, isErrorProcessedControlImage]); - - return ( - - - - - - - - {controlImage && ( - - } - tooltip={t('controlnet.resetControlImage')} - /> - } - tooltip={t('controlnet.saveControlImage')} - /> - } - tooltip={t('controlnet.setControlImageDimensions')} - /> - - )} - - {pendingControlImages.includes(id) && ( - - - - )} - - ); -}; - -export default memo(ControlAdapterImagePreview); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterProcessorComponent.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterProcessorComponent.tsx deleted file mode 100644 index 2e37d88e272..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterProcessorComponent.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled'; -import { useControlAdapterProcessorNode } from 'features/controlAdapters/hooks/useControlAdapterProcessorNode'; -import { memo } from 'react'; - -import CannyProcessor from './processors/CannyProcessor'; -import ColorMapProcessor from './processors/ColorMapProcessor'; -import ContentShuffleProcessor from './processors/ContentShuffleProcessor'; -import DepthAnyThingProcessor from './processors/DepthAnyThingProcessor'; -import DWOpenposeProcessor from './processors/DWOpenposeProcessor'; -import HedProcessor from './processors/HedProcessor'; -import LineartAnimeProcessor from './processors/LineartAnimeProcessor'; -import LineartProcessor from './processors/LineartProcessor'; -import MediapipeFaceProcessor from './processors/MediapipeFaceProcessor'; -import MidasDepthProcessor from './processors/MidasDepthProcessor'; -import MlsdImageProcessor from './processors/MlsdImageProcessor'; -import NormalBaeProcessor from './processors/NormalBaeProcessor'; -import PidiProcessor from './processors/PidiProcessor'; -import ZoeDepthProcessor from './processors/ZoeDepthProcessor'; - -type Props = { - id: string; -}; - -const ControlAdapterProcessorComponent = ({ id }: Props) => { - const isEnabled = useControlAdapterIsEnabled(id); - const processorNode = useControlAdapterProcessorNode(id); - - if (!processorNode) { - return null; - } - - if (processorNode.type === 'canny_image_processor') { - return ; - } - - if (processorNode.type === 'color_map_image_processor') { - return ; - } - - if (processorNode.type === 'depth_anything_image_processor') { - return ; - } - - if (processorNode.type === 'hed_image_processor') { - return ; - } - - if (processorNode.type === 'lineart_image_processor') { - return ; - } - - if (processorNode.type === 'content_shuffle_image_processor') { - return ; - } - - if (processorNode.type === 'lineart_anime_image_processor') { - return ; - } - - if (processorNode.type === 'mediapipe_face_processor') { - return ; - } - - if (processorNode.type === 'midas_depth_image_processor') { - return ; - } - - if (processorNode.type === 'mlsd_image_processor') { - return ; - } - - if (processorNode.type === 'normalbae_image_processor') { - return ; - } - - if (processorNode.type === 'dw_openpose_image_processor') { - return ; - } - - if (processorNode.type === 'pidi_image_processor') { - return ; - } - - if (processorNode.type === 'zoe_depth_image_processor') { - return ; - } - - return null; -}; - -export default memo(ControlAdapterProcessorComponent); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterShouldAutoConfig.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterShouldAutoConfig.tsx deleted file mode 100644 index cb3d36c58d9..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterShouldAutoConfig.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled'; -import { useControlAdapterModel } from 'features/controlAdapters/hooks/useControlAdapterModel'; -import { useControlAdapterShouldAutoConfig } from 'features/controlAdapters/hooks/useControlAdapterShouldAutoConfig'; -import { controlAdapterAutoConfigToggled } from 'features/controlAdapters/store/controlAdaptersSlice'; -import { isNil } from 'lodash-es'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -type Props = { - id: string; -}; - -const ControlAdapterShouldAutoConfig = ({ id }: Props) => { - const isEnabled = useControlAdapterIsEnabled(id); - const shouldAutoConfig = useControlAdapterShouldAutoConfig(id); - const { modelConfig } = useControlAdapterModel(id); - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - - const handleShouldAutoConfigChanged = useCallback(() => { - dispatch(controlAdapterAutoConfigToggled({ id, modelConfig })); - }, [id, dispatch, modelConfig]); - - if (isNil(shouldAutoConfig)) { - return null; - } - - return ( - - {t('controlnet.autoConfigure')} - - - ); -}; - -export default memo(ControlAdapterShouldAutoConfig); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/hooks/useProcessorNodeChanged.ts b/invokeai/frontend/web/src/features/controlAdapters/components/hooks/useProcessorNodeChanged.ts deleted file mode 100644 index d76717cbf3e..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/hooks/useProcessorNodeChanged.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useAppDispatch } from 'app/store/storeHooks'; -import { controlAdapterProcessorParamsChanged } from 'features/controlAdapters/store/controlAdaptersSlice'; -import type { ControlAdapterProcessorNode } from 'features/controlAdapters/store/types'; -import { useCallback } from 'react'; - -export const useProcessorNodeChanged = () => { - const dispatch = useAppDispatch(); - const handleProcessorNodeChanged = useCallback( - (id: string, params: Partial) => { - dispatch( - controlAdapterProcessorParamsChanged({ - id, - params, - }) - ); - }, - [dispatch] - ); - return handleProcessorNodeChanged; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/imports/ControlNetCanvasImageImports.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/imports/ControlNetCanvasImageImports.tsx deleted file mode 100644 index fada3e3abf2..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/imports/ControlNetCanvasImageImports.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Flex, IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { canvasImageToControlAdapter, canvasMaskToControlAdapter } from 'features/canvas/store/actions'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiExcludeBold, PiImageSquareBold } from 'react-icons/pi'; - -type ControlNetCanvasImageImportsProps = { - id: string; -}; - -const ControlNetCanvasImageImports = (props: ControlNetCanvasImageImportsProps) => { - const { id } = props; - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - - const handleImportImageFromCanvas = useCallback(() => { - dispatch(canvasImageToControlAdapter({ id })); - }, [id, dispatch]); - - const handleImportMaskFromCanvas = useCallback(() => { - dispatch(canvasMaskToControlAdapter({ id })); - }, [id, dispatch]); - - return ( - - } - tooltip={t('controlnet.importImageFromCanvas')} - aria-label={t('controlnet.importImageFromCanvas')} - onClick={handleImportImageFromCanvas} - /> - } - tooltip={t('controlnet.importMaskFromCanvas')} - aria-label={t('controlnet.importMaskFromCanvas')} - onClick={handleImportMaskFromCanvas} - /> - - ); -}; - -export default memo(ControlNetCanvasImageImports); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterBeginEnd.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterBeginEnd.tsx deleted file mode 100644 index 245c182b9fb..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterBeginEnd.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { CompositeRangeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { useControlAdapterBeginEndStepPct } from 'features/controlAdapters/hooks/useControlAdapterBeginEndStepPct'; -import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled'; -import { - controlAdapterBeginStepPctChanged, - controlAdapterEndStepPctChanged, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -type Props = { - id: string; -}; - -const formatPct = (v: number) => `${Math.round(v * 100)}%`; - -export const ParamControlAdapterBeginEnd = memo(({ id }: Props) => { - const isEnabled = useControlAdapterIsEnabled(id); - const stepPcts = useControlAdapterBeginEndStepPct(id); - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - - const onChange = useCallback( - (v: [number, number]) => { - dispatch( - controlAdapterBeginStepPctChanged({ - id, - beginStepPct: v[0], - }) - ); - dispatch( - controlAdapterEndStepPctChanged({ - id, - endStepPct: v[1], - }) - ); - }, - [dispatch, id] - ); - - const onReset = useCallback(() => { - dispatch( - controlAdapterBeginStepPctChanged({ - id, - beginStepPct: 0, - }) - ); - dispatch( - controlAdapterEndStepPctChanged({ - id, - endStepPct: 1, - }) - ); - }, [dispatch, id]); - - const value = useMemo<[number, number]>(() => [stepPcts?.beginStepPct ?? 0, stepPcts?.endStepPct ?? 1], [stepPcts]); - - if (!stepPcts) { - return null; - } - - return ( - - - {t('controlnet.beginEndStepPercent')} - - - - ); -}); - -ParamControlAdapterBeginEnd.displayName = 'ParamControlAdapterBeginEnd'; - -const ariaLabel = ['Begin Step %', 'End Step %']; diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterControlMode.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterControlMode.tsx deleted file mode 100644 index ea16d6bc1ff..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterControlMode.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import type { ComboboxOnChange } from '@invoke-ai/ui-library'; -import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { useControlAdapterControlMode } from 'features/controlAdapters/hooks/useControlAdapterControlMode'; -import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled'; -import { controlAdapterControlModeChanged } from 'features/controlAdapters/store/controlAdaptersSlice'; -import type { ControlMode } from 'features/controlAdapters/store/types'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -type Props = { - id: string; -}; - -const ParamControlAdapterControlMode = ({ id }: Props) => { - const isEnabled = useControlAdapterIsEnabled(id); - const controlMode = useControlAdapterControlMode(id); - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - - const CONTROL_MODE_DATA = useMemo( - () => [ - { label: t('controlnet.balanced'), value: 'balanced' }, - { label: t('controlnet.prompt'), value: 'more_prompt' }, - { label: t('controlnet.control'), value: 'more_control' }, - { label: t('controlnet.megaControl'), value: 'unbalanced' }, - ], - [t] - ); - - const handleControlModeChange = useCallback( - (v) => { - if (!v) { - return; - } - dispatch( - controlAdapterControlModeChanged({ - id, - controlMode: v.value as ControlMode, - }) - ); - }, - [id, dispatch] - ); - - const value = useMemo( - () => CONTROL_MODE_DATA.filter((o) => o.value === controlMode)[0], - [CONTROL_MODE_DATA, controlMode] - ); - - if (!controlMode) { - return null; - } - - return ( - - - {t('controlnet.controlMode')} - - - - ); -}; - -export default memo(ParamControlAdapterControlMode); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterIPMethod.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterIPMethod.tsx deleted file mode 100644 index c7aaa9f26c8..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterIPMethod.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import type { ComboboxOnChange } from '@invoke-ai/ui-library'; -import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { useControlAdapterIPMethod } from 'features/controlAdapters/hooks/useControlAdapterIPMethod'; -import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled'; -import { controlAdapterIPMethodChanged } from 'features/controlAdapters/store/controlAdaptersSlice'; -import type { IPMethod } from 'features/controlAdapters/store/types'; -import { isIPMethod } from 'features/controlAdapters/store/types'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -type Props = { - id: string; -}; - -const ParamControlAdapterIPMethod = ({ id }: Props) => { - const isEnabled = useControlAdapterIsEnabled(id); - const method = useControlAdapterIPMethod(id); - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - - const options: { label: string; value: IPMethod }[] = useMemo( - () => [ - { label: t('controlnet.full'), value: 'full' }, - { label: `${t('controlnet.style')} (${t('common.beta')})`, value: 'style' }, - { label: `${t('controlnet.composition')} (${t('common.beta')})`, value: 'composition' }, - ], - [t] - ); - - const handleIPMethodChanged = useCallback( - (v) => { - if (!isIPMethod(v?.value)) { - return; - } - dispatch( - controlAdapterIPMethodChanged({ - id, - method: v.value, - }) - ); - }, - [id, dispatch] - ); - - const value = useMemo(() => options.find((o) => o.value === method), [options, method]); - - if (!method) { - return null; - } - - return ( - - - {t('controlnet.ipAdapterMethod')} - - - - ); -}; - -export default memo(ParamControlAdapterIPMethod); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterModel.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterModel.tsx deleted file mode 100644 index 00c7d5859da..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterModel.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; -import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; -import { useControlAdapterCLIPVisionModel } from 'features/controlAdapters/hooks/useControlAdapterCLIPVisionModel'; -import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled'; -import { useControlAdapterModel } from 'features/controlAdapters/hooks/useControlAdapterModel'; -import { useControlAdapterModels } from 'features/controlAdapters/hooks/useControlAdapterModels'; -import { useControlAdapterType } from 'features/controlAdapters/hooks/useControlAdapterType'; -import { - controlAdapterCLIPVisionModelChanged, - controlAdapterModelChanged, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import type { CLIPVisionModel } from 'features/controlAdapters/store/types'; -import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import type { - AnyModelConfig, - ControlNetModelConfig, - IPAdapterModelConfig, - T2IAdapterModelConfig, -} from 'services/api/types'; - -type ParamControlAdapterModelProps = { - id: string; -}; - -const selectMainModel = createMemoizedSelector(selectGenerationSlice, (generation) => generation.model); - -const ParamControlAdapterModel = ({ id }: ParamControlAdapterModelProps) => { - const isEnabled = useControlAdapterIsEnabled(id); - const controlAdapterType = useControlAdapterType(id); - const { modelConfig } = useControlAdapterModel(id); - const dispatch = useAppDispatch(); - const currentBaseModel = useAppSelector((s) => s.generation.model?.base); - const currentCLIPVisionModel = useControlAdapterCLIPVisionModel(id); - const mainModel = useAppSelector(selectMainModel); - const { t } = useTranslation(); - - const [modelConfigs, { isLoading }] = useControlAdapterModels(controlAdapterType); - - const _onChange = useCallback( - (modelConfig: ControlNetModelConfig | IPAdapterModelConfig | T2IAdapterModelConfig | null) => { - if (!modelConfig) { - return; - } - dispatch( - controlAdapterModelChanged({ - id, - modelConfig, - }) - ); - }, - [dispatch, id] - ); - - const onCLIPVisionModelChange = useCallback( - (v) => { - if (!v?.value) { - return; - } - dispatch(controlAdapterCLIPVisionModelChanged({ id, clipVisionModel: v.value as CLIPVisionModel })); - }, - [dispatch, id] - ); - - const selectedModel = useMemo( - () => (modelConfig && controlAdapterType ? { ...modelConfig, model_type: controlAdapterType } : null), - [controlAdapterType, modelConfig] - ); - - const getIsDisabled = useCallback( - (model: AnyModelConfig): boolean => { - const isCompatible = currentBaseModel === model.base; - const hasMainModel = Boolean(currentBaseModel); - return !hasMainModel || !isCompatible; - }, - [currentBaseModel] - ); - - const { options, value, onChange, noOptionsMessage } = useGroupedModelCombobox({ - modelConfigs, - onChange: _onChange, - selectedModel, - getIsDisabled, - isLoading, - }); - - const clipVisionOptions = useMemo( - () => [ - { label: 'ViT-H', value: 'ViT-H' }, - { label: 'ViT-G', value: 'ViT-G' }, - ], - [] - ); - - const clipVisionModel = useMemo( - () => clipVisionOptions.find((o) => o.value === currentCLIPVisionModel), - [clipVisionOptions, currentCLIPVisionModel] - ); - - return ( - - - - - - - {modelConfig?.type === 'ip_adapter' && modelConfig.format === 'checkpoint' && ( - - - - )} - - ); -}; - -export default memo(ParamControlAdapterModel); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterProcessorSelect.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterProcessorSelect.tsx deleted file mode 100644 index 5257a471283..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterProcessorSelect.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; -import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled'; -import { useControlAdapterProcessorNode } from 'features/controlAdapters/hooks/useControlAdapterProcessorNode'; -import { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants'; -import { controlAdapterProcessortTypeChanged } from 'features/controlAdapters/store/controlAdaptersSlice'; -import type { ControlAdapterProcessorType } from 'features/controlAdapters/store/types'; -import { configSelector } from 'features/system/store/configSelectors'; -import { map } from 'lodash-es'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -type Props = { - id: string; -}; - -const selectOptions = createMemoizedSelector(configSelector, (config) => { - const options: ComboboxOption[] = map(CONTROLNET_PROCESSORS, (p) => ({ - value: p.type, - label: p.label, - })) - .sort((a, b) => - // sort 'none' to the top - a.value === 'none' ? -1 : b.value === 'none' ? 1 : a.label.localeCompare(b.label) - ) - .filter((d) => !config.sd.disabledControlNetProcessors.includes(d.value as ControlAdapterProcessorType)); - - return options; -}); - -const ParamControlAdapterProcessorSelect = ({ id }: Props) => { - const isEnabled = useControlAdapterIsEnabled(id); - const processorNode = useControlAdapterProcessorNode(id); - const dispatch = useAppDispatch(); - const options = useAppSelector(selectOptions); - const { t } = useTranslation(); - - const onChange = useCallback( - (v) => { - if (!v) { - return; - } - dispatch( - controlAdapterProcessortTypeChanged({ - id, - processorType: v.value as ControlAdapterProcessorType, // TODO: need runtime check... - }) - ); - }, - [id, dispatch] - ); - const value = useMemo(() => options.find((o) => o.value === processorNode?.type), [options, processorNode]); - - if (!processorNode) { - return null; - } - return ( - - - {t('controlnet.processor')} - - - - ); -}; - -export default memo(ParamControlAdapterProcessorSelect); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterResizeMode.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterResizeMode.tsx deleted file mode 100644 index 58b8905f5ac..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterResizeMode.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import type { ComboboxOnChange } from '@invoke-ai/ui-library'; -import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled'; -import { useControlAdapterResizeMode } from 'features/controlAdapters/hooks/useControlAdapterResizeMode'; -import { controlAdapterResizeModeChanged } from 'features/controlAdapters/store/controlAdaptersSlice'; -import type { ResizeMode } from 'features/controlAdapters/store/types'; -import { isResizeMode } from 'features/controlAdapters/store/types'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -type Props = { - id: string; -}; - -const ParamControlAdapterResizeMode = ({ id }: Props) => { - const isEnabled = useControlAdapterIsEnabled(id); - const resizeMode = useControlAdapterResizeMode(id); - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - - const options: { label: string; value: ResizeMode }[] = useMemo( - () => [ - { label: t('controlnet.resize'), value: 'just_resize' }, - { label: t('controlnet.crop'), value: 'crop_resize' }, - { label: t('controlnet.fill'), value: 'fill_resize' }, - { label: t('controlnet.resizeSimple'), value: 'just_resize_simple' }, - ], - [t] - ); - - const handleResizeModeChange = useCallback( - (v) => { - if (!isResizeMode(v?.value)) { - return; - } - dispatch( - controlAdapterResizeModeChanged({ - id, - resizeMode: v.value, - }) - ); - }, - [id, dispatch] - ); - - const value = useMemo(() => options.find((o) => o.value === resizeMode), [options, resizeMode]); - - if (!resizeMode) { - return null; - } - - return ( - - - {t('controlnet.resizeMode')} - - - - ); -}; - -export default memo(ParamControlAdapterResizeMode); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterWeight.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterWeight.tsx deleted file mode 100644 index 7f45901f538..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterWeight.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled'; -import { useControlAdapterWeight } from 'features/controlAdapters/hooks/useControlAdapterWeight'; -import { controlAdapterWeightChanged } from 'features/controlAdapters/store/controlAdaptersSlice'; -import { isNil } from 'lodash-es'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -type ParamControlAdapterWeightProps = { - id: string; -}; - -const formatValue = (v: number) => v.toFixed(2); - -const ParamControlAdapterWeight = ({ id }: ParamControlAdapterWeightProps) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const isEnabled = useControlAdapterIsEnabled(id); - const weight = useControlAdapterWeight(id); - const initial = useAppSelector((s) => s.config.sd.ca.weight.initial); - const sliderMin = useAppSelector((s) => s.config.sd.ca.weight.sliderMin); - const sliderMax = useAppSelector((s) => s.config.sd.ca.weight.sliderMax); - const numberInputMin = useAppSelector((s) => s.config.sd.ca.weight.numberInputMin); - const numberInputMax = useAppSelector((s) => s.config.sd.ca.weight.numberInputMax); - const coarseStep = useAppSelector((s) => s.config.sd.ca.weight.coarseStep); - const fineStep = useAppSelector((s) => s.config.sd.ca.weight.fineStep); - - const onChange = useCallback( - (weight: number) => { - dispatch(controlAdapterWeightChanged({ id, weight })); - }, - [dispatch, id] - ); - - if (isNil(weight)) { - // should never happen - return null; - } - - return ( - - - {t('controlnet.weight')} - - - - - ); -}; - -export default memo(ParamControlAdapterWeight); - -const marks = [0, 1, 2]; diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/CannyProcessor.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/CannyProcessor.tsx deleted file mode 100644 index 0d547b9490b..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/CannyProcessor.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useProcessorNodeChanged } from 'features/controlAdapters/components/hooks/useProcessorNodeChanged'; -import { useGetDefaultForControlnetProcessor } from 'features/controlAdapters/hooks/useGetDefaultForControlnetProcessor'; -import type { RequiredCannyImageProcessorInvocation } from 'features/controlAdapters/store/types'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './common/ProcessorWrapper'; - -type CannyProcessorProps = { - controlNetId: string; - processorNode: RequiredCannyImageProcessorInvocation; - isEnabled: boolean; -}; - -const CannyProcessor = (props: CannyProcessorProps) => { - const { controlNetId, processorNode, isEnabled } = props; - const { low_threshold, high_threshold, image_resolution, detect_resolution } = processorNode; - const processorChanged = useProcessorNodeChanged(); - const { t } = useTranslation(); - const defaults = useGetDefaultForControlnetProcessor( - 'canny_image_processor' - ) as RequiredCannyImageProcessorInvocation; - - const handleLowThresholdChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { low_threshold: v }); - }, - [controlNetId, processorChanged] - ); - - const handleHighThresholdChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { high_threshold: v }); - }, - [controlNetId, processorChanged] - ); - - const handleImageResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { image_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleDetectResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { detect_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - return ( - - - {t('controlnet.lowThreshold')} - - - - - {t('controlnet.highThreshold')} - - - - - {t('controlnet.imageResolution')} - - - - - {t('controlnet.detectResolution')} - - - - - ); -}; - -export default memo(CannyProcessor); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/ColorMapProcessor.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/ColorMapProcessor.tsx deleted file mode 100644 index 3dd6bf0aa98..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/ColorMapProcessor.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useProcessorNodeChanged } from 'features/controlAdapters/components/hooks/useProcessorNodeChanged'; -import { useGetDefaultForControlnetProcessor } from 'features/controlAdapters/hooks/useGetDefaultForControlnetProcessor'; -import type { RequiredColorMapImageProcessorInvocation } from 'features/controlAdapters/store/types'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './common/ProcessorWrapper'; - -type ColorMapProcessorProps = { - controlNetId: string; - processorNode: RequiredColorMapImageProcessorInvocation; - isEnabled: boolean; -}; - -const ColorMapProcessor = (props: ColorMapProcessorProps) => { - const { controlNetId, processorNode, isEnabled } = props; - const { color_map_tile_size } = processorNode; - const processorChanged = useProcessorNodeChanged(); - const { t } = useTranslation(); - const defaults = useGetDefaultForControlnetProcessor( - 'color_map_image_processor' - ) as RequiredColorMapImageProcessorInvocation; - - const handleColorMapTileSizeChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { color_map_tile_size: v }); - }, - [controlNetId, processorChanged] - ); - - return ( - - - {t('controlnet.colorMapTileSize')} - - - - - ); -}; - -export default memo(ColorMapProcessor); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/ContentShuffleProcessor.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/ContentShuffleProcessor.tsx deleted file mode 100644 index 9677a3a2233..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/ContentShuffleProcessor.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useProcessorNodeChanged } from 'features/controlAdapters/components/hooks/useProcessorNodeChanged'; -import { useGetDefaultForControlnetProcessor } from 'features/controlAdapters/hooks/useGetDefaultForControlnetProcessor'; -import type { RequiredContentShuffleImageProcessorInvocation } from 'features/controlAdapters/store/types'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './common/ProcessorWrapper'; - -type Props = { - controlNetId: string; - processorNode: RequiredContentShuffleImageProcessorInvocation; - isEnabled: boolean; -}; - -const ContentShuffleProcessor = (props: Props) => { - const { controlNetId, processorNode, isEnabled } = props; - const { image_resolution, detect_resolution, w, h, f } = processorNode; - const processorChanged = useProcessorNodeChanged(); - const { t } = useTranslation(); - - const defaults = useGetDefaultForControlnetProcessor( - 'content_shuffle_image_processor' - ) as RequiredContentShuffleImageProcessorInvocation; - - const handleDetectResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { detect_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleImageResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { image_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleWChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { w: v }); - }, - [controlNetId, processorChanged] - ); - - const handleHChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { h: v }); - }, - [controlNetId, processorChanged] - ); - - const handleFChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { f: v }); - }, - [controlNetId, processorChanged] - ); - - return ( - - - {t('controlnet.detectResolution')} - - - - - {t('controlnet.imageResolution')} - - - - - {t('controlnet.w')} - - - - - {t('controlnet.h')} - - - - - {t('controlnet.f')} - - - - - ); -}; - -export default memo(ContentShuffleProcessor); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/DWOpenposeProcessor.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/DWOpenposeProcessor.tsx deleted file mode 100644 index 6761bfd4e1d..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/DWOpenposeProcessor.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import { useProcessorNodeChanged } from 'features/controlAdapters/components/hooks/useProcessorNodeChanged'; -import { useGetDefaultForControlnetProcessor } from 'features/controlAdapters/hooks/useGetDefaultForControlnetProcessor'; -import type { RequiredDWOpenposeImageProcessorInvocation } from 'features/controlAdapters/store/types'; -import type { ChangeEvent } from 'react'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './common/ProcessorWrapper'; - -type Props = { - controlNetId: string; - processorNode: RequiredDWOpenposeImageProcessorInvocation; - isEnabled: boolean; -}; - -const DWOpenposeProcessor = (props: Props) => { - const { controlNetId, processorNode, isEnabled } = props; - const { image_resolution, draw_body, draw_face, draw_hands } = processorNode; - const processorChanged = useProcessorNodeChanged(); - const { t } = useTranslation(); - - const defaults = useGetDefaultForControlnetProcessor( - 'dw_openpose_image_processor' - ) as RequiredDWOpenposeImageProcessorInvocation; - - const handleDrawBodyChanged = useCallback( - (e: ChangeEvent) => { - processorChanged(controlNetId, { draw_body: e.target.checked }); - }, - [controlNetId, processorChanged] - ); - - const handleDrawFaceChanged = useCallback( - (e: ChangeEvent) => { - processorChanged(controlNetId, { draw_face: e.target.checked }); - }, - [controlNetId, processorChanged] - ); - - const handleDrawHandsChanged = useCallback( - (e: ChangeEvent) => { - processorChanged(controlNetId, { draw_hands: e.target.checked }); - }, - [controlNetId, processorChanged] - ); - - const handleImageResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { image_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - return ( - - - - {t('controlnet.body')} - - - - {t('controlnet.face')} - - - - {t('controlnet.hands')} - - - - - {t('controlnet.imageResolution')} - - - - - ); -}; - -export default memo(DWOpenposeProcessor); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/DepthAnyThingProcessor.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/DepthAnyThingProcessor.tsx deleted file mode 100644 index 3f248a82bec..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/DepthAnyThingProcessor.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import type { ComboboxOnChange } from '@invoke-ai/ui-library'; -import { Combobox, CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useProcessorNodeChanged } from 'features/controlAdapters/components/hooks/useProcessorNodeChanged'; -import { useGetDefaultForControlnetProcessor } from 'features/controlAdapters/hooks/useGetDefaultForControlnetProcessor'; -import type { - DepthAnythingModelSize, - RequiredDepthAnythingImageProcessorInvocation, -} from 'features/controlAdapters/store/types'; -import { isDepthAnythingModelSize } from 'features/controlAdapters/store/types'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './common/ProcessorWrapper'; - -type Props = { - controlNetId: string; - processorNode: RequiredDepthAnythingImageProcessorInvocation; - isEnabled: boolean; -}; - -const DepthAnythingProcessor = (props: Props) => { - const { controlNetId, processorNode, isEnabled } = props; - const { model_size, resolution } = processorNode; - const processorChanged = useProcessorNodeChanged(); - const { t } = useTranslation(); - - const defaults = useGetDefaultForControlnetProcessor( - 'midas_depth_image_processor' - ) as RequiredDepthAnythingImageProcessorInvocation; - - const handleModelSizeChange = useCallback( - (v) => { - if (!isDepthAnythingModelSize(v?.value)) { - return; - } - processorChanged(controlNetId, { - model_size: v.value, - }); - }, - [controlNetId, processorChanged] - ); - - const options: { label: string; value: DepthAnythingModelSize }[] = useMemo( - () => [ - { label: t('controlnet.depthAnythingSmallV2'), value: 'small_v2' }, - { label: t('controlnet.small'), value: 'small' }, - { label: t('controlnet.base'), value: 'base' }, - { label: t('controlnet.large'), value: 'large' }, - ], - [t] - ); - - const value = useMemo(() => options.filter((o) => o.value === model_size)[0], [options, model_size]); - - const handleResolutionChange = useCallback( - (v: number) => { - processorChanged(controlNetId, { resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleResolutionDefaultChange = useCallback(() => { - processorChanged(controlNetId, { resolution: 512 }); - }, [controlNetId, processorChanged]); - - return ( - - - {t('controlnet.modelSize')} - - - - {t('controlnet.imageResolution')} - - - - - ); -}; - -export default memo(DepthAnythingProcessor); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/HedProcessor.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/HedProcessor.tsx deleted file mode 100644 index e75d50de363..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/HedProcessor.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import { useProcessorNodeChanged } from 'features/controlAdapters/components/hooks/useProcessorNodeChanged'; -import { useGetDefaultForControlnetProcessor } from 'features/controlAdapters/hooks/useGetDefaultForControlnetProcessor'; -import type { RequiredHedImageProcessorInvocation } from 'features/controlAdapters/store/types'; -import type { ChangeEvent } from 'react'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './common/ProcessorWrapper'; - -type HedProcessorProps = { - controlNetId: string; - processorNode: RequiredHedImageProcessorInvocation; - isEnabled: boolean; -}; - -const HedPreprocessor = (props: HedProcessorProps) => { - const { - controlNetId, - processorNode: { detect_resolution, image_resolution, scribble }, - isEnabled, - } = props; - const processorChanged = useProcessorNodeChanged(); - const { t } = useTranslation(); - - const defaults = useGetDefaultForControlnetProcessor('hed_image_processor') as RequiredHedImageProcessorInvocation; - - const handleDetectResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { detect_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleImageResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { image_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleScribbleChanged = useCallback( - (e: ChangeEvent) => { - processorChanged(controlNetId, { scribble: e.target.checked }); - }, - [controlNetId, processorChanged] - ); - - return ( - - - {t('controlnet.detectResolution')} - - - - - {t('controlnet.imageResolution')} - - - - - {t('controlnet.scribble')} - - - - ); -}; - -export default memo(HedPreprocessor); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/LineartAnimeProcessor.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/LineartAnimeProcessor.tsx deleted file mode 100644 index 9849bda7c89..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/LineartAnimeProcessor.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useProcessorNodeChanged } from 'features/controlAdapters/components/hooks/useProcessorNodeChanged'; -import { useGetDefaultForControlnetProcessor } from 'features/controlAdapters/hooks/useGetDefaultForControlnetProcessor'; -import type { RequiredLineartAnimeImageProcessorInvocation } from 'features/controlAdapters/store/types'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './common/ProcessorWrapper'; - -type Props = { - controlNetId: string; - processorNode: RequiredLineartAnimeImageProcessorInvocation; - isEnabled: boolean; -}; - -const LineartAnimeProcessor = (props: Props) => { - const { controlNetId, processorNode, isEnabled } = props; - const { image_resolution, detect_resolution } = processorNode; - const processorChanged = useProcessorNodeChanged(); - const { t } = useTranslation(); - - const defaults = useGetDefaultForControlnetProcessor( - 'lineart_anime_image_processor' - ) as RequiredLineartAnimeImageProcessorInvocation; - - const handleDetectResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { detect_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleImageResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { image_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - return ( - - - {t('controlnet.detectResolution')} - - - - - {t('controlnet.imageResolution')} - - - - - ); -}; - -export default memo(LineartAnimeProcessor); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/LineartProcessor.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/LineartProcessor.tsx deleted file mode 100644 index 51d082eb57f..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/LineartProcessor.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import { useProcessorNodeChanged } from 'features/controlAdapters/components/hooks/useProcessorNodeChanged'; -import { useGetDefaultForControlnetProcessor } from 'features/controlAdapters/hooks/useGetDefaultForControlnetProcessor'; -import type { RequiredLineartImageProcessorInvocation } from 'features/controlAdapters/store/types'; -import type { ChangeEvent } from 'react'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './common/ProcessorWrapper'; - -type LineartProcessorProps = { - controlNetId: string; - processorNode: RequiredLineartImageProcessorInvocation; - isEnabled: boolean; -}; - -const LineartProcessor = (props: LineartProcessorProps) => { - const { controlNetId, processorNode, isEnabled } = props; - const { image_resolution, detect_resolution, coarse } = processorNode; - const processorChanged = useProcessorNodeChanged(); - const { t } = useTranslation(); - - const defaults = useGetDefaultForControlnetProcessor( - 'lineart_image_processor' - ) as RequiredLineartImageProcessorInvocation; - - const handleDetectResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { detect_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleImageResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { image_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleCoarseChanged = useCallback( - (e: ChangeEvent) => { - processorChanged(controlNetId, { coarse: e.target.checked }); - }, - [controlNetId, processorChanged] - ); - - return ( - - - {t('controlnet.detectResolution')} - - - - - {t('controlnet.imageResolution')} - - - - - {t('controlnet.coarse')} - - - - ); -}; - -export default memo(LineartProcessor); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/MediapipeFaceProcessor.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/MediapipeFaceProcessor.tsx deleted file mode 100644 index de35d628d7c..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/MediapipeFaceProcessor.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useProcessorNodeChanged } from 'features/controlAdapters/components/hooks/useProcessorNodeChanged'; -import { useGetDefaultForControlnetProcessor } from 'features/controlAdapters/hooks/useGetDefaultForControlnetProcessor'; -import type { RequiredMediapipeFaceProcessorInvocation } from 'features/controlAdapters/store/types'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './common/ProcessorWrapper'; - -type Props = { - controlNetId: string; - processorNode: RequiredMediapipeFaceProcessorInvocation; - isEnabled: boolean; -}; - -const MediapipeFaceProcessor = (props: Props) => { - const { controlNetId, processorNode, isEnabled } = props; - const { max_faces, min_confidence, image_resolution, detect_resolution } = processorNode; - const processorChanged = useProcessorNodeChanged(); - const { t } = useTranslation(); - - const defaults = useGetDefaultForControlnetProcessor( - 'mediapipe_face_processor' - ) as RequiredMediapipeFaceProcessorInvocation; - - const handleMaxFacesChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { max_faces: v }); - }, - [controlNetId, processorChanged] - ); - - const handleMinConfidenceChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { min_confidence: v }); - }, - [controlNetId, processorChanged] - ); - - const handleImageResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { image_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleDetectResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { detect_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - return ( - - - {t('controlnet.maxFaces')} - - - - - {t('controlnet.minConfidence')} - - - - - {t('controlnet.imageResolution')} - - - - - {t('controlnet.detectResolution')} - - - - - ); -}; - -export default memo(MediapipeFaceProcessor); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/MidasDepthProcessor.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/MidasDepthProcessor.tsx deleted file mode 100644 index f4089ed48f3..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/MidasDepthProcessor.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useProcessorNodeChanged } from 'features/controlAdapters/components/hooks/useProcessorNodeChanged'; -import { useGetDefaultForControlnetProcessor } from 'features/controlAdapters/hooks/useGetDefaultForControlnetProcessor'; -import type { RequiredMidasDepthImageProcessorInvocation } from 'features/controlAdapters/store/types'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './common/ProcessorWrapper'; - -type Props = { - controlNetId: string; - processorNode: RequiredMidasDepthImageProcessorInvocation; - isEnabled: boolean; -}; - -const MidasDepthProcessor = (props: Props) => { - const { controlNetId, processorNode, isEnabled } = props; - const { a_mult, bg_th, image_resolution, detect_resolution } = processorNode; - const processorChanged = useProcessorNodeChanged(); - const { t } = useTranslation(); - - const defaults = useGetDefaultForControlnetProcessor( - 'midas_depth_image_processor' - ) as RequiredMidasDepthImageProcessorInvocation; - - const handleAMultChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { a_mult: v }); - }, - [controlNetId, processorChanged] - ); - - const handleBgThChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { bg_th: v }); - }, - [controlNetId, processorChanged] - ); - - const handleImageResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { image_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleDetectResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { detect_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - return ( - - - {t('controlnet.amult')} - - - - - {t('controlnet.bgth')} - - - - - {t('controlnet.imageResolution')} - - - - - {t('controlnet.detectResolution')} - - - - - ); -}; - -export default memo(MidasDepthProcessor); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/MlsdImageProcessor.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/MlsdImageProcessor.tsx deleted file mode 100644 index 69fd4f68074..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/MlsdImageProcessor.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useProcessorNodeChanged } from 'features/controlAdapters/components/hooks/useProcessorNodeChanged'; -import { useGetDefaultForControlnetProcessor } from 'features/controlAdapters/hooks/useGetDefaultForControlnetProcessor'; -import type { RequiredMlsdImageProcessorInvocation } from 'features/controlAdapters/store/types'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './common/ProcessorWrapper'; - -type Props = { - controlNetId: string; - processorNode: RequiredMlsdImageProcessorInvocation; - isEnabled: boolean; -}; - -const MlsdImageProcessor = (props: Props) => { - const { controlNetId, processorNode, isEnabled } = props; - const { image_resolution, detect_resolution, thr_d, thr_v } = processorNode; - const processorChanged = useProcessorNodeChanged(); - const { t } = useTranslation(); - - const defaults = useGetDefaultForControlnetProcessor('mlsd_image_processor') as RequiredMlsdImageProcessorInvocation; - - const handleDetectResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { detect_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleImageResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { image_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleThrDChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { thr_d: v }); - }, - [controlNetId, processorChanged] - ); - - const handleThrVChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { thr_v: v }); - }, - [controlNetId, processorChanged] - ); - - return ( - - - {t('controlnet.detectResolution')} - - - - - {t('controlnet.imageResolution')} - - - - - {t('controlnet.w')} - - - - - {t('controlnet.h')} - - - - - ); -}; - -export default memo(MlsdImageProcessor); - -const marks0to4096 = [0, 4096]; -const marks0to1 = [0, 1]; diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/NormalBaeProcessor.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/NormalBaeProcessor.tsx deleted file mode 100644 index 43f7115df51..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/NormalBaeProcessor.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useProcessorNodeChanged } from 'features/controlAdapters/components/hooks/useProcessorNodeChanged'; -import { useGetDefaultForControlnetProcessor } from 'features/controlAdapters/hooks/useGetDefaultForControlnetProcessor'; -import type { RequiredNormalbaeImageProcessorInvocation } from 'features/controlAdapters/store/types'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './common/ProcessorWrapper'; - -type Props = { - controlNetId: string; - processorNode: RequiredNormalbaeImageProcessorInvocation; - isEnabled: boolean; -}; - -const NormalBaeProcessor = (props: Props) => { - const { controlNetId, processorNode, isEnabled } = props; - const { image_resolution, detect_resolution } = processorNode; - const processorChanged = useProcessorNodeChanged(); - const { t } = useTranslation(); - - const defaults = useGetDefaultForControlnetProcessor( - 'normalbae_image_processor' - ) as RequiredNormalbaeImageProcessorInvocation; - - const handleDetectResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { detect_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleImageResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { image_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - return ( - - - {t('controlnet.detectResolution')} - - - - - {t('controlnet.imageResolution')} - - - - - ); -}; - -export default memo(NormalBaeProcessor); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/PidiProcessor.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/PidiProcessor.tsx deleted file mode 100644 index 763069f769a..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/PidiProcessor.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import { useProcessorNodeChanged } from 'features/controlAdapters/components/hooks/useProcessorNodeChanged'; -import { useGetDefaultForControlnetProcessor } from 'features/controlAdapters/hooks/useGetDefaultForControlnetProcessor'; -import type { RequiredPidiImageProcessorInvocation } from 'features/controlAdapters/store/types'; -import type { ChangeEvent } from 'react'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './common/ProcessorWrapper'; - -type Props = { - controlNetId: string; - processorNode: RequiredPidiImageProcessorInvocation; - isEnabled: boolean; -}; - -const PidiProcessor = (props: Props) => { - const { controlNetId, processorNode, isEnabled } = props; - const { image_resolution, detect_resolution, scribble, safe } = processorNode; - const processorChanged = useProcessorNodeChanged(); - const { t } = useTranslation(); - - const defaults = useGetDefaultForControlnetProcessor('pidi_image_processor') as RequiredPidiImageProcessorInvocation; - - const handleDetectResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { detect_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleImageResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { image_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleScribbleChanged = useCallback( - (e: ChangeEvent) => { - processorChanged(controlNetId, { scribble: e.target.checked }); - }, - [controlNetId, processorChanged] - ); - - const handleSafeChanged = useCallback( - (e: ChangeEvent) => { - processorChanged(controlNetId, { safe: e.target.checked }); - }, - [controlNetId, processorChanged] - ); - - return ( - - - {t('controlnet.detectResolution')} - - - - - {t('controlnet.imageResolution')} - - - - - {t('controlnet.scribble')} - - - - {t('controlnet.safe')} - - - - ); -}; - -export default memo(PidiProcessor); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/ZoeDepthProcessor.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/ZoeDepthProcessor.tsx deleted file mode 100644 index 61897a5d27f..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/ZoeDepthProcessor.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import type { RequiredZoeDepthImageProcessorInvocation } from 'features/controlAdapters/store/types'; -import { memo } from 'react'; - -type Props = { - controlNetId: string; - processorNode: RequiredZoeDepthImageProcessorInvocation; - isEnabled: boolean; -}; - -const ZoeDepthProcessor = (_props: Props) => { - // Has no parameters? - return null; -}; - -export default memo(ZoeDepthProcessor); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/common/ProcessorWrapper.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/common/ProcessorWrapper.tsx deleted file mode 100644 index 0b99887b539..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/common/ProcessorWrapper.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Flex } from '@invoke-ai/ui-library'; -import type { PropsWithChildren } from 'react'; -import { memo } from 'react'; - -type Props = PropsWithChildren; - -const ProcessorWrapper = (props: Props) => { - return ( - - {props.children} - - ); -}; - -export default memo(ProcessorWrapper); diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useAddControlAdapter.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useAddControlAdapter.ts deleted file mode 100644 index 1af2fc81b99..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useAddControlAdapter.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useControlAdapterModels } from 'features/controlAdapters/hooks/useControlAdapterModels'; -import { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants'; -import { controlAdapterAdded } from 'features/controlAdapters/store/controlAdaptersSlice'; -import { type ControlAdapterType, isControlAdapterProcessorType } from 'features/controlAdapters/store/types'; -import { useCallback, useMemo } from 'react'; -import type { ControlNetModelConfig, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types'; - -export const useAddControlAdapter = (type: ControlAdapterType) => { - const baseModel = useAppSelector((s) => s.generation.model?.base); - const dispatch = useAppDispatch(); - - const [models] = useControlAdapterModels(type); - - const firstModel: ControlNetModelConfig | T2IAdapterModelConfig | IPAdapterModelConfig | undefined = useMemo(() => { - // prefer to use a model that matches the base model - const firstCompatibleModel = models.filter((m) => (baseModel ? m.base === baseModel : true))[0]; - - if (firstCompatibleModel) { - return firstCompatibleModel; - } - - return models[0]; - }, [baseModel, models]); - - const isDisabled = useMemo(() => !firstModel, [firstModel]); - - const addControlAdapter = useCallback(() => { - if (isDisabled) { - return; - } - - if ( - (type === 'controlnet' || type === 't2i_adapter') && - (firstModel?.type === 'controlnet' || firstModel?.type === 't2i_adapter') - ) { - const defaultPreprocessor = firstModel.default_settings?.preprocessor; - const processorType = isControlAdapterProcessorType(defaultPreprocessor) ? defaultPreprocessor : 'none'; - const processorNode = CONTROLNET_PROCESSORS[processorType].buildDefaults(baseModel); - dispatch( - controlAdapterAdded({ - type, - overrides: { - model: firstModel, - processorType, - processorNode, - }, - }) - ); - return; - } - dispatch( - controlAdapterAdded({ - type, - overrides: { model: firstModel }, - }) - ); - }, [dispatch, firstModel, isDisabled, type, baseModel]); - - return [addControlAdapter, isDisabled] as const; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterBeginEndStepPct.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterBeginEndStepPct.ts deleted file mode 100644 index c9c99fcb2e4..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterBeginEndStepPct.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterById, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { useMemo } from 'react'; - -export const useControlAdapterBeginEndStepPct = (id: string) => { - const selector = useMemo( - () => - createMemoizedSelector(selectControlAdaptersSlice, (controlAdapters) => { - const cn = selectControlAdapterById(controlAdapters, id); - return cn - ? { - beginStepPct: cn.beginStepPct, - endStepPct: cn.endStepPct, - } - : undefined; - }), - [id] - ); - - const stepPcts = useAppSelector(selector); - - return stepPcts; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterCLIPVisionModel.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterCLIPVisionModel.ts deleted file mode 100644 index 249d2022fe8..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterCLIPVisionModel.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterById, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { useMemo } from 'react'; - -export const useControlAdapterCLIPVisionModel = (id: string) => { - const selector = useMemo( - () => - createMemoizedSelector(selectControlAdaptersSlice, (controlAdapters) => { - const cn = selectControlAdapterById(controlAdapters, id); - if (cn && cn?.type === 'ip_adapter') { - return cn.clipVisionModel; - } - }), - [id] - ); - - const clipVisionModel = useAppSelector(selector); - - return clipVisionModel; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterControlImage.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterControlImage.ts deleted file mode 100644 index c8efdf91253..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterControlImage.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterById, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { useMemo } from 'react'; - -export const useControlAdapterControlImage = (id: string) => { - const selector = useMemo( - () => - createSelector( - selectControlAdaptersSlice, - (controlAdapters) => selectControlAdapterById(controlAdapters, id)?.controlImage - ), - [id] - ); - - const controlImageName = useAppSelector(selector); - - return controlImageName; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterControlMode.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterControlMode.ts deleted file mode 100644 index 50ddd80156e..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterControlMode.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterById, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { isControlNet } from 'features/controlAdapters/store/types'; -import { useMemo } from 'react'; - -export const useControlAdapterControlMode = (id: string) => { - const selector = useMemo( - () => - createSelector(selectControlAdaptersSlice, (controlAdapters) => { - const ca = selectControlAdapterById(controlAdapters, id); - if (ca && isControlNet(ca)) { - return ca.controlMode; - } - return undefined; - }), - [id] - ); - - const controlMode = useAppSelector(selector); - - return controlMode; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterIPMethod.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterIPMethod.ts deleted file mode 100644 index a179899396d..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterIPMethod.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterById, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { useMemo } from 'react'; - -export const useControlAdapterIPMethod = (id: string) => { - const selector = useMemo( - () => - createMemoizedSelector(selectControlAdaptersSlice, (controlAdapters) => { - const cn = selectControlAdapterById(controlAdapters, id); - if (cn && cn?.type === 'ip_adapter') { - return cn.method; - } - }), - [id] - ); - - const method = useAppSelector(selector); - - return method; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterIsEnabled.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterIsEnabled.ts deleted file mode 100644 index 58bb956ce39..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterIsEnabled.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterById, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { useMemo } from 'react'; - -export const useControlAdapterIsEnabled = (id: string) => { - const selector = useMemo( - () => - createSelector( - selectControlAdaptersSlice, - (controlAdapters) => selectControlAdapterById(controlAdapters, id)?.isEnabled ?? false - ), - [id] - ); - - const isEnabled = useAppSelector(selector); - - return isEnabled; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterModel.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterModel.ts deleted file mode 100644 index 4de2aeac7fb..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterModel.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { skipToken } from '@reduxjs/toolkit/query'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterById, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { useMemo } from 'react'; -import { useGetModelConfigWithTypeGuard } from 'services/api/hooks/useGetModelConfigWithTypeGuard'; -import { isControlAdapterModelConfig } from 'services/api/types'; - -export const useControlAdapterModel = (id: string) => { - const selector = useMemo( - () => - createMemoizedSelector( - selectControlAdaptersSlice, - (controlAdapters) => selectControlAdapterById(controlAdapters, id)?.model?.key - ), - [id] - ); - - const key = useAppSelector(selector); - - const result = useGetModelConfigWithTypeGuard(key ?? skipToken, isControlAdapterModelConfig); - - return result; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterModels.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterModels.ts deleted file mode 100644 index 4fe5ae78112..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterModels.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { ControlAdapterType } from 'features/controlAdapters/store/types'; -import { useControlNetModels, useIPAdapterModels, useT2IAdapterModels } from 'services/api/hooks/modelsByType'; - -export const useControlAdapterModels = (type: ControlAdapterType) => { - const controlNetModels = useControlNetModels(); - const t2iAdapterModels = useT2IAdapterModels(); - const ipAdapterModels = useIPAdapterModels(); - - if (type === 'controlnet') { - return controlNetModels; - } - if (type === 't2i_adapter') { - return t2iAdapterModels; - } - if (type === 'ip_adapter') { - return ipAdapterModels; - } - - // Assert that the end of the function is not reachable. - const exhaustiveCheck: never = type; - return exhaustiveCheck; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterProcessedControlImage.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterProcessedControlImage.ts deleted file mode 100644 index a02e1f9ed94..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterProcessedControlImage.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterById, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; -import { useMemo } from 'react'; - -export const useControlAdapterProcessedControlImage = (id: string) => { - const selector = useMemo( - () => - createSelector(selectControlAdaptersSlice, (controlAdapters) => { - const ca = selectControlAdapterById(controlAdapters, id); - - return ca && isControlNetOrT2IAdapter(ca) ? ca.processedControlImage : undefined; - }), - [id] - ); - - const weight = useAppSelector(selector); - - return weight; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterProcessorNode.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterProcessorNode.ts deleted file mode 100644 index 272af132a68..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterProcessorNode.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterById, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; -import { useMemo } from 'react'; - -export const useControlAdapterProcessorNode = (id: string) => { - const selector = useMemo( - () => - createMemoizedSelector(selectControlAdaptersSlice, (controlAdapters) => { - const ca = selectControlAdapterById(controlAdapters, id); - - return ca && isControlNetOrT2IAdapter(ca) ? ca.processorNode : undefined; - }), - [id] - ); - - const processorNode = useAppSelector(selector); - - return processorNode; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterProcessorType.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterProcessorType.ts deleted file mode 100644 index 777bfc05b47..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterProcessorType.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterById, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; -import { useMemo } from 'react'; - -export const useControlAdapterProcessorType = (id: string) => { - const selector = useMemo( - () => - createSelector(selectControlAdaptersSlice, (controlAdapters) => { - const ca = selectControlAdapterById(controlAdapters, id); - - return ca && isControlNetOrT2IAdapter(ca) ? ca.processorType : undefined; - }), - [id] - ); - - const processorType = useAppSelector(selector); - - return processorType; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterResizeMode.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterResizeMode.ts deleted file mode 100644 index c6140bced74..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterResizeMode.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterById, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; -import { useMemo } from 'react'; - -export const useControlAdapterResizeMode = (id: string) => { - const selector = useMemo( - () => - createMemoizedSelector(selectControlAdaptersSlice, (controlAdapters) => { - const ca = selectControlAdapterById(controlAdapters, id); - if (ca && isControlNetOrT2IAdapter(ca)) { - return ca.resizeMode; - } - return undefined; - }), - [id] - ); - - const controlMode = useAppSelector(selector); - - return controlMode; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterShouldAutoConfig.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterShouldAutoConfig.ts deleted file mode 100644 index 07fbd2982ca..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterShouldAutoConfig.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterById, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; -import { useMemo } from 'react'; - -export const useControlAdapterShouldAutoConfig = (id: string) => { - const selector = useMemo( - () => - createSelector(selectControlAdaptersSlice, (controlAdapters) => { - const ca = selectControlAdapterById(controlAdapters, id); - if (ca && isControlNetOrT2IAdapter(ca)) { - return ca.shouldAutoConfig; - } - return undefined; - }), - [id] - ); - - const controlMode = useAppSelector(selector); - - return controlMode; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterType.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterType.ts deleted file mode 100644 index fe818f32875..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterType.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterById, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { useMemo } from 'react'; -import { assert } from 'tsafe'; - -export const useControlAdapterType = (id: string) => { - const selector = useMemo( - () => - createMemoizedSelector(selectControlAdaptersSlice, (controlAdapters) => { - const type = selectControlAdapterById(controlAdapters, id)?.type; - assert(type !== undefined, `Control adapter with id ${id} not found`); - return type; - }), - [id] - ); - - const type = useAppSelector(selector); - - return type; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterWeight.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterWeight.ts deleted file mode 100644 index 9e65993fde2..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterWeight.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterById, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { useMemo } from 'react'; - -export const useControlAdapterWeight = (id: string) => { - const selector = useMemo( - () => - createSelector( - selectControlAdaptersSlice, - (controlAdapters) => selectControlAdapterById(controlAdapters, id)?.weight - ), - [id] - ); - - const weight = useAppSelector(selector); - - return weight; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useGetDefaultForControlnetProcessor.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useGetDefaultForControlnetProcessor.ts deleted file mode 100644 index 99d2e0da8c3..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useGetDefaultForControlnetProcessor.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useAppSelector } from 'app/store/storeHooks'; -import { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants'; -import type { ControlAdapterProcessorType } from 'features/controlAdapters/store/types'; -import { useMemo } from 'react'; - -export const useGetDefaultForControlnetProcessor = (processorType: ControlAdapterProcessorType) => { - const baseModel = useAppSelector((s) => s.generation.model?.base); - - const defaults = useMemo(() => { - return CONTROLNET_PROCESSORS[processorType].buildDefaults(baseModel); - }, [baseModel, processorType]); - - return defaults; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/store/actions.ts b/invokeai/frontend/web/src/features/controlAdapters/store/actions.ts deleted file mode 100644 index 99ea84ed139..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/store/actions.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createAction } from '@reduxjs/toolkit'; - -export const controlAdapterImageProcessed = createAction<{ - id: string; -}>('controlAdapters/imageProcessed'); diff --git a/invokeai/frontend/web/src/features/controlAdapters/store/constants.ts b/invokeai/frontend/web/src/features/controlAdapters/store/constants.ts deleted file mode 100644 index 1e01e5627ec..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/store/constants.ts +++ /dev/null @@ -1,261 +0,0 @@ -import i18n from 'i18next'; -import type { BaseModelType } from 'services/api/types'; - -import type { ControlAdapterProcessorType, RequiredControlAdapterProcessorNode } from './types'; - -type ControlNetProcessorsDict = Record< - ControlAdapterProcessorType, - { - type: ControlAdapterProcessorType | 'none'; - label: string; - description: string; - buildDefaults(baseModel?: BaseModelType): RequiredControlAdapterProcessorNode | { type: 'none' }; - } ->; -/** - * A dict of ControlNet processors, including: - * - type - * - label - * - description - * - default values - * - * TODO: Generate from the OpenAPI schema - */ -export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = { - none: { - type: 'none', - get label() { - return i18n.t('controlnet.none'); - }, - get description() { - return i18n.t('controlnet.noneDescription'); - }, - buildDefaults: () => ({ - type: 'none', - }), - }, - canny_image_processor: { - type: 'canny_image_processor', - get label() { - return i18n.t('controlnet.canny'); - }, - get description() { - return i18n.t('controlnet.cannyDescription'); - }, - buildDefaults: (baseModel?: BaseModelType) => ({ - id: 'canny_image_processor', - type: 'canny_image_processor', - low_threshold: 100, - high_threshold: 200, - image_resolution: baseModel === 'sdxl' ? 1024 : 512, - detect_resolution: baseModel === 'sdxl' ? 1024 : 512, - }), - }, - color_map_image_processor: { - type: 'color_map_image_processor', - get label() { - return i18n.t('controlnet.colorMap'); - }, - get description() { - return i18n.t('controlnet.colorMapDescription'); - }, - buildDefaults: () => ({ - id: 'color_map_image_processor', - type: 'color_map_image_processor', - color_map_tile_size: 64, - }), - }, - content_shuffle_image_processor: { - type: 'content_shuffle_image_processor', - get label() { - return i18n.t('controlnet.contentShuffle'); - }, - get description() { - return i18n.t('controlnet.contentShuffleDescription'); - }, - buildDefaults: (baseModel?: BaseModelType) => ({ - id: 'content_shuffle_image_processor', - type: 'content_shuffle_image_processor', - detect_resolution: baseModel === 'sdxl' ? 1024 : 512, - image_resolution: baseModel === 'sdxl' ? 1024 : 512, - h: baseModel === 'sdxl' ? 1024 : 512, - w: baseModel === 'sdxl' ? 1024 : 512, - f: baseModel === 'sdxl' ? 512 : 256, - }), - }, - depth_anything_image_processor: { - type: 'depth_anything_image_processor', - get label() { - return i18n.t('controlnet.depthAnything'); - }, - get description() { - return i18n.t('controlnet.depthAnythingDescription'); - }, - buildDefaults: (baseModel?: BaseModelType) => ({ - id: 'depth_anything_image_processor', - type: 'depth_anything_image_processor', - model_size: 'small_v2', - resolution: baseModel === 'sdxl' ? 1024 : 512, - }), - }, - hed_image_processor: { - type: 'hed_image_processor', - get label() { - return i18n.t('controlnet.hed'); - }, - get description() { - return i18n.t('controlnet.hedDescription'); - }, - buildDefaults: (baseModel?: BaseModelType) => ({ - id: 'hed_image_processor', - type: 'hed_image_processor', - detect_resolution: baseModel === 'sdxl' ? 1024 : 512, - image_resolution: baseModel === 'sdxl' ? 1024 : 512, - scribble: false, - }), - }, - lineart_anime_image_processor: { - type: 'lineart_anime_image_processor', - get label() { - return i18n.t('controlnet.lineartAnime'); - }, - get description() { - return i18n.t('controlnet.lineartAnimeDescription'); - }, - buildDefaults: (baseModel?: BaseModelType) => ({ - id: 'lineart_anime_image_processor', - type: 'lineart_anime_image_processor', - detect_resolution: baseModel === 'sdxl' ? 1024 : 512, - image_resolution: baseModel === 'sdxl' ? 1024 : 512, - }), - }, - lineart_image_processor: { - type: 'lineart_image_processor', - get label() { - return i18n.t('controlnet.lineart'); - }, - get description() { - return i18n.t('controlnet.lineartDescription'); - }, - buildDefaults: (baseModel?: BaseModelType) => ({ - id: 'lineart_image_processor', - type: 'lineart_image_processor', - detect_resolution: baseModel === 'sdxl' ? 1024 : 512, - image_resolution: baseModel === 'sdxl' ? 1024 : 512, - coarse: false, - }), - }, - mediapipe_face_processor: { - type: 'mediapipe_face_processor', - get label() { - return i18n.t('controlnet.mediapipeFace'); - }, - get description() { - return i18n.t('controlnet.mediapipeFaceDescription'); - }, - buildDefaults: (baseModel?: BaseModelType) => ({ - id: 'mediapipe_face_processor', - type: 'mediapipe_face_processor', - max_faces: 1, - min_confidence: 0.5, - image_resolution: baseModel === 'sdxl' ? 1024 : 512, - detect_resolution: baseModel === 'sdxl' ? 1024 : 512, - }), - }, - midas_depth_image_processor: { - type: 'midas_depth_image_processor', - get label() { - return i18n.t('controlnet.depthMidas'); - }, - get description() { - return i18n.t('controlnet.depthMidasDescription'); - }, - buildDefaults: (baseModel?: BaseModelType) => ({ - id: 'midas_depth_image_processor', - type: 'midas_depth_image_processor', - a_mult: 2, - bg_th: 0.1, - image_resolution: baseModel === 'sdxl' ? 1024 : 512, - detect_resolution: baseModel === 'sdxl' ? 1024 : 512, - }), - }, - mlsd_image_processor: { - type: 'mlsd_image_processor', - get label() { - return i18n.t('controlnet.mlsd'); - }, - get description() { - return i18n.t('controlnet.mlsdDescription'); - }, - buildDefaults: (baseModel?: BaseModelType) => ({ - id: 'mlsd_image_processor', - type: 'mlsd_image_processor', - detect_resolution: baseModel === 'sdxl' ? 1024 : 512, - image_resolution: baseModel === 'sdxl' ? 1024 : 512, - thr_d: 0.1, - thr_v: 0.1, - }), - }, - normalbae_image_processor: { - type: 'normalbae_image_processor', - get label() { - return i18n.t('controlnet.normalBae'); - }, - get description() { - return i18n.t('controlnet.normalBaeDescription'); - }, - buildDefaults: (baseModel?: BaseModelType) => ({ - id: 'normalbae_image_processor', - type: 'normalbae_image_processor', - detect_resolution: baseModel === 'sdxl' ? 1024 : 512, - image_resolution: baseModel === 'sdxl' ? 1024 : 512, - }), - }, - dw_openpose_image_processor: { - type: 'dw_openpose_image_processor', - get label() { - return i18n.t('controlnet.dwOpenpose'); - }, - get description() { - return i18n.t('controlnet.dwOpenposeDescription'); - }, - buildDefaults: (baseModel?: BaseModelType) => ({ - id: 'dw_openpose_image_processor', - type: 'dw_openpose_image_processor', - image_resolution: baseModel === 'sdxl' ? 1024 : 512, - draw_body: true, - draw_face: false, - draw_hands: false, - }), - }, - pidi_image_processor: { - type: 'pidi_image_processor', - get label() { - return i18n.t('controlnet.pidi'); - }, - get description() { - return i18n.t('controlnet.pidiDescription'); - }, - buildDefaults: (baseModel?: BaseModelType) => ({ - id: 'pidi_image_processor', - type: 'pidi_image_processor', - detect_resolution: baseModel === 'sdxl' ? 1024 : 512, - image_resolution: baseModel === 'sdxl' ? 1024 : 512, - scribble: false, - safe: false, - }), - }, - zoe_depth_image_processor: { - type: 'zoe_depth_image_processor', - get label() { - return i18n.t('controlnet.depthZoe'); - }, - get description() { - return i18n.t('controlnet.depthZoeDescription'); - }, - buildDefaults: () => ({ - id: 'zoe_depth_image_processor', - type: 'zoe_depth_image_processor', - }), - }, -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/store/controlAdaptersSlice.ts b/invokeai/frontend/web/src/features/controlAdapters/store/controlAdaptersSlice.ts deleted file mode 100644 index 8ec397f99c9..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/store/controlAdaptersSlice.ts +++ /dev/null @@ -1,433 +0,0 @@ -import type { PayloadAction, Update } from '@reduxjs/toolkit'; -import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'; -import { getSelectorsOptions } from 'app/store/createMemoizedSelector'; -import type { PersistConfig, RootState } from 'app/store/store'; -import { deepClone } from 'common/util/deepClone'; -import { buildControlAdapter } from 'features/controlAdapters/util/buildControlAdapter'; -import { buildControlAdapterProcessor } from 'features/controlAdapters/util/buildControlAdapterProcessor'; -import { zModelIdentifierField } from 'features/nodes/types/common'; -import { merge, uniq } from 'lodash-es'; -import type { ControlNetModelConfig, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types'; -import { socketInvocationError } from 'services/events/actions'; -import { v4 as uuidv4 } from 'uuid'; - -import { controlAdapterImageProcessed } from './actions'; -import { CONTROLNET_PROCESSORS } from './constants'; -import type { - CLIPVisionModel, - ControlAdapterConfig, - ControlAdapterProcessorType, - ControlAdaptersState, - ControlAdapterType, - ControlMode, - ControlNetConfig, - IPMethod, - RequiredControlAdapterProcessorNode, - ResizeMode, - T2IAdapterConfig, -} from './types'; -import { isControlNet, isControlNetOrT2IAdapter, isIPAdapter, isT2IAdapter } from './types'; - -const caAdapter = createEntityAdapter({ - selectId: (ca) => ca.id, -}); -const caAdapterSelectors = caAdapter.getSelectors(undefined, getSelectorsOptions); - -export const { - selectById: selectControlAdapterById, - selectAll: selectControlAdapterAll, - selectIds: selectControlAdapterIds, -} = caAdapterSelectors; - -const initialControlAdaptersState: ControlAdaptersState = caAdapter.getInitialState<{ - _version: 2; - pendingControlImages: string[]; -}>({ - _version: 2, - pendingControlImages: [], -}); - -export const selectAllControlNets = (controlAdapters: ControlAdaptersState) => - selectControlAdapterAll(controlAdapters).filter(isControlNet); - -export const selectValidControlNets = (controlAdapters: ControlAdaptersState) => - selectControlAdapterAll(controlAdapters) - .filter(isControlNet) - .filter( - (ca) => - ca.isEnabled && - ca.model && - (Boolean(ca.processedControlImage) || (ca.processorType === 'none' && Boolean(ca.controlImage))) - ); - -export const selectAllIPAdapters = (controlAdapters: ControlAdaptersState) => - selectControlAdapterAll(controlAdapters).filter(isIPAdapter); - -export const selectValidIPAdapters = (controlAdapters: ControlAdaptersState) => - selectControlAdapterAll(controlAdapters) - .filter(isIPAdapter) - .filter((ca) => ca.isEnabled && ca.model && Boolean(ca.controlImage)); - -export const selectAllT2IAdapters = (controlAdapters: ControlAdaptersState) => - selectControlAdapterAll(controlAdapters).filter(isT2IAdapter); - -export const selectValidT2IAdapters = (controlAdapters: ControlAdaptersState) => - selectControlAdapterAll(controlAdapters) - .filter(isT2IAdapter) - .filter( - (ca) => - ca.isEnabled && - ca.model && - (Boolean(ca.processedControlImage) || (ca.processorType === 'none' && Boolean(ca.controlImage))) - ); - -export const controlAdaptersSlice = createSlice({ - name: 'controlAdapters', - initialState: initialControlAdaptersState, - reducers: { - controlAdapterAdded: { - reducer: ( - state, - action: PayloadAction<{ - id: string; - type: ControlAdapterType; - overrides?: Partial; - }> - ) => { - const { id, type, overrides } = action.payload; - caAdapter.addOne(state, buildControlAdapter(id, type, overrides)); - }, - prepare: ({ type, overrides }: { type: ControlAdapterType; overrides?: Partial }) => { - return { payload: { id: uuidv4(), type, overrides } }; - }, - }, - controlAdapterRecalled: (state, action: PayloadAction) => { - caAdapter.addOne(state, action.payload); - }, - controlAdapterDuplicated: { - reducer: ( - state, - action: PayloadAction<{ - id: string; - newId: string; - }> - ) => { - const { id, newId } = action.payload; - const controlAdapter = selectControlAdapterById(state, id); - if (!controlAdapter) { - return; - } - const newControlAdapter = merge(deepClone(controlAdapter), { - id: newId, - isEnabled: true, - }); - caAdapter.addOne(state, newControlAdapter); - }, - prepare: (id: string) => { - return { payload: { id, newId: uuidv4() } }; - }, - }, - controlAdapterRemoved: (state, action: PayloadAction<{ id: string }>) => { - caAdapter.removeOne(state, action.payload.id); - }, - controlAdapterIsEnabledChanged: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => { - const { id, isEnabled } = action.payload; - caAdapter.updateOne(state, { id, changes: { isEnabled } }); - }, - controlAdapterImageChanged: ( - state, - action: PayloadAction<{ - id: string; - controlImage: string | null; - }> - ) => { - const { id, controlImage } = action.payload; - const ca = selectControlAdapterById(state, id); - if (!ca) { - return; - } - - caAdapter.updateOne(state, { - id, - changes: { controlImage, processedControlImage: null }, - }); - - if (controlImage !== null && isControlNetOrT2IAdapter(ca) && ca.processorType !== 'none') { - state.pendingControlImages.push(id); - } - }, - controlAdapterProcessedImageChanged: ( - state, - action: PayloadAction<{ - id: string; - processedControlImage: string | null; - }> - ) => { - const { id, processedControlImage } = action.payload; - const cn = selectControlAdapterById(state, id); - if (!cn) { - return; - } - - if (!isControlNetOrT2IAdapter(cn)) { - return; - } - - caAdapter.updateOne(state, { - id, - changes: { - processedControlImage, - }, - }); - - state.pendingControlImages = state.pendingControlImages.filter((pendingId) => pendingId !== id); - }, - controlAdapterModelCleared: (state, action: PayloadAction<{ id: string }>) => { - caAdapter.updateOne(state, { - id: action.payload.id, - changes: { model: null }, - }); - }, - controlAdapterModelChanged: ( - state, - action: PayloadAction<{ - id: string; - modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | IPAdapterModelConfig; - }> - ) => { - const { id, modelConfig } = action.payload; - const cn = selectControlAdapterById(state, id); - if (!cn) { - return; - } - - const model = zModelIdentifierField.parse(modelConfig); - - if (!isControlNetOrT2IAdapter(cn)) { - caAdapter.updateOne(state, { id, changes: { model } }); - return; - } - - const update: Update = { - id, - changes: { model, shouldAutoConfig: true }, - }; - - update.changes.processedControlImage = null; - - if (modelConfig.type === 'ip_adapter') { - // should never happen... - return; - } - - const processor = buildControlAdapterProcessor(modelConfig); - update.changes.processorType = processor.processorType; - update.changes.processorNode = processor.processorNode; - - caAdapter.updateOne(state, update); - }, - controlAdapterWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { - const { id, weight } = action.payload; - caAdapter.updateOne(state, { id, changes: { weight } }); - }, - controlAdapterBeginStepPctChanged: (state, action: PayloadAction<{ id: string; beginStepPct: number }>) => { - const { id, beginStepPct } = action.payload; - caAdapter.updateOne(state, { id, changes: { beginStepPct } }); - }, - controlAdapterEndStepPctChanged: (state, action: PayloadAction<{ id: string; endStepPct: number }>) => { - const { id, endStepPct } = action.payload; - caAdapter.updateOne(state, { id, changes: { endStepPct } }); - }, - controlAdapterControlModeChanged: (state, action: PayloadAction<{ id: string; controlMode: ControlMode }>) => { - const { id, controlMode } = action.payload; - const cn = selectControlAdapterById(state, id); - if (!cn || !isControlNet(cn)) { - return; - } - caAdapter.updateOne(state, { id, changes: { controlMode } }); - }, - controlAdapterIPMethodChanged: (state, action: PayloadAction<{ id: string; method: IPMethod }>) => { - const { id, method } = action.payload; - caAdapter.updateOne(state, { id, changes: { method } }); - }, - controlAdapterCLIPVisionModelChanged: ( - state, - action: PayloadAction<{ id: string; clipVisionModel: CLIPVisionModel }> - ) => { - const { id, clipVisionModel } = action.payload; - caAdapter.updateOne(state, { id, changes: { clipVisionModel } }); - }, - controlAdapterResizeModeChanged: ( - state, - action: PayloadAction<{ - id: string; - resizeMode: ResizeMode; - }> - ) => { - const { id, resizeMode } = action.payload; - const cn = selectControlAdapterById(state, id); - if (!cn || !isControlNetOrT2IAdapter(cn)) { - return; - } - caAdapter.updateOne(state, { id, changes: { resizeMode } }); - }, - controlAdapterProcessorParamsChanged: ( - state, - action: PayloadAction<{ - id: string; - params: Partial; - }> - ) => { - const { id, params } = action.payload; - const cn = selectControlAdapterById(state, id); - if (!cn || !isControlNetOrT2IAdapter(cn) || !cn.processorNode) { - return; - } - - const processorNode = merge(deepClone(cn.processorNode), params); - - caAdapter.updateOne(state, { - id, - changes: { - shouldAutoConfig: false, - processorNode, - }, - }); - }, - controlAdapterProcessortTypeChanged: ( - state, - action: PayloadAction<{ - id: string; - processorType: ControlAdapterProcessorType; - }> - ) => { - const { id, processorType } = action.payload; - const cn = selectControlAdapterById(state, id); - if (!cn || !isControlNetOrT2IAdapter(cn)) { - return; - } - - const processorNode = deepClone( - CONTROLNET_PROCESSORS[processorType].buildDefaults(cn.model?.base) - ) as RequiredControlAdapterProcessorNode; - - caAdapter.updateOne(state, { - id, - changes: { - processorType, - processedControlImage: null, - processorNode, - shouldAutoConfig: false, - }, - }); - }, - controlAdapterAutoConfigToggled: ( - state, - action: PayloadAction<{ - id: string; - modelConfig?: ControlNetModelConfig | T2IAdapterModelConfig | IPAdapterModelConfig; - }> - ) => { - const { id, modelConfig } = action.payload; - const cn = selectControlAdapterById(state, id); - if (!cn || !isControlNetOrT2IAdapter(cn) || modelConfig?.type === 'ip_adapter') { - return; - } - const update: Update = { - id, - changes: { shouldAutoConfig: !cn.shouldAutoConfig }, - }; - - if (update.changes.shouldAutoConfig && modelConfig) { - const processor = buildControlAdapterProcessor(modelConfig); - update.changes.processorType = processor.processorType; - update.changes.processorNode = processor.processorNode; - } - - caAdapter.updateOne(state, update); - }, - controlAdaptersReset: () => { - return deepClone(initialControlAdaptersState); - }, - pendingControlImagesCleared: (state) => { - state.pendingControlImages = []; - }, - ipAdaptersReset: (state) => { - selectAllIPAdapters(state).forEach((ca) => { - caAdapter.removeOne(state, ca.id); - }); - }, - controlNetsReset: (state) => { - selectAllControlNets(state).forEach((ca) => { - caAdapter.removeOne(state, ca.id); - }); - }, - t2iAdaptersReset: (state) => { - selectAllT2IAdapters(state).forEach((ca) => { - caAdapter.removeOne(state, ca.id); - }); - }, - }, - extraReducers: (builder) => { - builder.addCase(controlAdapterImageProcessed, (state, action) => { - const cn = selectControlAdapterById(state, action.payload.id); - if (!cn) { - return; - } - if (cn.controlImage !== null) { - state.pendingControlImages = uniq(state.pendingControlImages.concat(action.payload.id)); - } - }); - - builder.addCase(socketInvocationError, (state) => { - state.pendingControlImages = []; - }); - }, -}); - -export const { - controlAdapterAdded, - controlAdapterRecalled, - controlAdapterDuplicated, - controlAdapterRemoved, - controlAdapterImageChanged, - controlAdapterProcessedImageChanged, - controlAdapterIsEnabledChanged, - controlAdapterModelChanged, - controlAdapterCLIPVisionModelChanged, - controlAdapterIPMethodChanged, - controlAdapterWeightChanged, - controlAdapterBeginStepPctChanged, - controlAdapterEndStepPctChanged, - controlAdapterControlModeChanged, - controlAdapterResizeModeChanged, - controlAdapterProcessorParamsChanged, - controlAdapterProcessortTypeChanged, - controlAdaptersReset, - controlAdapterAutoConfigToggled, - pendingControlImagesCleared, - controlAdapterModelCleared, - ipAdaptersReset, - controlNetsReset, - t2iAdaptersReset, -} = controlAdaptersSlice.actions; - -export const selectControlAdaptersSlice = (state: RootState) => state.controlAdapters; - -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrateControlAdaptersState = (state: any): any => { - if (!('_version' in state)) { - state._version = 1; - } - if (state._version === 1) { - state = deepClone(initialControlAdaptersState); - } - return state; -}; - -export const controlAdaptersPersistConfig: PersistConfig = { - name: controlAdaptersSlice.name, - initialState: initialControlAdaptersState, - migrate: migrateControlAdaptersState, - persistDenylist: ['pendingControlImages'], -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/store/types.test.ts b/invokeai/frontend/web/src/features/controlAdapters/store/types.test.ts deleted file mode 100644 index 3bde8bc6c6f..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/store/types.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { ControlAdapterProcessorType, zControlAdapterProcessorType } from 'features/controlAdapters/store/types'; -import type { Equals } from 'tsafe'; -import { assert } from 'tsafe'; -import { describe, test } from 'vitest'; -import type { z } from 'zod'; - -describe('Control Adapter Types', () => { - test('ControlAdapterProcessorType', () => - assert>>()); -}); diff --git a/invokeai/frontend/web/src/features/controlAdapters/store/types.ts b/invokeai/frontend/web/src/features/controlAdapters/store/types.ts deleted file mode 100644 index cceb3d594ea..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/store/types.ts +++ /dev/null @@ -1,274 +0,0 @@ -import type { EntityState } from '@reduxjs/toolkit'; -import type { - ParameterControlNetModel, - ParameterIPAdapterModel, - ParameterT2IAdapterModel, -} from 'features/parameters/types/parameterSchemas'; -import type { components } from 'services/api/schema'; -import type { Invocation } from 'services/api/types'; -import type { O } from 'ts-toolbelt'; -import { z } from 'zod'; - -/** - * Any ControlNet processor node - */ -export type ControlAdapterProcessorNode = - | Invocation<'canny_image_processor'> - | Invocation<'color_map_image_processor'> - | Invocation<'content_shuffle_image_processor'> - | Invocation<'depth_anything_image_processor'> - | Invocation<'hed_image_processor'> - | Invocation<'lineart_anime_image_processor'> - | Invocation<'lineart_image_processor'> - | Invocation<'mediapipe_face_processor'> - | Invocation<'midas_depth_image_processor'> - | Invocation<'mlsd_image_processor'> - | Invocation<'normalbae_image_processor'> - | Invocation<'dw_openpose_image_processor'> - | Invocation<'pidi_image_processor'> - | Invocation<'zoe_depth_image_processor'>; - -/** - * Any ControlNet processor type - */ -export type ControlAdapterProcessorType = NonNullable; -export const zControlAdapterProcessorType = z.enum([ - 'canny_image_processor', - 'color_map_image_processor', - 'content_shuffle_image_processor', - 'depth_anything_image_processor', - 'hed_image_processor', - 'lineart_anime_image_processor', - 'lineart_image_processor', - 'mediapipe_face_processor', - 'midas_depth_image_processor', - 'mlsd_image_processor', - 'normalbae_image_processor', - 'dw_openpose_image_processor', - 'pidi_image_processor', - 'zoe_depth_image_processor', - 'none', -]); -export const isControlAdapterProcessorType = (v: unknown): v is ControlAdapterProcessorType => - zControlAdapterProcessorType.safeParse(v).success; - -/** - * The Canny processor node, with parameters flagged as required - */ -export type RequiredCannyImageProcessorInvocation = O.Required< - Invocation<'canny_image_processor'>, - 'type' | 'low_threshold' | 'high_threshold' | 'image_resolution' | 'detect_resolution' ->; - -/** - * The Color Map processor node, with parameters flagged as required - */ -export type RequiredColorMapImageProcessorInvocation = O.Required< - Invocation<'color_map_image_processor'>, - 'type' | 'color_map_tile_size' ->; - -/** - * The ContentShuffle processor node, with parameters flagged as required - */ -export type RequiredContentShuffleImageProcessorInvocation = O.Required< - Invocation<'content_shuffle_image_processor'>, - 'type' | 'detect_resolution' | 'image_resolution' | 'w' | 'h' | 'f' ->; - -/** - * The DepthAnything processor node, with parameters flagged as required - */ -export type RequiredDepthAnythingImageProcessorInvocation = O.Required< - Invocation<'depth_anything_image_processor'>, - 'type' | 'model_size' | 'resolution' | 'offload' ->; - -const zDepthAnythingModelSize = z.enum(['large', 'base', 'small', 'small_v2']); -export type DepthAnythingModelSize = z.infer; -export const isDepthAnythingModelSize = (v: unknown): v is DepthAnythingModelSize => - zDepthAnythingModelSize.safeParse(v).success; - -/** - * The HED processor node, with parameters flagged as required - */ -export type RequiredHedImageProcessorInvocation = O.Required< - Invocation<'hed_image_processor'>, - 'type' | 'detect_resolution' | 'image_resolution' | 'scribble' ->; - -/** - * The Lineart Anime processor node, with parameters flagged as required - */ -export type RequiredLineartAnimeImageProcessorInvocation = O.Required< - Invocation<'lineart_anime_image_processor'>, - 'type' | 'detect_resolution' | 'image_resolution' ->; - -/** - * The Lineart processor node, with parameters flagged as required - */ -export type RequiredLineartImageProcessorInvocation = O.Required< - Invocation<'lineart_image_processor'>, - 'type' | 'detect_resolution' | 'image_resolution' | 'coarse' ->; - -/** - * The MediapipeFace processor node, with parameters flagged as required - */ -export type RequiredMediapipeFaceProcessorInvocation = O.Required< - Invocation<'mediapipe_face_processor'>, - 'type' | 'max_faces' | 'min_confidence' | 'image_resolution' | 'detect_resolution' ->; - -/** - * The MidasDepth processor node, with parameters flagged as required - */ -export type RequiredMidasDepthImageProcessorInvocation = O.Required< - Invocation<'midas_depth_image_processor'>, - 'type' | 'a_mult' | 'bg_th' | 'image_resolution' | 'detect_resolution' ->; - -/** - * The MLSD processor node, with parameters flagged as required - */ -export type RequiredMlsdImageProcessorInvocation = O.Required< - Invocation<'mlsd_image_processor'>, - 'type' | 'detect_resolution' | 'image_resolution' | 'thr_v' | 'thr_d' ->; - -/** - * The NormalBae processor node, with parameters flagged as required - */ -export type RequiredNormalbaeImageProcessorInvocation = O.Required< - Invocation<'normalbae_image_processor'>, - 'type' | 'detect_resolution' | 'image_resolution' ->; - -/** - * The DW Openpose processor node, with parameters flagged as required - */ -export type RequiredDWOpenposeImageProcessorInvocation = O.Required< - Invocation<'dw_openpose_image_processor'>, - 'type' | 'image_resolution' | 'draw_body' | 'draw_face' | 'draw_hands' ->; - -/** - * The Pidi processor node, with parameters flagged as required - */ -export type RequiredPidiImageProcessorInvocation = O.Required< - Invocation<'pidi_image_processor'>, - 'type' | 'detect_resolution' | 'image_resolution' | 'safe' | 'scribble' ->; - -/** - * The ZoeDepth processor node, with parameters flagged as required - */ -export type RequiredZoeDepthImageProcessorInvocation = O.Required, 'type'>; - -/** - * Any ControlNet Processor node, with its parameters flagged as required - */ -export type RequiredControlAdapterProcessorNode = - | O.Required< - | RequiredCannyImageProcessorInvocation - | RequiredColorMapImageProcessorInvocation - | RequiredContentShuffleImageProcessorInvocation - | RequiredDepthAnythingImageProcessorInvocation - | RequiredHedImageProcessorInvocation - | RequiredLineartAnimeImageProcessorInvocation - | RequiredLineartImageProcessorInvocation - | RequiredMediapipeFaceProcessorInvocation - | RequiredMidasDepthImageProcessorInvocation - | RequiredMlsdImageProcessorInvocation - | RequiredNormalbaeImageProcessorInvocation - | RequiredDWOpenposeImageProcessorInvocation - | RequiredPidiImageProcessorInvocation - | RequiredZoeDepthImageProcessorInvocation, - 'id' - > - | { type: 'none' }; - -export type ControlMode = NonNullable; - -const zResizeMode = z.enum(['just_resize', 'crop_resize', 'fill_resize', 'just_resize_simple']); -export type ResizeMode = z.infer; -export const isResizeMode = (v: unknown): v is ResizeMode => zResizeMode.safeParse(v).success; - -const zIPMethod = z.enum(['full', 'style', 'composition']); -export type IPMethod = z.infer; -export const isIPMethod = (v: unknown): v is IPMethod => zIPMethod.safeParse(v).success; - -export type ControlNetConfig = { - type: 'controlnet'; - id: string; - isEnabled: boolean; - model: ParameterControlNetModel | null; - weight: number; - beginStepPct: number; - endStepPct: number; - controlMode: ControlMode; - resizeMode: ResizeMode; - controlImage: string | null; - processedControlImage: string | null; - processorType: ControlAdapterProcessorType; - processorNode: RequiredControlAdapterProcessorNode; - shouldAutoConfig: boolean; -}; - -export type T2IAdapterConfig = { - type: 't2i_adapter'; - id: string; - isEnabled: boolean; - model: ParameterT2IAdapterModel | null; - weight: number; - beginStepPct: number; - endStepPct: number; - resizeMode: ResizeMode; - controlImage: string | null; - processedControlImage: string | null; - processorType: ControlAdapterProcessorType; - processorNode: RequiredControlAdapterProcessorNode; - shouldAutoConfig: boolean; -}; - -export type CLIPVisionModel = 'ViT-H' | 'ViT-G'; - -export type IPAdapterConfig = { - type: 'ip_adapter'; - id: string; - isEnabled: boolean; - controlImage: string | null; - model: ParameterIPAdapterModel | null; - clipVisionModel: CLIPVisionModel; - weight: number; - method: IPMethod; - beginStepPct: number; - endStepPct: number; -}; - -export type ControlAdapterConfig = ControlNetConfig | IPAdapterConfig | T2IAdapterConfig; - -export type ControlAdapterType = ControlAdapterConfig['type']; - -export type ControlAdaptersState = EntityState & { - pendingControlImages: string[]; -}; - -export const isControlNet = (controlAdapter: ControlAdapterConfig): controlAdapter is ControlNetConfig => { - return controlAdapter.type === 'controlnet'; -}; - -export const isIPAdapter = (controlAdapter: ControlAdapterConfig): controlAdapter is IPAdapterConfig => { - return controlAdapter.type === 'ip_adapter'; -}; - -export const isT2IAdapter = (controlAdapter: ControlAdapterConfig): controlAdapter is T2IAdapterConfig => { - return controlAdapter.type === 't2i_adapter'; -}; - -export const isControlNetOrT2IAdapter = ( - controlAdapter: ControlAdapterConfig -): controlAdapter is ControlNetConfig | T2IAdapterConfig => { - return isControlNet(controlAdapter) || isT2IAdapter(controlAdapter); -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/util/buildControlAdapter.ts b/invokeai/frontend/web/src/features/controlAdapters/util/buildControlAdapter.ts deleted file mode 100644 index ad7bdba3639..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/util/buildControlAdapter.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { deepClone } from 'common/util/deepClone'; -import { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants'; -import type { - ControlAdapterConfig, - ControlAdapterType, - ControlNetConfig, - IPAdapterConfig, - RequiredCannyImageProcessorInvocation, - T2IAdapterConfig, -} from 'features/controlAdapters/store/types'; -import { merge } from 'lodash-es'; - -export const initialControlNet: Omit = { - type: 'controlnet', - isEnabled: true, - model: null, - weight: 1, - beginStepPct: 0, - endStepPct: 1, - controlMode: 'balanced', - resizeMode: 'just_resize', - controlImage: null, - processedControlImage: null, - processorType: 'canny_image_processor', - processorNode: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults() as RequiredCannyImageProcessorInvocation, - shouldAutoConfig: true, -}; - -export const initialT2IAdapter: Omit = { - type: 't2i_adapter', - isEnabled: true, - model: null, - weight: 1, - beginStepPct: 0, - endStepPct: 1, - resizeMode: 'just_resize', - controlImage: null, - processedControlImage: null, - processorType: 'canny_image_processor', - processorNode: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults() as RequiredCannyImageProcessorInvocation, - shouldAutoConfig: true, -}; - -export const initialIPAdapter: Omit = { - type: 'ip_adapter', - isEnabled: true, - controlImage: null, - model: null, - method: 'full', - clipVisionModel: 'ViT-H', - weight: 1, - beginStepPct: 0, - endStepPct: 1, -}; - -export const buildControlAdapter = ( - id: string, - type: ControlAdapterType, - overrides: Partial = {} -): ControlAdapterConfig => { - switch (type) { - case 'controlnet': - return merge(deepClone(initialControlNet), { id, ...overrides }); - case 't2i_adapter': - return merge(deepClone(initialT2IAdapter), { id, ...overrides }); - case 'ip_adapter': - return merge(deepClone(initialIPAdapter), { id, ...overrides }); - default: - throw new Error(`Unknown control adapter type: ${type}`); - } -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/util/buildControlAdapterProcessor.ts b/invokeai/frontend/web/src/features/controlAdapters/util/buildControlAdapterProcessor.ts deleted file mode 100644 index 63766b8e6ea..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/util/buildControlAdapterProcessor.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants'; -import { isControlAdapterProcessorType } from 'features/controlAdapters/store/types'; -import type { ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types'; - -export const buildControlAdapterProcessor = (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => { - const defaultPreprocessor = modelConfig.default_settings?.preprocessor; - const processorType = isControlAdapterProcessorType(defaultPreprocessor) ? defaultPreprocessor : 'none'; - const processorNode = CONTROLNET_PROCESSORS[processorType].buildDefaults(modelConfig.base); - - return { processorType, processorNode }; -}; From 9b8db063490ddb6ab3ba5b65dfd1d68114a02e5b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 15 Jun 2024 22:50:31 +1000 Subject: [PATCH 044/678] refactor(ui): update size/prompts state --- .../listeners/modelsLoaded.ts | 6 +- .../src/common/hooks/useIsReadyToEnqueue.ts | 41 +++------- .../components/StageComponent.tsx | 74 +++++++++---------- .../controlLayers/store/canvasV2Slice.ts | 55 ++++---------- .../src/features/controlLayers/store/types.ts | 13 +--- .../util/graph/generation/addControlLayers.ts | 4 +- .../nodes/util/graph/generation/addHRF.ts | 2 +- .../generation/buildGenerationTabGraph.ts | 2 +- .../generation/buildGenerationTabSDXLGraph.ts | 2 +- .../nodes/util/graph/graphBuilderUtils.ts | 2 +- .../components/Core/ParamNegativePrompt.tsx | 4 +- .../components/Core/ParamPositivePrompt.tsx | 4 +- .../parameters/store/generationSlice.ts | 37 ++++++++-- .../src/features/parameters/store/types.ts | 9 +++ .../ParamSDXLNegativeStylePrompt.tsx | 4 +- .../ParamSDXLPositiveStylePrompt.tsx | 4 +- .../SDXLPrompts/SDXLConcatButton.tsx | 4 +- .../ImageSettingsAccordion.tsx | 2 +- .../ImageSizeLinear.tsx | 6 +- 19 files changed, 124 insertions(+), 151 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts index 5645567613d..9238c5ddb14 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts @@ -79,15 +79,15 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => { const optimalDimension = getOptimalDimension(defaultModelInList); if ( getIsSizeOptimal( - state.canvasV2.size.width, - state.canvasV2.size.height, + state.canvasV2.document.width, + state.canvasV2.document.height, optimalDimension ) ) { return; } const { width, height } = calculateNewSize( - state.canvasV2.size.aspectRatio.value, + state.canvasV2.document.aspectRatio.value, optimalDimension * optimalDimension ); diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index 2f2847f058c..d61e3167456 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -1,11 +1,7 @@ import { useStore } from '@nanostores/react'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectControlAdaptersV2Slice } from 'features/controlLayers/store/controlAdaptersSlice'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; -import { selectIPAdaptersSlice } from 'features/controlLayers/store/ipAdaptersSlice'; -import { selectLayersSlice } from 'features/controlLayers/store/layersSlice'; -import { selectRegionalGuidanceSlice } from 'features/controlLayers/store/regionalGuidanceSlice'; import type { CanvasEntity } from 'features/controlLayers/store/types'; import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; @@ -40,32 +36,13 @@ const createSelector = (templates: Templates) => selectWorkflowSettingsSlice, selectDynamicPromptsSlice, selectCanvasV2Slice, - selectLayersSlice, - selectControlAdaptersV2Slice, - selectRegionalGuidanceSlice, - selectIPAdaptersSlice, - activeTabNameSelector, selectUpscalelice, selectConfigSlice, + activeTabNameSelector, ], - ( - generation, - system, - nodes, - workflowSettings, - dynamicPrompts, - canvasV2, - layersState, - controlAdaptersState, - regionalGuidanceState, - ipAdaptersState, - activeTabName, - upscale, - config - ) => { - const { model } = generation; - const { size } = canvasV2; - const { positivePrompt } = canvasV2.prompts; + (generation, system, nodes, workflowSettings, dynamicPrompts, canvasV2, upscale, config, activeTabName) => { + const { model, positivePrompt } = generation; + const { bbox } = canvasV2; const { isConnected } = system; @@ -149,7 +126,7 @@ const createSelector = (templates: Templates) => reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') }); } - controlAdaptersState.controlAdapters + canvasV2.controlAdapters .filter((ca) => ca.isEnabled) .forEach((ca, i) => { const layerLiteral = i18n.t('controlLayers.layers_one'); @@ -174,7 +151,7 @@ const createSelector = (templates: Templates) => // T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL) if (!ca.controlMode) { const multiple = model?.base === 'sdxl' ? 32 : 64; - if (size.width % multiple !== 0 || size.height % multiple !== 0) { + if (bbox.width % multiple !== 0 || bbox.height % multiple !== 0) { problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions', { multiple })); } } @@ -185,7 +162,7 @@ const createSelector = (templates: Templates) => } }); - ipAdaptersState.ipAdapters + canvasV2.ipAdapters .filter((ipa) => ipa.isEnabled) .forEach((ipa, i) => { const layerLiteral = i18n.t('controlLayers.layers_one'); @@ -213,7 +190,7 @@ const createSelector = (templates: Templates) => } }); - regionalGuidanceState.regions + canvasV2.regions .filter((rg) => rg.isEnabled) .forEach((rg, i) => { const layerLiteral = i18n.t('controlLayers.layers_one'); @@ -250,7 +227,7 @@ const createSelector = (templates: Templates) => } }); - layersState.layers + canvasV2.layers .filter((l) => l.isEnabled) .forEach((l, i) => { const layerLiteral = i18n.t('controlLayers.layers_one'); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index 3dd7b1df35a..055070717d3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -35,7 +35,6 @@ import { rgLinePointAdded, rgRectAdded, rgTranslated, - selectCanvasV2Slice, toolBufferChanged, toolChanged, } from 'features/controlLayers/store/canvasV2Slice'; @@ -65,50 +64,51 @@ const log = logger('controlLayers'); const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, asPreview: boolean) => { const dispatch = useAppDispatch(); - const canvasV2State = useAppSelector(selectCanvasV2Slice); + const controlAdapters = useAppSelector((s) => s.canvasV2.controlAdapters); + const ipAdapters = useAppSelector((s) => s.canvasV2.ipAdapters); + const layers = useAppSelector((s) => s.canvasV2.layers); + const regions = useAppSelector((s) => s.canvasV2.regions); + const tool = useAppSelector((s) => s.canvasV2.tool); + const selectedEntityIdentifier = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier); + const maskFillOpacity = useAppSelector((s) => s.canvasV2.maskFillOpacity); + const bbox = useAppSelector((s) => s.canvasV2.bbox); const lastCursorPos = useStore($lastCursorPos); const lastMouseDownPos = useStore($lastMouseDownPos); const isMouseDown = useStore($isMouseDown); const isDrawing = useStore($isDrawing); const selectedEntity = useMemo(() => { - const identifier = canvasV2State.selectedEntityIdentifier; + const identifier = selectedEntityIdentifier; if (!identifier) { return null; } else if (identifier.type === 'layer') { - return canvasV2State.layers.find((i) => i.id === identifier.id) ?? null; + return layers.find((i) => i.id === identifier.id) ?? null; } else if (identifier.type === 'control_adapter') { - return canvasV2State.controlAdapters.find((i) => i.id === identifier.id) ?? null; + return controlAdapters.find((i) => i.id === identifier.id) ?? null; } else if (identifier.type === 'ip_adapter') { - return canvasV2State.ipAdapters.find((i) => i.id === identifier.id) ?? null; + return ipAdapters.find((i) => i.id === identifier.id) ?? null; } else if (identifier.type === 'regional_guidance') { - return canvasV2State.regions.find((i) => i.id === identifier.id) ?? null; + return regions.find((i) => i.id === identifier.id) ?? null; } else { return null; } - }, [ - canvasV2State.controlAdapters, - canvasV2State.ipAdapters, - canvasV2State.layers, - canvasV2State.regions, - canvasV2State.selectedEntityIdentifier, - ]); + }, [controlAdapters, ipAdapters, layers, regions, selectedEntityIdentifier]); const currentFill = useMemo(() => { if (selectedEntity && selectedEntity.type === 'regional_guidance') { - return { ...selectedEntity.fill, a: canvasV2State.maskFillOpacity }; + return { ...selectedEntity.fill, a: maskFillOpacity }; } - return canvasV2State.tool.fill; - }, [canvasV2State.maskFillOpacity, canvasV2State.tool.fill, selectedEntity]); + return tool.fill; + }, [maskFillOpacity, selectedEntity, tool.fill]); const renderers = useMemo(() => (asPreview ? debouncedRenderers : normalRenderers), [asPreview]); const dpr = useDevicePixelRatio({ round: false }); useLayoutEffect(() => { - $toolState.set(canvasV2State.tool); + $toolState.set(tool); $selectedEntity.set(selectedEntity); - $bbox.set(canvasV2State.bbox); + $bbox.set(bbox); $currentFill.set(currentFill); - }, [selectedEntity, canvasV2State.tool, canvasV2State.bbox, currentFill]); + }, [selectedEntity, tool, bbox, currentFill]); const onPosChanged = useCallback( (arg: PosChangedArg, entityType: CanvasEntity['type']) => { @@ -305,7 +305,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, log.trace('Rendering tool preview'); renderers.renderToolPreview( stage, - canvasV2State.tool, + tool, currentFill, selectedEntity, lastCursorPos, @@ -315,7 +315,6 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, ); }, [ asPreview, - canvasV2State.tool, currentFill, isDrawing, isMouseDown, @@ -324,6 +323,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, renderers, selectedEntity, stage, + tool, ]); useLayoutEffect(() => { @@ -334,8 +334,8 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, log.trace('Rendering bbox preview'); renderers.renderBboxPreview( stage, - canvasV2State.bbox, - canvasV2State.tool.selected, + bbox, + tool.selected, $bbox.get, onBboxTransformed, $shift.get, @@ -343,31 +343,31 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, $meta.get, $alt.get ); - }, [asPreview, canvasV2State.bbox, canvasV2State.tool.selected, onBboxTransformed, renderers, stage]); + }, [asPreview, bbox, onBboxTransformed, renderers, stage, tool.selected]); useLayoutEffect(() => { log.trace('Rendering layers'); renderers.renderLayers( stage, - canvasV2State.layers, - canvasV2State.controlAdapters, - canvasV2State.regions, - canvasV2State.maskFillOpacity, - canvasV2State.tool.selected, + layers, + controlAdapters, + regions, + maskFillOpacity, + tool.selected, selectedEntity, getImageDTO, onPosChanged ); }, [ - stage, - renderers, + controlAdapters, + layers, + maskFillOpacity, onPosChanged, - canvasV2State.tool.selected, + regions, + renderers, selectedEntity, - canvasV2State.layers, - canvasV2State.controlAdapters, - canvasV2State.regions, - canvasV2State.maskFillOpacity, + stage, + tool.selected, ]); // useLayoutEffect(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 206ac47da6d..3cf24a0fa5e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -21,13 +21,6 @@ import { DEFAULT_RGBA_COLOR } from './types'; const initialState: CanvasV2State = { _version: 3, selectedEntityIdentifier: null, - prompts: { - positivePrompt: '', - negativePrompt: '', - positivePrompt2: '', - negativePrompt2: '', - shouldConcatPrompts: true, - }, tool: { selected: 'bbox', selectedBuffer: null, @@ -40,7 +33,7 @@ const initialState: CanvasV2State = { width: 50, }, }, - size: { + document: { width: 512, height: 512, aspectRatio: deepClone(initialAspectRatioState), @@ -66,41 +59,26 @@ export const canvasV2Slice = createSlice({ ...ipAdaptersReducers, ...controlAdaptersReducers, ...regionsReducers, - positivePromptChanged: (state, action: PayloadAction) => { - state.prompts.positivePrompt = action.payload; - }, - negativePromptChanged: (state, action: PayloadAction) => { - state.prompts.negativePrompt = action.payload; - }, - positivePrompt2Changed: (state, action: PayloadAction) => { - state.prompts.positivePrompt2 = action.payload; - }, - negativePrompt2Changed: (state, action: PayloadAction) => { - state.prompts.negativePrompt2 = action.payload; - }, - shouldConcatPromptsChanged: (state, action: PayloadAction) => { - state.prompts.shouldConcatPrompts = action.payload; - }, widthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }>) => { const { width, updateAspectRatio, clamp } = action.payload; - state.size.width = clamp ? Math.max(roundDownToMultiple(width, 8), 64) : width; + state.document.width = clamp ? Math.max(roundDownToMultiple(width, 8), 64) : width; if (updateAspectRatio) { - state.size.aspectRatio.value = state.size.width / state.size.height; - state.size.aspectRatio.id = 'Free'; - state.size.aspectRatio.isLocked = false; + state.document.aspectRatio.value = state.document.width / state.document.height; + state.document.aspectRatio.id = 'Free'; + state.document.aspectRatio.isLocked = false; } }, heightChanged: (state, action: PayloadAction<{ height: number; updateAspectRatio?: boolean; clamp?: boolean }>) => { const { height, updateAspectRatio, clamp } = action.payload; - state.size.height = clamp ? Math.max(roundDownToMultiple(height, 8), 64) : height; + state.document.height = clamp ? Math.max(roundDownToMultiple(height, 8), 64) : height; if (updateAspectRatio) { - state.size.aspectRatio.value = state.size.width / state.size.height; - state.size.aspectRatio.id = 'Free'; - state.size.aspectRatio.isLocked = false; + state.document.aspectRatio.value = state.document.width / state.document.height; + state.document.aspectRatio.id = 'Free'; + state.document.aspectRatio.isLocked = false; } }, aspectRatioChanged: (state, action: PayloadAction) => { - state.size.aspectRatio = action.payload; + state.document.aspectRatio = action.payload; }, bboxChanged: (state, action: PayloadAction) => { state.bbox = action.payload; @@ -144,22 +122,17 @@ export const canvasV2Slice = createSlice({ return; } const optimalDimension = getOptimalDimension(newModel); - if (getIsSizeOptimal(state.size.width, state.size.height, optimalDimension)) { + if (getIsSizeOptimal(state.document.width, state.document.height, optimalDimension)) { return; } - const { width, height } = calculateNewSize(state.size.aspectRatio.value, optimalDimension * optimalDimension); - state.size.width = width; - state.size.height = height; + const { width, height } = calculateNewSize(state.document.aspectRatio.value, optimalDimension * optimalDimension); + state.document.width = width; + state.document.height = height; }); }, }); export const { - positivePromptChanged, - negativePromptChanged, - positivePrompt2Changed, - negativePrompt2Changed, - shouldConcatPromptsChanged, widthChanged, heightChanged, aspectRatioChanged, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index da1610559b9..a38b6500ec5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -3,10 +3,6 @@ import { zModelIdentifierField } from 'features/nodes/types/common'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; import type { ParameterHeight, - ParameterNegativePrompt, - ParameterNegativeStylePromptSDXL, - ParameterPositivePrompt, - ParameterPositiveStylePromptSDXL, ParameterWidth, } from 'features/parameters/types/parameterSchemas'; import { @@ -758,13 +754,6 @@ export type CanvasEntityIdentifier = Pick; export type CanvasV2State = { _version: 3; selectedEntityIdentifier: CanvasEntityIdentifier | null; - prompts: { - positivePrompt: ParameterPositivePrompt; - negativePrompt: ParameterNegativePrompt; - positivePrompt2: ParameterPositiveStylePromptSDXL; - negativePrompt2: ParameterNegativeStylePromptSDXL; - shouldConcatPrompts: boolean; - }; tool: { selected: Tool; selectedBuffer: Tool | null; @@ -777,7 +766,7 @@ export type CanvasV2State = { }; fill: RgbaColor; }; - size: { + document: { width: ParameterWidth; height: ParameterHeight; aspectRatio: AspectRatioState; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts index 78702fccfef..e740660bca3 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts @@ -421,7 +421,7 @@ const addInitialImageLayerToGraph = ( ) => { const { vaePrecision } = state.generation; const { refinerModel, refinerStart } = state.sdxl; - const { width, height } = state.canvasV2.size; + const { width, height } = state.canvasV2.document; assert(layer.isEnabled, 'Initial image layer is not enabled'); assert(layer.image, 'Initial image layer has no image'); @@ -568,7 +568,7 @@ const buildControlImage = ( const getRGLayerBlobs = async (layerIds?: string[], preview: boolean = false): Promise> => { const state = getStore().getState(); const { layers } = state.canvasV2; - const { width, height } = state.canvasV2.size; + const { width, height } = state.canvasV2.document; const reduxLayers = layers.filter(isRegionalGuidanceLayer); const container = document.createElement('div'); const stage = new Konva.Stage({ container, width, height }); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addHRF.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addHRF.ts index e03d54ebc2a..4b38fc20c63 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addHRF.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addHRF.ts @@ -74,7 +74,7 @@ export const addHRF = ( vaeSource: Invocation<'vae_loader'> | Invocation<'main_model_loader'> | Invocation<'seamless'> ): Invocation<'l2i'> => { const { hrfStrength, hrfEnabled, hrfMethod } = state.hrf; - const { width, height } = state.canvasV2.size; + const { width, height } = state.canvasV2.document; const optimalDimension = selectOptimalDimension(state); const { newWidth: hrfWidth, newHeight: hrfHeight } = calculateHrfRes(optimalDimension, width, height); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts index 288f0a944fd..f22d0e3f8d5 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts @@ -40,7 +40,7 @@ export const buildGenerationTabGraph = async (state: RootState): Promise { const { positivePrompt, negativePrompt, positivePrompt2, negativePrompt2, shouldConcatPrompts } = - state.canvasV2.prompts; + state.generation; const { activeStylePresetId } = state.stylePreset; if (activeStylePresetId) { diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx index e415c2d5fa1..eabcfcd911d 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx @@ -1,9 +1,9 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { negativePromptChanged } from 'features/controlLayers/store/canvasV2Slice'; import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { ViewModePrompt } from 'features/parameters/components/Prompts/ViewModePrompt'; +import { negativePromptChanged } from 'features/parameters/store/generationSlice'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; import { usePrompt } from 'features/prompt/usePrompt'; @@ -13,7 +13,7 @@ import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets'; export const ParamNegativePrompt = memo(() => { const dispatch = useAppDispatch(); - const prompt = useAppSelector((s) => s.canvasV2.prompts.negativePrompt); + const prompt = useAppSelector((s) => s.generation.negativePrompt); const viewMode = useAppSelector((s) => s.stylePreset.viewMode); const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId); diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx index f7c2c285ffb..c39bf050cd7 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx @@ -1,10 +1,10 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { positivePromptChanged } from 'features/controlLayers/store/canvasV2Slice'; import { ShowDynamicPromptsPreviewButton } from 'features/dynamicPrompts/components/ShowDynamicPromptsPreviewButton'; import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { ViewModePrompt } from 'features/parameters/components/Prompts/ViewModePrompt'; +import { positivePromptChanged } from 'features/parameters/store/generationSlice'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; import { usePrompt } from 'features/prompt/usePrompt'; @@ -17,7 +17,7 @@ import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets'; export const ParamPositivePrompt = memo(() => { const dispatch = useAppDispatch(); - const prompt = useAppSelector((s) => s.canvasV2.positivePrompt); + const prompt = useAppSelector((s) => s.generation.positivePrompt); const baseModel = useAppSelector((s) => s.generation.model)?.base; const viewMode = useAppSelector((s) => s.stylePreset.viewMode); const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId); diff --git a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts index 573e9c1bbe6..47f33b2f963 100644 --- a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts +++ b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts @@ -24,14 +24,8 @@ const initialGenerationState: GenerationState = { cfgScale: 7.5, cfgRescaleMultiplier: 0, img2imgStrength: 0.75, - infillMethod: 'patchmatch', iterations: 1, scheduler: 'euler', - maskBlur: 16, - maskBlurMethod: 'box', - canvasCoherenceMode: 'Gaussian Blur', - canvasCoherenceMinDenoise: 0, - canvasCoherenceEdgeSize: 16, seed: 0, shouldRandomizeSeed: true, steps: 50, @@ -43,6 +37,12 @@ const initialGenerationState: GenerationState = { clipSkip: 0, shouldUseCpuNoise: true, shouldShowAdvancedOptions: false, + maskBlur: 16, + maskBlurMethod: 'box', + canvasCoherenceMode: 'Gaussian Blur', + canvasCoherenceMinDenoise: 0, + canvasCoherenceEdgeSize: 16, + infillMethod: 'patchmatch', infillTileSize: 32, infillPatchmatchDownscaleSize: 1, infillMosaicTileWidth: 64, @@ -50,6 +50,11 @@ const initialGenerationState: GenerationState = { infillMosaicMinColor: { r: 0, g: 0, b: 0, a: 1 }, infillMosaicMaxColor: { r: 255, g: 255, b: 255, a: 1 }, infillColorValue: { r: 0, g: 0, b: 0, a: 1 }, + positivePrompt: '', + negativePrompt: '', + positivePrompt2: '', + negativePrompt2: '', + shouldConcatPrompts: true, }; export const generationSlice = createSlice({ @@ -166,6 +171,21 @@ export const generationSlice = createSlice({ setInfillColorValue: (state, action: PayloadAction) => { state.infillColorValue = action.payload; }, + positivePromptChanged: (state, action: PayloadAction) => { + state.positivePrompt = action.payload; + }, + negativePromptChanged: (state, action: PayloadAction) => { + state.negativePrompt = action.payload; + }, + positivePrompt2Changed: (state, action: PayloadAction) => { + state.positivePrompt2 = action.payload; + }, + negativePrompt2Changed: (state, action: PayloadAction) => { + state.negativePrompt2 = action.payload; + }, + shouldConcatPromptsChanged: (state, action: PayloadAction) => { + state.shouldConcatPrompts = action.payload; + }, }, extraReducers: (builder) => { builder.addCase(configChanged, (state, action) => { @@ -210,6 +230,11 @@ export const { setInfillMosaicMinColor, setInfillMosaicMaxColor, setInfillColorValue, + positivePromptChanged, + negativePromptChanged, + positivePrompt2Changed, + negativePrompt2Changed, + shouldConcatPromptsChanged, } = generationSlice.actions; export const { selectOptimalDimension } = generationSlice.selectors; diff --git a/invokeai/frontend/web/src/features/parameters/store/types.ts b/invokeai/frontend/web/src/features/parameters/store/types.ts index 51ab6146cf7..0d0c6e4b8b3 100644 --- a/invokeai/frontend/web/src/features/parameters/store/types.ts +++ b/invokeai/frontend/web/src/features/parameters/store/types.ts @@ -5,6 +5,10 @@ import type { ParameterCFGScale, ParameterMaskBlurMethod, ParameterModel, + ParameterNegativePrompt, + ParameterNegativeStylePromptSDXL, + ParameterPositivePrompt, + ParameterPositiveStylePromptSDXL, ParameterPrecision, ParameterScheduler, ParameterSeed, @@ -45,6 +49,11 @@ export interface GenerationState { infillMosaicMinColor: RgbaColor; infillMosaicMaxColor: RgbaColor; infillColorValue: RgbaColor; + positivePrompt: ParameterPositivePrompt; + negativePrompt: ParameterNegativePrompt; + positivePrompt2: ParameterPositiveStylePromptSDXL; + negativePrompt2: ParameterNegativeStylePromptSDXL; + shouldConcatPrompts: boolean; } export type PayloadActionWithOptimalDimension = PayloadAction; diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt.tsx index f295ffd32f5..a087df83685 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt.tsx @@ -1,8 +1,8 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { negativePrompt2Changed } from 'features/controlLayers/store/canvasV2Slice'; import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; +import { negativePrompt2Changed } from 'features/parameters/store/generationSlice'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; import { usePrompt } from 'features/prompt/usePrompt'; @@ -12,7 +12,7 @@ import { useTranslation } from 'react-i18next'; export const ParamSDXLNegativeStylePrompt = memo(() => { const dispatch = useAppDispatch(); - const prompt = useAppSelector((s) => s.canvasV2.negativePrompt2); + const prompt = useAppSelector((s) => s.generation.negativePrompt2); const textareaRef = useRef(null); const { t } = useTranslation(); const handleChange = useCallback( diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt.tsx index 8e31185345f..f076e6e176c 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt.tsx @@ -1,8 +1,8 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { positivePrompt2Changed } from 'features/controlLayers/store/canvasV2Slice'; import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; +import { positivePrompt2Changed } from 'features/parameters/store/generationSlice'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; import { usePrompt } from 'features/prompt/usePrompt'; @@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next'; export const ParamSDXLPositiveStylePrompt = memo(() => { const dispatch = useAppDispatch(); - const prompt = useAppSelector((s) => s.canvasV2.positivePrompt2); + const prompt = useAppSelector((s) => s.generation.positivePrompt2); const textareaRef = useRef(null); const { t } = useTranslation(); const handleChange = useCallback( diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/SDXLConcatButton.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/SDXLConcatButton.tsx index 9048437e3ab..62dd12a6962 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/SDXLConcatButton.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/SDXLConcatButton.tsx @@ -1,12 +1,12 @@ import { IconButton, Tooltip } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { shouldConcatPromptsChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { shouldConcatPromptsChanged } from 'features/parameters/store/generationSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiLinkSimpleBold, PiLinkSimpleBreakBold } from 'react-icons/pi'; export const SDXLConcatButton = memo(() => { - const shouldConcatPrompts = useAppSelector((s) => s.canvasV2.shouldConcatPrompts); + const shouldConcatPrompts = useAppSelector((s) => s.generation.shouldConcatPrompts); const dispatch = useAppDispatch(); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx index 52278cfd60e..1d18292da3b 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx @@ -29,7 +29,7 @@ const selector = createMemoizedSelector( const badges: string[] = []; const isSDXL = model?.base === 'sdxl'; - const { aspectRatio, width, height } = canvasV2.size; + const { aspectRatio, width, height } = canvasV2.document; badges.push(`${width}×${height}`); badges.push(aspectRatio.id); diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx index 13670c674f5..7c24c3d79b0 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx @@ -9,9 +9,9 @@ import { memo, useCallback } from 'react'; export const ImageSizeLinear = memo(() => { const dispatch = useAppDispatch(); - const width = useAppSelector((s) => s.canvasV2.size.width); - const height = useAppSelector((s) => s.canvasV2.size.height); - const aspectRatioState = useAppSelector((s) => s.canvasV2.size.aspectRatio); + const width = useAppSelector((s) => s.canvasV2.document.width); + const height = useAppSelector((s) => s.canvasV2.document.height); + const aspectRatioState = useAppSelector((s) => s.canvasV2.document.aspectRatio); const onChangeWidth = useCallback( (width: number) => { From 4388f006074019ecd0b2d2b0490ce204e779ec51 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 15 Jun 2024 22:55:57 +1000 Subject: [PATCH 045/678] refactor(ui): update dnd/image upload --- .../listeners/imageDropped.ts | 108 +++--------------- .../listeners/imageUploaded.ts | 23 ++-- .../web/src/features/dnd/util/isValidDrop.ts | 14 +-- 3 files changed, 24 insertions(+), 121 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index f5d04ccd020..fb4ffbca7cb 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -2,17 +2,11 @@ import { createAction } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { parseify } from 'common/util/serialize'; -import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; import { - controlAdapterImageChanged, - controlAdapterIsEnabledChanged, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { - controlAdapterImageChanged, - iiLayerImageChanged, + caImageChanged, + ipaImageChanged, layerImageAdded, - ipAdapterImageChanged, - regionalGuidanceIPAdapterImageChanged, + rgIPAdapterImageChanged, } from 'features/controlLayers/store/canvasV2Slice'; import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import { isValidDrop } from 'features/dnd/util/isValidDrop'; @@ -23,7 +17,6 @@ import { selectionChanged, } from 'features/gallery/store/gallerySlice'; import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice'; import { imagesApi } from 'services/api/endpoints/images'; @@ -65,45 +58,16 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => return; } - /** - * Image dropped on ControlNet - */ - if ( - overData.actionType === 'SET_CONTROL_ADAPTER_IMAGE' && - activeData.payloadType === 'IMAGE_DTO' && - activeData.payload.imageDTO - ) { - const { id } = overData.context; - dispatch( - controlAdapterImageChanged({ - id, - controlImage: activeData.payload.imageDTO.image_name, - }) - ); - dispatch( - controlAdapterIsEnabledChanged({ - id, - isEnabled: true, - }) - ); - return; - } - /** * Image dropped on Control Adapter Layer */ if ( - overData.actionType === 'SET_CA_LAYER_IMAGE' && + overData.actionType === 'SET_CA_IMAGE' && activeData.payloadType === 'IMAGE_DTO' && activeData.payload.imageDTO ) { - const { layerId } = overData.context; - dispatch( - controlAdapterImageChanged({ - layerId, - imageDTO: activeData.payload.imageDTO, - }) - ); + const { id } = overData.context; + dispatch(caImageChanged({ id, imageDTO: activeData.payload.imageDTO })); return; } @@ -111,17 +75,12 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => * Image dropped on IP Adapter Layer */ if ( - overData.actionType === 'SET_IPA_LAYER_IMAGE' && + overData.actionType === 'SET_IPA_IMAGE' && activeData.payloadType === 'IMAGE_DTO' && activeData.payload.imageDTO ) { - const { layerId } = overData.context; - dispatch( - ipAdapterImageChanged({ - layerId, - imageDTO: activeData.payload.imageDTO, - }) - ); + const { id } = overData.context; + dispatch(ipaImageChanged({ id, imageDTO: activeData.payload.imageDTO })); return; } @@ -129,36 +88,12 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => * Image dropped on RG Layer IP Adapter */ if ( - overData.actionType === 'SET_RG_LAYER_IP_ADAPTER_IMAGE' && + overData.actionType === 'SET_RG_IP_ADAPTER_IMAGE' && activeData.payloadType === 'IMAGE_DTO' && activeData.payload.imageDTO ) { - const { layerId, ipAdapterId } = overData.context; - dispatch( - regionalGuidanceIPAdapterImageChanged({ - layerId, - ipAdapterId, - imageDTO: activeData.payload.imageDTO, - }) - ); - return; - } - - /** - * Image dropped on II Layer Image - */ - if ( - overData.actionType === 'SET_II_LAYER_IMAGE' && - activeData.payloadType === 'IMAGE_DTO' && - activeData.payload.imageDTO - ) { - const { layerId } = overData.context; - dispatch( - iiLayerImageChanged({ - layerId, - imageDTO: activeData.payload.imageDTO, - }) - ); + const { id, ipAdapterId } = overData.context; + dispatch(rgIPAdapterImageChanged({ id, ipAdapterId, imageDTO: activeData.payload.imageDTO })); return; } @@ -171,24 +106,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => activeData.payload.imageDTO ) { const { layerId } = overData.context; - dispatch( - layerImageAdded({ - layerId, - imageDTO: activeData.payload.imageDTO, - }) - ); - return; - } - - /** - * Image dropped on Canvas - */ - if ( - overData.actionType === 'SET_CANVAS_INITIAL_IMAGE' && - activeData.payloadType === 'IMAGE_DTO' && - activeData.payload.imageDTO - ) { - dispatch(setInitialCanvasImage(activeData.payload.imageDTO, selectOptimalDimension(getState()))); + dispatch(layerImageAdded({ id: layerId, imageDTO: activeData.payload.imageDTO })); return; } diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts index e63d0aa8c39..fa23cdfc064 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts @@ -82,37 +82,28 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis if (postUploadAction?.type === 'SET_CA_IMAGE') { const { id } = postUploadAction; dispatch(caImageChanged({ id, imageDTO })); - toast({ - ...DEFAULT_UPLOADED_TOAST, - description: t('toast.setControlImage'), - }); + toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage') }); + return; } if (postUploadAction?.type === 'SET_IPA_IMAGE') { const { id } = postUploadAction; dispatch(ipaImageChanged({ id, imageDTO })); - toast({ - ...DEFAULT_UPLOADED_TOAST, - description: t('toast.setControlImage'), - }); + toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage') }); + return; } if (postUploadAction?.type === 'SET_RG_IP_ADAPTER_IMAGE') { const { id, ipAdapterId } = postUploadAction; dispatch(rgIPAdapterImageChanged({ id, ipAdapterId, imageDTO })); - toast({ - ...DEFAULT_UPLOADED_TOAST, - description: t('toast.setControlImage'), - }); + toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage') }); + return; } if (postUploadAction?.type === 'SET_NODES_IMAGE') { const { nodeId, fieldName } = postUploadAction; dispatch(fieldImageValueChanged({ nodeId, fieldName, value: imageDTO })); - toast({ - ...DEFAULT_UPLOADED_TOAST, - description: `${t('toast.setNodeField')} ${fieldName}`, - }); + toast({ ...DEFAULT_UPLOADED_TOAST, description: `${t('toast.setNodeField')} ${fieldName}` }); return; } }, diff --git a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts index 4f0a31d387c..128e5c5d501 100644 --- a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts +++ b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts @@ -15,17 +15,13 @@ export const isValidDrop = (overData?: TypesafeDroppableData | null, activeData? switch (actionType) { case 'SET_CURRENT_IMAGE': return payloadType === 'IMAGE_DTO'; - case 'SET_CONTROL_ADAPTER_IMAGE': + case 'SET_CA_IMAGE': return payloadType === 'IMAGE_DTO'; - case 'SET_CA_LAYER_IMAGE': + case 'SET_IPA_IMAGE': return payloadType === 'IMAGE_DTO'; - case 'SET_IPA_LAYER_IMAGE': + case 'SET_RG_IP_ADAPTER_IMAGE': return payloadType === 'IMAGE_DTO'; - case 'SET_RG_LAYER_IP_ADAPTER_IMAGE': - return payloadType === 'IMAGE_DTO'; - case 'SET_II_LAYER_IMAGE': - return payloadType === 'IMAGE_DTO'; - case 'SET_CANVAS_INITIAL_IMAGE': + case 'ADD_LAYER_IMAGE': return payloadType === 'IMAGE_DTO'; case 'SET_UPSCALE_INITIAL_IMAGE': return payloadType === 'IMAGE_DTO'; @@ -33,8 +29,6 @@ export const isValidDrop = (overData?: TypesafeDroppableData | null, activeData? return payloadType === 'IMAGE_DTO'; case 'SELECT_FOR_COMPARE': return payloadType === 'IMAGE_DTO'; - case 'ADD_RASTER_LAYER_IMAGE': - return payloadType === 'IMAGE_DTO'; case 'ADD_TO_BOARD': { // If the board is the same, don't allow the drop From e9033040f6b3a1467b29187ef73e8e87df494cae Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 15 Jun 2024 23:04:21 +1000 Subject: [PATCH 046/678] refactor(ui): add scaled bbox state --- .../controlLayers/store/canvasV2Slice.ts | 5 ++++ .../src/features/controlLayers/store/types.ts | 28 ++++++++++++------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 3cf24a0fa5e..1142a8cab71 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -44,6 +44,11 @@ const initialState: CanvasV2State = { width: 512, height: 512, }, + scaledBbox: { + width: 512, + height: 512, + scaleMethod: 'auto', + }, controlAdapters: [], ipAdapters: [], regions: [], diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index a38b6500ec5..86ded89ed4d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,10 +1,7 @@ import { deepClone } from 'common/util/deepClone'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; -import type { - ParameterHeight, - ParameterWidth, -} from 'features/parameters/types/parameterSchemas'; +import type { ParameterHeight, ParameterWidth } from 'features/parameters/types/parameterSchemas'; import { zAutoNegative, zParameterNegativePrompt, @@ -748,9 +745,19 @@ export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO) height, }); +const zBoundingBoxScaleMethod = z.enum(['none', 'auto', 'manual']); +export type BoundingBoxScaleMethod = z.infer; +export const isBoundingBoxScaleMethod = (v: unknown): v is BoundingBoxScaleMethod => + zBoundingBoxScaleMethod.safeParse(v).success; + export type CanvasEntity = LayerData | IPAdapterData | ControlAdapterData | RegionalGuidanceData | InpaintMaskData; export type CanvasEntityIdentifier = Pick; +export type Dimensions = { + width: number; + height: number; +}; + export type CanvasV2State = { _version: 3; selectedEntityIdentifier: CanvasEntityIdentifier | null; @@ -758,12 +765,8 @@ export type CanvasV2State = { selected: Tool; selectedBuffer: Tool | null; invertScroll: boolean; - brush: { - width: number; - }; - eraser: { - width: number; - }; + brush: { width: number }; + eraser: { width: number }; fill: RgbaColor; }; document: { @@ -772,6 +775,11 @@ export type CanvasV2State = { aspectRatio: AspectRatioState; }; bbox: IRect; + scaledBbox: { + scaleMethod: BoundingBoxScaleMethod; + width: ParameterWidth; + height: ParameterHeight; + }; layers: LayerData[]; controlAdapters: ControlAdapterData[]; ipAdapters: IPAdapterData[]; From b1b41a9b0c2fb941639307d981f52d7cf7659a9d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 15 Jun 2024 23:48:21 +1000 Subject: [PATCH 047/678] refactor(ui): merge compositing, params into canvasV2 slice --- invokeai/frontend/web/src/app/store/store.ts | 10 +- .../controlLayers/store/canvasV2Slice.ts | 101 +++++++++++--- .../store/compositingReducers.ts | 30 ++++ .../controlLayers/store/paramsReducers.ts | 132 ++++++++++++++++++ .../src/features/controlLayers/store/types.ts | 61 +++++++- 5 files changed, 308 insertions(+), 26 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/compositingReducers.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 3cedd0db26f..95de810cdd7 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -15,10 +15,8 @@ import { modelManagerV2PersistConfig, modelManagerV2Slice } from 'features/model import { nodesPersistConfig, nodesSlice, nodesUndoableConfig } from 'features/nodes/store/nodesSlice'; import { workflowSettingsPersistConfig, workflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice'; import { workflowPersistConfig, workflowSlice } from 'features/nodes/store/workflowSlice'; -import { generationPersistConfig, generationSlice } from 'features/parameters/store/generationSlice'; import { upscalePersistConfig, upscaleSlice } from 'features/parameters/store/upscaleSlice'; import { queueSlice } from 'features/queue/store/queueSlice'; -import { sdxlPersistConfig, sdxlSlice } from 'features/sdxl/store/sdxlSlice'; import { stylePresetPersistConfig, stylePresetSlice } from 'features/stylePresets/store/stylePresetSlice'; import { configSlice } from 'features/system/store/configSlice'; import { systemPersistConfig, systemSlice } from 'features/system/store/systemSlice'; @@ -42,7 +40,7 @@ import { listenerMiddleware } from './middleware/listenerMiddleware'; const allReducers = { [api.reducerPath]: api.reducer, [gallerySlice.name]: gallerySlice.reducer, - [generationSlice.name]: generationSlice.reducer, + // [generationSlice.name]: generationSlice.reducer, [nodesSlice.name]: undoable(nodesSlice.reducer, nodesUndoableConfig), [systemSlice.name]: systemSlice.reducer, [configSlice.name]: configSlice.reducer, @@ -52,7 +50,7 @@ const allReducers = { [changeBoardModalSlice.name]: changeBoardModalSlice.reducer, [loraSlice.name]: loraSlice.reducer, [modelManagerV2Slice.name]: modelManagerV2Slice.reducer, - [sdxlSlice.name]: sdxlSlice.reducer, + // [sdxlSlice.name]: sdxlSlice.reducer, [queueSlice.name]: queueSlice.reducer, [workflowSlice.name]: workflowSlice.reducer, [hrfSlice.name]: hrfSlice.reducer, @@ -90,13 +88,13 @@ export type PersistConfig = { const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = { [galleryPersistConfig.name]: galleryPersistConfig, - [generationPersistConfig.name]: generationPersistConfig, + // [generationPersistConfig.name]: generationPersistConfig, [nodesPersistConfig.name]: nodesPersistConfig, [systemPersistConfig.name]: systemPersistConfig, [workflowPersistConfig.name]: workflowPersistConfig, [uiPersistConfig.name]: uiPersistConfig, [dynamicPromptsPersistConfig.name]: dynamicPromptsPersistConfig, - [sdxlPersistConfig.name]: sdxlPersistConfig, + // [sdxlPersistConfig.name]: sdxlPersistConfig, [loraPersistConfig.name]: loraPersistConfig, [modelManagerV2PersistConfig.name]: modelManagerV2PersistConfig, [hrfPersistConfig.name]: hrfPersistConfig, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 1142a8cab71..8b1721cb3f5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -3,15 +3,14 @@ import { createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { deepClone } from 'common/util/deepClone'; import { roundDownToMultiple } from 'common/util/roundDownToMultiple'; +import { compositingReducers } from 'features/controlLayers/store/compositingReducers'; import { controlAdaptersReducers } from 'features/controlLayers/store/controlAdaptersReducers'; import { ipAdaptersReducers } from 'features/controlLayers/store/ipAdaptersReducers'; import { layersReducers } from 'features/controlLayers/store/layersReducers'; +import { paramsReducers } from 'features/controlLayers/store/paramsReducers'; import { regionsReducers } from 'features/controlLayers/store/regionsReducers'; -import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; -import { modelChanged } from 'features/parameters/store/generationSlice'; -import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension'; import type { IRect, Vector2d } from 'konva/lib/types'; import { atom } from 'nanostores'; @@ -54,6 +53,46 @@ const initialState: CanvasV2State = { regions: [], layers: [], maskFillOpacity: 0.3, + compositing: { + maskBlur: 16, + maskBlurMethod: 'box', + canvasCoherenceMode: 'Gaussian Blur', + canvasCoherenceMinDenoise: 0, + canvasCoherenceEdgeSize: 16, + infillMethod: 'patchmatch', + infillTileSize: 32, + infillPatchmatchDownscaleSize: 1, + infillColorValue: { r: 0, g: 0, b: 0, a: 1 }, + }, + params: { + cfgScale: 7.5, + cfgRescaleMultiplier: 0, + img2imgStrength: 0.75, + iterations: 1, + scheduler: 'euler', + seed: 0, + shouldRandomizeSeed: true, + steps: 50, + model: null, + vae: null, + vaePrecision: 'fp32', + seamlessXAxis: false, + seamlessYAxis: false, + clipSkip: 0, + shouldUseCpuNoise: true, + positivePrompt: '', + negativePrompt: '', + positivePrompt2: '', + negativePrompt2: '', + shouldConcatPrompts: true, + refinerModel: null, + refinerSteps: 20, + refinerCFGScale: 7.5, + refinerScheduler: 'euler', + refinerPositiveAestheticScore: 6, + refinerNegativeAestheticScore: 2.5, + refinerStart: 0.8, + }, }; export const canvasV2Slice = createSlice({ @@ -64,6 +103,8 @@ export const canvasV2Slice = createSlice({ ...ipAdaptersReducers, ...controlAdaptersReducers, ...regionsReducers, + ...paramsReducers, + ...compositingReducers, widthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }>) => { const { width, updateAspectRatio, clamp } = action.payload; state.document.width = clamp ? Math.max(roundDownToMultiple(width, 8), 64) : width; @@ -119,22 +160,6 @@ export const canvasV2Slice = createSlice({ state.controlAdapters = []; }, }, - extraReducers(builder) { - builder.addCase(modelChanged, (state, action) => { - const newModel = action.payload; - if (!newModel || action.meta.previousModel?.base === newModel.base) { - // Model was cleared or the base didn't change - return; - } - const optimalDimension = getOptimalDimension(newModel); - if (getIsSizeOptimal(state.document.width, state.document.height, optimalDimension)) { - return; - } - const { width, height } = calculateNewSize(state.document.aspectRatio.value, optimalDimension * optimalDimension); - state.document.width = width; - state.document.height = height; - }); - }, }); export const { @@ -153,6 +178,7 @@ export const { allEntitiesDeleted, // layers layerAdded, + layerRecalled, layerDeleted, layerReset, layerMovedForwardOne, @@ -230,6 +256,43 @@ export const { rgEraserLineAdded, rgLinePointAdded, rgRectAdded, + // Compositing + setInfillMethod, + setInfillTileSize, + setInfillPatchmatchDownscaleSize, + setInfillColorValue, + setMaskBlur, + setCanvasCoherenceMode, + setCanvasCoherenceEdgeSize, + setCanvasCoherenceMinDenoise, + // Parameters + setIterations, + setSteps, + setCfgScale, + setCfgRescaleMultiplier, + setScheduler, + setSeed, + setImg2imgStrength, + setSeamlessXAxis, + setSeamlessYAxis, + setShouldRandomizeSeed, + vaeSelected, + vaePrecisionChanged, + setClipSkip, + shouldUseCpuNoiseChanged, + positivePromptChanged, + negativePromptChanged, + positivePrompt2Changed, + negativePrompt2Changed, + shouldConcatPromptsChanged, + refinerModelChanged, + setRefinerSteps, + setRefinerCFGScale, + setRefinerScheduler, + setRefinerPositiveAestheticScore, + setRefinerNegativeAestheticScore, + setRefinerStart, + modelChanged, } = canvasV2Slice.actions; export const selectCanvasV2Slice = (state: RootState) => state.canvasV2; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/compositingReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/compositingReducers.ts new file mode 100644 index 00000000000..03bb81ce04e --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/compositingReducers.ts @@ -0,0 +1,30 @@ +import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import type { CanvasV2State, RgbaColor } from 'features/controlLayers/store/types'; +import type { ParameterCanvasCoherenceMode } from 'features/parameters/types/parameterSchemas'; + +export const compositingReducers = { + setInfillMethod: (state, action: PayloadAction) => { + state.compositing.infillMethod = action.payload; + }, + setInfillTileSize: (state, action: PayloadAction) => { + state.compositing.infillTileSize = action.payload; + }, + setInfillPatchmatchDownscaleSize: (state, action: PayloadAction) => { + state.compositing.infillPatchmatchDownscaleSize = action.payload; + }, + setInfillColorValue: (state, action: PayloadAction) => { + state.compositing.infillColorValue = action.payload; + }, + setMaskBlur: (state, action: PayloadAction) => { + state.compositing.maskBlur = action.payload; + }, + setCanvasCoherenceMode: (state, action: PayloadAction) => { + state.compositing.canvasCoherenceMode = action.payload; + }, + setCanvasCoherenceEdgeSize: (state, action: PayloadAction) => { + state.compositing.canvasCoherenceEdgeSize = action.payload; + }, + setCanvasCoherenceMinDenoise: (state, action: PayloadAction) => { + state.compositing.canvasCoherenceMinDenoise = action.payload; + }, +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts new file mode 100644 index 00000000000..023ea78f021 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts @@ -0,0 +1,132 @@ +import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import getScaledBoundingBoxDimensions from 'features/canvas/util/getScaledBoundingBoxDimensions'; +import type { CanvasV2State } from 'features/controlLayers/store/types'; +import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; +import { CLIP_SKIP_MAP } from 'features/parameters/types/constants'; +import type { + ParameterCFGRescaleMultiplier, + ParameterCFGScale, + ParameterModel, + ParameterPrecision, + ParameterScheduler, + ParameterSDXLRefinerModel, + ParameterVAEModel, +} from 'features/parameters/types/parameterSchemas'; +import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension'; +import { clamp } from 'lodash-es'; + +export const paramsReducers = { + setIterations: (state, action: PayloadAction) => { + state.params.iterations = action.payload; + }, + setSteps: (state, action: PayloadAction) => { + state.params.steps = action.payload; + }, + setCfgScale: (state, action: PayloadAction) => { + state.params.cfgScale = action.payload; + }, + setCfgRescaleMultiplier: (state, action: PayloadAction) => { + state.params.cfgRescaleMultiplier = action.payload; + }, + setScheduler: (state, action: PayloadAction) => { + state.params.scheduler = action.payload; + }, + setSeed: (state, action: PayloadAction) => { + state.params.seed = action.payload; + state.params.shouldRandomizeSeed = false; + }, + setImg2imgStrength: (state, action: PayloadAction) => { + state.params.img2imgStrength = action.payload; + }, + setSeamlessXAxis: (state, action: PayloadAction) => { + state.params.seamlessXAxis = action.payload; + }, + setSeamlessYAxis: (state, action: PayloadAction) => { + state.params.seamlessYAxis = action.payload; + }, + setShouldRandomizeSeed: (state, action: PayloadAction) => { + state.params.shouldRandomizeSeed = action.payload; + }, + modelChanged: (state, action: PayloadAction<{ model: ParameterModel | null; previousModel?: ParameterModel }>) => { + const { model, previousModel } = action.payload; + state.params.model = model; + + // If the model base changes (e.g. SD1.5 -> SDXL), we need to change a few things + if (model === null || previousModel?.base === model.base) { + return; + } + + // Update the bbox size to match the new model's optimal size + // TODO(psyche): Should we change the document size too? + const optimalDimension = getOptimalDimension(model); + if (!getIsSizeOptimal(state.document.width, state.document.height, optimalDimension)) { + const bboxDims = calculateNewSize(state.document.aspectRatio.value, optimalDimension * optimalDimension); + state.bbox.width = bboxDims.width; + state.bbox.height = bboxDims.height; + + if (state.scaledBbox.scaleMethod === 'auto') { + const scaledBboxDims = getScaledBoundingBoxDimensions(bboxDims, optimalDimension); + state.scaledBbox.width = scaledBboxDims.width; + state.scaledBbox.height = scaledBboxDims.height; + } + } + + // Clamp CLIP skip layer count to the bounds of the new model + if (model.base === 'sdxl') { + // We don't support user-defined CLIP skip for SDXL because it doesn't do anything useful + state.params.clipSkip = 0; + } else { + const { maxClip } = CLIP_SKIP_MAP[model.base]; + state.params.clipSkip = clamp(state.params.clipSkip, 0, maxClip); + } + }, + vaeSelected: (state, action: PayloadAction) => { + // null is a valid VAE! + state.params.vae = action.payload; + }, + vaePrecisionChanged: (state, action: PayloadAction) => { + state.params.vaePrecision = action.payload; + }, + setClipSkip: (state, action: PayloadAction) => { + state.params.clipSkip = action.payload; + }, + shouldUseCpuNoiseChanged: (state, action: PayloadAction) => { + state.params.shouldUseCpuNoise = action.payload; + }, + positivePromptChanged: (state, action: PayloadAction) => { + state.params.positivePrompt = action.payload; + }, + negativePromptChanged: (state, action: PayloadAction) => { + state.params.negativePrompt = action.payload; + }, + positivePrompt2Changed: (state, action: PayloadAction) => { + state.params.positivePrompt2 = action.payload; + }, + negativePrompt2Changed: (state, action: PayloadAction) => { + state.params.negativePrompt2 = action.payload; + }, + shouldConcatPromptsChanged: (state, action: PayloadAction) => { + state.params.shouldConcatPrompts = action.payload; + }, + refinerModelChanged: (state, action: PayloadAction) => { + state.params.refinerModel = action.payload; + }, + setRefinerSteps: (state, action: PayloadAction) => { + state.params.refinerSteps = action.payload; + }, + setRefinerCFGScale: (state, action: PayloadAction) => { + state.params.refinerCFGScale = action.payload; + }, + setRefinerScheduler: (state, action: PayloadAction) => { + state.params.refinerScheduler = action.payload; + }, + setRefinerPositiveAestheticScore: (state, action: PayloadAction) => { + state.params.refinerPositiveAestheticScore = action.payload; + }, + setRefinerNegativeAestheticScore: (state, action: PayloadAction) => { + state.params.refinerNegativeAestheticScore = action.payload; + }, + setRefinerStart: (state, action: PayloadAction) => { + state.params.refinerStart = action.payload; + }, +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 86ded89ed4d..64e2ffea05d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,7 +1,26 @@ import { deepClone } from 'common/util/deepClone'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; -import type { ParameterHeight, ParameterWidth } from 'features/parameters/types/parameterSchemas'; +import type { + ParameterCanvasCoherenceMode, + ParameterCFGRescaleMultiplier, + ParameterCFGScale, + ParameterHeight, + ParameterMaskBlurMethod, + ParameterModel, + ParameterNegativePrompt, + ParameterNegativeStylePromptSDXL, + ParameterPositivePrompt, + ParameterPositiveStylePromptSDXL, + ParameterPrecision, + ParameterScheduler, + ParameterSDXLRefinerModel, + ParameterSeed, + ParameterSteps, + ParameterStrength, + ParameterVAEModel, + ParameterWidth, +} from 'features/parameters/types/parameterSchemas'; import { zAutoNegative, zParameterNegativePrompt, @@ -785,6 +804,46 @@ export type CanvasV2State = { ipAdapters: IPAdapterData[]; regions: RegionalGuidanceData[]; maskFillOpacity: number; + compositing: { + maskBlur: number; + maskBlurMethod: ParameterMaskBlurMethod; + canvasCoherenceMode: ParameterCanvasCoherenceMode; + canvasCoherenceMinDenoise: ParameterStrength; + canvasCoherenceEdgeSize: number; + infillMethod: string; + infillTileSize: number; + infillPatchmatchDownscaleSize: number; + infillColorValue: RgbaColor; + }; + params: { + cfgScale: ParameterCFGScale; + cfgRescaleMultiplier: ParameterCFGRescaleMultiplier; + img2imgStrength: ParameterStrength; + iterations: number; + scheduler: ParameterScheduler; + seed: ParameterSeed; + shouldRandomizeSeed: boolean; + steps: ParameterSteps; + model: ParameterModel | null; + vae: ParameterVAEModel | null; + vaePrecision: ParameterPrecision; + seamlessXAxis: boolean; + seamlessYAxis: boolean; + clipSkip: number; + shouldUseCpuNoise: boolean; + positivePrompt: ParameterPositivePrompt; + negativePrompt: ParameterNegativePrompt; + positivePrompt2: ParameterPositiveStylePromptSDXL; + negativePrompt2: ParameterNegativeStylePromptSDXL; + shouldConcatPrompts: boolean; + refinerModel: ParameterSDXLRefinerModel | null; + refinerSteps: number; + refinerCFGScale: number; + refinerScheduler: ParameterScheduler; + refinerPositiveAestheticScore: number; + refinerNegativeAestheticScore: number; + refinerStart: number; + }; }; export type StageAttrs = { x: number; y: number; width: number; height: number; scale: number }; From 8d7de1543bb64e600b83d886b2e0562d2ec5241f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 16 Jun 2024 00:08:00 +1000 Subject: [PATCH 048/678] refactor(ui): update components & logic to use new unified slice --- .../frontend/web/.storybook/ReduxInit.tsx | 2 +- .../listeners/appConfigReceived.ts | 2 +- .../listeners/enqueueRequestedLinear.ts | 2 +- .../listeners/enqueueRequestedNodes.ts | 2 +- .../listeners/modelSelected.ts | 8 +- .../listeners/modelsLoaded.ts | 37 ++- .../listeners/setDefaultSettings.ts | 4 +- .../common/hooks/useGroupedModelCombobox.ts | 2 +- .../src/common/hooks/useIsReadyToEnqueue.ts | 6 +- .../IAICanvasToolbar/IAICanvasBoundingBox.tsx | 2 +- .../src/features/canvas/store/canvasSlice.ts | 2 +- .../ControlAdapter/CAImagePreview.tsx | 2 +- .../ControlAdapter/CAModelCombobox.tsx | 2 +- .../components/IPAdapter/IPAImagePreview.tsx | 2 +- .../components/IPAdapter/IPAModelCombobox.tsx | 2 +- .../controlLayers/hooks/addLayerHooks.ts | 6 +- .../features/controlLayers/store/selectors.ts | 5 + .../SingleSelectionMenuItems.tsx | 1 + .../features/lora/components/LoRASelect.tsx | 2 +- .../src/features/metadata/util/recallers.ts | 2 +- .../util/graph/buildLinearBatchConfig.ts | 2 +- .../canvas/addControlNetToLinearGraph.ts | 2 +- .../graph/canvas/addIPAdapterToLinearGraph.ts | 2 +- .../graph/canvas/addSDXLRefinerToGraph.ts | 4 +- .../graph/canvas/addSeamlessToLinearGraph.ts | 2 +- .../canvas/addT2IAdapterToLinearGraph.ts | 2 +- .../nodes/util/graph/canvas/addVAEToGraph.ts | 4 +- .../util/graph/canvas/buildCanvasGraph.ts | 8 +- .../canvas/buildCanvasImageToImageGraph.ts | 2 +- .../graph/canvas/buildCanvasInpaintGraph.ts | 2 +- .../graph/canvas/buildCanvasOutpaintGraph.ts | 2 +- .../buildCanvasSDXLImageToImageGraph.ts | 4 +- .../canvas/buildCanvasSDXLInpaintGraph.ts | 4 +- .../canvas/buildCanvasSDXLOutpaintGraph.ts | 4 +- .../canvas/buildCanvasSDXLTextToImageGraph.ts | 4 +- .../canvas/buildCanvasTextToImageGraph.ts | 2 +- .../util/graph/generation/addControlLayers.ts | 4 +- .../nodes/util/graph/generation/addHRF.ts | 2 +- .../util/graph/generation/addSDXLRefiner.ts | 2 +- .../util/graph/generation/addSeamless.ts | 2 +- .../generation/buildGenerationTabGraph.ts | 2 +- .../generation/buildGenerationTabSDXLGraph.ts | 4 +- .../Advanced/ParamCFGRescaleMultiplier.tsx | 4 +- .../components/Advanced/ParamClipSkip.tsx | 6 +- .../BoundingBox/ParamBoundingBoxHeight.tsx | 2 +- .../BoundingBox/ParamBoundingBoxWidth.tsx | 2 +- .../ParamCanvasCoherenceEdgeSize.tsx | 4 +- .../ParamCanvasCoherenceMinDenoise.tsx | 4 +- .../ParamCanvasCoherenceMode.tsx | 4 +- .../MaskAdjustment/ParamMaskBlur.tsx | 4 +- .../ParamInfillColorOptions.tsx | 10 +- .../InfillAndScaling/ParamInfillMethod.tsx | 4 +- .../ParamInfillMosaicOptions.tsx | 115 -------- .../InfillAndScaling/ParamInfillOptions.tsx | 7 +- .../ParamInfillPatchmatchDownscaleSize.tsx | 6 +- .../InfillAndScaling/ParamInfillTilesize.tsx | 6 +- .../ParamScaleBeforeProcessing.tsx | 4 +- .../InfillAndScaling/ParamScaledHeight.tsx | 6 +- .../InfillAndScaling/ParamScaledWidth.tsx | 6 +- .../Canvas/ParamImageToImageStrength.tsx | 4 +- .../components/Core/ParamCFGScale.tsx | 4 +- .../components/Core/ParamHeight.tsx | 2 +- .../components/Core/ParamNegativePrompt.tsx | 4 +- .../components/Core/ParamPositivePrompt.tsx | 6 +- .../components/Core/ParamScheduler.tsx | 4 +- .../parameters/components/Core/ParamSteps.tsx | 4 +- .../parameters/components/Core/ParamWidth.tsx | 2 +- .../components/ImageSize/ImageSizeContext.ts | 2 +- .../ImageSize/SetOptimalSizeButton.tsx | 2 +- .../MainModel/ParamMainModelSelect.tsx | 6 +- .../MainModel/UseDefaultSettingsButton.tsx | 2 +- .../Seamless/ParamSeamlessXAxis.tsx | 4 +- .../Seamless/ParamSeamlessYAxis.tsx | 4 +- .../components/Seed/ParamSeedNumberInput.tsx | 6 +- .../components/Seed/ParamSeedRandomize.tsx | 4 +- .../components/Seed/ParamSeedShuffle.tsx | 4 +- .../VAEModel/ParamVAEModelSelect.tsx | 11 +- .../components/VAEModel/ParamVAEPrecision.tsx | 4 +- .../parameters/hooks/usePreselectedImage.ts | 2 +- .../parameters/store/generationSlice.ts | 264 ------------------ .../src/features/parameters/store/types.ts | 59 ---- .../features/prompt/PromptTriggerSelect.tsx | 10 +- .../queue/components/QueueButtonTooltip.tsx | 2 +- .../components/QueueIterationsNumberInput.tsx | 4 +- .../ParamSDXLNegativeStylePrompt.tsx | 4 +- .../ParamSDXLPositiveStylePrompt.tsx | 4 +- .../SDXLPrompts/SDXLConcatButton.tsx | 4 +- .../SDXLRefiner/ParamSDXLRefinerCFGScale.tsx | 2 +- ...ParamSDXLRefinerNegativeAestheticScore.tsx | 2 +- ...ParamSDXLRefinerPositiveAestheticScore.tsx | 2 +- .../SDXLRefiner/ParamSDXLRefinerScheduler.tsx | 2 +- .../SDXLRefiner/ParamSDXLRefinerStart.tsx | 2 +- .../SDXLRefiner/ParamSDXLRefinerSteps.tsx | 2 +- .../web/src/features/sdxl/store/sdxlSlice.ts | 86 ------ .../AdvancedSettingsAccordion.tsx | 4 +- .../ImageSettingsAccordion.tsx | 2 +- .../RefinerSettingsAccordion.tsx | 2 +- .../SettingsModal/SettingsModal.tsx | 4 +- .../ParametersPanelTextToImage.tsx | 6 +- .../api/hooks/useSelectedModelConfig.ts | 2 +- 100 files changed, 176 insertions(+), 727 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillMosaicOptions.tsx delete mode 100644 invokeai/frontend/web/src/features/parameters/store/generationSlice.ts delete mode 100644 invokeai/frontend/web/src/features/parameters/store/types.ts delete mode 100644 invokeai/frontend/web/src/features/sdxl/store/sdxlSlice.ts diff --git a/invokeai/frontend/web/.storybook/ReduxInit.tsx b/invokeai/frontend/web/.storybook/ReduxInit.tsx index d50d52754c2..ede6b5f34ad 100644 --- a/invokeai/frontend/web/.storybook/ReduxInit.tsx +++ b/invokeai/frontend/web/.storybook/ReduxInit.tsx @@ -1,5 +1,5 @@ import { PropsWithChildren, memo, useEffect } from 'react'; -import { modelChanged } from '../src/features/parameters/store/generationSlice'; +import { modelChanged } from '../src/features/controlLayers/store/canvasV2Slice'; import { useAppDispatch } from '../src/app/store/storeHooks'; import { useGlobalModifiersInit } from '@invoke-ai/ui-library'; /** diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appConfigReceived.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appConfigReceived.ts index 4ee73af6423..080c4dff92b 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appConfigReceived.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appConfigReceived.ts @@ -1,5 +1,5 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { setInfillMethod } from 'features/parameters/store/generationSlice'; +import { setInfillMethod } from 'features/canvas/store/canvasSlice'; import { shouldUseNSFWCheckerChanged, shouldUseWatermarkerChanged } from 'features/system/store/systemSlice'; import { appInfoApi } from 'services/api/endpoints/appInfo'; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index 6ca7ee7ffa9..6da0b82dc3b 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -13,7 +13,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) effect: async (action, { getState, dispatch }) => { const state = getState(); const { shouldShowProgressInViewer } = state.ui; - const model = state.generation.model; + const model = state.canvasV2.params.model; const { prepend } = action.payload; let graph; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts index c4087aacded..3f5e1333b94 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts @@ -29,7 +29,7 @@ export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) = batch: { graph, workflow: builtWorkflow, - runs: state.generation.iterations, + runs: state.canvasV2.params.iterations, }, prepend: action.payload.prepend, }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts index 239a5b863d8..3ec87b6225d 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts @@ -6,7 +6,7 @@ import { } from 'features/controlAdapters/store/controlAdaptersSlice'; import { loraRemoved } from 'features/lora/store/loraSlice'; import { modelSelected } from 'features/parameters/store/actions'; -import { modelChanged, vaeSelected } from 'features/parameters/store/generationSlice'; +import { modelChanged, vaeSelected } from 'features/canvas/store/canvasSlice'; import { zParameterModel } from 'features/parameters/types/parameterSchemas'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; @@ -29,7 +29,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = const newModel = result.data; const newBaseModel = newModel.base; - const didBaseModelChange = state.generation.model?.base !== newBaseModel; + const didBaseModelChange = state.canvasV2.params.model?.base !== newBaseModel; if (didBaseModelChange) { // we may need to reset some incompatible submodels @@ -44,7 +44,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = }); // handle incompatible vae - const { vae } = state.generation; + const { vae } = state.canvasV2.params; if (vae && vae.base !== newBaseModel) { dispatch(vaeSelected(null)); modelsCleared += 1; @@ -70,7 +70,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = } } - dispatch(modelChanged(newModel, state.generation.model)); + dispatch(modelChanged(newModel, state.canvasV2.params.model)); }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts index 9238c5ddb14..a7723c57751 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts @@ -3,17 +3,18 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware' import type { AppDispatch, RootState } from 'app/store/store'; import type { JSONObject } from 'common/types'; import { - controlAdapterModelCleared, - selectControlAdapterAll, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { heightChanged, widthChanged } from 'features/controlLayers/store/canvasV2Slice'; + caModelChanged, + heightChanged, + modelChanged, + refinerModelChanged, + vaeSelected, + widthChanged, +} from 'features/controlLayers/store/canvasV2Slice'; import { loraRemoved } from 'features/lora/store/loraSlice'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; -import { modelChanged, vaeSelected } from 'features/parameters/store/generationSlice'; import { postProcessingModelChanged, upscaleModelChanged } from 'features/parameters/store/upscaleSlice'; import { zParameterModel, zParameterVAEModel } from 'features/parameters/types/parameterSchemas'; import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension'; -import { refinerModelChanged } from 'features/sdxl/store/sdxlSlice'; import { forEach } from 'lodash-es'; import type { Logger } from 'roarr'; import { modelConfigsAdapterSelectors, modelsApi } from 'services/api/endpoints/models'; @@ -55,11 +56,11 @@ type ModelHandler = ( ) => undefined; const handleMainModels: ModelHandler = (models, state, dispatch, log) => { - const currentModel = state.generation.model; + const currentModel = state.canvasV2.params.model; const mainModels = models.filter(isNonRefinerMainModelConfig); if (mainModels.length === 0) { // No models loaded at all - dispatch(modelChanged(null)); + dispatch(modelChanged({ model: null })); return; } @@ -74,16 +75,10 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => { if (defaultModelInList) { const result = zParameterModel.safeParse(defaultModelInList); if (result.success) { - dispatch(modelChanged(defaultModelInList, currentModel)); + dispatch(modelChanged({ model: defaultModelInList, previousModel: currentModel ?? undefined })); const optimalDimension = getOptimalDimension(defaultModelInList); - if ( - getIsSizeOptimal( - state.canvasV2.document.width, - state.canvasV2.document.height, - optimalDimension - ) - ) { + if (getIsSizeOptimal(state.canvasV2.document.width, state.canvasV2.document.height, optimalDimension)) { return; } const { width, height } = calculateNewSize( @@ -104,11 +99,11 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => { return; } - dispatch(modelChanged(result.data, currentModel)); + dispatch(modelChanged({ model: result.data, previousModel: currentModel ?? undefined })); }; const handleRefinerModels: ModelHandler = (models, state, dispatch, _log) => { - const currentRefinerModel = state.sdxl.refinerModel; + const currentRefinerModel = state.canvasV2.params.refinerModel; const refinerModels = models.filter(isRefinerMainModelModelConfig); if (models.length === 0) { // No models loaded at all @@ -127,7 +122,7 @@ const handleRefinerModels: ModelHandler = (models, state, dispatch, _log) => { }; const handleVAEModels: ModelHandler = (models, state, dispatch, log) => { - const currentVae = state.generation.vae; + const currentVae = state.canvasV2.params.vae; if (currentVae === null) { // null is a valid VAE! it means "use the default with the main model" @@ -174,14 +169,14 @@ const handleLoRAModels: ModelHandler = (models, state, dispatch, _log) => { }; const handleControlAdapterModels: ModelHandler = (models, state, dispatch, _log) => { - selectControlAdapterAll(state.controlAdapters).forEach((ca) => { + state.canvasV2.controlAdapters.forEach((ca) => { const isModelAvailable = models.some((m) => m.key === ca.model?.key); if (isModelAvailable) { return; } - dispatch(controlAdapterModelCleared({ id: ca.id })); + dispatch(caModelChanged({ id: ca.id, modelConfig: null })); }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts index c706b55c7da..a89a1351032 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts @@ -8,7 +8,7 @@ import { setSteps, vaePrecisionChanged, vaeSelected, -} from 'features/parameters/store/generationSlice'; +} from 'features/canvas/store/canvasSlice'; import { isParameterCFGRescaleMultiplier, isParameterCFGScale, @@ -30,7 +30,7 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni effect: async (action, { dispatch, getState }) => { const state = getState(); - const currentModel = state.generation.model; + const currentModel = state.canvasV2.params.model; if (!currentModel) { return; diff --git a/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts b/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts index 5b57fcd2bbd..d620c72eaf2 100644 --- a/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts +++ b/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts @@ -32,7 +32,7 @@ export const useGroupedModelCombobox = ( arg: UseGroupedModelComboboxArg ): UseGroupedModelComboboxReturn => { const { t } = useTranslation(); - const base_model = useAppSelector((s) => s.generation.model?.base ?? 'sdxl'); + const base_model = useAppSelector((s) => s.canvasV2.params.model?.base ?? 'sdxl'); const { modelConfigs, selectedModel, getIsDisabled, onChange, isLoading, groupByType = false } = arg; const options = useMemo[]>(() => { if (!modelConfigs) { diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index d61e3167456..f0f83ab986e 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -9,7 +9,6 @@ import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; import type { Templates } from 'features/nodes/store/types'; import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice'; import { isInvocationNode } from 'features/nodes/types/invocation'; -import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; import { selectUpscalelice } from 'features/parameters/store/upscaleSlice'; import { selectConfigSlice } from 'features/system/store/configSlice'; import { selectSystemSlice } from 'features/system/store/systemSlice'; @@ -30,7 +29,6 @@ const LAYER_TYPE_TO_TKEY: Record = { const createSelector = (templates: Templates) => createMemoizedSelector( [ - selectGenerationSlice, selectSystemSlice, selectNodesSlice, selectWorkflowSettingsSlice, @@ -40,9 +38,9 @@ const createSelector = (templates: Templates) => selectConfigSlice, activeTabNameSelector, ], - (generation, system, nodes, workflowSettings, dynamicPrompts, canvasV2, upscale, config, activeTabName) => { - const { model, positivePrompt } = generation; + (system, nodes, workflowSettings, dynamicPrompts, canvasV2, upscale, config, activeTabName) => { const { bbox } = canvasV2; + const { model, positivePrompt } = canvasV2.params; const { isConnected } = system; diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx index 98ca55351cc..4650506a429 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx @@ -17,8 +17,8 @@ import { setShouldSnapToGrid, } from 'features/canvas/store/canvasSlice'; import { CANVAS_GRID_SIZE_COARSE, CANVAS_GRID_SIZE_FINE } from 'features/canvas/store/constants'; +import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import type Konva from 'konva'; import type { GroupConfig } from 'konva/lib/Group'; import type { KonvaEventObject } from 'konva/lib/Node'; diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts index fbb63781662..e06caea3574 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts @@ -3,6 +3,7 @@ import { createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { deepClone } from 'common/util/deepClone'; import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple'; +import { modelChanged } from 'features/canvas/store/canvasSlice'; import calculateCoordinates from 'features/canvas/util/calculateCoordinates'; import calculateScale from 'features/canvas/util/calculateScale'; import { STAGE_PADDING_PERCENTAGE } from 'features/canvas/util/constants'; @@ -11,7 +12,6 @@ import getScaledBoundingBoxDimensions from 'features/canvas/util/getScaledBoundi import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; -import { modelChanged } from 'features/parameters/store/generationSlice'; import type { PayloadActionWithOptimalDimension } from 'features/parameters/store/types'; import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension'; import type { IRect, Vector2d } from 'konva/lib/types'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx index fc574fbb045..2fe84170b32 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx @@ -4,10 +4,10 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; import { heightChanged, widthChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import type { ControlAdapterData } from 'features/controlLayers/store/types'; import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowCounterClockwiseBold, PiFloppyDiskBold, PiRulerBold } from 'react-icons/pi'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAModelCombobox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAModelCombobox.tsx index 9b6a5ad9dbf..703caf64c1e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAModelCombobox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAModelCombobox.tsx @@ -13,7 +13,7 @@ type Props = { export const CAModelCombobox = memo(({ modelKey, onChange: onChangeModel }: Props) => { const { t } = useTranslation(); - const currentBaseModel = useAppSelector((s) => s.generation.model?.base); + const currentBaseModel = useAppSelector((s) => s.canvasV2.params.model?.base); const [modelConfigs, { isLoading }] = useControlNetAndT2IAdapterModels(); const selectedModel = useMemo(() => modelConfigs.find((m) => m.key === modelKey), [modelConfigs, modelKey]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx index 71018b5cc65..c2b3ec48d9c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx @@ -7,7 +7,7 @@ import { heightChanged, widthChanged } from 'features/controlLayers/store/canvas import type { ImageWithDims } from 'features/controlLayers/store/types'; import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; +import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { memo, useCallback, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAModelCombobox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAModelCombobox.tsx index 08c3faeb2db..34e215dde7b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAModelCombobox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAModelCombobox.tsx @@ -24,7 +24,7 @@ type Props = { export const IPAModelCombobox = memo(({ modelKey, onChangeModel, clipVisionModel, onChangeCLIPVisionModel }: Props) => { const { t } = useTranslation(); - const currentBaseModel = useAppSelector((s) => s.generation.model?.base); + const currentBaseModel = useAppSelector((s) => s.canvasV2.params.model?.base); const [modelConfigs, { isLoading }] = useIPAdapterModels(); const selectedModel = useMemo(() => modelConfigs.find((m) => m.key === modelKey), [modelConfigs, modelKey]); diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts index 8d3def70b1a..7c2b1813d7e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts @@ -15,7 +15,7 @@ import { v4 as uuidv4 } from 'uuid'; export const useAddCALayer = () => { const dispatch = useAppDispatch(); - const baseModel = useAppSelector((s) => s.generation.model?.base); + const baseModel = useAppSelector((s) => s.canvasV2.params.model?.base); const [modelConfigs] = useControlNetAndT2IAdapterModels(); const model: ControlNetModelConfig | T2IAdapterModelConfig | null = useMemo(() => { // prefer to use a model that matches the base model @@ -48,7 +48,7 @@ export const useAddCALayer = () => { export const useAddIPALayer = () => { const dispatch = useAppDispatch(); - const baseModel = useAppSelector((s) => s.generation.model?.base); + const baseModel = useAppSelector((s) => s.canvasV2.params.model?.base); const [modelConfigs] = useIPAdapterModels(); const model: IPAdapterModelConfig | null = useMemo(() => { // prefer to use a model that matches the base model @@ -72,7 +72,7 @@ export const useAddIPALayer = () => { export const useAddIPAdapterToRGLayer = (id: string) => { const dispatch = useAppDispatch(); - const baseModel = useAppSelector((s) => s.generation.model?.base); + const baseModel = useAppSelector((s) => s.canvasV2.params.model?.base); const [modelConfigs] = useIPAdapterModels(); const model: IPAdapterModelConfig | null = useMemo(() => { // prefer to use a model that matches the base model diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts index 2269f393f32..d57fee3d49e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts @@ -1,8 +1,13 @@ import { createSelector } from '@reduxjs/toolkit'; import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; +import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; export const selectEntityCount = createSelector(selectCanvasSlice, (canvasV2) => { return ( canvasV2.regions.length + canvasV2.controlAdapters.length + canvasV2.ipAdapters.length + canvasV2.layers.length ); }); + +export const selectOptimalDimension = createSelector(selectCanvasSlice, (canvasV2) => { + return getOptimalDimension(canvasV2.params.model); +}); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx index b2b1f9eff0f..6af1aaf8b2b 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx @@ -7,6 +7,7 @@ import { useDownloadImage } from 'common/hooks/useDownloadImage'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; import { imagesToChangeSelected, isModalOpenChanged } from 'features/changeBoardModal/store/slice'; import { iiLayerAdded } from 'features/controlLayers/store/canvasV2Slice'; +import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice'; import { useImageActions } from 'features/gallery/hooks/useImageActions'; import { sentImageToCanvas, sentImageToImg2Img } from 'features/gallery/store/actions'; diff --git a/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx b/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx index 4209784725c..3c3e0375e24 100644 --- a/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx +++ b/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx @@ -17,7 +17,7 @@ const LoRASelect = () => { const [modelConfigs, { isLoading }] = useLoRAModels(); const { t } = useTranslation(); const addedLoRAs = useAppSelector(selectAddedLoRAs); - const currentBaseModel = useAppSelector((s) => s.generation.model?.base); + const currentBaseModel = useAppSelector((s) => s.canvasV2.params.model?.base); const getIsDisabled = (lora: LoRAModelConfig): boolean => { const isCompatible = currentBaseModel === lora.base; diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index 70027d0c975..7de1d31a878 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -40,7 +40,7 @@ import { setSeed, setSteps, vaeSelected, -} from 'features/parameters/store/generationSlice'; +} from 'features/canvas/store/canvasSlice'; import type { ParameterCFGRescaleMultiplier, ParameterCFGScale, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts index 268ec553517..64170041982 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts @@ -9,7 +9,7 @@ import { getHasMetadata, removeMetadata } from './canvas/metadata'; import { CANVAS_COHERENCE_NOISE, METADATA, NOISE, POSITIVE_CONDITIONING } from './constants'; export const prepareLinearUIBatch = (state: RootState, graph: NonNullableGraph, prepend: boolean): BatchConfig => { - const { iterations, model, shouldRandomizeSeed, seed } = state.generation; + const { iterations, model, shouldRandomizeSeed, seed } = state.canvasV2.params; const { shouldConcatPrompts } = state.canvasV2; const { prompts, seedBehaviour } = state.dynamicPrompts; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addControlNetToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addControlNetToLinearGraph.ts index 110a20e5a75..4558b5a0d82 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addControlNetToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addControlNetToLinearGraph.ts @@ -17,7 +17,7 @@ export const addControlNetToLinearGraph = async ( const controlNets = selectValidControlNets(state.controlAdapters).filter( ({ model, processedControlImage, processorType, controlImage, isEnabled }) => { const hasModel = Boolean(model); - const doesBaseMatch = model?.base === state.generation.model?.base; + const doesBaseMatch = model?.base === state.canvasV2.params.model?.base; const hasControlImage = (processedControlImage && processorType !== 'none') || controlImage; return isEnabled && hasModel && doesBaseMatch && hasControlImage; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addIPAdapterToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addIPAdapterToLinearGraph.ts index 1f244634193..fc279c2c874 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addIPAdapterToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addIPAdapterToLinearGraph.ts @@ -19,7 +19,7 @@ export const addIPAdapterToLinearGraph = async ( const ipAdapters = selectValidIPAdapters(state.controlAdapters).filter(({ model, controlImage, isEnabled }) => { const hasModel = Boolean(model); - const doesBaseMatch = model?.base === state.generation.model?.base; + const doesBaseMatch = model?.base === state.canvasV2.params.model?.base; const hasControlImage = controlImage; return isEnabled && hasModel && doesBaseMatch && hasControlImage; }); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSDXLRefinerToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSDXLRefinerToGraph.ts index 7809744afaf..4a8572a69ff 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSDXLRefinerToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSDXLRefinerToGraph.ts @@ -34,13 +34,13 @@ export const addSDXLRefinerToGraph = async ( refinerScheduler, refinerCFGScale, refinerStart, - } = state.sdxl; + } = state.canvasV2.params; if (!refinerModel) { return; } - const { seamlessXAxis, seamlessYAxis } = state.generation; + const { seamlessXAxis, seamlessYAxis } = state.canvasV2.params; const { boundingBoxScaleMethod } = state.canvas; const isUsingScaledDimensions = ['auto', 'manual'].includes(boundingBoxScaleMethod); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSeamlessToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSeamlessToLinearGraph.ts index 357b3357e29..d4fb8647262 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSeamlessToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSeamlessToLinearGraph.ts @@ -19,7 +19,7 @@ export const addSeamlessToLinearGraph = ( modelLoaderNodeId: string ): void => { // Remove Existing UNet Connections - const { seamlessXAxis, seamlessYAxis, vae } = state.generation; + const { seamlessXAxis, seamlessYAxis, vae } = state.canvasV2.params; const isAutoVae = !vae; graph.nodes[SEAMLESS] = { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addT2IAdapterToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addT2IAdapterToLinearGraph.ts index 72cf9ca0f8b..cd142580b32 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addT2IAdapterToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addT2IAdapterToLinearGraph.ts @@ -20,7 +20,7 @@ export const addT2IAdaptersToLinearGraph = async ( const t2iAdapters = selectValidT2IAdapters(state.controlAdapters).filter( ({ model, processedControlImage, processorType, controlImage, isEnabled }) => { const hasModel = Boolean(model); - const doesBaseMatch = model?.base === state.generation.model?.base; + const doesBaseMatch = model?.base === state.canvasV2.params.model?.base; const hasControlImage = (processedControlImage && processorType !== 'none') || controlImage; return isEnabled && hasModel && doesBaseMatch && hasControlImage; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addVAEToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addVAEToGraph.ts index dfa3818d070..00129ac4b33 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addVAEToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addVAEToGraph.ts @@ -28,9 +28,9 @@ export const addVAEToGraph = async ( graph: NonNullableGraph, modelLoaderNodeId: string = MAIN_MODEL_LOADER ): Promise => { - const { vae, seamlessXAxis, seamlessYAxis } = state.generation; + const { vae, seamlessXAxis, seamlessYAxis } = state.canvasV2.params; const { boundingBoxScaleMethod } = state.canvas; - const { refinerModel } = state.sdxl; + const { refinerModel } = state.canvasV2.params; const isUsingScaledDimensions = ['auto', 'manual'].includes(boundingBoxScaleMethod); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasGraph.ts index abf8b88773d..dca39772a55 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasGraph.ts @@ -19,7 +19,7 @@ export const buildCanvasGraph = async ( let graph: NonNullableGraph; if (generationMode === 'txt2img') { - if (state.generation.model && state.generation.model.base === 'sdxl') { + if (state.canvasV2.params.model && state.canvasV2.params.model.base === 'sdxl') { graph = await buildCanvasSDXLTextToImageGraph(state); } else { graph = await buildCanvasTextToImageGraph(state); @@ -28,7 +28,7 @@ export const buildCanvasGraph = async ( if (!canvasInitImage) { throw new Error('Missing canvas init image'); } - if (state.generation.model && state.generation.model.base === 'sdxl') { + if (state.canvasV2.params.model && state.canvasV2.params.model.base === 'sdxl') { graph = await buildCanvasSDXLImageToImageGraph(state, canvasInitImage); } else { graph = await buildCanvasImageToImageGraph(state, canvasInitImage); @@ -37,7 +37,7 @@ export const buildCanvasGraph = async ( if (!canvasInitImage || !canvasMaskImage) { throw new Error('Missing canvas init and mask images'); } - if (state.generation.model && state.generation.model.base === 'sdxl') { + if (state.canvasV2.params.model && state.canvasV2.params.model.base === 'sdxl') { graph = await buildCanvasSDXLInpaintGraph(state, canvasInitImage, canvasMaskImage); } else { graph = await buildCanvasInpaintGraph(state, canvasInitImage, canvasMaskImage); @@ -46,7 +46,7 @@ export const buildCanvasGraph = async ( if (!canvasInitImage) { throw new Error('Missing canvas init image'); } - if (state.generation.model && state.generation.model.base === 'sdxl') { + if (state.canvasV2.params.model && state.canvasV2.params.model.base === 'sdxl') { graph = await buildCanvasSDXLOutpaintGraph(state, canvasInitImage, canvasMaskImage); } else { graph = await buildCanvasOutpaintGraph(state, canvasInitImage, canvasMaskImage); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasImageToImageGraph.ts index 3d17da9b4a9..5d674dba910 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasImageToImageGraph.ts @@ -54,7 +54,7 @@ export const buildCanvasImageToImageGraph = async ( shouldUseCpuNoise, seamlessXAxis, seamlessYAxis, - } = state.generation; + } = state.canvasV2.params; // The bounding box determines width and height, not the width and height params const { width, height } = state.canvas.boundingBoxDimensions; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasInpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasInpaintGraph.ts index 39cdbb31cd5..dfd69e202d4 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasInpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasInpaintGraph.ts @@ -61,7 +61,7 @@ export const buildCanvasInpaintGraph = async ( canvasCoherenceMinDenoise, canvasCoherenceEdgeSize, maskBlur, - } = state.generation; + } = state.canvasV2.params; if (!model) { log.error('No model found in state'); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasOutpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasOutpaintGraph.ts index ce8f86223f2..1fa6b83e088 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasOutpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasOutpaintGraph.ts @@ -73,7 +73,7 @@ export const buildCanvasOutpaintGraph = async ( canvasCoherenceMinDenoise, canvasCoherenceEdgeSize, maskBlur, - } = state.generation; + } = state.canvasV2.params; if (!model) { log.error('No model found in state'); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLImageToImageGraph.ts index bcf2f8a93c1..ddc2b11855b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLImageToImageGraph.ts @@ -54,9 +54,9 @@ export const buildCanvasSDXLImageToImageGraph = async ( seamlessXAxis, seamlessYAxis, img2imgStrength: strength, - } = state.generation; + } = state.canvasV2.params; - const { refinerModel, refinerStart } = state.sdxl; + const { refinerModel, refinerStart } = state.canvasV2.params; // The bounding box determines width and height, not the width and height params const { width, height } = state.canvas.boundingBoxDimensions; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLInpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLInpaintGraph.ts index e56e4d0ddb9..3ecf2a02c06 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLInpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLInpaintGraph.ts @@ -61,9 +61,9 @@ export const buildCanvasSDXLInpaintGraph = async ( canvasCoherenceMinDenoise, canvasCoherenceEdgeSize, maskBlur, - } = state.generation; + } = state.canvasV2.params; - const { refinerModel, refinerStart } = state.sdxl; + const { refinerModel, refinerStart } = state.canvasV2.params; if (!model) { log.error('No model found in state'); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLOutpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLOutpaintGraph.ts index 095918ab71c..1beadc5a96f 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLOutpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLOutpaintGraph.ts @@ -73,9 +73,9 @@ export const buildCanvasSDXLOutpaintGraph = async ( canvasCoherenceMinDenoise, canvasCoherenceEdgeSize, maskBlur, - } = state.generation; + } = state.canvasV2.params; - const { refinerModel, refinerStart } = state.sdxl; + const { refinerModel, refinerStart } = state.canvasV2.params; if (!model) { log.error('No model found in state'); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLTextToImageGraph.ts index 2b37255070c..b75dd210d63 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLTextToImageGraph.ts @@ -47,7 +47,7 @@ export const buildCanvasSDXLTextToImageGraph = async (state: RootState): Promise shouldUseCpuNoise, seamlessXAxis, seamlessYAxis, - } = state.generation; + } = state.canvasV2.params; // The bounding box determines width and height, not the width and height params const { width, height } = state.canvas.boundingBoxDimensions; @@ -58,7 +58,7 @@ export const buildCanvasSDXLTextToImageGraph = async (state: RootState): Promise const is_intermediate = true; const isUsingScaledDimensions = ['auto', 'manual'].includes(boundingBoxScaleMethod); - const { refinerModel, refinerStart } = state.sdxl; + const { refinerModel, refinerStart } = state.canvasV2.params; if (!model) { log.error('No model found in state'); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasTextToImageGraph.ts index 352dc0771db..045a83f6ea9 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasTextToImageGraph.ts @@ -47,7 +47,7 @@ export const buildCanvasTextToImageGraph = async (state: RootState): Promise, layer: InitialImageLayer ) => { - const { vaePrecision } = state.generation; - const { refinerModel, refinerStart } = state.sdxl; + const { vaePrecision } = state.canvasV2.params; + const { refinerModel, refinerStart } = state.canvasV2.params; const { width, height } = state.canvasV2.document; assert(layer.isEnabled, 'Initial image layer is not enabled'); assert(layer.image, 'Initial image layer has no image'); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addHRF.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addHRF.ts index 4b38fc20c63..3faa9eccae5 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addHRF.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addHRF.ts @@ -12,7 +12,7 @@ import { } from 'features/nodes/util/graph/constants'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { getBoardField } from 'features/nodes/util/graph/graphBuilderUtils'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; +import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import type { Invocation } from 'services/api/types'; /** diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLRefiner.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLRefiner.ts index caab153b603..f92e3cf7f89 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLRefiner.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLRefiner.ts @@ -30,7 +30,7 @@ export const addSDXLRefiner = async ( refinerScheduler, refinerCFGScale, refinerStart, - } = state.sdxl; + } = state.canvasV2.params; assert(refinerModel, 'No refiner model found in state'); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSeamless.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSeamless.ts index 25a3e7e3ac3..3fdcfbe28e3 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSeamless.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSeamless.ts @@ -21,7 +21,7 @@ export const addSeamless = ( modelLoader: Invocation<'main_model_loader'> | Invocation<'sdxl_model_loader'>, vaeLoader: Invocation<'vae_loader'> | null ): Invocation<'seamless'> | null => { - const { seamlessXAxis: seamless_x, seamlessYAxis: seamless_y } = state.generation; + const { seamlessXAxis: seamless_x, seamlessYAxis: seamless_y } = state.canvasV2.params; if (!seamless_x && !seamless_y) { return null; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts index f22d0e3f8d5..088ba0301be 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts @@ -39,7 +39,7 @@ export const buildGenerationTabGraph = async (state: RootState): Promise { - const cfgRescaleMultiplier = useAppSelector((s) => s.generation.cfgRescaleMultiplier); + const cfgRescaleMultiplier = useAppSelector((s) => s.canvasV2.params.cfgRescaleMultiplier); const initial = useAppSelector((s) => s.config.sd.cfgRescaleMultiplier.initial); const sliderMin = useAppSelector((s) => s.config.sd.cfgRescaleMultiplier.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.cfgRescaleMultiplier.sliderMax); diff --git a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamClipSkip.tsx b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamClipSkip.tsx index c23d5416137..25649581334 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamClipSkip.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamClipSkip.tsx @@ -1,19 +1,19 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setClipSkip } from 'features/parameters/store/generationSlice'; +import { setClipSkip } from 'features/controlLayers/store/canvasV2Slice'; import { CLIP_SKIP_MAP } from 'features/parameters/types/constants'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamClipSkip = () => { - const clipSkip = useAppSelector((s) => s.generation.clipSkip); + const clipSkip = useAppSelector((s) => s.canvasV2.params.clipSkip); const initial = useAppSelector((s) => s.config.sd.clipSkip.initial); const sliderMin = useAppSelector((s) => s.config.sd.clipSkip.sliderMin); const numberInputMin = useAppSelector((s) => s.config.sd.clipSkip.numberInputMin); const coarseStep = useAppSelector((s) => s.config.sd.clipSkip.coarseStep); const fineStep = useAppSelector((s) => s.config.sd.clipSkip.fineStep); - const { model } = useAppSelector((s) => s.generation); + const model = useAppSelector((s) => s.canvasV2.params.model); const dispatch = useAppDispatch(); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxHeight.tsx index bc9d389cc5a..538337944e9 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxHeight.tsx @@ -1,8 +1,8 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; +import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxWidth.tsx index bc57074e35a..64a99175c79 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxWidth.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxWidth.tsx @@ -2,7 +2,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@ import { useAppSelector } from 'app/store/storeHooks'; import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; +import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceEdgeSize.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceEdgeSize.tsx index c39ad2916ea..63826ca71cf 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceEdgeSize.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceEdgeSize.tsx @@ -1,13 +1,13 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setCanvasCoherenceEdgeSize } from 'features/parameters/store/generationSlice'; +import { setCanvasCoherenceEdgeSize } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamCanvasCoherenceEdgeSize = () => { const dispatch = useAppDispatch(); - const canvasCoherenceEdgeSize = useAppSelector((s) => s.generation.canvasCoherenceEdgeSize); + const canvasCoherenceEdgeSize = useAppSelector((s) => s.canvasV2.compositing.canvasCoherenceEdgeSize); const initial = useAppSelector((s) => s.config.sd.canvasCoherenceEdgeSize.initial); const sliderMin = useAppSelector((s) => s.config.sd.canvasCoherenceEdgeSize.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.canvasCoherenceEdgeSize.sliderMax); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMinDenoise.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMinDenoise.tsx index 00cdb5070be..841ac727baa 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMinDenoise.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMinDenoise.tsx @@ -1,13 +1,13 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setCanvasCoherenceMinDenoise } from 'features/parameters/store/generationSlice'; +import { setCanvasCoherenceMinDenoise } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamCanvasCoherenceMinDenoise = () => { const dispatch = useAppDispatch(); - const canvasCoherenceMinDenoise = useAppSelector((s) => s.generation.canvasCoherenceMinDenoise); + const canvasCoherenceMinDenoise = useAppSelector((s) => s.canvasV2.compositing.canvasCoherenceMinDenoise); const { t } = useTranslation(); const handleChange = useCallback( diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMode.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMode.tsx index fad6149a1fb..376756285fe 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMode.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMode.tsx @@ -2,14 +2,14 @@ import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setCanvasCoherenceMode } from 'features/parameters/store/generationSlice'; +import { setCanvasCoherenceMode } from 'features/controlLayers/store/canvasV2Slice'; import { isParameterCanvasCoherenceMode } from 'features/parameters/types/parameterSchemas'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamCanvasCoherenceMode = () => { const dispatch = useAppDispatch(); - const canvasCoherenceMode = useAppSelector((s) => s.generation.canvasCoherenceMode); + const canvasCoherenceMode = useAppSelector((s) => s.canvasV2.compositing.canvasCoherenceMode); const { t } = useTranslation(); const options = useMemo( diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/MaskAdjustment/ParamMaskBlur.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/MaskAdjustment/ParamMaskBlur.tsx index 2218002449f..9bc7150d0f2 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/MaskAdjustment/ParamMaskBlur.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/MaskAdjustment/ParamMaskBlur.tsx @@ -1,14 +1,14 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setMaskBlur } from 'features/parameters/store/generationSlice'; +import { setMaskBlur } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamMaskBlur = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const maskBlur = useAppSelector((s) => s.generation.maskBlur); + const maskBlur = useAppSelector((s) => s.canvasV2.compositing.maskBlur); const initial = useAppSelector((s) => s.config.sd.maskBlur.initial); const sliderMin = useAppSelector((s) => s.config.sd.maskBlur.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.maskBlur.sliderMax); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillColorOptions.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillColorOptions.tsx index be173ca6ca5..0e65ceb06f9 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillColorOptions.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillColorOptions.tsx @@ -1,20 +1,16 @@ import { Box, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIColorPicker from 'common/components/IAIColorPicker'; -import { selectGenerationSlice, setInfillColorValue } from 'features/parameters/store/generationSlice'; +import { setInfillColorValue } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import type { RgbaColor } from 'react-colorful'; import { useTranslation } from 'react-i18next'; -const selectInfillColor = createMemoizedSelector(selectGenerationSlice, (generation) => generation.infillColorValue); - const ParamInfillColorOptions = () => { const dispatch = useAppDispatch(); - const infillColor = useAppSelector(selectInfillColor); - - const infillMethod = useAppSelector((s) => s.generation.infillMethod); + const infillColor = useAppSelector((s) => s.canvasV2.compositing.infillColorValue); + const infillMethod = useAppSelector((s) => s.canvasV2.compositing.infillMethod); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillMethod.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillMethod.tsx index 0414bf6a258..f66ae697184 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillMethod.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillMethod.tsx @@ -2,7 +2,7 @@ import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setInfillMethod } from 'features/parameters/store/generationSlice'; +import { setInfillMethod } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useGetAppConfigQuery } from 'services/api/endpoints/appInfo'; @@ -10,7 +10,7 @@ import { useGetAppConfigQuery } from 'services/api/endpoints/appInfo'; const ParamInfillMethod = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const infillMethod = useAppSelector((s) => s.generation.infillMethod); + const infillMethod = useAppSelector((s) => s.canvasV2.compositing.infillMethod); const { data: appConfigData } = useGetAppConfigQuery(); const options = useMemo( () => diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillMosaicOptions.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillMosaicOptions.tsx deleted file mode 100644 index cfdd7fb010c..00000000000 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillMosaicOptions.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { Box, CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIColorPicker from 'common/components/IAIColorPicker'; -import { - setInfillMosaicMaxColor, - setInfillMosaicMinColor, - setInfillMosaicTileHeight, - setInfillMosaicTileWidth, -} from 'features/parameters/store/generationSlice'; -import { memo, useCallback } from 'react'; -import type { RgbaColor } from 'react-colorful'; -import { useTranslation } from 'react-i18next'; - -const ParamInfillMosaicTileSize = () => { - const dispatch = useAppDispatch(); - - const infillMosaicTileWidth = useAppSelector((s) => s.generation.infillMosaicTileWidth); - const infillMosaicTileHeight = useAppSelector((s) => s.generation.infillMosaicTileHeight); - const infillMosaicMinColor = useAppSelector((s) => s.generation.infillMosaicMinColor); - const infillMosaicMaxColor = useAppSelector((s) => s.generation.infillMosaicMaxColor); - const infillMethod = useAppSelector((s) => s.generation.infillMethod); - - const { t } = useTranslation(); - - const handleInfillMosaicTileWidthChange = useCallback( - (v: number) => { - dispatch(setInfillMosaicTileWidth(v)); - }, - [dispatch] - ); - - const handleInfillMosaicTileHeightChange = useCallback( - (v: number) => { - dispatch(setInfillMosaicTileHeight(v)); - }, - [dispatch] - ); - - const handleInfillMosaicMinColor = useCallback( - (v: RgbaColor) => { - dispatch(setInfillMosaicMinColor(v)); - }, - [dispatch] - ); - - const handleInfillMosaicMaxColor = useCallback( - (v: RgbaColor) => { - dispatch(setInfillMosaicMaxColor(v)); - }, - [dispatch] - ); - - return ( - - - {t('parameters.infillMosaicTileWidth')} - - - - - {t('parameters.infillMosaicTileHeight')} - - - - - {t('parameters.infillMosaicMinColor')} - - - - - - {t('parameters.infillMosaicMaxColor')} - - - - - - ); -}; - -export default memo(ParamInfillMosaicTileSize); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillOptions.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillOptions.tsx index 04e4727885c..9da4b0df1ae 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillOptions.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillOptions.tsx @@ -2,12 +2,11 @@ import { useAppSelector } from 'app/store/storeHooks'; import { memo } from 'react'; import ParamInfillColorOptions from './ParamInfillColorOptions'; -import ParamInfillMosaicOptions from './ParamInfillMosaicOptions'; import ParamInfillPatchmatchDownscaleSize from './ParamInfillPatchmatchDownscaleSize'; import ParamInfillTilesize from './ParamInfillTilesize'; const ParamInfillOptions = () => { - const infillMethod = useAppSelector((s) => s.generation.infillMethod); + const infillMethod = useAppSelector((s) => s.canvasV2.compositing.infillMethod); if (infillMethod === 'tile') { return ; } @@ -16,10 +15,6 @@ const ParamInfillOptions = () => { return ; } - if (infillMethod === 'mosaic') { - return ; - } - if (infillMethod === 'color') { return ; } diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillPatchmatchDownscaleSize.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillPatchmatchDownscaleSize.tsx index fbeed1a1be3..6589a72a37b 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillPatchmatchDownscaleSize.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillPatchmatchDownscaleSize.tsx @@ -1,14 +1,14 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setInfillPatchmatchDownscaleSize } from 'features/parameters/store/generationSlice'; +import { setInfillPatchmatchDownscaleSize } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamInfillPatchmatchDownscaleSize = () => { const dispatch = useAppDispatch(); - const infillMethod = useAppSelector((s) => s.generation.infillMethod); - const infillPatchmatchDownscaleSize = useAppSelector((s) => s.generation.infillPatchmatchDownscaleSize); + const infillMethod = useAppSelector((s) => s.canvasV2.compositing.infillMethod); + const infillPatchmatchDownscaleSize = useAppSelector((s) => s.canvasV2.compositing.infillPatchmatchDownscaleSize); const initial = useAppSelector((s) => s.config.sd.infillPatchmatchDownscaleSize.initial); const sliderMin = useAppSelector((s) => s.config.sd.infillPatchmatchDownscaleSize.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.infillPatchmatchDownscaleSize.sliderMax); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillTilesize.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillTilesize.tsx index f4eb0a94d54..ffcfa2be236 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillTilesize.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillTilesize.tsx @@ -1,12 +1,12 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { setInfillTileSize } from 'features/parameters/store/generationSlice'; +import { setInfillTileSize } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamInfillTileSize = () => { const dispatch = useAppDispatch(); - const infillTileSize = useAppSelector((s) => s.generation.infillTileSize); + const infillTileSize = useAppSelector((s) => s.canvasV2.compositing.infillTileSize); const initial = useAppSelector((s) => s.config.sd.infillTileSize.initial); const sliderMin = useAppSelector((s) => s.config.sd.infillTileSize.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.infillTileSize.sliderMax); @@ -15,7 +15,7 @@ const ParamInfillTileSize = () => { const coarseStep = useAppSelector((s) => s.config.sd.infillTileSize.coarseStep); const fineStep = useAppSelector((s) => s.config.sd.infillTileSize.fineStep); - const infillMethod = useAppSelector((s) => s.generation.infillMethod); + const infillMethod = useAppSelector((s) => s.canvasV2.compositing.infillMethod); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing.tsx index 7e747460f87..5273b8c508f 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing.tsx @@ -4,14 +4,14 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { setBoundingBoxScaleMethod } from 'features/canvas/store/canvasSlice'; import { isBoundingBoxScaleMethod } from 'features/canvas/store/canvasTypes'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; +import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamScaleBeforeProcessing = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const boundingBoxScaleMethod = useAppSelector((s) => s.canvas.boundingBoxScaleMethod); + const boundingBoxScaleMethod = useAppSelector((s) => s.canvasV2.scaledBbox.scaleMethod); const optimalDimension = useAppSelector(selectOptimalDimension); const OPTIONS: ComboboxOption[] = useMemo( diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx index 502cdac996b..0c6aa502ce1 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { setScaledBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; +import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,8 +9,8 @@ const ParamScaledHeight = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const optimalDimension = useAppSelector(selectOptimalDimension); - const isManual = useAppSelector((s) => s.canvas.boundingBoxScaleMethod === 'manual'); - const height = useAppSelector((s) => s.canvas.scaledBoundingBoxDimensions.height); + const isManual = useAppSelector((s) => s.canvasV2.scaledBbox.scaleMethod === 'manual'); + const height = useAppSelector((s) => s.canvasV2.scaledBbox.height); const sliderMin = useAppSelector((s) => s.config.sd.scaledBoundingBoxHeight.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.scaledBoundingBoxHeight.sliderMax); const numberInputMin = useAppSelector((s) => s.config.sd.scaledBoundingBoxHeight.numberInputMin); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx index d6c53e39b3a..52bc567c183 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { setScaledBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; +import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,8 +9,8 @@ const ParamScaledWidth = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const optimalDimension = useAppSelector(selectOptimalDimension); - const isManual = useAppSelector((s) => s.canvas.boundingBoxScaleMethod === 'manual'); - const width = useAppSelector((s) => s.canvas.scaledBoundingBoxDimensions.width); + const isManual = useAppSelector((s) => s.canvasV2.scaledBbox.scaleMethod === 'manual'); + const width = useAppSelector((s) => s.canvasV2.scaledBbox.width); const sliderMin = useAppSelector((s) => s.config.sd.scaledBoundingBoxWidth.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.scaledBoundingBoxWidth.sliderMax); const numberInputMin = useAppSelector((s) => s.config.sd.scaledBoundingBoxWidth.numberInputMin); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/ParamImageToImageStrength.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/ParamImageToImageStrength.tsx index 2519f9dad0a..128aec2ddd0 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/ParamImageToImageStrength.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/ParamImageToImageStrength.tsx @@ -1,10 +1,10 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { setImg2imgStrength } from 'features/controlLayers/store/canvasV2Slice'; import ImageToImageStrength from 'features/parameters/components/ImageToImage/ImageToImageStrength'; -import { setImg2imgStrength } from 'features/parameters/store/generationSlice'; import { memo, useCallback } from 'react'; const ParamImageToImageStrength = () => { - const img2imgStrength = useAppSelector((s) => s.generation.img2imgStrength); + const img2imgStrength = useAppSelector((s) => s.canvasV2.params.img2imgStrength); const dispatch = useAppDispatch(); const onChange = useCallback( diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamCFGScale.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamCFGScale.tsx index b75db17c4e2..2b774e7e39a 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamCFGScale.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamCFGScale.tsx @@ -1,12 +1,12 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setCfgScale } from 'features/parameters/store/generationSlice'; +import { setCfgScale } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamCFGScale = () => { - const cfgScale = useAppSelector((s) => s.generation.cfgScale); + const cfgScale = useAppSelector((s) => s.canvasV2.params.cfgScale); const sliderMin = useAppSelector((s) => s.config.sd.guidance.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.guidance.sliderMax); const numberInputMin = useAppSelector((s) => s.config.sd.guidance.numberInputMin); diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx index 09f051719fc..552402a8bcd 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx @@ -2,7 +2,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@ import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; +import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx index eabcfcd911d..ff157dfa53a 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx @@ -1,9 +1,9 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { negativePromptChanged } from 'features/controlLayers/store/canvasV2Slice'; import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { ViewModePrompt } from 'features/parameters/components/Prompts/ViewModePrompt'; -import { negativePromptChanged } from 'features/parameters/store/generationSlice'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; import { usePrompt } from 'features/prompt/usePrompt'; @@ -13,7 +13,7 @@ import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets'; export const ParamNegativePrompt = memo(() => { const dispatch = useAppDispatch(); - const prompt = useAppSelector((s) => s.generation.negativePrompt); + const prompt = useAppSelector((s) => s.canvasV2.params.negativePrompt); const viewMode = useAppSelector((s) => s.stylePreset.viewMode); const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId); diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx index c39bf050cd7..de19dfd29bf 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx @@ -1,10 +1,10 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { positivePromptChanged } from 'features/controlLayers/store/canvasV2Slice'; import { ShowDynamicPromptsPreviewButton } from 'features/dynamicPrompts/components/ShowDynamicPromptsPreviewButton'; import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { ViewModePrompt } from 'features/parameters/components/Prompts/ViewModePrompt'; -import { positivePromptChanged } from 'features/parameters/store/generationSlice'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; import { usePrompt } from 'features/prompt/usePrompt'; @@ -17,8 +17,8 @@ import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets'; export const ParamPositivePrompt = memo(() => { const dispatch = useAppDispatch(); - const prompt = useAppSelector((s) => s.generation.positivePrompt); - const baseModel = useAppSelector((s) => s.generation.model)?.base; + const prompt = useAppSelector((s) => s.canvasV2.params.positivePrompt); + const baseModel = useAppSelector((s) => s.canvasV2.params.model)?.base; const viewMode = useAppSelector((s) => s.stylePreset.viewMode); const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId); diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamScheduler.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamScheduler.tsx index b0d79596e24..1f9b12357be 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamScheduler.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamScheduler.tsx @@ -2,7 +2,7 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setScheduler } from 'features/parameters/store/generationSlice'; +import { setScheduler } from 'features/controlLayers/store/canvasV2Slice'; import { SCHEDULER_OPTIONS } from 'features/parameters/types/constants'; import { isParameterScheduler } from 'features/parameters/types/parameterSchemas'; import { memo, useCallback, useMemo } from 'react'; @@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next'; const ParamScheduler = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const scheduler = useAppSelector((s) => s.generation.scheduler); + const scheduler = useAppSelector((s) => s.canvasV2.params.scheduler); const onChange = useCallback( (v) => { diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx index f119ae888af..2b9806d8cfa 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx @@ -1,12 +1,12 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setSteps } from 'features/parameters/store/generationSlice'; +import { setSteps } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSteps = () => { - const steps = useAppSelector((s) => s.generation.steps); + const steps = useAppSelector((s) => s.canvasV2.params.steps); const initial = useAppSelector((s) => s.config.sd.steps.initial); const sliderMin = useAppSelector((s) => s.config.sd.steps.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.steps.sliderMax); diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamWidth.tsx index 2de6da22209..36fb4f849b5 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamWidth.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamWidth.tsx @@ -2,7 +2,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@ import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; +import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/ImageSizeContext.ts b/invokeai/frontend/web/src/features/parameters/components/ImageSize/ImageSizeContext.ts index f8057f2fd36..e66a1b83326 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/ImageSizeContext.ts +++ b/invokeai/frontend/web/src/features/parameters/components/ImageSize/ImageSizeContext.ts @@ -3,7 +3,7 @@ import { roundToMultiple } from 'common/util/roundDownToMultiple'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; import { ASPECT_RATIO_MAP, initialAspectRatioState } from 'features/parameters/components/ImageSize/constants'; import type { AspectRatioID, AspectRatioState } from 'features/parameters/components/ImageSize/types'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; +import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { createContext, useCallback, useContext, useMemo } from 'react'; export type ImageSizeContextInnerValue = { diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/SetOptimalSizeButton.tsx b/invokeai/frontend/web/src/features/parameters/components/ImageSize/SetOptimalSizeButton.tsx index 83770e4e153..dcad2918f17 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/SetOptimalSizeButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ImageSize/SetOptimalSizeButton.tsx @@ -1,7 +1,7 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; +import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { getIsSizeTooLarge, getIsSizeTooSmall } from 'features/parameters/util/optimalDimension'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/parameters/components/MainModel/ParamMainModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/MainModel/ParamMainModelSelect.tsx index 89f7b0bc4d0..07ad047d7ab 100644 --- a/invokeai/frontend/web/src/features/parameters/components/MainModel/ParamMainModelSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/MainModel/ParamMainModelSelect.tsx @@ -1,22 +1,18 @@ import { Box, Combobox, FormControl, FormLabel, Tooltip } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { modelSelected } from 'features/parameters/store/actions'; -import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useSDMainModels } from 'services/api/hooks/modelsByType'; import type { MainModelConfig } from 'services/api/types'; -const selectModel = createMemoizedSelector(selectGenerationSlice, (generation) => generation.model); - const ParamMainModelSelect = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const selectedModel = useAppSelector(selectModel); + const selectedModel = useAppSelector((s) => s.canvasV2.params.model); const [modelConfigs, { isLoading }] = useSDMainModels(); const tooltipLabel = useMemo(() => { if (!modelConfigs.length || !selectedModel) { diff --git a/invokeai/frontend/web/src/features/parameters/components/MainModel/UseDefaultSettingsButton.tsx b/invokeai/frontend/web/src/features/parameters/components/MainModel/UseDefaultSettingsButton.tsx index 7b322a3227e..600a27bb7e0 100644 --- a/invokeai/frontend/web/src/features/parameters/components/MainModel/UseDefaultSettingsButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/MainModel/UseDefaultSettingsButton.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'; import { RiSparklingFill } from 'react-icons/ri'; export const UseDefaultSettingsButton = () => { - const model = useAppSelector((s) => s.generation.model); + const model = useAppSelector((s) => s.canvasV2.params.model); const { t } = useTranslation(); const dispatch = useAppDispatch(); diff --git a/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessXAxis.tsx b/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessXAxis.tsx index 739cf7d83f8..b99bb637a1a 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessXAxis.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessXAxis.tsx @@ -1,14 +1,14 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setSeamlessXAxis } from 'features/parameters/store/generationSlice'; +import { setSeamlessXAxis } from 'features/controlLayers/store/canvasV2Slice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSeamlessXAxis = () => { const { t } = useTranslation(); - const seamlessXAxis = useAppSelector((s) => s.generation.seamlessXAxis); + const seamlessXAxis = useAppSelector((s) => s.canvasV2.params.seamlessXAxis); const dispatch = useAppDispatch(); diff --git a/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessYAxis.tsx b/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessYAxis.tsx index 455e50b90fe..c5ab0f68c82 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessYAxis.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessYAxis.tsx @@ -1,14 +1,14 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setSeamlessYAxis } from 'features/parameters/store/generationSlice'; +import { setSeamlessYAxis } from 'features/controlLayers/store/canvasV2Slice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSeamlessYAxis = () => { const { t } = useTranslation(); - const seamlessYAxis = useAppSelector((s) => s.generation.seamlessYAxis); + const seamlessYAxis = useAppSelector((s) => s.canvasV2.params.seamlessYAxis); const dispatch = useAppDispatch(); const handleChange = useCallback( (e: ChangeEvent) => { diff --git a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedNumberInput.tsx b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedNumberInput.tsx index de893e001e1..6f2b57259f7 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedNumberInput.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedNumberInput.tsx @@ -2,13 +2,13 @@ import { CompositeNumberInput, FormControl, FormLabel } from '@invoke-ai/ui-libr import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from 'app/constants'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setSeed } from 'features/parameters/store/generationSlice'; +import { setSeed } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; export const ParamSeedNumberInput = memo(() => { - const seed = useAppSelector((s) => s.generation.seed); - const shouldRandomizeSeed = useAppSelector((s) => s.generation.shouldRandomizeSeed); + const seed = useAppSelector((s) => s.canvasV2.params.seed); + const shouldRandomizeSeed = useAppSelector((s) => s.canvasV2.params.shouldRandomizeSeed); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedRandomize.tsx b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedRandomize.tsx index 3648cae016c..0cbff60b59b 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedRandomize.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedRandomize.tsx @@ -1,6 +1,6 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { setShouldRandomizeSeed } from 'features/parameters/store/generationSlice'; +import { setShouldRandomizeSeed } from 'features/controlLayers/store/canvasV2Slice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,7 +9,7 @@ export const ParamSeedRandomize = memo(() => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const shouldRandomizeSeed = useAppSelector((s) => s.generation.shouldRandomizeSeed); + const shouldRandomizeSeed = useAppSelector((s) => s.canvasV2.params.shouldRandomizeSeed); const handleChangeShouldRandomizeSeed = useCallback( (e: ChangeEvent) => dispatch(setShouldRandomizeSeed(e.target.checked)), diff --git a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedShuffle.tsx b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedShuffle.tsx index 88c58133516..2ca2ec5d197 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedShuffle.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedShuffle.tsx @@ -2,14 +2,14 @@ import { Button } from '@invoke-ai/ui-library'; import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from 'app/constants'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import randomInt from 'common/util/randomInt'; -import { setSeed } from 'features/parameters/store/generationSlice'; +import { setSeed } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiShuffleBold } from 'react-icons/pi'; export const ParamSeedShuffle = memo(() => { const dispatch = useAppDispatch(); - const shouldRandomizeSeed = useAppSelector((s) => s.generation.shouldRandomizeSeed); + const shouldRandomizeSeed = useAppSelector((s) => s.canvasV2.params.shouldRandomizeSeed); const { t } = useTranslation(); const handleClickRandomizeSeed = useCallback( diff --git a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx index 3552b09292c..efcddf0ac3f 100644 --- a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx @@ -1,24 +1,19 @@ import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; +import { vaeSelected } from 'features/controlLayers/store/canvasV2Slice'; import { zModelIdentifierField } from 'features/nodes/types/common'; -import { selectGenerationSlice, vaeSelected } from 'features/parameters/store/generationSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useVAEModels } from 'services/api/hooks/modelsByType'; import type { VAEModelConfig } from 'services/api/types'; -const selector = createMemoizedSelector(selectGenerationSlice, (generation) => { - const { model, vae } = generation; - return { model, vae }; -}); - const ParamVAEModelSelect = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const { model, vae } = useAppSelector(selector); + const model = useAppSelector((s) => s.canvasV2.params.model); + const vae = useAppSelector((s) => s.canvasV2.params.vae); const [modelConfigs, { isLoading }] = useVAEModels(); const getIsDisabled = useCallback( (vae: VAEModelConfig): boolean => { diff --git a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEPrecision.tsx b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEPrecision.tsx index 63ac4a621d9..ae1cac2152a 100644 --- a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEPrecision.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEPrecision.tsx @@ -2,7 +2,7 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { vaePrecisionChanged } from 'features/parameters/store/generationSlice'; +import { vaePrecisionChanged } from 'features/controlLayers/store/canvasV2Slice'; import { isParameterPrecision } from 'features/parameters/types/parameterSchemas'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -15,7 +15,7 @@ const options = [ const ParamVAEModelSelect = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const vaePrecision = useAppSelector((s) => s.generation.vaePrecision); + const vaePrecision = useAppSelector((s) => s.canvasV2.params.vaePrecision); const onChange = useCallback( (v) => { diff --git a/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts b/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts index 74aef5f79ca..7769a7ee137 100644 --- a/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts +++ b/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts @@ -2,8 +2,8 @@ import { skipToken } from '@reduxjs/toolkit/query'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; import { iiLayerAdded } from 'features/controlLayers/store/canvasV2Slice'; +import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { parseAndRecallAllMetadata } from 'features/metadata/util/handlers'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { toast } from 'features/toast/toast'; import { setActiveTab } from 'features/ui/store/uiSlice'; import { t } from 'i18next'; diff --git a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts deleted file mode 100644 index 47f33b2f963..00000000000 --- a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts +++ /dev/null @@ -1,264 +0,0 @@ -import type { PayloadAction } from '@reduxjs/toolkit'; -import { createSlice } from '@reduxjs/toolkit'; -import type { PersistConfig, RootState } from 'app/store/store'; -import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants'; -import { CLIP_SKIP_MAP } from 'features/parameters/types/constants'; -import type { - ParameterCanvasCoherenceMode, - ParameterCFGRescaleMultiplier, - ParameterCFGScale, - ParameterModel, - ParameterPrecision, - ParameterScheduler, - ParameterVAEModel, -} from 'features/parameters/types/parameterSchemas'; -import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; -import { configChanged } from 'features/system/store/configSlice'; -import { clamp } from 'lodash-es'; -import type { RgbaColor } from 'react-colorful'; - -import type { GenerationState } from './types'; - -const initialGenerationState: GenerationState = { - _version: 2, - cfgScale: 7.5, - cfgRescaleMultiplier: 0, - img2imgStrength: 0.75, - iterations: 1, - scheduler: 'euler', - seed: 0, - shouldRandomizeSeed: true, - steps: 50, - model: null, - vae: null, - vaePrecision: 'fp32', - seamlessXAxis: false, - seamlessYAxis: false, - clipSkip: 0, - shouldUseCpuNoise: true, - shouldShowAdvancedOptions: false, - maskBlur: 16, - maskBlurMethod: 'box', - canvasCoherenceMode: 'Gaussian Blur', - canvasCoherenceMinDenoise: 0, - canvasCoherenceEdgeSize: 16, - infillMethod: 'patchmatch', - infillTileSize: 32, - infillPatchmatchDownscaleSize: 1, - infillMosaicTileWidth: 64, - infillMosaicTileHeight: 64, - infillMosaicMinColor: { r: 0, g: 0, b: 0, a: 1 }, - infillMosaicMaxColor: { r: 255, g: 255, b: 255, a: 1 }, - infillColorValue: { r: 0, g: 0, b: 0, a: 1 }, - positivePrompt: '', - negativePrompt: '', - positivePrompt2: '', - negativePrompt2: '', - shouldConcatPrompts: true, -}; - -export const generationSlice = createSlice({ - name: 'generation', - initialState: initialGenerationState, - reducers: { - setIterations: (state, action: PayloadAction) => { - state.iterations = action.payload; - }, - setSteps: (state, action: PayloadAction) => { - state.steps = action.payload; - }, - setCfgScale: (state, action: PayloadAction) => { - state.cfgScale = action.payload; - }, - setCfgRescaleMultiplier: (state, action: PayloadAction) => { - state.cfgRescaleMultiplier = action.payload; - }, - setScheduler: (state, action: PayloadAction) => { - state.scheduler = action.payload; - }, - setSeed: (state, action: PayloadAction) => { - state.seed = action.payload; - state.shouldRandomizeSeed = false; - }, - setImg2imgStrength: (state, action: PayloadAction) => { - state.img2imgStrength = action.payload; - }, - setSeamlessXAxis: (state, action: PayloadAction) => { - state.seamlessXAxis = action.payload; - }, - setSeamlessYAxis: (state, action: PayloadAction) => { - state.seamlessYAxis = action.payload; - }, - setShouldRandomizeSeed: (state, action: PayloadAction) => { - state.shouldRandomizeSeed = action.payload; - }, - setMaskBlur: (state, action: PayloadAction) => { - state.maskBlur = action.payload; - }, - setCanvasCoherenceMode: (state, action: PayloadAction) => { - state.canvasCoherenceMode = action.payload; - }, - setCanvasCoherenceEdgeSize: (state, action: PayloadAction) => { - state.canvasCoherenceEdgeSize = action.payload; - }, - setCanvasCoherenceMinDenoise: (state, action: PayloadAction) => { - state.canvasCoherenceMinDenoise = action.payload; - }, - modelChanged: { - reducer: ( - state, - action: PayloadAction - ) => { - const newModel = action.payload; - state.model = newModel; - - if (newModel === null) { - return; - } - - // Clamp ClipSkip Based On Selected Model - // TODO(psyche): remove this special handling when https://github.com/invoke-ai/InvokeAI/issues/4583 is resolved - // WIP PR here: https://github.com/invoke-ai/InvokeAI/pull/4624 - if (newModel.base === 'sdxl') { - // We don't support clip skip for SDXL yet - it's not in the graphs - state.clipSkip = 0; - } else { - const { maxClip } = CLIP_SKIP_MAP[newModel.base]; - state.clipSkip = clamp(state.clipSkip, 0, maxClip); - } - }, - prepare: (payload: ParameterModel | null, previousModel?: ParameterModel | null) => ({ - payload, - meta: { - previousModel, - }, - }), - }, - vaeSelected: (state, action: PayloadAction) => { - // null is a valid VAE! - state.vae = action.payload; - }, - vaePrecisionChanged: (state, action: PayloadAction) => { - state.vaePrecision = action.payload; - }, - setClipSkip: (state, action: PayloadAction) => { - state.clipSkip = action.payload; - }, - shouldUseCpuNoiseChanged: (state, action: PayloadAction) => { - state.shouldUseCpuNoise = action.payload; - }, - setInfillMethod: (state, action: PayloadAction) => { - state.infillMethod = action.payload; - }, - setInfillTileSize: (state, action: PayloadAction) => { - state.infillTileSize = action.payload; - }, - setInfillPatchmatchDownscaleSize: (state, action: PayloadAction) => { - state.infillPatchmatchDownscaleSize = action.payload; - }, - setInfillMosaicTileWidth: (state, action: PayloadAction) => { - state.infillMosaicTileWidth = action.payload; - }, - setInfillMosaicTileHeight: (state, action: PayloadAction) => { - state.infillMosaicTileHeight = action.payload; - }, - setInfillMosaicMinColor: (state, action: PayloadAction) => { - state.infillMosaicMinColor = action.payload; - }, - setInfillMosaicMaxColor: (state, action: PayloadAction) => { - state.infillMosaicMaxColor = action.payload; - }, - setInfillColorValue: (state, action: PayloadAction) => { - state.infillColorValue = action.payload; - }, - positivePromptChanged: (state, action: PayloadAction) => { - state.positivePrompt = action.payload; - }, - negativePromptChanged: (state, action: PayloadAction) => { - state.negativePrompt = action.payload; - }, - positivePrompt2Changed: (state, action: PayloadAction) => { - state.positivePrompt2 = action.payload; - }, - negativePrompt2Changed: (state, action: PayloadAction) => { - state.negativePrompt2 = action.payload; - }, - shouldConcatPromptsChanged: (state, action: PayloadAction) => { - state.shouldConcatPrompts = action.payload; - }, - }, - extraReducers: (builder) => { - builder.addCase(configChanged, (state, action) => { - if (action.payload.sd?.scheduler) { - state.scheduler = action.payload.sd.scheduler; - } - if (action.payload.sd?.vaePrecision) { - state.vaePrecision = action.payload.sd.vaePrecision; - } - }); - }, - selectors: { - selectOptimalDimension: (slice) => getOptimalDimension(slice.model), - }, -}); - -export const { - setCfgScale, - setCfgRescaleMultiplier, - setImg2imgStrength, - setInfillMethod, - setIterations, - setScheduler, - setMaskBlur, - setCanvasCoherenceMode, - setCanvasCoherenceEdgeSize, - setCanvasCoherenceMinDenoise, - setSeed, - setShouldRandomizeSeed, - setSteps, - modelChanged, - vaeSelected, - setSeamlessXAxis, - setSeamlessYAxis, - setClipSkip, - shouldUseCpuNoiseChanged, - vaePrecisionChanged, - setInfillTileSize, - setInfillPatchmatchDownscaleSize, - setInfillMosaicTileWidth, - setInfillMosaicTileHeight, - setInfillMosaicMinColor, - setInfillMosaicMaxColor, - setInfillColorValue, - positivePromptChanged, - negativePromptChanged, - positivePrompt2Changed, - negativePrompt2Changed, - shouldConcatPromptsChanged, -} = generationSlice.actions; - -export const { selectOptimalDimension } = generationSlice.selectors; - -export const selectGenerationSlice = (state: RootState) => state.generation; - -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrateGenerationState = (state: any): GenerationState => { - if (!('_version' in state)) { - state._version = 1; - state.aspectRatio = initialAspectRatioState; - } - if (state._version === 1) { - // The signature of the model has changed, so we need to reset it - state._version = 2; - state.model = null; - state.canvasCoherenceMode = initialGenerationState.canvasCoherenceMode; - } - return state; -}; - -export const generationPersistConfig: PersistConfig = { - name: generationSlice.name, - initialState: initialGenerationState, - migrate: migrateGenerationState, - persistDenylist: [], -}; diff --git a/invokeai/frontend/web/src/features/parameters/store/types.ts b/invokeai/frontend/web/src/features/parameters/store/types.ts deleted file mode 100644 index 0d0c6e4b8b3..00000000000 --- a/invokeai/frontend/web/src/features/parameters/store/types.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { PayloadAction } from '@reduxjs/toolkit'; -import type { - ParameterCanvasCoherenceMode, - ParameterCFGRescaleMultiplier, - ParameterCFGScale, - ParameterMaskBlurMethod, - ParameterModel, - ParameterNegativePrompt, - ParameterNegativeStylePromptSDXL, - ParameterPositivePrompt, - ParameterPositiveStylePromptSDXL, - ParameterPrecision, - ParameterScheduler, - ParameterSeed, - ParameterSteps, - ParameterStrength, - ParameterVAEModel, -} from 'features/parameters/types/parameterSchemas'; -import type { RgbaColor } from 'react-colorful'; - -export interface GenerationState { - _version: 2; - cfgScale: ParameterCFGScale; - cfgRescaleMultiplier: ParameterCFGRescaleMultiplier; - img2imgStrength: ParameterStrength; - infillMethod: string; - iterations: number; - scheduler: ParameterScheduler; - maskBlur: number; - maskBlurMethod: ParameterMaskBlurMethod; - canvasCoherenceMode: ParameterCanvasCoherenceMode; - canvasCoherenceMinDenoise: ParameterStrength; - canvasCoherenceEdgeSize: number; - seed: ParameterSeed; - shouldRandomizeSeed: boolean; - steps: ParameterSteps; - model: ParameterModel | null; - vae: ParameterVAEModel | null; - vaePrecision: ParameterPrecision; - seamlessXAxis: boolean; - seamlessYAxis: boolean; - clipSkip: number; - shouldUseCpuNoise: boolean; - shouldShowAdvancedOptions: boolean; - infillTileSize: number; - infillPatchmatchDownscaleSize: number; - infillMosaicTileWidth: number; - infillMosaicTileHeight: number; - infillMosaicMinColor: RgbaColor; - infillMosaicMaxColor: RgbaColor; - infillColorValue: RgbaColor; - positivePrompt: ParameterPositivePrompt; - negativePrompt: ParameterNegativePrompt; - positivePrompt2: ParameterPositiveStylePromptSDXL; - negativePrompt2: ParameterNegativeStylePromptSDXL; - shouldConcatPrompts: boolean; -} - -export type PayloadActionWithOptimalDimension = PayloadAction; diff --git a/invokeai/frontend/web/src/features/prompt/PromptTriggerSelect.tsx b/invokeai/frontend/web/src/features/prompt/PromptTriggerSelect.tsx index 9e79a4dcaed..e8f0032d101 100644 --- a/invokeai/frontend/web/src/features/prompt/PromptTriggerSelect.tsx +++ b/invokeai/frontend/web/src/features/prompt/PromptTriggerSelect.tsx @@ -1,11 +1,8 @@ import type { ChakraProps, ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; import { Combobox, FormControl } from '@invoke-ai/ui-library'; import { skipToken } from '@reduxjs/toolkit/query'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import type { GroupBase } from 'chakra-react-select'; -import { selectLoraSlice } from 'features/lora/store/loraSlice'; -import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; import type { PromptTriggerSelectProps } from 'features/prompt/types'; import { t } from 'i18next'; import { flatten, map } from 'lodash-es'; @@ -17,14 +14,11 @@ import { isNonRefinerMainModelConfig } from 'services/api/types'; const noOptionsMessage = () => t('prompt.noMatchingTriggers'); -const selectLoRAs = createMemoizedSelector(selectLoraSlice, (loras) => loras.loras); -const selectMainModel = createMemoizedSelector(selectGenerationSlice, (generation) => generation.model); - export const PromptTriggerSelect = memo(({ onSelect, onClose }: PromptTriggerSelectProps) => { const { t } = useTranslation(); - const mainModel = useAppSelector(selectMainModel); - const addedLoRAs = useAppSelector(selectLoRAs); + const mainModel = useAppSelector((s) => s.canvasV2.params.model); + const addedLoRAs = useAppSelector((s) => s.lora.loras); const { data: mainModelConfig, isLoading: isLoadingMainModelConfig } = useGetModelConfigQuery( mainModel?.key ?? skipToken ); diff --git a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx b/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx index 834d66f0d0f..5d6badaefd7 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx @@ -35,7 +35,7 @@ const TooltipContent = memo(({ prepend = false }: Props) => { const { isReady, reasons } = useIsReadyToEnqueue(); const isLoadingDynamicPrompts = useAppSelector((s) => s.dynamicPrompts.isLoading); const promptsCount = useAppSelector(selectPromptsCount); - const iterationsCount = useAppSelector((s) => s.generation.iterations); + const iterationsCount = useAppSelector((s) => s.canvasV2.params.iterations); const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId); const autoAddBoardName = useBoardName(autoAddBoardId); const [_, { isLoading }] = useEnqueueBatchMutation({ diff --git a/invokeai/frontend/web/src/features/queue/components/QueueIterationsNumberInput.tsx b/invokeai/frontend/web/src/features/queue/components/QueueIterationsNumberInput.tsx index 977a9f8989d..f0b3c9b674e 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueIterationsNumberInput.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueIterationsNumberInput.tsx @@ -1,11 +1,11 @@ import { CompositeNumberInput } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setIterations } from 'features/parameters/store/generationSlice'; +import { setIterations } from 'features/canvas/store/canvasSlice'; import { memo, useCallback } from 'react'; export const QueueIterationsNumberInput = memo(() => { - const iterations = useAppSelector((s) => s.generation.iterations); + const iterations = useAppSelector((s) => s.canvasV2.params.iterations); const coarseStep = useAppSelector((s) => s.config.sd.iterations.coarseStep); const fineStep = useAppSelector((s) => s.config.sd.iterations.fineStep); const dispatch = useAppDispatch(); diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt.tsx index a087df83685..ba1918d05f8 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt.tsx @@ -1,8 +1,8 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { negativePrompt2Changed } from 'features/controlLayers/store/canvasV2Slice'; import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; -import { negativePrompt2Changed } from 'features/parameters/store/generationSlice'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; import { usePrompt } from 'features/prompt/usePrompt'; @@ -12,7 +12,7 @@ import { useTranslation } from 'react-i18next'; export const ParamSDXLNegativeStylePrompt = memo(() => { const dispatch = useAppDispatch(); - const prompt = useAppSelector((s) => s.generation.negativePrompt2); + const prompt = useAppSelector((s) => s.canvasV2.params.negativePrompt2); const textareaRef = useRef(null); const { t } = useTranslation(); const handleChange = useCallback( diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt.tsx index f076e6e176c..ee8b2419652 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt.tsx @@ -1,8 +1,8 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { positivePrompt2Changed } from 'features/controlLayers/store/canvasV2Slice'; import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; -import { positivePrompt2Changed } from 'features/parameters/store/generationSlice'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; import { usePrompt } from 'features/prompt/usePrompt'; @@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next'; export const ParamSDXLPositiveStylePrompt = memo(() => { const dispatch = useAppDispatch(); - const prompt = useAppSelector((s) => s.generation.positivePrompt2); + const prompt = useAppSelector((s) => s.canvasV2.params.positivePrompt2); const textareaRef = useRef(null); const { t } = useTranslation(); const handleChange = useCallback( diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/SDXLConcatButton.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/SDXLConcatButton.tsx index 62dd12a6962..31da96b197c 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/SDXLConcatButton.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/SDXLConcatButton.tsx @@ -1,12 +1,12 @@ import { IconButton, Tooltip } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { shouldConcatPromptsChanged } from 'features/parameters/store/generationSlice'; +import { shouldConcatPromptsChanged } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiLinkSimpleBold, PiLinkSimpleBreakBold } from 'react-icons/pi'; export const SDXLConcatButton = memo(() => { - const shouldConcatPrompts = useAppSelector((s) => s.generation.shouldConcatPrompts); + const shouldConcatPrompts = useAppSelector((s) => s.canvasV2.params.shouldConcatPrompts); const dispatch = useAppDispatch(); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale.tsx index a4409955cce..c206b8a7b1e 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale.tsx @@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerCFGScale = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const refinerCFGScale = useAppSelector((s) => s.sdxl.refinerCFGScale); + const refinerCFGScale = useAppSelector((s) => s.canvasV2.params.refinerCFGScale); const sliderMin = useAppSelector((s) => s.config.sd.guidance.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.guidance.sliderMax); const numberInputMin = useAppSelector((s) => s.config.sd.guidance.numberInputMin); diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore.tsx index 8da72538637..4cbf5beff68 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore.tsx @@ -6,7 +6,7 @@ import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerNegativeAestheticScore = () => { - const refinerNegativeAestheticScore = useAppSelector((s) => s.sdxl.refinerNegativeAestheticScore); + const refinerNegativeAestheticScore = useAppSelector((s) => s.canvasV2.params.refinerNegativeAestheticScore); const dispatch = useAppDispatch(); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerPositiveAestheticScore.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerPositiveAestheticScore.tsx index f872929c0e4..202a65acf41 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerPositiveAestheticScore.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerPositiveAestheticScore.tsx @@ -6,7 +6,7 @@ import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerPositiveAestheticScore = () => { - const refinerPositiveAestheticScore = useAppSelector((s) => s.sdxl.refinerPositiveAestheticScore); + const refinerPositiveAestheticScore = useAppSelector((s) => s.canvasV2.params.refinerPositiveAestheticScore); const dispatch = useAppDispatch(); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerScheduler.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerScheduler.tsx index d0aab11038f..a88a7009cf1 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerScheduler.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerScheduler.tsx @@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerScheduler = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const refinerScheduler = useAppSelector((s) => s.sdxl.refinerScheduler); + const refinerScheduler = useAppSelector((s) => s.canvasV2.params.refinerScheduler); const onChange = useCallback( (v) => { diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart.tsx index fd7b1f89cfe..0ff791fee59 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart.tsx @@ -6,7 +6,7 @@ import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerStart = () => { - const refinerStart = useAppSelector((s) => s.sdxl.refinerStart); + const refinerStart = useAppSelector((s) => s.canvasV2.params.refinerStart); const dispatch = useAppDispatch(); const handleChange = useCallback((v: number) => dispatch(setRefinerStart(v)), [dispatch]); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerSteps.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerSteps.tsx index a18edc8cf4c..01a7fed4f4f 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerSteps.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerSteps.tsx @@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerSteps = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const refinerSteps = useAppSelector((s) => s.sdxl.refinerSteps); + const refinerSteps = useAppSelector((s) => s.canvasV2.params.refinerSteps); const initial = useAppSelector((s) => s.config.sd.steps.initial); const sliderMin = useAppSelector((s) => s.config.sd.steps.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.steps.sliderMax); diff --git a/invokeai/frontend/web/src/features/sdxl/store/sdxlSlice.ts b/invokeai/frontend/web/src/features/sdxl/store/sdxlSlice.ts deleted file mode 100644 index 10a8f861f1f..00000000000 --- a/invokeai/frontend/web/src/features/sdxl/store/sdxlSlice.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type { PayloadAction } from '@reduxjs/toolkit'; -import { createSlice } from '@reduxjs/toolkit'; -import type { PersistConfig, RootState } from 'app/store/store'; -import type { ParameterScheduler, ParameterSDXLRefinerModel } from 'features/parameters/types/parameterSchemas'; - -type SDXLState = { - _version: 2; - refinerModel: ParameterSDXLRefinerModel | null; - refinerSteps: number; - refinerCFGScale: number; - refinerScheduler: ParameterScheduler; - refinerPositiveAestheticScore: number; - refinerNegativeAestheticScore: number; - refinerStart: number; -}; - -const initialSDXLState: SDXLState = { - _version: 2, - refinerModel: null, - refinerSteps: 20, - refinerCFGScale: 7.5, - refinerScheduler: 'euler', - refinerPositiveAestheticScore: 6, - refinerNegativeAestheticScore: 2.5, - refinerStart: 0.8, -}; - -export const sdxlSlice = createSlice({ - name: 'sdxl', - initialState: initialSDXLState, - reducers: { - refinerModelChanged: (state, action: PayloadAction) => { - state.refinerModel = action.payload; - }, - setRefinerSteps: (state, action: PayloadAction) => { - state.refinerSteps = action.payload; - }, - setRefinerCFGScale: (state, action: PayloadAction) => { - state.refinerCFGScale = action.payload; - }, - setRefinerScheduler: (state, action: PayloadAction) => { - state.refinerScheduler = action.payload; - }, - setRefinerPositiveAestheticScore: (state, action: PayloadAction) => { - state.refinerPositiveAestheticScore = action.payload; - }, - setRefinerNegativeAestheticScore: (state, action: PayloadAction) => { - state.refinerNegativeAestheticScore = action.payload; - }, - setRefinerStart: (state, action: PayloadAction) => { - state.refinerStart = action.payload; - }, - }, -}); - -export const { - refinerModelChanged, - setRefinerSteps, - setRefinerCFGScale, - setRefinerScheduler, - setRefinerPositiveAestheticScore, - setRefinerNegativeAestheticScore, - setRefinerStart, -} = sdxlSlice.actions; - -export const selectSdxlSlice = (state: RootState) => state.sdxl; - -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrateSDXLState = (state: any): any => { - if (!('_version' in state)) { - state._version = 1; - } - if (state._version === 1) { - // Model type has changed, so we need to reset the state - too risky to migrate - state._version = 2; - state.refinerModel = null; - } - return state; -}; - -export const sdxlPersistConfig: PersistConfig = { - name: sdxlSlice.name, - initialState: initialSDXLState, - migrate: migrateSDXLState, - persistDenylist: [], -}; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx index 7ed7eaf5ccd..8655cdc7402 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx @@ -12,7 +12,7 @@ import { ParamSeedRandomize } from 'features/parameters/components/Seed/ParamSee import { ParamSeedShuffle } from 'features/parameters/components/Seed/ParamSeedShuffle'; import ParamVAEModelSelect from 'features/parameters/components/VAEModel/ParamVAEModelSelect'; import ParamVAEPrecision from 'features/parameters/components/VAEModel/ParamVAEPrecision'; -import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; +import { selectGenerationSlice } from 'features/canvas/store/canvasSlice'; import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { memo, useMemo } from 'react'; @@ -28,7 +28,7 @@ const formLabelProps2: FormLabelProps = { }; export const AdvancedSettingsAccordion = memo(() => { - const vaeKey = useAppSelector((state) => state.generation.vae?.key); + const vaeKey = useAppSelector((state) => state.canvasV2.params.vae?.key); const { currentData: vaeConfig } = useGetModelConfigQuery(vaeKey ?? skipToken); const activeTabName = useAppSelector(activeTabNameSelector); diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx index 1d18292da3b..ef1d524584b 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx @@ -12,7 +12,7 @@ import ParamImageToImageStrength from 'features/parameters/components/Canvas/Par import { ParamSeedNumberInput } from 'features/parameters/components/Seed/ParamSeedNumberInput'; import { ParamSeedRandomize } from 'features/parameters/components/Seed/ParamSeedRandomize'; import { ParamSeedShuffle } from 'features/parameters/components/Seed/ParamSeedShuffle'; -import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; +import { selectGenerationSlice } from 'features/canvas/store/canvasSlice'; import { useExpanderToggle } from 'features/settingsAccordions/hooks/useExpanderToggle'; import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion.tsx index eb73ebe961d..dcff42c7e0b 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion.tsx @@ -58,7 +58,7 @@ const RefinerSettingsAccordionNoRefiner: React.FC = memo(() => { RefinerSettingsAccordionNoRefiner.displayName = 'RefinerSettingsAccordionNoRefiner'; const RefinerSettingsAccordionContent: React.FC = memo(() => { - const isRefinerModelSelected = useAppSelector((state) => !isNil(state.sdxl.refinerModel)); + const isRefinerModelSelected = useAppSelector((state) => !isNil(state.canvasV2.params.refinerModel)); return ( diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx index b7232ae33f5..60269ad64b4 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx @@ -19,7 +19,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import { useClearStorage } from 'common/hooks/useClearStorage'; -import { shouldUseCpuNoiseChanged } from 'features/parameters/store/generationSlice'; +import { shouldUseCpuNoiseChanged } from 'features/canvas/store/canvasSlice'; import { useClearIntermediates } from 'features/system/components/SettingsModal/useClearIntermediates'; import { StickyScrollable } from 'features/system/components/StickyScrollable'; import { @@ -88,7 +88,7 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => { const { isOpen: isRefreshModalOpen, onOpen: onRefreshModalOpen, onClose: onRefreshModalClose } = useDisclosure(); - const shouldUseCpuNoise = useAppSelector((s) => s.generation.shouldUseCpuNoise); + const shouldUseCpuNoise = useAppSelector((s) => s.canvasV2.params.shouldUseCpuNoise); const shouldConfirmOnDelete = useAppSelector((s) => s.system.shouldConfirmOnDelete); const enableImageDebugging = useAppSelector((s) => s.system.enableImageDebugging); const shouldShowProgressInViewer = useAppSelector((s) => s.ui.shouldShowProgressInViewer); diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx index 0196336e206..6214c9df848 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx @@ -16,7 +16,6 @@ import { RefinerSettingsAccordion } from 'features/settingsAccordions/components import { StylePresetMenu } from 'features/stylePresets/components/StylePresetMenu'; import { StylePresetMenuTrigger } from 'features/stylePresets/components/StylePresetMenuTrigger'; import { $isMenuOpen } from 'features/stylePresets/store/isMenuOpen'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; import type { CSSProperties } from 'react'; import { memo, useCallback, useMemo, useRef } from 'react'; @@ -42,15 +41,14 @@ const selectedStyles: ChakraProps['sx'] = { const ParametersPanelTextToImage = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const activeTabName = useAppSelector(activeTabNameSelector); - const controlLayersCount = useAppSelector((s) => s.layers.layers.length); + const controlLayersCount = useAppSelector((s) => s.canvasV2.layers.length); const controlLayersTitle = useMemo(() => { if (controlLayersCount === 0) { return t('controlLayers.controlLayers'); } return `${t('controlLayers.controlLayers')} (${controlLayersCount})`; }, [controlLayersCount, t]); - const isSDXL = useAppSelector((s) => s.generation.model?.base === 'sdxl'); + const isSDXL = useAppSelector((s) => s.canvasV2.params.model?.base === 'sdxl'); const onChangeTabs = useCallback( (i: number) => { if (i === 1) { diff --git a/invokeai/frontend/web/src/services/api/hooks/useSelectedModelConfig.ts b/invokeai/frontend/web/src/services/api/hooks/useSelectedModelConfig.ts index 4a8d8d72e2a..8b169a9e174 100644 --- a/invokeai/frontend/web/src/services/api/hooks/useSelectedModelConfig.ts +++ b/invokeai/frontend/web/src/services/api/hooks/useSelectedModelConfig.ts @@ -1,7 +1,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { skipToken } from '@reduxjs/toolkit/query'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; +import { selectGenerationSlice } from 'features/canvas/store/canvasSlice'; import { useGetModelConfigQuery } from 'services/api/endpoints/models'; const selectModelKey = createSelector(selectGenerationSlice, (generation) => generation.model?.key); From 2d8a29d2fd67ce4ae22c1ca5f01d75a41f50cb69 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 16 Jun 2024 00:13:24 +1000 Subject: [PATCH 049/678] refactor(ui): update components & logic to use new unified slice (again) --- .../components/QueueIterationsNumberInput.tsx | 2 +- .../SDXLRefiner/ParamSDXLRefinerCFGScale.tsx | 2 +- .../ParamSDXLRefinerModelSelect.tsx | 7 +-- ...ParamSDXLRefinerNegativeAestheticScore.tsx | 2 +- ...ParamSDXLRefinerPositiveAestheticScore.tsx | 2 +- .../SDXLRefiner/ParamSDXLRefinerScheduler.tsx | 2 +- .../SDXLRefiner/ParamSDXLRefinerStart.tsx | 2 +- .../SDXLRefiner/ParamSDXLRefinerSteps.tsx | 2 +- .../AdvancedSettingsAccordion.tsx | 22 +++---- .../ImageSettingsAccordion.tsx | 59 ++++++++----------- .../RefinerSettingsAccordion.tsx | 6 +- 11 files changed, 49 insertions(+), 59 deletions(-) diff --git a/invokeai/frontend/web/src/features/queue/components/QueueIterationsNumberInput.tsx b/invokeai/frontend/web/src/features/queue/components/QueueIterationsNumberInput.tsx index f0b3c9b674e..61278d9a2c2 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueIterationsNumberInput.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueIterationsNumberInput.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setIterations } from 'features/canvas/store/canvasSlice'; +import { setIterations } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; export const QueueIterationsNumberInput = memo(() => { diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale.tsx index c206b8a7b1e..e110a770e31 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setRefinerCFGScale } from 'features/sdxl/store/sdxlSlice'; +import { setRefinerCFGScale } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx index e18ccbfbb27..6e175b17623 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx @@ -1,22 +1,19 @@ import { Box, Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { useModelCombobox } from 'common/hooks/useModelCombobox'; +import { refinerModelChanged } from 'features/controlLayers/store/canvasV2Slice'; import { zModelIdentifierField } from 'features/nodes/types/common'; -import { refinerModelChanged, selectSdxlSlice } from 'features/sdxl/store/sdxlSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useRefinerModels } from 'services/api/hooks/modelsByType'; import type { MainModelConfig } from 'services/api/types'; -const selectModel = createMemoizedSelector(selectSdxlSlice, (sdxl) => sdxl.refinerModel); - const optionsFilter = (model: MainModelConfig) => model.base === 'sdxl-refiner'; const ParamSDXLRefinerModelSelect = () => { const dispatch = useAppDispatch(); - const model = useAppSelector(selectModel); + const model = useAppSelector((s) => s.canvasV2.params.refinerModel); const { t } = useTranslation(); const [modelConfigs, { isLoading }] = useRefinerModels(); const _onChange = useCallback( diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore.tsx index 4cbf5beff68..a98eae78e73 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setRefinerNegativeAestheticScore } from 'features/sdxl/store/sdxlSlice'; +import { setRefinerNegativeAestheticScore } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerPositiveAestheticScore.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerPositiveAestheticScore.tsx index 202a65acf41..61ff4de9256 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerPositiveAestheticScore.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerPositiveAestheticScore.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setRefinerPositiveAestheticScore } from 'features/sdxl/store/sdxlSlice'; +import { setRefinerPositiveAestheticScore } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerScheduler.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerScheduler.tsx index a88a7009cf1..7e0f0acfd5d 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerScheduler.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerScheduler.tsx @@ -2,9 +2,9 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; +import { setRefinerScheduler } from 'features/controlLayers/store/canvasV2Slice'; import { SCHEDULER_OPTIONS } from 'features/parameters/types/constants'; import { isParameterScheduler } from 'features/parameters/types/parameterSchemas'; -import { setRefinerScheduler } from 'features/sdxl/store/sdxlSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart.tsx index 0ff791fee59..fc364ab1a8a 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setRefinerStart } from 'features/sdxl/store/sdxlSlice'; +import { setRefinerStart } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerSteps.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerSteps.tsx index 01a7fed4f4f..49018932ed1 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerSteps.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerSteps.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setRefinerSteps } from 'features/sdxl/store/sdxlSlice'; +import { setRefinerSteps } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx index 8655cdc7402..2ca7c926b09 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx @@ -3,6 +3,7 @@ import { Flex, FormControlGroup, StandaloneAccordion } from '@invoke-ai/ui-libra import { skipToken } from '@reduxjs/toolkit/query'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import ParamCFGRescaleMultiplier from 'features/parameters/components/Advanced/ParamCFGRescaleMultiplier'; import ParamClipSkip from 'features/parameters/components/Advanced/ParamClipSkip'; import ParamSeamlessXAxis from 'features/parameters/components/Seamless/ParamSeamlessXAxis'; @@ -12,7 +13,6 @@ import { ParamSeedRandomize } from 'features/parameters/components/Seed/ParamSee import { ParamSeedShuffle } from 'features/parameters/components/Seed/ParamSeedShuffle'; import ParamVAEModelSelect from 'features/parameters/components/VAEModel/ParamVAEModelSelect'; import ParamVAEPrecision from 'features/parameters/components/VAEModel/ParamVAEPrecision'; -import { selectGenerationSlice } from 'features/canvas/store/canvasSlice'; import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { memo, useMemo } from 'react'; @@ -34,24 +34,24 @@ export const AdvancedSettingsAccordion = memo(() => { const selectBadges = useMemo( () => - createMemoizedSelector(selectGenerationSlice, (generation) => { + createMemoizedSelector(selectCanvasV2Slice, ({ params }) => { const badges: (string | number)[] = []; if (vaeConfig) { let vaeBadge = vaeConfig.name; - if (generation.vaePrecision === 'fp16') { - vaeBadge += ` ${generation.vaePrecision}`; + if (params.vaePrecision === 'fp16') { + vaeBadge += ` ${params.vaePrecision}`; } badges.push(vaeBadge); - } else if (generation.vaePrecision === 'fp16') { - badges.push(`VAE ${generation.vaePrecision}`); + } else if (params.vaePrecision === 'fp16') { + badges.push(`VAE ${params.vaePrecision}`); } - if (generation.clipSkip) { - badges.push(`Skip ${generation.clipSkip}`); + if (params.clipSkip) { + badges.push(`Skip ${params.clipSkip}`); } - if (generation.cfgRescaleMultiplier) { - badges.push(`Rescale ${generation.cfgRescaleMultiplier}`); + if (params.cfgRescaleMultiplier) { + badges.push(`Rescale ${params.cfgRescaleMultiplier}`); } - if (generation.seamlessXAxis || generation.seamlessYAxis) { + if (params.seamlessXAxis || params.seamlessYAxis) { badges.push('seamless'); } if (activeTabName === 'upscaling' && !generation.shouldRandomizeSeed) { diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx index ef1d524584b..1518e0ec2dd 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx @@ -12,41 +12,36 @@ import ParamImageToImageStrength from 'features/parameters/components/Canvas/Par import { ParamSeedNumberInput } from 'features/parameters/components/Seed/ParamSeedNumberInput'; import { ParamSeedRandomize } from 'features/parameters/components/Seed/ParamSeedRandomize'; import { ParamSeedShuffle } from 'features/parameters/components/Seed/ParamSeedShuffle'; -import { selectGenerationSlice } from 'features/canvas/store/canvasSlice'; import { useExpanderToggle } from 'features/settingsAccordions/hooks/useExpanderToggle'; import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { ImageSizeLinear } from './ImageSizeLinear'; -const selector = createMemoizedSelector( - [selectGenerationSlice, selectHrfSlice, selectCanvasV2Slice, activeTabNameSelector], - (generation, hrf, canvasV2, activeTabName) => { - const { shouldRandomizeSeed, model } = generation; - const { hrfEnabled } = hrf; - const badges: string[] = []; - const isSDXL = model?.base === 'sdxl'; +const selector = createMemoizedSelector([selectHrfSlice, selectCanvasV2Slice], (hrf, canvasV2) => { + const { shouldRandomizeSeed, model } = canvasV2.params; + const { hrfEnabled } = hrf; + const badges: string[] = []; + const isSDXL = model?.base === 'sdxl'; - const { aspectRatio, width, height } = canvasV2.document; - badges.push(`${width}×${height}`); - badges.push(aspectRatio.id); + const { aspectRatio, width, height } = canvasV2.document; + badges.push(`${width}×${height}`); + badges.push(aspectRatio.id); - if (aspectRatio.isLocked) { - badges.push('locked'); - } + if (aspectRatio.isLocked) { + badges.push('locked'); + } - if (!shouldRandomizeSeed) { - badges.push('Manual Seed'); - } + if (!shouldRandomizeSeed) { + badges.push('Manual Seed'); + } - if (hrfEnabled && !isSDXL) { - badges.push('HiRes Fix'); - } - return { badges, activeTabName, isSDXL }; + if (hrfEnabled && !isSDXL) { + badges.push('HiRes Fix'); } -); + return { badges, isSDXL }; +}); const scalingLabelProps: FormLabelProps = { minW: '4.5rem', @@ -54,7 +49,7 @@ const scalingLabelProps: FormLabelProps = { export const ImageSettingsAccordion = memo(() => { const { t } = useTranslation(); - const { badges, activeTabName, isSDXL } = useAppSelector(selector); + const { badges, isSDXL } = useAppSelector(selector); const { isOpen: isOpenAccordion, onToggle: onToggleAccordion } = useStandaloneAccordionToggle({ id: 'image-settings', defaultIsOpen: true, @@ -83,16 +78,12 @@ export const ImageSettingsAccordion = memo(() => { - {activeTabName === 'generation' && !isSDXL && } - {activeTabName === 'canvas' && ( - <> - - - - - - - )} + {!isSDXL && } + + + + + diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion.tsx index dcff42c7e0b..76f7ebe6841 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion.tsx @@ -2,6 +2,7 @@ import type { FormLabelProps } from '@invoke-ai/ui-library'; import { Flex, FormControlGroup, StandaloneAccordion, Text } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import ParamSDXLRefinerCFGScale from 'features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale'; import ParamSDXLRefinerModelSelect from 'features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect'; import ParamSDXLRefinerNegativeAestheticScore from 'features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore'; @@ -9,7 +10,6 @@ import ParamSDXLRefinerPositiveAestheticScore from 'features/sdxl/components/SDX import ParamSDXLRefinerScheduler from 'features/sdxl/components/SDXLRefiner/ParamSDXLRefinerScheduler'; import ParamSDXLRefinerStart from 'features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart'; import ParamSDXLRefinerSteps from 'features/sdxl/components/SDXLRefiner/ParamSDXLRefinerSteps'; -import { selectSdxlSlice } from 'features/sdxl/store/sdxlSlice'; import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle'; import { isNil } from 'lodash-es'; import { memo } from 'react'; @@ -24,7 +24,9 @@ const stepsScaleLabelProps: FormLabelProps = { minW: '5rem', }; -const selectBadges = createMemoizedSelector(selectSdxlSlice, (sdxl) => (sdxl.refinerModel ? ['Enabled'] : undefined)); +const selectBadges = createMemoizedSelector(selectCanvasV2Slice, ({ params }) => + params.refinerModel ? ['Enabled'] : undefined +); export const RefinerSettingsAccordion: React.FC = memo(() => { const { t } = useTranslation(); From ab0f096bf2563379c1aa3bc183362da072ed2d97 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 16 Jun 2024 09:10:56 +1000 Subject: [PATCH 050/678] refactor(ui): add `adapterType` to ControlAdapterData --- .../components/ControlAdapter/CASettings.tsx | 2 +- .../store/controlAdaptersReducers.ts | 28 ++++++++----- .../src/features/controlLayers/store/types.ts | 42 ++++++++++++++----- 3 files changed, 50 insertions(+), 22 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx index 292e4565842..70cde437e7d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx @@ -122,7 +122,7 @@ export const CASettings = memo(({ id }: Props) => { - {controlAdapter.controlMode && ( + {controlAdapter.adapterType === 'controlnet' && ( )} diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts index 6cda64871f5..84c12e5f3a4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts @@ -9,11 +9,14 @@ import { v4 as uuidv4 } from 'uuid'; import type { CanvasV2State, - ControlAdapterConfig, ControlAdapterData, ControlModeV2, + ControlNetConfig, + ControlNetData, Filter, ProcessorConfig, + T2IAdapterConfig, + T2IAdapterData, } from './types'; import { buildControlAdapterProcessorV2, imageDTOToImageWithDims } from './types'; @@ -26,7 +29,7 @@ export const selectCAOrThrow = (state: CanvasV2State, id: string) => { export const controlAdaptersReducers = { caAdded: { - reducer: (state, action: PayloadAction<{ id: string; config: ControlAdapterConfig }>) => { + reducer: (state, action: PayloadAction<{ id: string; config: ControlNetConfig | T2IAdapterConfig }>) => { const { id, config } = action.payload; state.controlAdapters.push({ id, @@ -42,7 +45,7 @@ export const controlAdaptersReducers = { ...config, }); }, - prepare: (config: ControlAdapterConfig) => ({ + prepare: (config: ControlNetConfig | T2IAdapterConfig) => ({ payload: { id: uuidv4(), config }, }), }, @@ -169,13 +172,6 @@ export const controlAdaptersReducers = { } ca.model = zModelIdentifierField.parse(modelConfig); - // We may need to convert the CA to match the model - if (!ca.controlMode && ca.model.type === 'controlnet') { - ca.controlMode = 'balanced'; - } else if (ca.controlMode && ca.model.type === 't2i_adapter') { - ca.controlMode = null; - } - const candidateProcessorConfig = buildControlAdapterProcessorV2(modelConfig); if (candidateProcessorConfig?.type !== ca.processorConfig?.type) { // The processor has changed. For example, the previous model was a Canny model and the new model is a Depth @@ -183,11 +179,21 @@ export const controlAdaptersReducers = { ca.processedImage = null; ca.processorConfig = candidateProcessorConfig; } + + // We may need to convert the CA to match the model + if (ca.adapterType === 't2i_adapter' && ca.model.type === 'controlnet') { + const convertedCA: ControlNetData = { ...ca, adapterType: 'controlnet', controlMode: 'balanced' }; + state.controlAdapters.splice(state.controlAdapters.indexOf(ca), 1, convertedCA); + } else if (ca.adapterType === 'controlnet' && ca.model.type === 't2i_adapter') { + const { controlMode: _, ...rest } = ca; + const convertedCA: T2IAdapterData = { ...rest, adapterType: 't2i_adapter' }; + state.controlAdapters.splice(state.controlAdapters.indexOf(ca), 1, convertedCA); + } }, caControlModeChanged: (state, action: PayloadAction<{ id: string; controlMode: ControlModeV2 }>) => { const { id, controlMode } = action.payload; const ca = selectCA(state, id); - if (!ca) { + if (!ca || ca.adapterType !== 'controlnet') { return; } ca.controlMode = controlMode; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 64e2ffea05d..cd8b112e492 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -681,7 +681,7 @@ export type InpaintMaskData = z.infer; const zFilter = z.enum(['none', 'LightnessToAlphaFilter']); export type Filter = z.infer; -const zControlAdapterData = z.object({ +const zControlAdapterDataBase = z.object({ id: zId, type: z.literal('control_adapter'), isEnabled: z.boolean(), @@ -698,15 +698,37 @@ const zControlAdapterData = z.object({ processorPendingBatchId: z.string().nullable().default(null), beginEndStepPct: zBeginEndStepPct, model: zModelIdentifierField.nullable(), - controlMode: zControlModeV2.nullable(), }); +const zControlNetData = zControlAdapterDataBase.extend({ + adapterType: z.literal('controlnet'), + controlMode: zControlModeV2, +}); +export type ControlNetData = z.infer; +const zT2IAdapterData = zControlAdapterDataBase.extend({ + adapterType: z.literal('t2i_adapter'), +}); +export type T2IAdapterData = z.infer; + +const zControlAdapterData = z.discriminatedUnion('adapterType', [zControlNetData, zT2IAdapterData]); export type ControlAdapterData = z.infer; -export type ControlAdapterConfig = Pick< - ControlAdapterData, - 'weight' | 'image' | 'processedImage' | 'processorConfig' | 'beginEndStepPct' | 'model' | 'controlMode' +export type ControlNetConfig = Pick< + ControlNetData, + | 'adapterType' + | 'weight' + | 'image' + | 'processedImage' + | 'processorConfig' + | 'beginEndStepPct' + | 'model' + | 'controlMode' +>; +export type T2IAdapterConfig = Pick< + T2IAdapterData, + 'adapterType' | 'weight' | 'image' | 'processedImage' | 'processorConfig' | 'beginEndStepPct' | 'model' >; -export const initialControlNetV2: ControlAdapterConfig = { +export const initialControlNetV2: ControlNetConfig = { + adapterType: 'controlnet', model: null, weight: 1, beginEndStepPct: [0, 1], @@ -716,11 +738,11 @@ export const initialControlNetV2: ControlAdapterConfig = { processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(), }; -export const initialT2IAdapterV2: ControlAdapterConfig = { +export const initialT2IAdapterV2: T2IAdapterConfig = { + adapterType: 't2i_adapter', model: null, weight: 1, beginEndStepPct: [0, 1], - controlMode: null, image: null, processedImage: null, processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(), @@ -735,11 +757,11 @@ export const initialIPAdapterV2: IPAdapterConfig = { weight: 1, }; -export const buildControlNet = (id: string, overrides?: Partial): ControlAdapterConfig => { +export const buildControlNet = (id: string, overrides?: Partial): ControlNetConfig => { return merge(deepClone(initialControlNetV2), { id, ...overrides }); }; -export const buildT2IAdapter = (id: string, overrides?: Partial): ControlAdapterConfig => { +export const buildT2IAdapter = (id: string, overrides?: Partial): T2IAdapterConfig => { return merge(deepClone(initialT2IAdapterV2), { id, ...overrides }); }; From 9a30bcbd940465f8eb67ddb979a4bb87305352bd Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 16 Jun 2024 09:48:20 +1000 Subject: [PATCH 051/678] refactor(ui): update generation tab graphs --- .../graph/generation/addControlAdapters.ts | 131 ++++ .../util/graph/generation/addControlLayers.ts | 617 ------------------ .../nodes/util/graph/generation/addHRF.ts | 2 +- .../util/graph/generation/addIPAdapters.ts | 64 ++ .../nodes/util/graph/generation/addRegions.ts | 303 +++++++++ .../generation/buildGenerationTabGraph.ts | 30 +- .../generation/buildGenerationTabSDXLGraph.ts | 23 +- 7 files changed, 532 insertions(+), 638 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts new file mode 100644 index 00000000000..ba74bfc1b3f --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts @@ -0,0 +1,131 @@ +import type { + ControlAdapterData, + ControlNetData, + ImageWithDims, + ProcessorConfig, + T2IAdapterData, +} from 'features/controlLayers/store/types'; +import type { ImageField } from 'features/nodes/types/common'; +import { CONTROL_NET_COLLECT, T2I_ADAPTER_COLLECT } from 'features/nodes/util/graph/constants'; +import type { Graph } from 'features/nodes/util/graph/generation/Graph'; +import type { BaseModelType, Invocation } from 'services/api/types'; +import { assert } from 'tsafe'; + +export const addControlAdapters = ( + controlAdapters: ControlAdapterData[], + g: Graph, + denoise: Invocation<'denoise_latents'>, + base: BaseModelType +): ControlAdapterData[] => { + const validControlAdapters = controlAdapters.filter((ca) => isValidControlAdapter(ca, base)); + for (const ca of validControlAdapters) { + if (ca.adapterType === 'controlnet') { + addControlNetToGraph(ca, g, denoise); + } else { + addT2IAdapterToGraph(ca, g, denoise); + } + } + return validControlAdapters; +}; + +const addControlNetCollectorSafe = (g: Graph, denoise: Invocation<'denoise_latents'>): Invocation<'collect'> => { + try { + // Attempt to retrieve the collector + const controlNetCollect = g.getNode(CONTROL_NET_COLLECT); + assert(controlNetCollect.type === 'collect'); + return controlNetCollect; + } catch { + // Add the ControlNet collector + const controlNetCollect = g.addNode({ + id: CONTROL_NET_COLLECT, + type: 'collect', + }); + g.addEdge(controlNetCollect, 'collection', denoise, 'control'); + return controlNetCollect; + } +}; + +const addControlNetToGraph = (ca: ControlNetData, g: Graph, denoise: Invocation<'denoise_latents'>) => { + const { id, beginEndStepPct, controlMode, image, model, processedImage, processorConfig, weight } = ca; + assert(model, 'ControlNet model is required'); + const controlImage = buildControlImage(image, processedImage, processorConfig); + const controlNetCollect = addControlNetCollectorSafe(g, denoise); + + const controlNet = g.addNode({ + id: `control_net_${id}`, + type: 'controlnet', + begin_step_percent: beginEndStepPct[0], + end_step_percent: beginEndStepPct[1], + control_mode: controlMode, + resize_mode: 'just_resize', + control_model: model, + control_weight: weight, + image: controlImage, + }); + g.addEdge(controlNet, 'control', controlNetCollect, 'item'); +}; + +const addT2IAdapterCollectorSafe = (g: Graph, denoise: Invocation<'denoise_latents'>): Invocation<'collect'> => { + try { + // You see, we've already got one! + const t2iAdapterCollect = g.getNode(T2I_ADAPTER_COLLECT); + assert(t2iAdapterCollect.type === 'collect'); + return t2iAdapterCollect; + } catch { + const t2iAdapterCollect = g.addNode({ + id: T2I_ADAPTER_COLLECT, + type: 'collect', + }); + + g.addEdge(t2iAdapterCollect, 'collection', denoise, 't2i_adapter'); + + return t2iAdapterCollect; + } +}; + +const addT2IAdapterToGraph = (ca: T2IAdapterData, g: Graph, denoise: Invocation<'denoise_latents'>) => { + const { id, beginEndStepPct, image, model, processedImage, processorConfig, weight } = ca; + assert(model, 'T2I Adapter model is required'); + const controlImage = buildControlImage(image, processedImage, processorConfig); + const t2iAdapterCollect = addT2IAdapterCollectorSafe(g, denoise); + + const t2iAdapter = g.addNode({ + id: `t2i_adapter_${id}`, + type: 't2i_adapter', + begin_step_percent: beginEndStepPct[0], + end_step_percent: beginEndStepPct[1], + resize_mode: 'just_resize', + t2i_adapter_model: model, + weight: weight, + image: controlImage, + }); + + g.addEdge(t2iAdapter, 't2i_adapter', t2iAdapterCollect, 'item'); +}; + +const buildControlImage = ( + image: ImageWithDims | null, + processedImage: ImageWithDims | null, + processorConfig: ProcessorConfig | null +): ImageField => { + if (processedImage && processorConfig) { + // We've processed the image in the app - use it for the control image. + return { + image_name: processedImage.name, + }; + } else if (image) { + // No processor selected, and we have an image - the user provided a processed image, use it for the control image. + return { + image_name: image.name, + }; + } + assert(false, 'Attempted to add unprocessed control image'); +}; + +const isValidControlAdapter = (ca: ControlAdapterData, base: BaseModelType): boolean => { + // Must be have a model that matches the current base and must have a control image + const hasModel = Boolean(ca.model); + const modelMatchesBase = ca.model?.base === base; + const hasControlImage = Boolean(ca.image || (ca.processedImage && ca.processorConfig)); + return hasModel && modelMatchesBase && hasControlImage; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts deleted file mode 100644 index 99fc769ffb3..00000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts +++ /dev/null @@ -1,617 +0,0 @@ -import { getStore } from 'app/store/nanostores/store'; -import type { RootState } from 'app/store/store'; -import { deepClone } from 'common/util/deepClone'; -import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; -import { blobToDataURL } from 'features/canvas/util/blobToDataURL'; -import { RG_LAYER_NAME } from 'features/controlLayers/konva/naming'; -import { renderers } from 'features/controlLayers/konva/renderers/layers'; -import { regionalGuidanceMaskImageUploaded } from 'features/controlLayers/store/canvasV2Slice'; -import type { InitialImageLayer, LayerData, RegionalGuidanceLayer } from 'features/controlLayers/store/types'; -import { - isControlAdapterLayer, - isInitialImageLayer, - isIPAdapterLayer, - isRegionalGuidanceLayer, -} from 'features/controlLayers/store/types'; -import type { - ControlNetConfigV2, - ImageWithDims, - IPAdapterConfigV2, - ProcessorConfig, - T2IAdapterConfigV2, -} from 'features/controlLayers/util/controlAdapters'; -import type { ImageField } from 'features/nodes/types/common'; -import { - CONTROL_NET_COLLECT, - IMAGE_TO_LATENTS, - IP_ADAPTER_COLLECT, - PROMPT_REGION_INVERT_TENSOR_MASK_PREFIX, - PROMPT_REGION_MASK_TO_TENSOR_PREFIX, - PROMPT_REGION_NEGATIVE_COND_PREFIX, - PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX, - PROMPT_REGION_POSITIVE_COND_PREFIX, - RESIZE, - T2I_ADAPTER_COLLECT, -} from 'features/nodes/util/graph/constants'; -import type { Graph } from 'features/nodes/util/graph/generation/Graph'; -import Konva from 'konva'; -import { size } from 'lodash-es'; -import { getImageDTO, imagesApi } from 'services/api/endpoints/images'; -import type { BaseModelType, ImageDTO, Invocation } from 'services/api/types'; -import { assert } from 'tsafe'; - -//#region addControlLayers -/** - * Adds the control layers to the graph - * @param state The app root state - * @param g The graph to add the layers to - * @param base The base model type - * @param denoise The main denoise node - * @param posCond The positive conditioning node - * @param negCond The negative conditioning node - * @param posCondCollect The positive conditioning collector - * @param negCondCollect The negative conditioning collector - * @param noise The noise node - * @param vaeSource The VAE source (either seamless, vae_loader, main_model_loader, or sdxl_model_loader) - * @returns A promise that resolves to the layers that were added to the graph - */ -export const addControlLayers = async ( - state: RootState, - g: Graph, - base: BaseModelType, - denoise: Invocation<'denoise_latents'>, - posCond: Invocation<'compel'> | Invocation<'sdxl_compel_prompt'>, - negCond: Invocation<'compel'> | Invocation<'sdxl_compel_prompt'>, - posCondCollect: Invocation<'collect'>, - negCondCollect: Invocation<'collect'>, - noise: Invocation<'noise'>, - vaeSource: - | Invocation<'seamless'> - | Invocation<'vae_loader'> - | Invocation<'main_model_loader'> - | Invocation<'sdxl_model_loader'> -): Promise => { - const isSDXL = base === 'sdxl'; - - const validLayers = state.canvasV2.layers.filter((l) => isValidLayer(l, base)); - - const validControlAdapters = validLayers.filter(isControlAdapterLayer).map((l) => l.controlAdapter); - for (const ca of validControlAdapters) { - addGlobalControlAdapterToGraph(ca, g, denoise); - } - - const validIPAdapters = validLayers.filter(isIPAdapterLayer).map((l) => l.ipAdapter); - for (const ipAdapter of validIPAdapters) { - addGlobalIPAdapterToGraph(ipAdapter, g, denoise); - } - - const initialImageLayers = validLayers.filter(isInitialImageLayer); - assert(initialImageLayers.length <= 1, 'Only one initial image layer allowed'); - if (initialImageLayers[0]) { - addInitialImageLayerToGraph(state, g, base, denoise, noise, vaeSource, initialImageLayers[0]); - } - // TODO: We should probably just use conditioning collectors by default, and skip all this fanagling with re-routing - // the existing conditioning nodes. - - const validRGLayers = validLayers.filter(isRegionalGuidanceLayer); - const layerIds = validRGLayers.map((l) => l.id); - const blobs = await getRGLayerBlobs(layerIds); - assert(size(blobs) === size(layerIds), 'Mismatch between layer IDs and blobs'); - - for (const layer of validRGLayers) { - const blob = blobs[layer.id]; - assert(blob, `Blob for layer ${layer.id} not found`); - // Upload the mask image, or get the cached image if it exists - const { image_name } = await getMaskImage(layer, blob); - - // The main mask-to-tensor node - const maskToTensor = g.addNode({ - id: `${PROMPT_REGION_MASK_TO_TENSOR_PREFIX}_${layer.id}`, - type: 'alpha_mask_to_tensor', - image: { - image_name, - }, - }); - - if (layer.positivePrompt) { - // The main positive conditioning node - const regionalPosCond = g.addNode( - isSDXL - ? { - type: 'sdxl_compel_prompt', - id: `${PROMPT_REGION_POSITIVE_COND_PREFIX}_${layer.id}`, - prompt: layer.positivePrompt, - style: layer.positivePrompt, // TODO: Should we put the positive prompt in both fields? - } - : { - type: 'compel', - id: `${PROMPT_REGION_POSITIVE_COND_PREFIX}_${layer.id}`, - prompt: layer.positivePrompt, - } - ); - // Connect the mask to the conditioning - g.addEdge(maskToTensor, 'mask', regionalPosCond, 'mask'); - // Connect the conditioning to the collector - g.addEdge(regionalPosCond, 'conditioning', posCondCollect, 'item'); - // Copy the connections to the "global" positive conditioning node to the regional cond - if (posCond.type === 'compel') { - for (const edge of g.getEdgesTo(posCond, ['clip', 'mask'])) { - // Clone the edge, but change the destination node to the regional conditioning node - const clone = deepClone(edge); - clone.destination.node_id = regionalPosCond.id; - g.addEdgeFromObj(clone); - } - } else { - for (const edge of g.getEdgesTo(posCond, ['clip', 'clip2', 'mask'])) { - // Clone the edge, but change the destination node to the regional conditioning node - const clone = deepClone(edge); - clone.destination.node_id = regionalPosCond.id; - g.addEdgeFromObj(clone); - } - } - } - - if (layer.negativePrompt) { - // The main negative conditioning node - const regionalNegCond = g.addNode( - isSDXL - ? { - type: 'sdxl_compel_prompt', - id: `${PROMPT_REGION_NEGATIVE_COND_PREFIX}_${layer.id}`, - prompt: layer.negativePrompt, - style: layer.negativePrompt, - } - : { - type: 'compel', - id: `${PROMPT_REGION_NEGATIVE_COND_PREFIX}_${layer.id}`, - prompt: layer.negativePrompt, - } - ); - // Connect the mask to the conditioning - g.addEdge(maskToTensor, 'mask', regionalNegCond, 'mask'); - // Connect the conditioning to the collector - g.addEdge(regionalNegCond, 'conditioning', negCondCollect, 'item'); - // Copy the connections to the "global" negative conditioning node to the regional cond - if (negCond.type === 'compel') { - for (const edge of g.getEdgesTo(negCond, ['clip', 'mask'])) { - const clone = deepClone(edge); - clone.destination.node_id = regionalNegCond.id; - g.addEdgeFromObj(clone); - } - } else { - for (const edge of g.getEdgesTo(negCond, ['clip', 'clip2', 'mask'])) { - const clone = deepClone(edge); - clone.destination.node_id = regionalNegCond.id; - g.addEdgeFromObj(clone); - } - } - } - - // If we are using the "invert" auto-negative setting, we need to add an additional negative conditioning node - if (layer.autoNegative === 'invert' && layer.positivePrompt) { - // We re-use the mask image, but invert it when converting to tensor - const invertTensorMask = g.addNode({ - id: `${PROMPT_REGION_INVERT_TENSOR_MASK_PREFIX}_${layer.id}`, - type: 'invert_tensor_mask', - }); - // Connect the OG mask image to the inverted mask-to-tensor node - g.addEdge(maskToTensor, 'mask', invertTensorMask, 'mask'); - // Create the conditioning node. It's going to be connected to the negative cond collector, but it uses the positive prompt - const regionalPosCondInverted = g.addNode( - isSDXL - ? { - type: 'sdxl_compel_prompt', - id: `${PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX}_${layer.id}`, - prompt: layer.positivePrompt, - style: layer.positivePrompt, - } - : { - type: 'compel', - id: `${PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX}_${layer.id}`, - prompt: layer.positivePrompt, - } - ); - // Connect the inverted mask to the conditioning - g.addEdge(invertTensorMask, 'mask', regionalPosCondInverted, 'mask'); - // Connect the conditioning to the negative collector - g.addEdge(regionalPosCondInverted, 'conditioning', negCondCollect, 'item'); - // Copy the connections to the "global" positive conditioning node to our regional node - if (posCond.type === 'compel') { - for (const edge of g.getEdgesTo(posCond, ['clip', 'mask'])) { - const clone = deepClone(edge); - clone.destination.node_id = regionalPosCondInverted.id; - g.addEdgeFromObj(clone); - } - } else { - for (const edge of g.getEdgesTo(posCond, ['clip', 'clip2', 'mask'])) { - const clone = deepClone(edge); - clone.destination.node_id = regionalPosCondInverted.id; - g.addEdgeFromObj(clone); - } - } - } - - const validRegionalIPAdapters: IPAdapterConfigV2[] = layer.ipAdapters.filter((ipa) => isValidIPAdapter(ipa, base)); - - for (const ipAdapterConfig of validRegionalIPAdapters) { - const ipAdapterCollect = addIPAdapterCollectorSafe(g, denoise); - const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapterConfig; - assert(model, 'IP Adapter model is required'); - assert(image, 'IP Adapter image is required'); - - const ipAdapter = g.addNode({ - id: `ip_adapter_${id}`, - type: 'ip_adapter', - weight, - method, - ip_adapter_model: model, - clip_vision_model: clipVisionModel, - begin_step_percent: beginEndStepPct[0], - end_step_percent: beginEndStepPct[1], - image: { - image_name: image.name, - }, - }); - - // Connect the mask to the conditioning - g.addEdge(maskToTensor, 'mask', ipAdapter, 'mask'); - g.addEdge(ipAdapter, 'ip_adapter', ipAdapterCollect, 'item'); - } - } - - g.upsertMetadata({ control_layers: { layers: validLayers, version: state.canvasV2._version } }); - return validLayers; -}; -//#endregion - -//#region Control Adapters -const addGlobalControlAdapterToGraph = ( - controlAdapterConfig: ControlNetConfigV2 | T2IAdapterConfigV2, - g: Graph, - denoise: Invocation<'denoise_latents'> -): void => { - if (controlAdapterConfig.type === 'controlnet') { - addGlobalControlNetToGraph(controlAdapterConfig, g, denoise); - } - if (controlAdapterConfig.type === 't2i_adapter') { - addGlobalT2IAdapterToGraph(controlAdapterConfig, g, denoise); - } -}; - -const addControlNetCollectorSafe = (g: Graph, denoise: Invocation<'denoise_latents'>): Invocation<'collect'> => { - try { - // Attempt to retrieve the collector - const controlNetCollect = g.getNode(CONTROL_NET_COLLECT); - assert(controlNetCollect.type === 'collect'); - return controlNetCollect; - } catch { - // Add the ControlNet collector - const controlNetCollect = g.addNode({ - id: CONTROL_NET_COLLECT, - type: 'collect', - }); - g.addEdge(controlNetCollect, 'collection', denoise, 'control'); - return controlNetCollect; - } -}; - -const addGlobalControlNetToGraph = ( - controlNetConfig: ControlNetConfigV2, - g: Graph, - denoise: Invocation<'denoise_latents'> -) => { - const { id, beginEndStepPct, controlMode, image, model, processedImage, processorConfig, weight } = controlNetConfig; - assert(model, 'ControlNet model is required'); - const controlImage = buildControlImage(image, processedImage, processorConfig); - const controlNetCollect = addControlNetCollectorSafe(g, denoise); - - const controlNet = g.addNode({ - id: `control_net_${id}`, - type: 'controlnet', - begin_step_percent: beginEndStepPct[0], - end_step_percent: beginEndStepPct[1], - control_mode: controlMode, - resize_mode: 'just_resize', - control_model: model, - control_weight: weight, - image: controlImage, - }); - g.addEdge(controlNet, 'control', controlNetCollect, 'item'); -}; - -const addT2IAdapterCollectorSafe = (g: Graph, denoise: Invocation<'denoise_latents'>): Invocation<'collect'> => { - try { - // You see, we've already got one! - const t2iAdapterCollect = g.getNode(T2I_ADAPTER_COLLECT); - assert(t2iAdapterCollect.type === 'collect'); - return t2iAdapterCollect; - } catch { - const t2iAdapterCollect = g.addNode({ - id: T2I_ADAPTER_COLLECT, - type: 'collect', - }); - - g.addEdge(t2iAdapterCollect, 'collection', denoise, 't2i_adapter'); - - return t2iAdapterCollect; - } -}; - -const addGlobalT2IAdapterToGraph = ( - t2iAdapterConfig: T2IAdapterConfigV2, - g: Graph, - denoise: Invocation<'denoise_latents'> -) => { - const { id, beginEndStepPct, image, model, processedImage, processorConfig, weight } = t2iAdapterConfig; - assert(model, 'T2I Adapter model is required'); - const controlImage = buildControlImage(image, processedImage, processorConfig); - const t2iAdapterCollect = addT2IAdapterCollectorSafe(g, denoise); - - const t2iAdapter = g.addNode({ - id: `t2i_adapter_${id}`, - type: 't2i_adapter', - begin_step_percent: beginEndStepPct[0], - end_step_percent: beginEndStepPct[1], - resize_mode: 'just_resize', - t2i_adapter_model: model, - weight: weight, - image: controlImage, - }); - - g.addEdge(t2iAdapter, 't2i_adapter', t2iAdapterCollect, 'item'); -}; - -//#region IP Adapter -const addIPAdapterCollectorSafe = (g: Graph, denoise: Invocation<'denoise_latents'>): Invocation<'collect'> => { - try { - // You see, we've already got one! - const ipAdapterCollect = g.getNode(IP_ADAPTER_COLLECT); - assert(ipAdapterCollect.type === 'collect'); - return ipAdapterCollect; - } catch { - const ipAdapterCollect = g.addNode({ - id: IP_ADAPTER_COLLECT, - type: 'collect', - }); - g.addEdge(ipAdapterCollect, 'collection', denoise, 'ip_adapter'); - return ipAdapterCollect; - } -}; - -const addGlobalIPAdapterToGraph = ( - ipAdapterConfig: IPAdapterConfigV2, - g: Graph, - denoise: Invocation<'denoise_latents'> -) => { - const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapterConfig; - assert(image, 'IP Adapter image is required'); - assert(model, 'IP Adapter model is required'); - const ipAdapterCollect = addIPAdapterCollectorSafe(g, denoise); - - const ipAdapter = g.addNode({ - id: `ip_adapter_${id}`, - type: 'ip_adapter', - weight, - method, - ip_adapter_model: model, - clip_vision_model: clipVisionModel, - begin_step_percent: beginEndStepPct[0], - end_step_percent: beginEndStepPct[1], - image: { - image_name: image.name, - }, - }); - g.addEdge(ipAdapter, 'ip_adapter', ipAdapterCollect, 'item'); -}; -//#endregion - -//#region Initial Image -const addInitialImageLayerToGraph = ( - state: RootState, - g: Graph, - base: BaseModelType, - denoise: Invocation<'denoise_latents'>, - noise: Invocation<'noise'>, - vaeSource: - | Invocation<'seamless'> - | Invocation<'vae_loader'> - | Invocation<'main_model_loader'> - | Invocation<'sdxl_model_loader'>, - layer: InitialImageLayer -) => { - const { vaePrecision } = state.canvasV2.params; - const { refinerModel, refinerStart } = state.canvasV2.params; - const { width, height } = state.canvasV2.document; - assert(layer.isEnabled, 'Initial image layer is not enabled'); - assert(layer.image, 'Initial image layer has no image'); - - const isSDXL = base === 'sdxl'; - const useRefinerStartEnd = isSDXL && Boolean(refinerModel); - - const { denoisingStrength } = layer; - denoise.denoising_start = useRefinerStartEnd ? Math.min(refinerStart, 1 - denoisingStrength) : 1 - denoisingStrength; - denoise.denoising_end = useRefinerStartEnd ? refinerStart : 1; - - const i2l = g.addNode({ - type: 'i2l', - id: IMAGE_TO_LATENTS, - fp32: vaePrecision === 'fp32', - }); - - g.addEdge(i2l, 'latents', denoise, 'latents'); - g.addEdge(vaeSource, 'vae', i2l, 'vae'); - - if (layer.image.width !== width || layer.image.height !== height) { - // The init image needs to be resized to the specified width and height before being passed to `IMAGE_TO_LATENTS` - - // Create a resize node, explicitly setting its image - const resize = g.addNode({ - id: RESIZE, - type: 'img_resize', - image: { - image_name: layer.image.name, - }, - width, - height, - }); - - // The `RESIZE` node then passes its image to `IMAGE_TO_LATENTS` - g.addEdge(resize, 'image', i2l, 'image'); - // The `RESIZE` node also passes its width and height to `NOISE` - g.addEdge(resize, 'width', noise, 'width'); - g.addEdge(resize, 'height', noise, 'height'); - } else { - // We are not resizing, so we need to set the image on the `IMAGE_TO_LATENTS` node explicitly - i2l.image = { - image_name: layer.image.name, - }; - - // Pass the image's dimensions to the `NOISE` node - g.addEdge(i2l, 'width', noise, 'width'); - g.addEdge(i2l, 'height', noise, 'height'); - } - - g.upsertMetadata({ generation_mode: isSDXL ? 'sdxl_img2img' : 'img2img' }); -}; -//#endregion - -//#region Layer validators -const isValidControlAdapter = (ca: ControlNetConfigV2 | T2IAdapterConfigV2, base: BaseModelType): boolean => { - // Must be have a model that matches the current base and must have a control image - const hasModel = Boolean(ca.model); - const modelMatchesBase = ca.model?.base === base; - const hasControlImage = Boolean(ca.image || (ca.processedImage && ca.processorConfig)); - return hasModel && modelMatchesBase && hasControlImage; -}; - -const isValidIPAdapter = (ipa: IPAdapterConfigV2, base: BaseModelType): boolean => { - // Must be have a model that matches the current base and must have a control image - const hasModel = Boolean(ipa.model); - const modelMatchesBase = ipa.model?.base === base; - const hasImage = Boolean(ipa.image); - return hasModel && modelMatchesBase && hasImage; -}; - -const isValidLayer = (layer: LayerData, base: BaseModelType) => { - if (!layer.isEnabled) { - return false; - } - if (isControlAdapterLayer(layer)) { - return isValidControlAdapter(layer.controlAdapter, base); - } - if (isIPAdapterLayer(layer)) { - return isValidIPAdapter(layer.ipAdapter, base); - } - if (isInitialImageLayer(layer)) { - const hasImage = Boolean(layer.image); - return hasImage; - } - if (isRegionalGuidanceLayer(layer)) { - const hasTextPrompt = Boolean(layer.positivePrompt || layer.negativePrompt); - const hasIPAdapter = layer.ipAdapters.filter((ipa) => isValidIPAdapter(ipa, base)).length > 0; - return hasTextPrompt || hasIPAdapter; - } - return false; -}; -//#endregion - -//#region getMaskImage -const getMaskImage = async (layer: RegionalGuidanceLayer, blob: Blob): Promise => { - if (layer.uploadedMaskImage) { - const imageDTO = await getImageDTO(layer.uploadedMaskImage.name); - if (imageDTO) { - return imageDTO; - } - } - const { dispatch } = getStore(); - // No cached mask, or the cached image no longer exists - we need to upload the mask image - const file = new File([blob], `${layer.id}_mask.png`, { type: 'image/png' }); - const req = dispatch( - imagesApi.endpoints.uploadImage.initiate({ file, image_category: 'mask', is_intermediate: true }) - ); - req.reset(); - - const imageDTO = await req.unwrap(); - dispatch(regionalGuidanceMaskImageUploaded({ layerId: layer.id, imageDTO })); - return imageDTO; -}; -//#endregion - -//#region buildControlImage -const buildControlImage = ( - image: ImageWithDims | null, - processedImage: ImageWithDims | null, - processorConfig: ProcessorConfig | null -): ImageField => { - if (processedImage && processorConfig) { - // We've processed the image in the app - use it for the control image. - return { - image_name: processedImage.name, - }; - } else if (image) { - // No processor selected, and we have an image - the user provided a processed image, use it for the control image. - return { - image_name: image.name, - }; - } - assert(false, 'Attempted to add unprocessed control image'); -}; -//#endregion - -//#region getRGLayerBlobs -/** - * Get the blobs of all regional prompt layers. Only visible layers are returned. - * @param layerIds The IDs of the layers to get blobs for. If not provided, all regional prompt layers are used. - * @param preview Whether to open a new tab displaying each layer. - * @returns A map of layer IDs to blobs. - */ -const getRGLayerBlobs = async (layerIds?: string[], preview: boolean = false): Promise> => { - const state = getStore().getState(); - const { layers } = state.canvasV2; - const { width, height } = state.canvasV2.document; - const reduxLayers = layers.filter(isRegionalGuidanceLayer); - const container = document.createElement('div'); - const stage = new Konva.Stage({ container, width, height }); - renderers.renderLayers(stage, reduxLayers, 1, 'brush', getImageDTO); - - const konvaLayers = stage.find(`.${RG_LAYER_NAME}`); - const blobs: Record = {}; - - // First remove all layers - for (const layer of konvaLayers) { - layer.remove(); - } - - // Next render each layer to a blob - for (const layer of konvaLayers) { - if (layerIds && !layerIds.includes(layer.id())) { - continue; - } - const reduxLayer = reduxLayers.find((l) => l.id === layer.id()); - assert(reduxLayer, `Redux layer ${layer.id()} not found`); - stage.add(layer); - const blob = await new Promise((resolve) => { - stage.toBlob({ - callback: (blob) => { - assert(blob, 'Blob is null'); - resolve(blob); - }, - }); - }); - - if (preview) { - const base64 = await blobToDataURL(blob); - openBase64ImageInTab([ - { - base64, - caption: `${reduxLayer.id}: ${reduxLayer.positivePrompt} / ${reduxLayer.negativePrompt}`, - }, - ]); - } - layer.remove(); - blobs[layer.id()] = blob; - } - - return blobs; -}; -//#endregion diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addHRF.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addHRF.ts index 3faa9eccae5..2d2e2369d95 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addHRF.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addHRF.ts @@ -1,6 +1,7 @@ import type { RootState } from 'app/store/store'; import { deepClone } from 'common/util/deepClone'; import { roundToMultiple } from 'common/util/roundDownToMultiple'; +import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { DENOISE_LATENTS_HRF, ESRGAN_HRF, @@ -12,7 +13,6 @@ import { } from 'features/nodes/util/graph/constants'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { getBoardField } from 'features/nodes/util/graph/graphBuilderUtils'; -import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import type { Invocation } from 'services/api/types'; /** diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts new file mode 100644 index 00000000000..dfbd4668ab4 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts @@ -0,0 +1,64 @@ +import type { IPAdapterData } from 'features/controlLayers/store/types'; +import { IP_ADAPTER_COLLECT } from 'features/nodes/util/graph/constants'; +import type { Graph } from 'features/nodes/util/graph/generation/Graph'; +import type { BaseModelType, Invocation } from 'services/api/types'; +import { assert } from 'tsafe'; + +export const addIPAdapters = ( + ipAdapters: IPAdapterData[], + g: Graph, + denoise: Invocation<'denoise_latents'>, + base: BaseModelType +): IPAdapterData[] => { + const validIPAdapters = ipAdapters.filter((ipa) => isValidIPAdapter(ipa, base)); + for (const ipa of validIPAdapters) { + addIPAdapter(ipa, g, denoise); + } + return validIPAdapters; +}; + +export const addIPAdapterCollectorSafe = (g: Graph, denoise: Invocation<'denoise_latents'>): Invocation<'collect'> => { + try { + // You see, we've already got one! + const ipAdapterCollect = g.getNode(IP_ADAPTER_COLLECT); + assert(ipAdapterCollect.type === 'collect'); + return ipAdapterCollect; + } catch { + const ipAdapterCollect = g.addNode({ + id: IP_ADAPTER_COLLECT, + type: 'collect', + }); + g.addEdge(ipAdapterCollect, 'collection', denoise, 'ip_adapter'); + return ipAdapterCollect; + } +}; + +const addIPAdapter = (ipa: IPAdapterData, g: Graph, denoise: Invocation<'denoise_latents'>) => { + const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipa; + assert(image, 'IP Adapter image is required'); + assert(model, 'IP Adapter model is required'); + const ipAdapterCollect = addIPAdapterCollectorSafe(g, denoise); + + const ipAdapter = g.addNode({ + id: `ip_adapter_${id}`, + type: 'ip_adapter', + weight, + method, + ip_adapter_model: model, + clip_vision_model: clipVisionModel, + begin_step_percent: beginEndStepPct[0], + end_step_percent: beginEndStepPct[1], + image: { + image_name: image.name, + }, + }); + g.addEdge(ipAdapter, 'ip_adapter', ipAdapterCollect, 'item'); +}; + +export const isValidIPAdapter = (ipa: IPAdapterData, base: BaseModelType): boolean => { + // Must be have a model that matches the current base and must have a control image + const hasModel = Boolean(ipa.model); + const modelMatchesBase = ipa.model?.base === base; + const hasImage = Boolean(ipa.image); + return hasModel && modelMatchesBase && hasImage; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts new file mode 100644 index 00000000000..06eaf0be12a --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts @@ -0,0 +1,303 @@ +import { getStore } from 'app/store/nanostores/store'; +import { deepClone } from 'common/util/deepClone'; +import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; +import { blobToDataURL } from 'features/canvas/util/blobToDataURL'; +import { RG_LAYER_NAME } from 'features/controlLayers/konva/naming'; +import { renderers } from 'features/controlLayers/konva/renderers/layers'; +import { rgMaskImageUploaded } from 'features/controlLayers/store/canvasV2Slice'; +import type { Dimensions, IPAdapterData, RegionalGuidanceData } from 'features/controlLayers/store/types'; +import { + PROMPT_REGION_INVERT_TENSOR_MASK_PREFIX, + PROMPT_REGION_MASK_TO_TENSOR_PREFIX, + PROMPT_REGION_NEGATIVE_COND_PREFIX, + PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX, + PROMPT_REGION_POSITIVE_COND_PREFIX, +} from 'features/nodes/util/graph/constants'; +import { addIPAdapterCollectorSafe, isValidIPAdapter } from 'features/nodes/util/graph/generation/addIPAdapters'; +import type { Graph } from 'features/nodes/util/graph/generation/Graph'; +import Konva from 'konva'; +import type { IRect } from 'konva/lib/types'; +import { size } from 'lodash-es'; +import { getImageDTO, imagesApi } from 'services/api/endpoints/images'; +import type { BaseModelType, ImageDTO, Invocation } from 'services/api/types'; +import { assert } from 'tsafe'; + +/** + * Adds regional guidance to the graph + * @param regions Array of regions to add + * @param g The graph to add the layers to + * @param base The base model type + * @param denoise The main denoise node + * @param posCond The positive conditioning node + * @param negCond The negative conditioning node + * @param posCondCollect The positive conditioning collector + * @param negCondCollect The negative conditioning collector + * @returns A promise that resolves to the regions that were successfully added to the graph + */ + +export const addRegions = async ( + regions: RegionalGuidanceData[], + g: Graph, + documentSize: Dimensions, + bbox: IRect, + base: BaseModelType, + denoise: Invocation<'denoise_latents'>, + posCond: Invocation<'compel'> | Invocation<'sdxl_compel_prompt'>, + negCond: Invocation<'compel'> | Invocation<'sdxl_compel_prompt'>, + posCondCollect: Invocation<'collect'>, + negCondCollect: Invocation<'collect'> +): Promise => { + const isSDXL = base === 'sdxl'; + + const validRegions = regions.filter((rg) => isValidRegion(rg, base)); + const blobs = await getRGMaskBlobs(validRegions, documentSize, bbox); + assert(size(blobs) === size(validRegions), 'Mismatch between layer IDs and blobs'); + + for (const rg of validRegions) { + const blob = blobs[rg.id]; + assert(blob, `Blob for layer ${rg.id} not found`); + // Upload the mask image, or get the cached image if it exists + const { image_name } = await getMaskImage(rg, blob); + + // The main mask-to-tensor node + const maskToTensor = g.addNode({ + id: `${PROMPT_REGION_MASK_TO_TENSOR_PREFIX}_${rg.id}`, + type: 'alpha_mask_to_tensor', + image: { + image_name, + }, + }); + + if (rg.positivePrompt) { + // The main positive conditioning node + const regionalPosCond = g.addNode( + isSDXL + ? { + type: 'sdxl_compel_prompt', + id: `${PROMPT_REGION_POSITIVE_COND_PREFIX}_${rg.id}`, + prompt: rg.positivePrompt, + style: rg.positivePrompt, // TODO: Should we put the positive prompt in both fields? + } + : { + type: 'compel', + id: `${PROMPT_REGION_POSITIVE_COND_PREFIX}_${rg.id}`, + prompt: rg.positivePrompt, + } + ); + // Connect the mask to the conditioning + g.addEdge(maskToTensor, 'mask', regionalPosCond, 'mask'); + // Connect the conditioning to the collector + g.addEdge(regionalPosCond, 'conditioning', posCondCollect, 'item'); + // Copy the connections to the "global" positive conditioning node to the regional cond + if (posCond.type === 'compel') { + for (const edge of g.getEdgesTo(posCond, ['clip', 'mask'])) { + // Clone the edge, but change the destination node to the regional conditioning node + const clone = deepClone(edge); + clone.destination.node_id = regionalPosCond.id; + g.addEdgeFromObj(clone); + } + } else { + for (const edge of g.getEdgesTo(posCond, ['clip', 'clip2', 'mask'])) { + // Clone the edge, but change the destination node to the regional conditioning node + const clone = deepClone(edge); + clone.destination.node_id = regionalPosCond.id; + g.addEdgeFromObj(clone); + } + } + } + + if (rg.negativePrompt) { + // The main negative conditioning node + const regionalNegCond = g.addNode( + isSDXL + ? { + type: 'sdxl_compel_prompt', + id: `${PROMPT_REGION_NEGATIVE_COND_PREFIX}_${rg.id}`, + prompt: rg.negativePrompt, + style: rg.negativePrompt, + } + : { + type: 'compel', + id: `${PROMPT_REGION_NEGATIVE_COND_PREFIX}_${rg.id}`, + prompt: rg.negativePrompt, + } + ); + // Connect the mask to the conditioning + g.addEdge(maskToTensor, 'mask', regionalNegCond, 'mask'); + // Connect the conditioning to the collector + g.addEdge(regionalNegCond, 'conditioning', negCondCollect, 'item'); + // Copy the connections to the "global" negative conditioning node to the regional cond + if (negCond.type === 'compel') { + for (const edge of g.getEdgesTo(negCond, ['clip', 'mask'])) { + const clone = deepClone(edge); + clone.destination.node_id = regionalNegCond.id; + g.addEdgeFromObj(clone); + } + } else { + for (const edge of g.getEdgesTo(negCond, ['clip', 'clip2', 'mask'])) { + const clone = deepClone(edge); + clone.destination.node_id = regionalNegCond.id; + g.addEdgeFromObj(clone); + } + } + } + + // If we are using the "invert" auto-negative setting, we need to add an additional negative conditioning node + if (rg.autoNegative === 'invert' && rg.positivePrompt) { + // We re-use the mask image, but invert it when converting to tensor + const invertTensorMask = g.addNode({ + id: `${PROMPT_REGION_INVERT_TENSOR_MASK_PREFIX}_${rg.id}`, + type: 'invert_tensor_mask', + }); + // Connect the OG mask image to the inverted mask-to-tensor node + g.addEdge(maskToTensor, 'mask', invertTensorMask, 'mask'); + // Create the conditioning node. It's going to be connected to the negative cond collector, but it uses the positive prompt + const regionalPosCondInverted = g.addNode( + isSDXL + ? { + type: 'sdxl_compel_prompt', + id: `${PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX}_${rg.id}`, + prompt: rg.positivePrompt, + style: rg.positivePrompt, + } + : { + type: 'compel', + id: `${PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX}_${rg.id}`, + prompt: rg.positivePrompt, + } + ); + // Connect the inverted mask to the conditioning + g.addEdge(invertTensorMask, 'mask', regionalPosCondInverted, 'mask'); + // Connect the conditioning to the negative collector + g.addEdge(regionalPosCondInverted, 'conditioning', negCondCollect, 'item'); + // Copy the connections to the "global" positive conditioning node to our regional node + if (posCond.type === 'compel') { + for (const edge of g.getEdgesTo(posCond, ['clip', 'mask'])) { + const clone = deepClone(edge); + clone.destination.node_id = regionalPosCondInverted.id; + g.addEdgeFromObj(clone); + } + } else { + for (const edge of g.getEdgesTo(posCond, ['clip', 'clip2', 'mask'])) { + const clone = deepClone(edge); + clone.destination.node_id = regionalPosCondInverted.id; + g.addEdgeFromObj(clone); + } + } + } + + const validRGIPAdapters: IPAdapterData[] = rg.ipAdapters.filter((ipa) => isValidIPAdapter(ipa, base)); + + for (const ipa of validRGIPAdapters) { + const ipAdapterCollect = addIPAdapterCollectorSafe(g, denoise); + const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipa; + assert(model, 'IP Adapter model is required'); + assert(image, 'IP Adapter image is required'); + + const ipAdapter = g.addNode({ + id: `ip_adapter_${id}`, + type: 'ip_adapter', + weight, + method, + ip_adapter_model: model, + clip_vision_model: clipVisionModel, + begin_step_percent: beginEndStepPct[0], + end_step_percent: beginEndStepPct[1], + image: { + image_name: image.name, + }, + }); + + // Connect the mask to the conditioning + g.addEdge(maskToTensor, 'mask', ipAdapter, 'mask'); + g.addEdge(ipAdapter, 'ip_adapter', ipAdapterCollect, 'item'); + } + } + + g.upsertMetadata({ regions: validRegions }); + return validRegions; +}; + +export const isValidRegion = (rg: RegionalGuidanceData, base: BaseModelType) => { + const hasTextPrompt = Boolean(rg.positivePrompt || rg.negativePrompt); + const hasIPAdapter = rg.ipAdapters.filter((ipa) => isValidIPAdapter(ipa, base)).length > 0; + return hasTextPrompt || hasIPAdapter; +}; + +export const getMaskImage = async (rg: RegionalGuidanceData, blob: Blob): Promise => { + const { id, imageCache } = rg; + if (imageCache) { + const imageDTO = await getImageDTO(imageCache.name); + if (imageDTO) { + return imageDTO; + } + } + const { dispatch } = getStore(); + // No cached mask, or the cached image no longer exists - we need to upload the mask image + const file = new File([blob], `${rg.id}_mask.png`, { type: 'image/png' }); + const req = dispatch( + imagesApi.endpoints.uploadImage.initiate({ file, image_category: 'mask', is_intermediate: true }) + ); + req.reset(); + + const imageDTO = await req.unwrap(); + dispatch(rgMaskImageUploaded({ id, imageDTO })); + return imageDTO; +}; + +/** + * Get the blobs of all regional prompt layers. Only visible layers are returned. + * @param layerIds The IDs of the layers to get blobs for. If not provided, all regional prompt layers are used. + * @param preview Whether to open a new tab displaying each layer. + * @returns A map of layer IDs to blobs. + */ + +export const getRGMaskBlobs = async ( + regions: RegionalGuidanceData[], + documentSize: Dimensions, + bbox: IRect, + preview: boolean = false +): Promise> => { + const container = document.createElement('div'); + const stage = new Konva.Stage({ container, ...documentSize }); + renderers.renderLayers(stage, [], [], regions, 1, 'brush', null, getImageDTO); + const konvaLayers = stage.find(`.${RG_LAYER_NAME}`); + const blobs: Record = {}; + + // First remove all layers + for (const layer of konvaLayers) { + layer.remove(); + } + + // Next render each layer to a blob + for (const layer of konvaLayers) { + const rg = regions.find((l) => l.id === layer.id()); + if (!rg) { + continue; + } + stage.add(layer); + const blob = await new Promise((resolve) => { + stage.toBlob({ + callback: (blob) => { + assert(blob, 'Blob is null'); + resolve(blob); + }, + ...bbox, + }); + }); + + if (preview) { + const base64 = await blobToDataURL(blob); + openBase64ImageInTab([ + { + base64, + caption: `${rg.id}: ${rg.positivePrompt} / ${rg.negativePrompt}`, + }, + ]); + } + layer.remove(); + blobs[layer.id()] = blob; + } + + return blobs; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts index 088ba0301be..acd502c4091 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts @@ -1,5 +1,4 @@ import type { RootState } from 'app/store/store'; -import { isInitialImageLayer, isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { CLIP_SKIP, @@ -14,8 +13,9 @@ import { POSITIVE_CONDITIONING_COLLECT, VAE_LOADER, } from 'features/nodes/util/graph/constants'; -import { addControlLayers } from 'features/nodes/util/graph/generation/addControlLayers'; -import { addHRF } from 'features/nodes/util/graph/generation/addHRF'; +import { addControlAdapters } from 'features/nodes/util/graph/generation/addControlAdapters'; +// import { addHRF } from 'features/nodes/util/graph/generation/addHRF'; +import { addIPAdapters } from 'features/nodes/util/graph/generation/addIPAdapters'; import { addLoRAs } from 'features/nodes/util/graph/generation/addLoRAs'; import { addNSFWChecker } from 'features/nodes/util/graph/generation/addNSFWChecker'; import { addSeamless } from 'features/nodes/util/graph/generation/addSeamless'; @@ -27,6 +27,8 @@ import type { Invocation } from 'services/api/types'; import { isNonRefinerMainModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; +import { addRegions } from './addRegions'; + export const buildGenerationTabGraph = async (state: RootState): Promise => { const { model, @@ -39,6 +41,8 @@ export const buildGenerationTabGraph = async (state: RootState): Promise isInitialImageLayer(l) || isRegionalGuidanceLayer(l)); - if (isHRFAllowed && state.hrf.hrfEnabled) { - imageOutput = addHRF(state, g, denoise, noise, l2i, vaeSource); - } + // const isHRFAllowed = !addedLayers.some((l) => isInitialImageLayer(l) || isRegionalGuidanceLayer(l)); + // if (isHRFAllowed && state.hrf.hrfEnabled) { + // imageOutput = addHRF(state, g, denoise, noise, l2i, vaeSource); + // } if (state.system.shouldUseNSFWChecker) { imageOutput = addNSFWChecker(g, imageOutput); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabSDXLGraph.ts index 9f0d86477af..f68eb920519 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabSDXLGraph.ts @@ -12,7 +12,8 @@ import { SDXL_MODEL_LOADER, VAE_LOADER, } from 'features/nodes/util/graph/constants'; -import { addControlLayers } from 'features/nodes/util/graph/generation/addControlLayers'; +import { addControlAdapters } from 'features/nodes/util/graph/generation/addControlAdapters'; +import { addIPAdapters } from 'features/nodes/util/graph/generation/addIPAdapters'; import { addNSFWChecker } from 'features/nodes/util/graph/generation/addNSFWChecker'; import { addSDXLLoRas } from 'features/nodes/util/graph/generation/addSDXLLoRAs'; import { addSDXLRefiner } from 'features/nodes/util/graph/generation/addSDXLRefiner'; @@ -24,6 +25,8 @@ import type { Invocation, NonNullableGraph } from 'services/api/types'; import { isNonRefinerMainModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; +import { addRegions } from './addRegions'; + export const buildGenerationTabSDXLGraph = async (state: RootState): Promise => { const { model, @@ -35,11 +38,13 @@ export const buildGenerationTabSDXLGraph = async (state: RootState): Promise Date: Sun, 16 Jun 2024 09:49:11 +1000 Subject: [PATCH 052/678] refactor(ui): fix useIsReadyToEnqueue for new adapterType field --- invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index f0f83ab986e..52edd663daf 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -147,7 +147,7 @@ const createSelector = (templates: Templates) => problems.push(i18n.t('parameters.invoke.layer.controlAdapterImageNotProcessed')); } // T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL) - if (!ca.controlMode) { + if (ca.adapterType === 't2i_adapter') { const multiple = model?.base === 'sdxl' ? 32 : 64; if (bbox.width % multiple !== 0 || bbox.height % multiple !== 0) { problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions', { multiple })); From 9387903491ff87c2bba18f3d0b53d24d786cd913 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 16 Jun 2024 11:36:36 +1000 Subject: [PATCH 053/678] refactor(ui): fix delete image stuff --- .../listeners/boardAndImagesDeleted.ts | 28 +++--- .../listeners/imageDeletionListeners.ts | 92 +++++++------------ .../listeners/imageToDeleteSelected.ts | 7 +- .../controlLayers/store/canvasV2Slice.ts | 4 + .../store/controlAdaptersReducers.ts | 3 + .../controlLayers/store/ipAdaptersReducers.ts | 6 +- .../controlLayers/store/layersReducers.ts | 3 + .../controlLayers/store/regionsReducers.ts | 3 + .../components/DeleteImageModal.tsx | 23 ++--- .../components/ImageUsageMessage.tsx | 6 +- .../deleteImageModal/store/selectors.ts | 69 ++++---------- .../features/deleteImageModal/store/types.ts | 6 +- .../components/Boards/DeleteBoardModal.tsx | 31 +++---- .../SingleSelectionMenuItems.tsx | 2 - 14 files changed, 107 insertions(+), 176 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts index eb7a793d3a4..d3f20971b78 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts @@ -1,7 +1,5 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { resetCanvas } from 'features/canvas/store/canvasSlice'; -import { controlAdaptersReset } from 'features/controlAdapters/store/controlAdaptersSlice'; -import { allLayersDeleted } from 'features/controlLayers/store/canvasV2Slice'; +import { caAllDeleted, ipaAllDeleted, layerAllDeleted } from 'features/controlLayers/store/canvasV2Slice'; import { getImageUsage } from 'features/deleteImageModal/store/selectors'; import { nodeEditorReset } from 'features/nodes/store/nodesSlice'; import { imagesApi } from 'services/api/endpoints/images'; @@ -14,18 +12,18 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS // Remove all deleted images from the UI - let wasCanvasReset = false; + let wereLayersReset = false; let wasNodeEditorReset = false; let wereControlAdaptersReset = false; - let wereControlLayersReset = false; + let wereIPAdaptersReset = false; - const { canvas, nodes, controlAdapters, canvasV2 } = getState(); + const { nodes, canvasV2 } = getState(); deleted_images.forEach((image_name) => { - const imageUsage = getImageUsage(canvas, nodes.present, controlAdapters, canvasV2, image_name); + const imageUsage = getImageUsage(nodes.present, canvasV2, image_name); - if (imageUsage.isCanvasImage && !wasCanvasReset) { - dispatch(resetCanvas()); - wasCanvasReset = true; + if (imageUsage.isLayerImage && !wereLayersReset) { + dispatch(layerAllDeleted()); + wereLayersReset = true; } if (imageUsage.isNodesImage && !wasNodeEditorReset) { @@ -33,14 +31,14 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS wasNodeEditorReset = true; } - if (imageUsage.isControlImage && !wereControlAdaptersReset) { - dispatch(controlAdaptersReset()); + if (imageUsage.isControlAdapterImage && !wereControlAdaptersReset) { + dispatch(caAllDeleted()); wereControlAdaptersReset = true; } - if (imageUsage.isControlLayerImage && !wereControlLayersReset) { - dispatch(allLayersDeleted()); - wereControlLayersReset = true; + if (imageUsage.isIPAdapterImage && !wereIPAdaptersReset) { + dispatch(ipaAllDeleted()); + wereIPAdaptersReset = true; } }); }, diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts index cb4e9ec8c8d..7b579d2a4ab 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts @@ -3,18 +3,11 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware' import type { AppDispatch, RootState } from 'app/store/store'; import { resetCanvas } from 'features/canvas/store/canvasSlice'; import { - controlAdapterImageChanged, - controlAdapterProcessedImageChanged, - selectControlAdapterAll, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; -import { layerDeleted } from 'features/controlLayers/store/canvasV2Slice'; -import { - isControlAdapterLayer, - isInitialImageLayer, - isIPAdapterLayer, - isRegionalGuidanceLayer, -} from 'features/controlLayers/store/types'; + caImageChanged, + caProcessedImageChanged, + ipaImageChanged, + layerDeleted, +} from 'features/controlLayers/store/canvasV2Slice'; import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions'; import { isModalOpenChanged } from 'features/deleteImageModal/store/slice'; import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors'; @@ -48,51 +41,33 @@ const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: Im }; const deleteControlAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { - forEach(selectControlAdapterAll(state.controlAdapters), (ca) => { - if ( - ca.controlImage === imageDTO.image_name || - (isControlNetOrT2IAdapter(ca) && ca.processedControlImage === imageDTO.image_name) - ) { - dispatch( - controlAdapterImageChanged({ - id: ca.id, - controlImage: null, - }) - ); - dispatch( - controlAdapterProcessedImageChanged({ - id: ca.id, - processedControlImage: null, - }) - ); + state.canvasV2.controlAdapters.forEach(({ id, image, processedImage }) => { + if (image?.name === imageDTO.image_name || processedImage?.name === imageDTO.image_name) { + dispatch(caImageChanged({ id, imageDTO: null })); + dispatch(caProcessedImageChanged({ id, imageDTO: null })); } }); }; -const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { - state.canvasV2.layers.forEach((l) => { - if (isRegionalGuidanceLayer(l)) { - if (l.ipAdapters.some((ipa) => ipa.image?.name === imageDTO.image_name)) { - dispatch(layerDeleted(l.id)); - } - } - if (isControlAdapterLayer(l)) { - if ( - l.controlAdapter.image?.name === imageDTO.image_name || - l.controlAdapter.processedImage?.name === imageDTO.image_name - ) { - dispatch(layerDeleted(l.id)); - } +const deleteIPAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { + state.canvasV2.ipAdapters.forEach(({ id, image }) => { + if (image?.name === imageDTO.image_name) { + dispatch(ipaImageChanged({ id, imageDTO: null })); } - if (isIPAdapterLayer(l)) { - if (l.ipAdapter.image?.name === imageDTO.image_name) { - dispatch(layerDeleted(l.id)); + }); +}; + +const deleteLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { + state.canvasV2.layers.forEach(({ id, objects }) => { + let shouldDelete = false; + for (const obj of objects) { + if (obj.type === 'image' && obj.image.name === imageDTO.image_name) { + shouldDelete = true; + break; } } - if (isInitialImageLayer(l)) { - if (l.image?.name === imageDTO.image_name) { - dispatch(layerDeleted(l.id)); - } + if (shouldDelete) { + dispatch(layerDeleted({ id })); } }); }; @@ -145,14 +120,10 @@ export const addImageDeletionListeners = (startAppListening: AppStartListening) } } - // We need to reset the features where the image is in use - none of these work if their image(s) don't exist - if (imageUsage.isCanvasImage) { - dispatch(resetCanvas()); - } - - deleteControlAdapterImages(state, dispatch, imageDTO); deleteNodesImages(state, dispatch, imageDTO); - deleteControlLayerImages(state, dispatch, imageDTO); + deleteControlAdapterImages(state, dispatch, imageDTO); + deleteIPAdapterImages(state, dispatch, imageDTO); + deleteLayerImages(state, dispatch, imageDTO); } catch { // no-op } finally { @@ -189,14 +160,15 @@ export const addImageDeletionListeners = (startAppListening: AppStartListening) // We need to reset the features where the image is in use - none of these work if their image(s) don't exist - if (imagesUsage.some((i) => i.isCanvasImage)) { + if (imagesUsage.some((i) => i.isLayerImage)) { dispatch(resetCanvas()); } imageDTOs.forEach((imageDTO) => { - deleteControlAdapterImages(state, dispatch, imageDTO); deleteNodesImages(state, dispatch, imageDTO); - deleteControlLayerImages(state, dispatch, imageDTO); + deleteControlAdapterImages(state, dispatch, imageDTO); + deleteIPAdapterImages(state, dispatch, imageDTO); + deleteLayerImages(state, dispatch, imageDTO); }); } catch { // no-op diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageToDeleteSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageToDeleteSelected.ts index 845c9a21f2b..2e20d97b468 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageToDeleteSelected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageToDeleteSelected.ts @@ -13,9 +13,10 @@ export const addImageToDeleteSelectedListener = (startAppListening: AppStartList const imagesUsage = selectImageUsage(getState()); const isImageInUse = - imagesUsage.some((i) => i.isCanvasImage) || - imagesUsage.some((i) => i.isControlImage) || - imagesUsage.some((i) => i.isNodesImage); + imagesUsage.some((i) => i.isLayerImage) || + imagesUsage.some((i) => i.isControlAdapterImage) || + imagesUsage.some((i) => i.isIPAdapterImage) || + imagesUsage.some((i) => i.isLayerImage); if (shouldConfirmOnDelete || isImageInUse) { dispatch(isModalOpenChanged(true)); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 8b1721cb3f5..60abd47769c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -194,11 +194,13 @@ export const { layerLinePointAdded, layerRectAdded, layerImageAdded, + layerAllDeleted, // IP Adapters ipaAdded, ipaRecalled, ipaIsEnabledToggled, ipaDeleted, + ipaAllDeleted, ipaImageChanged, ipaMethodChanged, ipaModelChanged, @@ -209,6 +211,7 @@ export const { caAdded, caBboxChanged, caDeleted, + caAllDeleted, caIsEnabledToggled, caMovedBackwardOne, caMovedForwardOne, @@ -234,6 +237,7 @@ export const { rgTranslated, rgBboxChanged, rgDeleted, + rgAllDeleted, rgGlobalOpacityChanged, rgMovedForwardOne, rgMovedToFront, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts index 84c12e5f3a4..0a744c6c28d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts @@ -82,6 +82,9 @@ export const controlAdaptersReducers = { const { id } = action.payload; state.controlAdapters = state.controlAdapters.filter((ca) => ca.id !== id); }, + caAllDeleted: (state) => { + state.controlAdapters = []; + }, caOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => { const { id, opacity } = action.payload; const ca = selectCA(state, id); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts index 16cf222b60c..fb6ff8d987f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts @@ -39,7 +39,11 @@ export const ipAdaptersReducers = { } }, ipaDeleted: (state, action: PayloadAction<{ id: string }>) => { - state.ipAdapters = state.ipAdapters.filter((ipa) => ipa.id !== action.payload.id); + const { id } = action.payload; + state.ipAdapters = state.ipAdapters.filter((ipa) => ipa.id !== id); + }, + ipaAllDeleted: (state) => { + state.ipAdapters = []; }, ipaImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => { const { id, imageDTO } = action.payload; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index d44585fd4fa..6b3e58f8a2e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -88,6 +88,9 @@ export const layersReducers = { const { id } = action.payload; state.layers = state.layers.filter((l) => l.id !== id); }, + layerAllDeleted: (state) => { + state.layers = []; + }, layerOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => { const { id, opacity } = action.payload; const layer = selectLayer(state, id); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index 56f3b935d97..35b584adb74 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -117,6 +117,9 @@ export const regionsReducers = { const { id } = action.payload; state.regions = state.regions.filter((ca) => ca.id !== id); }, + rgAllDeleted: (state) => { + state.regions = []; + }, rgGlobalOpacityChanged: (state, action: PayloadAction<{ opacity: number }>) => { const { opacity } = action.payload; state.maskFillOpacity = opacity; diff --git a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx index 3d33999c717..30f59aed1fc 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx +++ b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx @@ -1,8 +1,6 @@ import { ConfirmationAlertDialog, Divider, Flex, FormControl, FormLabel, Switch, Text } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions'; import { getImageUsage, selectImageUsage } from 'features/deleteImageModal/store/selectors'; @@ -22,26 +20,17 @@ import { useTranslation } from 'react-i18next'; import ImageUsageMessage from './ImageUsageMessage'; const selectImageUsages = createMemoizedSelector( - [ - selectDeleteImageModalSlice, - selectCanvasSlice, - selectNodesSlice, - selectControlAdaptersSlice, - selectCanvasV2Slice, - selectImageUsage, - ], - (deleteImageModal, canvas, nodes, controlAdapters, controlLayers, imagesUsage) => { + [selectDeleteImageModalSlice, selectNodesSlice, selectCanvasV2Slice, selectImageUsage], + (deleteImageModal, nodes, canvasV2, imagesUsage) => { const { imagesToDelete } = deleteImageModal; - const allImageUsage = (imagesToDelete ?? []).map(({ image_name }) => - getImageUsage(canvas, nodes, controlAdapters, canvasV2, image_name) - ); + const allImageUsage = (imagesToDelete ?? []).map(({ image_name }) => getImageUsage(nodes, canvasV2, image_name)); const imageUsageSummary: ImageUsage = { - isCanvasImage: some(allImageUsage, (i) => i.isCanvasImage), + isLayerImage: some(allImageUsage, (i) => i.isLayerImage), isNodesImage: some(allImageUsage, (i) => i.isNodesImage), - isControlImage: some(allImageUsage, (i) => i.isControlImage), - isControlLayerImage: some(allImageUsage, (i) => i.isControlLayerImage), + isControlAdapterImage: some(allImageUsage, (i) => i.isControlAdapterImage), + isIPAdapterImage: some(allImageUsage, (i) => i.isIPAdapterImage), }; return { diff --git a/invokeai/frontend/web/src/features/deleteImageModal/components/ImageUsageMessage.tsx b/invokeai/frontend/web/src/features/deleteImageModal/components/ImageUsageMessage.tsx index d76716d01d7..8f71922e0ac 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/components/ImageUsageMessage.tsx +++ b/invokeai/frontend/web/src/features/deleteImageModal/components/ImageUsageMessage.tsx @@ -29,10 +29,10 @@ const ImageUsageMessage = (props: Props) => { <> {topMessage} - {imageUsage.isCanvasImage && {t('ui.tabs.canvasTab')}} - {imageUsage.isControlImage && {t('common.controlNet')}} + {imageUsage.isLayerImage && {t('controlLayers.layers')}} + {imageUsage.isControlAdapterImage && {t('controlLayers.controlAdapters')}} + {imageUsage.isIPAdapterImage && {t('controlLayers.ipAdapters')}} {imageUsage.isNodesImage && {t('ui.tabs.workflowsTab')}} - {imageUsage.isControlLayerImage && {t('ui.tabs.generationTab')}} {bottomMessage} diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts index ce36e9080d3..5874fe9c710 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts @@ -1,20 +1,6 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -import type { CanvasState } from 'features/canvas/store/canvasTypes'; -import { - selectControlAdapterAll, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import type { ControlAdaptersState } from 'features/controlAdapters/store/types'; -import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import type { CanvasV2State } from 'features/controlLayers/store/types'; -import { - isControlAdapterLayer, - isInitialImageLayer, - isIPAdapterLayer, - isRegionalGuidanceLayer, -} from 'features/controlLayers/store/types'; import { selectDeleteImageModalSlice } from 'features/deleteImageModal/store/slice'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; import type { NodesState } from 'features/nodes/store/types'; @@ -24,47 +10,28 @@ import { some } from 'lodash-es'; import type { ImageUsage } from './types'; -export const getImageUsage = ( - canvas: CanvasState, - nodes: NodesState, - controlAdapters: ControlAdaptersState, - controlLayers: CanvasV2State, - image_name: string -) => { - const isCanvasImage = canvas.layerState.objects.some((obj) => obj.kind === 'image' && obj.imageName === image_name); +export const getImageUsage = (nodes: NodesState, canvasV2: CanvasV2State, image_name: string) => { + const isLayerImage = canvasV2.layers.some((layer) => + layer.objects.some((obj) => obj.type === 'image' && obj.image.name === image_name) + ); - const isNodesImage = nodes.nodes.filter(isInvocationNode).some((node) => { - return some( - node.data.inputs, - (input) => isImageFieldInputInstance(input) && input.value?.image_name === image_name + const isNodesImage = nodes.nodes + .filter(isInvocationNode) + .some((node) => + some(node.data.inputs, (input) => isImageFieldInputInstance(input) && input.value?.image_name === image_name) ); - }); - const isControlImage = selectControlAdapterAll(controlAdapters).some( - (ca) => ca.controlImage === image_name || (isControlNetOrT2IAdapter(ca) && ca.processedControlImage === image_name) + const isControlAdapterImage = canvasV2.controlAdapters.some( + (ca) => ca.image?.name === image_name || ca.processedImage?.name === image_name ); - const isControlLayerImage = controlLayers.layers.some((l) => { - if (isRegionalGuidanceLayer(l)) { - return l.ipAdapters.some((ipa) => ipa.image?.name === image_name); - } - if (isControlAdapterLayer(l)) { - return l.controlAdapter.image?.name === image_name || l.controlAdapter.processedImage?.name === image_name; - } - if (isIPAdapterLayer(l)) { - return l.ipAdapter.image?.name === image_name; - } - if (isInitialImageLayer(l)) { - return l.image?.name === image_name; - } - return false; - }); + const isIPAdapterImage = canvasV2.ipAdapters.some((ipa) => ipa.image?.name === image_name); const imageUsage: ImageUsage = { - isCanvasImage, + isLayerImage, isNodesImage, - isControlImage, - isControlLayerImage, + isControlAdapterImage, + isIPAdapterImage, }; return imageUsage; @@ -72,20 +39,16 @@ export const getImageUsage = ( export const selectImageUsage = createMemoizedSelector( selectDeleteImageModalSlice, - selectCanvasSlice, selectNodesSlice, - selectControlAdaptersSlice, selectCanvasV2Slice, - (deleteImageModal, canvas, nodes, controlAdapters, controlLayers) => { + (deleteImageModal, nodes, canvasV2) => { const { imagesToDelete } = deleteImageModal; if (!imagesToDelete.length) { return []; } - const imagesUsage = imagesToDelete.map((i) => - getImageUsage(canvas, nodes, controlAdapters, canvasV2, i.image_name) - ); + const imagesUsage = imagesToDelete.map((i) => getImageUsage(nodes, canvasV2, i.image_name)); return imagesUsage; } diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/types.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/types.ts index 2cc3dd90b4a..259a510646f 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/types.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/types.ts @@ -6,8 +6,8 @@ export type DeleteImageState = { }; export type ImageUsage = { - isCanvasImage: boolean; isNodesImage: boolean; - isControlImage: boolean; - isControlLayerImage: boolean; + isControlAdapterImage: boolean; + isIPAdapterImage: boolean; + isLayerImage: boolean; }; diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx index fd9a52a69d7..9ef21177da6 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx @@ -13,8 +13,6 @@ import { import { skipToken } from '@reduxjs/toolkit/query'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import ImageUsageMessage from 'features/deleteImageModal/components/ImageUsageMessage'; import { getImageUsage } from 'features/deleteImageModal/store/selectors'; @@ -41,23 +39,18 @@ const DeleteBoardModal = (props: Props) => { const selectImageUsageSummary = useMemo( () => - createMemoizedSelector( - [selectCanvasSlice, selectNodesSlice, selectControlAdaptersSlice, selectCanvasV2Slice], - (canvas, nodes, controlAdapters, controlLayers) => { - const allImageUsage = (boardImageNames ?? []).map((imageName) => - getImageUsage(canvas, nodes, controlAdapters, canvasV2, imageName) - ); - - const imageUsageSummary: ImageUsage = { - isCanvasImage: some(allImageUsage, (i) => i.isCanvasImage), - isNodesImage: some(allImageUsage, (i) => i.isNodesImage), - isControlImage: some(allImageUsage, (i) => i.isControlImage), - isControlLayerImage: some(allImageUsage, (i) => i.isControlLayerImage), - }; - - return imageUsageSummary; - } - ), + createMemoizedSelector([selectNodesSlice, selectCanvasV2Slice], (nodes, canvasV2) => { + const allImageUsage = (boardImageNames ?? []).map((imageName) => getImageUsage(nodes, canvasV2, imageName)); + + const imageUsageSummary: ImageUsage = { + isLayerImage: some(allImageUsage, (i) => i.isLayerImage), + isNodesImage: some(allImageUsage, (i) => i.isNodesImage), + isControlAdapterImage: some(allImageUsage, (i) => i.isControlAdapterImage), + isIPAdapterImage: some(allImageUsage, (i) => i.isIPAdapterImage), + }; + + return imageUsageSummary; + }), [boardImageNames] ); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx index 6af1aaf8b2b..19ad53eedee 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx @@ -15,7 +15,6 @@ import { imageToCompareChanged } from 'features/gallery/store/gallerySlice'; import { $templates } from 'features/nodes/store/nodesSlice'; import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice'; -import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { toast } from 'features/toast/toast'; import { setActiveTab } from 'features/ui/store/uiSlice'; import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow'; @@ -52,7 +51,6 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { const maySelectForCompare = useAppSelector((s) => s.gallery.imageToCompare?.image_name !== imageDTO.image_name); const dispatch = useAppDispatch(); const { t } = useTranslation(); - const isCanvasEnabled = useFeatureStatus('canvas'); const customStarUi = useStore($customStarUI); const { downloadImage } = useDownloadImage(); const templates = useStore($templates); From 5bae455e4e8bde350ff0bdc4ecdfb61796c8fb6e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 16 Jun 2024 11:43:43 +1000 Subject: [PATCH 054/678] refactor(ui): fix gallery stuff --- .../SingleSelectionMenuItems.tsx | 27 ++++++------------- .../ImageViewer/CurrentImageButtons.tsx | 3 +-- .../gallery/hooks/useGalleryHotkeys.ts | 19 +++---------- 3 files changed, 13 insertions(+), 36 deletions(-) diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx index 19ad53eedee..3c2d8578aec 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx @@ -4,23 +4,18 @@ import { $customStarUI } from 'app/store/nanostores/customStarUI'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard'; import { useDownloadImage } from 'common/hooks/useDownloadImage'; -import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; import { imagesToChangeSelected, isModalOpenChanged } from 'features/changeBoardModal/store/slice'; -import { iiLayerAdded } from 'features/controlLayers/store/canvasV2Slice'; -import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice'; import { useImageActions } from 'features/gallery/hooks/useImageActions'; import { sentImageToCanvas, sentImageToImg2Img } from 'features/gallery/store/actions'; import { imageToCompareChanged } from 'features/gallery/store/gallerySlice'; import { $templates } from 'features/nodes/store/nodesSlice'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice'; import { toast } from 'features/toast/toast'; import { setActiveTab } from 'features/ui/store/uiSlice'; import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow'; import { size } from 'lodash-es'; import { memo, useCallback } from 'react'; -import { flushSync } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { PiArrowsCounterClockwiseBold, @@ -47,7 +42,6 @@ type SingleSelectionMenuItemsProps = { const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { const { imageDTO } = props; - const optimalDimension = useAppSelector(selectOptimalDimension); const maySelectForCompare = useAppSelector((s) => s.gallery.imageToCompare?.image_name !== imageDTO.image_name); const dispatch = useAppDispatch(); const { t } = useTranslation(); @@ -86,24 +80,21 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { }, [dispatch, imageDTO]); const handleSendToImageToImage = useCallback(() => { + // TODO(psyche): restore send to img2img functionality dispatch(sentImageToImg2Img()); - dispatch(iiLayerAdded(imageDTO)); dispatch(setActiveTab('generation')); - }, [dispatch, imageDTO]); + }, [dispatch]); const handleSendToCanvas = useCallback(() => { + // TODO(psyche): restore send to canvas functionality dispatch(sentImageToCanvas()); - flushSync(() => { - dispatch(setActiveTab('canvas')); - }); - dispatch(setInitialCanvasImage(imageDTO, optimalDimension)); - + dispatch(setActiveTab('generation')); toast({ id: 'SENT_TO_CANVAS', title: t('toast.sentToUnifiedCanvas'), status: 'success', }); - }, [dispatch, imageDTO, t, optimalDimension]); + }, [dispatch, t]); const handleChangeBoard = useCallback(() => { dispatch(imagesToChangeSelected([imageDTO])); @@ -202,13 +193,11 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { } onClickCapture={handleSendToImageToImage} id="send-to-img2img"> {t('parameters.sendToImg2Img')} - {isCanvasEnabled && ( - } onClickCapture={handleSendToCanvas} id="send-to-canvas"> - {t('parameters.sendToUnifiedCanvas')} - - )} } onClickCapture={handleSendToUpscale} id="send-to-upscale"> {t('parameters.sendToUpscale')} + + } onClickCapture={handleSendToCanvas} id="send-to-canvas"> + {t('parameters.sendToUnifiedCanvas')} } onClickCapture={handleChangeBoard}> diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx index d5c23ecb90a..1ef91e7e2eb 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx @@ -4,7 +4,6 @@ import { createSelector } from '@reduxjs/toolkit'; import { skipToken } from '@reduxjs/toolkit/query'; import { adHocPostProcessingRequested } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { iiLayerAdded } from 'features/controlLayers/store/canvasV2Slice'; import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton'; import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice'; import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems'; @@ -86,8 +85,8 @@ const CurrentImageButtons = () => { if (!imageDTO) { return; } + // TODO(psyche): restore send to img2img functionality dispatch(sentImageToImg2Img()); - dispatch(iiLayerAdded(imageDTO)); dispatch(setActiveTab('generation')); }, [dispatch, imageDTO]); diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts index 5b14fb7a8cd..a499b45f648 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts @@ -1,10 +1,7 @@ import { useAppSelector } from 'app/store/storeHooks'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; import { useGalleryNavigation } from 'features/gallery/hooks/useGalleryNavigation'; import { useGalleryPagination } from 'features/gallery/hooks/useGalleryPagination'; import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useListImagesQuery } from 'services/api/endpoints/images'; @@ -12,12 +9,7 @@ import { useListImagesQuery } from 'services/api/endpoints/images'; * Registers gallery hotkeys. This hook is a singleton. */ export const useGalleryHotkeys = () => { - const activeTabName = useAppSelector(activeTabNameSelector); - const isStaging = useAppSelector(isStagingSelector); - // block navigation on Unified Canvas tab when staging new images - const canNavigateGallery = useMemo(() => { - return activeTabName !== 'canvas' || !isStaging; - }, [activeTabName, isStaging]); + // TODO(psyche): Hotkeys when staging - cannot navigate gallery with arrow keys when staging! const { goNext, goPrev, isNextEnabled, isPrevEnabled } = useGalleryPagination(); const queryArgs = useAppSelector(selectListImagesQueryArgs); @@ -41,17 +33,14 @@ export const useGalleryHotkeys = () => { goPrev(e.altKey ? 'alt+arrow' : 'arrow'); return; } - canNavigateGallery && handleLeftImage(e.altKey); + handleLeftImage(e.altKey); }, - [handleLeftImage, canNavigateGallery, isOnFirstImageOfView, goPrev, isPrevEnabled, queryResult.isFetching] + [handleLeftImage, isOnFirstImageOfView, goPrev, isPrevEnabled, queryResult.isFetching] ); useHotkeys( ['right', 'alt+right'], (e) => { - if (!canNavigateGallery) { - return; - } if (isOnLastImageOfView && isNextEnabled && !queryResult.isFetching) { goNext(e.altKey ? 'alt+arrow' : 'arrow'); return; @@ -60,7 +49,7 @@ export const useGalleryHotkeys = () => { handleRightImage(e.altKey); } }, - [isOnLastImageOfView, goNext, isNextEnabled, queryResult.isFetching, handleRightImage, canNavigateGallery] + [isOnLastImageOfView, goNext, isNextEnabled, queryResult.isFetching, handleRightImage] ); useHotkeys( From 728fd5b758b99a401842eeca33a47da0888a02d6 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 16 Jun 2024 11:44:56 +1000 Subject: [PATCH 055/678] refactor(ui): fix misc types --- invokeai/frontend/web/src/app/types/invokeai.ts | 2 +- .../src/features/queue/components/QueueButtonTooltip.tsx | 7 ++----- .../system/components/SettingsModal/SettingsModal.tsx | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index 4d54485f616..4268bc5411d 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -83,7 +83,7 @@ export type AppConfig = { sd: { defaultModel?: string; disabledControlNetModels: string[]; - disabledControlNetProcessors: ProcessorTypeV2; + disabledControlNetProcessors: ProcessorTypeV2[]; // Core parameters iterations: NumericalParameterConfig; width: NumericalParameterConfig; // initial value comes from model diff --git a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx b/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx index 5d6badaefd7..fe44e550256 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx @@ -11,11 +11,8 @@ import { useTranslation } from 'react-i18next'; import { useEnqueueBatchMutation } from 'services/api/endpoints/queue'; import { useBoardName } from 'services/api/hooks/useBoardName'; -const selectPromptsCount = createSelector( - selectCanvasV2Slice, - selectDynamicPromptsSlice, - (controlLayers, dynamicPrompts) => - getShouldProcessPrompt(canvasV2.positivePrompt) ? dynamicPrompts.prompts.length : 1 +const selectPromptsCount = createSelector(selectCanvasV2Slice, selectDynamicPromptsSlice, (canvasV2, dynamicPrompts) => + getShouldProcessPrompt(canvasV2.params.positivePrompt) ? dynamicPrompts.prompts.length : 1 ); type Props = { diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx index 60269ad64b4..1853183c9bd 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx @@ -19,7 +19,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import { useClearStorage } from 'common/hooks/useClearStorage'; -import { shouldUseCpuNoiseChanged } from 'features/canvas/store/canvasSlice'; +import { shouldUseCpuNoiseChanged } from 'features/controlLayers/store/canvasV2Slice'; import { useClearIntermediates } from 'features/system/components/SettingsModal/useClearIntermediates'; import { StickyScrollable } from 'features/system/components/StickyScrollable'; import { From 6ef5c2e0c336760104391b561e1bc11fb69ea149 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 16 Jun 2024 11:50:14 +1000 Subject: [PATCH 056/678] refactor(ui): fix renderer stuff --- .../controlLayers/konva/renderers/bbox.ts | 49 ++++-- .../controlLayers/konva/renderers/iiLayer.ts | 155 ------------------ 2 files changed, 32 insertions(+), 172 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/renderers/iiLayer.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts index bc4c42fbd28..5b3df88aeae 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts @@ -1,13 +1,13 @@ import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; import { imageDataToDataURL } from 'features/canvas/util/blobToDataURL'; import { + CA_LAYER_IMAGE_NAME, LAYER_BBOX_NAME, RASTER_LAYER_OBJECT_GROUP_NAME, RG_LAYER_OBJECT_GROUP_NAME, } from 'features/controlLayers/konva/naming'; import { createBboxRect } from 'features/controlLayers/konva/renderers/objects'; -import type { LayerData } from 'features/controlLayers/store/types'; -import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; +import type { ControlAdapterData, LayerData, RegionalGuidanceData } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; import { assert } from 'tsafe'; @@ -175,37 +175,52 @@ export const getLayerBboxFast = (layer: Konva.Layer): IRect => { }; const filterRGChildren = (node: Konva.Node): boolean => node.name() === RG_LAYER_OBJECT_GROUP_NAME; -const filterRasterChildren = (node: Konva.Node): boolean => node.name() === RASTER_LAYER_OBJECT_GROUP_NAME; +const filterLayerChildren = (node: Konva.Node): boolean => node.name() === RASTER_LAYER_OBJECT_GROUP_NAME; +const filterCAChildren = (node: Konva.Node): boolean => node.name() === CA_LAYER_IMAGE_NAME; /** * Calculates the bbox of each regional guidance layer. Only calculates if the mask has changed. * @param stage The konva stage - * @param layerStates An array of layers to calculate bboxes for + * @param entityStates An array of layers to calculate bboxes for * @param onBboxChanged Callback for when the bounding box changes */ export const updateBboxes = ( stage: Konva.Stage, - layerStates: LayerData[], + entityStates: (ControlAdapterData | LayerData | RegionalGuidanceData)[], onBboxChanged: (layerId: string, bbox: IRect | null) => void ): void => { - for (const layerState of layerStates) { - const konvaLayer = stage.findOne(`#${layerState.id}`); - assert(konvaLayer, `Layer ${layerState.id} not found in stage`); + for (const entityState of entityStates) { + const konvaLayer = stage.findOne(`#${entityState.id}`); + assert(konvaLayer, `Layer ${entityState.id} not found in stage`); // We only need to recalculate the bbox if the layer has changed - if (layerState.bboxNeedsUpdate) { - const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer); + if (entityState.bboxNeedsUpdate) { + const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(entityState, konvaLayer); // Hide the bbox while we calculate the new bbox, else the bbox will be included in the calculation const visible = bboxRect.visible(); bboxRect.visible(false); - if (layerState.objects.length === 0) { - // No objects - no bbox to calculate - onBboxChanged(layerState.id, null); - } else { - // Calculate the bbox by rendering the layer and checking its pixels - const filterChildren = isRegionalGuidanceLayer(layerState) ? filterRGChildren : filterRasterChildren; - onBboxChanged(layerState.id, getLayerBboxPixels(konvaLayer, filterChildren)); + if (entityState.type === 'layer') { + if (entityState.objects.length === 0) { + // No objects - no bbox to calculate + onBboxChanged(entityState.id, null); + } else { + onBboxChanged(entityState.id, getLayerBboxPixels(konvaLayer, filterLayerChildren)); + } + } else if (entityState.type === 'control_adapter') { + if (!entityState.image && !entityState.processedImage) { + // No objects - no bbox to calculate + onBboxChanged(entityState.id, null); + } else { + onBboxChanged(entityState.id, getLayerBboxPixels(konvaLayer, filterCAChildren)); + } + } else if (entityState.type === 'regional_guidance') { + if (entityState.objects.length === 0) { + // No objects - no bbox to calculate + onBboxChanged(entityState.id, null); + } else { + onBboxChanged(entityState.id, getLayerBboxPixels(konvaLayer, filterRGChildren)); + } } // Restore the visibility of the bbox diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/iiLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/iiLayer.ts deleted file mode 100644 index a638f69f397..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/iiLayer.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { - getCALayerImageId, - getIILayerImageId, - INITIAL_IMAGE_LAYER_IMAGE_NAME, - INITIAL_IMAGE_LAYER_NAME, -} from 'features/controlLayers/konva/naming'; -import type { InitialImageLayer } from 'features/controlLayers/store/types'; -import Konva from 'konva'; -import type { ImageDTO } from 'services/api/types'; - -/** - * Logic for creating and rendering initial image layers. Well, just the one, actually, because it's a singleton. - * TODO(psyche): Raster layers effectively supersede the initial image layer type. - */ - -/** - * 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({ - id: layerState.id, - name: INITIAL_IMAGE_LAYER_NAME, - imageSmoothingEnabled: true, - listening: false, - }); - stage.add(konvaLayer); - return konvaLayer; -}; - -/** - * 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({ - name: INITIAL_IMAGE_LAYER_IMAGE_NAME, - image: imageEl, - }); - konvaLayer.add(konvaImage); - return konvaImage; -}; - -/** - * Updates an initial image layer's attributes (width, height, opacity, visibility). - * @param stage The konva stage - * @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, - // but it doesn't seem to break anything. - // TODO(psyche): Investigate and report upstream. - const newWidth = stage.width() / stage.scaleX(); - const newHeight = stage.height() / stage.scaleY(); - if ( - konvaImage.width() !== newWidth || - konvaImage.height() !== newHeight || - konvaImage.visible() !== layerState.isEnabled - ) { - konvaImage.setAttrs({ - opacity: layerState.opacity, - scaleX: 1, - scaleY: 1, - width: stage.width() / stage.scaleX(), - height: stage.height() / stage.scaleY(), - visible: layerState.isEnabled, - }); - } - if (konvaImage.opacity() !== layerState.opacity) { - konvaImage.opacity(layerState.opacity); - } -}; - -/** - * 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, - konvaLayer: Konva.Layer, - layerState: InitialImageLayer, - getImageDTO: (imageName: string) => Promise -): Promise => { - if (layerState.image) { - const imageName = layerState.image.name; - const imageDTO = await getImageDTO(imageName); - if (!imageDTO) { - return; - } - const imageEl = new Image(); - const imageId = getIILayerImageId(layerState.id, imageName); - imageEl.onload = () => { - // Find the existing image or create a new one - must find using the name, bc the id may have just changed - const konvaImage = - konvaLayer.findOne(`.${INITIAL_IMAGE_LAYER_IMAGE_NAME}`) ?? - createIILayerImage(konvaLayer, imageEl); - - // Update the image's attributes - konvaImage.setAttrs({ - id: imageId, - image: imageEl, - }); - updateIILayerImageAttrs(stage, konvaImage, layerState); - imageEl.id = imageId; - }; - imageEl.src = imageDTO.image_url; - } else { - konvaLayer.findOne(`.${INITIAL_IMAGE_LAYER_IMAGE_NAME}`)?.destroy(); - } -}; - -/** - * 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 - */ -export const renderIILayer = ( - stage: Konva.Stage, - layerState: InitialImageLayer, - zIndex: number, - getImageDTO: (imageName: string) => Promise -): void => { - const konvaLayer = stage.findOne(`#${layerState.id}`) ?? createIILayer(stage, layerState); - - konvaLayer.zIndex(zIndex); - - const konvaImage = konvaLayer.findOne(`.${INITIAL_IMAGE_LAYER_IMAGE_NAME}`); - const canvasImageSource = konvaImage?.image(); - - let imageSourceNeedsUpdate = false; - - if (canvasImageSource instanceof HTMLImageElement) { - const image = layerState.image; - if (image && canvasImageSource.id !== getCALayerImageId(layerState.id, image.name)) { - imageSourceNeedsUpdate = true; - } else if (!image) { - imageSourceNeedsUpdate = true; - } - } else if (!canvasImageSource) { - imageSourceNeedsUpdate = true; - } - - if (imageSourceNeedsUpdate) { - updateIILayerImageSource(stage, konvaLayer, layerState, getImageDTO); - } else if (konvaImage) { - updateIILayerImageAttrs(stage, konvaImage, layerState); - } -}; From b118bc959d5b71566bf81a09607c25318a7effb1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 16 Jun 2024 11:51:46 +1000 Subject: [PATCH 057/678] refactor(ui): undo/redo button temp fix --- .../components/UndoRedoButtonGroup.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/UndoRedoButtonGroup.tsx b/invokeai/frontend/web/src/features/controlLayers/components/UndoRedoButtonGroup.tsx index eae2bc65cf4..0d0b8651c10 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/UndoRedoButtonGroup.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/UndoRedoButtonGroup.tsx @@ -1,7 +1,6 @@ /* eslint-disable i18next/no-literal-string */ import { ButtonGroup, IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { redo, undo } from 'features/controlLayers/store/canvasV2Slice'; +import { useAppSelector } from 'app/store/storeHooks'; import { memo, useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; @@ -9,18 +8,19 @@ import { PiArrowClockwiseBold, PiArrowCounterClockwiseBold } from 'react-icons/p export const UndoRedoButtonGroup = memo(() => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const mayUndo = useAppSelector((s) => s.controlLayers.past.length > 0); + const mayUndo = useAppSelector(() => false); const handleUndo = useCallback(() => { - dispatch(undo()); - }, [dispatch]); + // TODO(psyche): Implement undo + // dispatch(undo()); + }, []); useHotkeys(['meta+z', 'ctrl+z'], handleUndo, { enabled: mayUndo, preventDefault: true }, [mayUndo, handleUndo]); - const mayRedo = useAppSelector((s) => s.controlLayers.future.length > 0); + const mayRedo = useAppSelector(() => false); const handleRedo = useCallback(() => { - dispatch(redo()); - }, [dispatch]); + // TODO(psyche): Implement redo + // dispatch(redo()); + }, []); useHotkeys(['meta+shift+z', 'ctrl+shift+z'], handleRedo, { enabled: mayRedo, preventDefault: true }, [ mayRedo, handleRedo, From c2f60f33e6fcad041e27a06734dd318c9dd5569d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 16 Jun 2024 12:24:06 +1000 Subject: [PATCH 058/678] refactor(ui): metadata recall (wip) just enough let the app run --- .../frontend/web/src/app/logging/logger.ts | 3 +- .../middleware/listenerMiddleware/index.ts | 35 +-- .../features/controlLayers/konva/naming.ts | 23 +- .../controlLayers/konva/renderers/caLayer.ts | 6 +- .../src/features/controlLayers/store/types.ts | 8 +- .../web/src/features/metadata/util/parsers.ts | 135 +++++------ .../src/features/metadata/util/recallers.ts | 223 ++++++++++-------- .../src/features/metadata/util/validators.ts | 2 +- .../util/graph/buildLinearBatchConfig.ts | 3 +- 9 files changed, 222 insertions(+), 216 deletions(-) diff --git a/invokeai/frontend/web/src/app/logging/logger.ts b/invokeai/frontend/web/src/app/logging/logger.ts index c0de4e3685c..c0a3089fe45 100644 --- a/invokeai/frontend/web/src/app/logging/logger.ts +++ b/invokeai/frontend/web/src/app/logging/logger.ts @@ -27,7 +27,8 @@ export type LoggerNamespace = | 'session' | 'queue' | 'dnd' - | 'controlLayers'; + | 'controlLayers' + | 'metadata'; export const logger = (namespace: LoggerNamespace) => $logger.get().child({ namespace }); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts index a1ce52b407d..7709770d81f 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -1,7 +1,6 @@ import type { TypedStartListening } from '@reduxjs/toolkit'; import { createListenerMiddleware } from '@reduxjs/toolkit'; import { addAdHocPostProcessingRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener'; -import { addCommitStagingAreaImageListener } from 'app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener'; import { addAnyEnqueuedListener } from 'app/store/middleware/listenerMiddleware/listeners/anyEnqueued'; import { addAppConfigReceivedListener } from 'app/store/middleware/listenerMiddleware/listeners/appConfigReceived'; import { addAppStartedListener } from 'app/store/middleware/listenerMiddleware/listeners/appStarted'; @@ -9,17 +8,7 @@ import { addBatchEnqueuedListener } from 'app/store/middleware/listenerMiddlewar import { addDeleteBoardAndImagesFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted'; import { addBoardIdSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/boardIdSelected'; import { addBulkDownloadListeners } from 'app/store/middleware/listenerMiddleware/listeners/bulkDownload'; -import { addCanvasCopiedToClipboardListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard'; -import { addCanvasDownloadedAsImageListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage'; -import { addCanvasImageToControlNetListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet'; -import { addCanvasMaskSavedToGalleryListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMaskSavedToGallery'; -import { addCanvasMaskToControlNetListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet'; -import { addCanvasMergedListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMerged'; -import { addCanvasSavedToGalleryListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery'; import { addControlAdapterPreprocessor } from 'app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor'; -import { addControlNetAutoProcessListener } from 'app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess'; -import { addControlNetImageProcessedListener } from 'app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed'; -import { addEnqueueRequestedCanvasListener } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas'; import { addEnqueueRequestedLinear } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear'; import { addEnqueueRequestedNodes } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes'; import { addGalleryImageClickedListener } from 'app/store/middleware/listenerMiddleware/listeners/galleryImageClicked'; @@ -46,7 +35,6 @@ import { addInvocationStartedEventListener } from 'app/store/middleware/listener import { addModelInstallEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketModelInstall'; import { addModelLoadEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketModelLoad'; import { addSocketQueueItemStatusChangedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged'; -import { addStagingAreaImageSavedListener } from 'app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved'; import { addUpdateAllNodesRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested'; import { addWorkflowLoadRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested'; import type { AppDispatch, RootState } from 'app/store/store'; @@ -83,7 +71,6 @@ addGalleryImageClickedListener(startAppListening); addGalleryOffsetChangedListener(startAppListening); // User Invoked -addEnqueueRequestedCanvasListener(startAppListening); addEnqueueRequestedNodes(startAppListening); addEnqueueRequestedLinear(startAppListening); addEnqueueRequestedUpscale(startAppListening); @@ -91,15 +78,15 @@ addAnyEnqueuedListener(startAppListening); addBatchEnqueuedListener(startAppListening); // Canvas actions -addCanvasSavedToGalleryListener(startAppListening); -addCanvasMaskSavedToGalleryListener(startAppListening); -addCanvasImageToControlNetListener(startAppListening); -addCanvasMaskToControlNetListener(startAppListening); -addCanvasDownloadedAsImageListener(startAppListening); -addCanvasCopiedToClipboardListener(startAppListening); -addCanvasMergedListener(startAppListening); -addStagingAreaImageSavedListener(startAppListening); -addCommitStagingAreaImageListener(startAppListening); +// addCanvasSavedToGalleryListener(startAppListening); +// addCanvasMaskSavedToGalleryListener(startAppListening); +// addCanvasImageToControlNetListener(startAppListening); +// addCanvasMaskToControlNetListener(startAppListening); +// addCanvasDownloadedAsImageListener(startAppListening); +// addCanvasCopiedToClipboardListener(startAppListening); +// addCanvasMergedListener(startAppListening); +// addStagingAreaImageSavedListener(startAppListening); +// addCommitStagingAreaImageListener(startAppListening); // Socket.IO addGeneratorProgressEventListener(startAppListening); @@ -113,10 +100,6 @@ addModelInstallEventListener(startAppListening); addSocketQueueItemStatusChangedEventListener(startAppListening); addBulkDownloadListeners(startAppListening); -// ControlNet -addControlNetImageProcessedListener(startAppListening); -addControlNetAutoProcessListener(startAppListening); - // Boards addImageAddedToBoardFulfilledListener(startAppListening); addImageRemovedFromBoardFulfilledListener(startAppListening); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts index 6c2b019fd8e..3b897b86798 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts @@ -44,15 +44,14 @@ export const RASTER_LAYER_IMAGE_NAME = 'raster_layer.image'; export const INPAINT_MASK_LAYER_NAME = 'inpaint_mask_layer'; // 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 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}`; -export const getCALayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`; -export const getIILayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`; -export const getIPALayerId = (layerId: string) => `ip_adapter_layer_${layerId}`; +export const getRGId = (entityId: string) => `${RG_LAYER_NAME}_${entityId}`; +export const getLayerId = (entityId: string) => `${RASTER_LAYER_NAME}_${entityId}`; +export const getBrushLineId = (entityId: string, lineId: string) => `${entityId}.brush_line_${lineId}`; +export const getEraserLineId = (entityId: string, lineId: string) => `${entityId}.eraser_line_${lineId}`; +export const getRectShapeId = (entityId: string, rectId: string) => `${entityId}.rect_${rectId}`; +export const getImageObjectId = (entityId: string, imageName: string) => `${entityId}.image_${imageName}`; +export const getObjectGroupId = (entityId: string, groupId: string) => `${entityId}.objectGroup_${groupId}`; +export const getLayerBboxId = (entityId: string) => `${entityId}.bbox`; +export const getCAId = (entityId: string) => `control_adapter_${entityId}`; +export const getCAImageId = (entityId: string, imageName: string) => `${entityId}.image_${imageName}`; +export const getIPAId = (entityId: string) => `ip_adapter_${entityId}`; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts index 8301a97573e..4170b87ffe5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts @@ -1,5 +1,5 @@ import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; -import { CA_LAYER_IMAGE_NAME, CA_LAYER_NAME, getCALayerImageId } from 'features/controlLayers/konva/naming'; +import { CA_LAYER_IMAGE_NAME, CA_LAYER_NAME, getCAImageId } from 'features/controlLayers/konva/naming'; import type { ControlAdapterData } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { ImageDTO } from 'services/api/types'; @@ -61,7 +61,7 @@ const updateCALayerImageSource = async ( return; } const imageEl = new Image(); - const imageId = getCALayerImageId(ca.id, imageName); + const imageId = getCAImageId(ca.id, imageName); imageEl.onload = () => { // Find the existing image or create a new one - must find using the name, bc the id may have just changed const konvaImage = @@ -144,7 +144,7 @@ export const renderCALayer = ( if (canvasImageSource instanceof HTMLImageElement) { const image = ca.processedImage ?? ca.image; - if (image && canvasImageSource.id !== getCALayerImageId(ca.id, image.name)) { + if (image && canvasImageSource.id !== getCAImageId(ca.id, image.name)) { imageSourceNeedsUpdate = true; } else if (!image) { imageSourceNeedsUpdate = true; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index cd8b112e492..bce9d98fdf6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -574,7 +574,7 @@ const zRect = z.object({ height: z.number().min(1), }); -const zLayerData = z.object({ +export const zLayerData = z.object({ id: zId, type: z.literal('layer'), isEnabled: z.boolean(), @@ -587,7 +587,7 @@ const zLayerData = z.object({ }); export type LayerData = z.infer; -const zIPAdapterData = z.object({ +export const zIPAdapterData = z.object({ id: zId, type: z.literal('ip_adapter'), isEnabled: z.boolean(), @@ -637,7 +637,7 @@ const zMaskObject = z }) .pipe(z.discriminatedUnion('type', [zBrushLine, zEraserline, zRectShape])); -const zRegionalGuidanceData = z.object({ +export const zRegionalGuidanceData = z.object({ id: zId, type: z.literal('regional_guidance'), isEnabled: z.boolean(), @@ -709,7 +709,7 @@ const zT2IAdapterData = zControlAdapterDataBase.extend({ }); export type T2IAdapterData = z.infer; -const zControlAdapterData = z.discriminatedUnion('adapterType', [zControlNetData, zT2IAdapterData]); +export const zControlAdapterData = z.discriminatedUnion('adapterType', [zControlNetData, zT2IAdapterData]); export type ControlAdapterData = z.infer; export type ControlNetConfig = Pick< ControlNetData, diff --git a/invokeai/frontend/web/src/features/metadata/util/parsers.ts b/invokeai/frontend/web/src/features/metadata/util/parsers.ts index 3707d6b32d5..734b457c82e 100644 --- a/invokeai/frontend/web/src/features/metadata/util/parsers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/parsers.ts @@ -1,12 +1,5 @@ -import { - initialControlNet, - initialIPAdapter, - initialT2IAdapter, -} from 'features/controlAdapters/util/buildControlAdapter'; -import { buildControlAdapterProcessor } from 'features/controlAdapters/util/buildControlAdapterProcessor'; -import { getCALayerId, getIPALayerId, INITIAL_IMAGE_LAYER_ID } from 'features/controlLayers/konva/naming'; -import type { ControlAdapterLayer, InitialImageLayer, IPAdapterLayer, LayerData } from 'features/controlLayers/store/types'; -import { zLayer } from 'features/controlLayers/store/types'; +import { getCAId, getImageObjectId, getIPAId, getLayerId } from 'features/controlLayers/konva/naming'; +import type { ControlAdapterData, IPAdapterData, LayerData } from 'features/controlLayers/store/types'; import { CA_PROCESSOR_DATA, imageDTOToImageWithDims, @@ -14,7 +7,8 @@ import { initialIPAdapterV2, initialT2IAdapterV2, isProcessorTypeV2, -} from 'features/controlLayers/util/controlAdapters'; + zLayerData, +} from 'features/controlLayers/store/types'; import type { LoRA } from 'features/lora/store/loraSlice'; import { defaultLoRAConfig } from 'features/lora/store/loraSlice'; import type { @@ -431,7 +425,7 @@ const parseAllIPAdapters: MetadataParseFunc = async ( }; //#region Control Layers -const parseLayer: MetadataParseFunc = async (metadataItem) => zLayer.parseAsync(metadataItem); +const parseLayer: MetadataParseFunc = async (metadataItem) => zLayerData.parseAsync(metadataItem); const parseLayers: MetadataParseFunc = async (metadata) => { // We need to support recalling pre-Control Layers metadata into Control Layers. A separate set of parsers handles @@ -459,7 +453,7 @@ const parseLayers: MetadataParseFunc = async (metadata) => { controlNetsRaw.map(async (cn) => await parseControlNetToControlAdapterLayer(cn)) ); const controlNetsAsLayers = controlNetsParseResults - .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') .map((result) => result.value); layers.push(...controlNetsAsLayers); } catch { @@ -472,7 +466,7 @@ const parseLayers: MetadataParseFunc = async (metadata) => { t2iAdaptersRaw.map(async (cn) => await parseT2IAdapterToControlAdapterLayer(cn)) ); const t2iAdaptersAsLayers = t2iAdaptersParseResults - .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') .map((result) => result.value); layers.push(...t2iAdaptersAsLayers); } catch { @@ -485,7 +479,7 @@ const parseLayers: MetadataParseFunc = async (metadata) => { ipAdaptersRaw.map(async (cn) => await parseIPAdapterToIPAdapterLayer(cn)) ); const ipAdaptersAsLayers = ipAdaptersParseResults - .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') .map((result) => result.value); layers.push(...ipAdaptersAsLayers); } catch { @@ -505,28 +499,38 @@ const parseLayers: MetadataParseFunc = async (metadata) => { } }; -const parseInitialImageToInitialImageLayer: MetadataParseFunc = async (metadata) => { - const denoisingStrength = await getProperty(metadata, 'strength', isParameterStrength); +const parseInitialImageToInitialImageLayer: MetadataParseFunc = async (metadata) => { + // TODO(psyche): recall denoise strength + // const denoisingStrength = await getProperty(metadata, 'strength', isParameterStrength); const imageName = await getProperty(metadata, 'init_image', isString); const imageDTO = await getImageDTO(imageName); assert(imageDTO, 'ImageDTO is null'); - const layer: InitialImageLayer = { - id: INITIAL_IMAGE_LAYER_ID, - type: 'initial_image_layer', + const id = getLayerId(uuidv4()); + const layer: LayerData = { + id, + type: 'layer', bbox: null, bboxNeedsUpdate: true, x: 0, y: 0, isEnabled: true, opacity: 1, - image: imageDTOToImageWithDims(imageDTO), - isSelected: true, - denoisingStrength, + objects: [ + { + type: 'image', + id: getImageObjectId(id, imageDTO.image_name), + width: imageDTO.width, + height: imageDTO.height, + image: imageDTOToImageWithDims(imageDTO), + x: 0, + y: 0, + }, + ], }; return layer; }; -const parseControlNetToControlAdapterLayer: MetadataParseFunc = async (metadataItem) => { +const parseControlNetToControlAdapterLayer: MetadataParseFunc = async (metadataItem) => { const control_model = await getProperty(metadataItem, 'control_model'); const key = await getModelKey(control_model, 'controlnet'); const controlNetModel = await fetchModelConfigWithTypeGuard(key, isControlNetModelConfig); @@ -566,35 +570,31 @@ const parseControlNetToControlAdapterLayer: MetadataParseFunc = async (metadataItem) => { +const parseT2IAdapterToControlAdapterLayer: MetadataParseFunc = async (metadataItem) => { const t2i_adapter_model = await getProperty(metadataItem, 't2i_adapter_model'); const key = await getModelKey(t2i_adapter_model, 't2i_adapter'); const t2iAdapterModel = await fetchModelConfigWithTypeGuard(key, isT2IAdapterModelConfig); @@ -631,34 +631,30 @@ const parseT2IAdapterToControlAdapterLayer: MetadataParseFunc = async (metadataItem) => { +const parseIPAdapterToIPAdapterLayer: MetadataParseFunc = async (metadataItem) => { const ip_adapter_model = await getProperty(metadataItem, 'ip_adapter_model'); const key = await getModelKey(ip_adapter_model, 'ip_adapter'); const ipAdapterModel = await fetchModelConfigWithTypeGuard(key, isIPAdapterModelConfig); @@ -690,21 +686,16 @@ const parseIPAdapterToIPAdapterLayer: MetadataParseFunc = async ]; const imageDTO = image ? await getImageDTO(image.image_name) : null; - const layer: IPAdapterLayer = { - id: getIPALayerId(uuidv4()), - type: 'ip_adapter_layer', + const layer: IPAdapterData = { + id: getIPAId(uuidv4()), + type: 'ip_adapter', isEnabled: true, - isSelected: true, - ipAdapter: { - id: uuidv4(), - type: 'ip_adapter', - model: zModelIdentifierField.parse(ipAdapterModel), - weight: typeof weight === 'number' ? weight : initialIPAdapterV2.weight, - beginEndStepPct, - image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null, - clipVisionModel: initialIPAdapterV2.clipVisionModel, // TODO: This needs to be added to the zIPAdapterField... - method: method ?? initialIPAdapterV2.method, - }, + model: zModelIdentifierField.parse(ipAdapterModel), + weight: typeof weight === 'number' ? weight : initialIPAdapterV2.weight, + beginEndStepPct, + image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null, + clipVisionModel: initialIPAdapterV2.clipVisionModel, // TODO: This needs to be added to the zIPAdapterField... + method: method ?? initialIPAdapterV2.method, }; return layer; diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index 7de1d31a878..0f3399833ef 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -1,26 +1,48 @@ +import { logger } from 'app/logging/logger'; import { getStore } from 'app/store/nanostores/store'; import { deepClone } from 'common/util/deepClone'; import { - controlAdapterRecalled, - controlNetsReset, - ipAdaptersReset, - t2iAdaptersReset, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { getCALayerId, getIPALayerId, getRGLayerId } from 'features/controlLayers/konva/naming'; + getBrushLineId, + getCAId, + getEraserLineId, + getImageObjectId, + getIPAId, + getRectShapeId, + getRGId, +} from 'features/controlLayers/konva/naming'; import { - allLayersDeleted, - controlAdapterRecalled, + caRecalled, heightChanged, - iiLayerRecalled, - ipAdapterRecalled, + ipaRecalled, + layerAllDeleted, + layerRecalled, negativePrompt2Changed, negativePromptChanged, positivePrompt2Changed, positivePromptChanged, - regionalGuidanceRecalled, + refinerModelChanged, + rgRecalled, + setCfgRescaleMultiplier, + setCfgScale, + setImg2imgStrength, + setRefinerCFGScale, + setRefinerNegativeAestheticScore, + setRefinerPositiveAestheticScore, + setRefinerScheduler, + setRefinerStart, + setRefinerSteps, + setScheduler, + setSeed, + setSteps, + vaeSelected, widthChanged, } from 'features/controlLayers/store/canvasV2Slice'; -import type { LayerData } from 'features/controlLayers/store/types'; +import type { + ControlAdapterData, + IPAdapterData, + LayerData, + RegionalGuidanceData, +} from 'features/controlLayers/store/types'; import { setHrfEnabled, setHrfMethod, setHrfStrength } from 'features/hrf/store/hrfSlice'; import type { LoRA } from 'features/lora/store/loraSlice'; import { loraRecalled, lorasReset } from 'features/lora/store/loraSlice'; @@ -32,15 +54,6 @@ import type { } from 'features/metadata/types'; import { fetchModelConfigByIdentifier } from 'features/metadata/util/modelFetchingHelpers'; import { modelSelected } from 'features/parameters/store/actions'; -import { - setCfgRescaleMultiplier, - setCfgScale, - setImg2imgStrength, - setScheduler, - setSeed, - setSteps, - vaeSelected, -} from 'features/canvas/store/canvasSlice'; import type { ParameterCFGRescaleMultiplier, ParameterCFGScale, @@ -63,15 +76,6 @@ import type { ParameterVAEModel, ParameterWidth, } from 'features/parameters/types/parameterSchemas'; -import { - refinerModelChanged, - setRefinerCFGScale, - setRefinerNegativeAestheticScore, - setRefinerPositiveAestheticScore, - setRefinerScheduler, - setRefinerStart, - setRefinerSteps, -} from 'features/sdxl/store/sdxlSlice'; import { getImageDTO } from 'services/api/endpoints/images'; import { v4 as uuidv4 } from 'uuid'; @@ -241,93 +245,122 @@ const recallIPAdapters: MetadataRecallFunc = (ipAdapt }); }; -//#region Control Layers -const recallLayer: MetadataRecallFunc = async (layer) => { +const recallCA: MetadataRecallFunc = async (ca) => { const { dispatch } = getStore(); - // We need to check for the existence of all images and models when recalling. If they do not exist, SMITE THEM! - // Also, we need fresh IDs for all objects when recalling, to prevent multiple layers with the same ID. - if (layer.type === 'control_adapter_layer') { - const clone = deepClone(layer); - if (clone.controlAdapter.image) { - const imageDTO = await getImageDTO(clone.controlAdapter.image.name); - if (!imageDTO) { - clone.controlAdapter.image = null; - } + const clone = deepClone(ca); + if (clone.image) { + const imageDTO = await getImageDTO(clone.image.name); + if (!imageDTO) { + clone.image = null; } - if (clone.controlAdapter.processedImage) { - const imageDTO = await getImageDTO(clone.controlAdapter.processedImage.name); - if (!imageDTO) { - clone.controlAdapter.processedImage = null; - } + } + if (clone.processedImage) { + const imageDTO = await getImageDTO(clone.processedImage.name); + if (!imageDTO) { + clone.processedImage = null; } - if (clone.controlAdapter.model) { - try { - await fetchModelConfigByIdentifier(clone.controlAdapter.model); - } catch { - clone.controlAdapter.model = null; - } + } + if (clone.model) { + try { + await fetchModelConfigByIdentifier(clone.model); + } catch { + // MODEL SMITED! + clone.model = null; + } + } + // No clobber + clone.id = getCAId(uuidv4()); + dispatch(caRecalled({ data: clone })); + return; +}; + +const recallIPA: MetadataRecallFunc = async (ipa) => { + const { dispatch } = getStore(); + const clone = deepClone(ipa); + if (clone.image) { + const imageDTO = await getImageDTO(clone.image.name); + if (!imageDTO) { + clone.image = null; + } + } + if (clone.model) { + try { + await fetchModelConfigByIdentifier(clone.model); + } catch { + // MODEL SMITED! + clone.model = null; } - clone.id = getCALayerId(uuidv4()); - clone.controlAdapter.id = uuidv4(); - dispatch(controlAdapterRecalled(clone)); - return; } - if (layer.type === 'ip_adapter_layer') { - const clone = deepClone(layer); - if (clone.ipAdapter.image) { - const imageDTO = await getImageDTO(clone.ipAdapter.image.name); + // No clobber + clone.id = getIPAId(uuidv4()); + dispatch(ipaRecalled({ data: clone })); + return; +}; + +const recallRG: MetadataRecallFunc = async (rg) => { + const { dispatch } = getStore(); + const clone = deepClone(rg); + // Strip out the uploaded mask image property - this is an intermediate image + clone.imageCache = null; + + for (const ipAdapter of clone.ipAdapters) { + if (ipAdapter.image) { + const imageDTO = await getImageDTO(ipAdapter.image.name); if (!imageDTO) { - clone.ipAdapter.image = null; + ipAdapter.image = null; } } - if (clone.ipAdapter.model) { + if (ipAdapter.model) { try { - await fetchModelConfigByIdentifier(clone.ipAdapter.model); + await fetchModelConfigByIdentifier(ipAdapter.model); } catch { - clone.ipAdapter.model = null; + // MODEL SMITED! + ipAdapter.model = null; } } - clone.id = getIPALayerId(uuidv4()); - clone.ipAdapter.id = uuidv4(); - dispatch(ipAdapterRecalled(clone)); - return; + // No clobber + ipAdapter.id = uuidv4(); } + clone.id = getRGId(uuidv4()); + dispatch(rgRecalled({ data: clone })); + return; +}; - if (layer.type === 'regional_guidance_layer') { - const clone = deepClone(layer); - // Strip out the uploaded mask image property - this is an intermediate image - clone.uploadedMaskImage = null; - - for (const ipAdapter of clone.ipAdapters) { - if (ipAdapter.image) { - const imageDTO = await getImageDTO(ipAdapter.image.name); - if (!imageDTO) { - ipAdapter.image = null; - } - } - if (ipAdapter.model) { - try { - await fetchModelConfigByIdentifier(ipAdapter.model); - } catch { - ipAdapter.model = null; - } +//#region Control Layers +const recallLayer: MetadataRecallFunc = async (layer) => { + const { dispatch } = getStore(); + const clone = deepClone(layer); + const invalidObjects: string[] = []; + for (const obj of clone.objects) { + if (obj.type === 'image') { + const imageDTO = await getImageDTO(obj.image.name); + if (!imageDTO) { + invalidObjects.push(obj.id); } - ipAdapter.id = uuidv4(); } - clone.id = getRGLayerId(uuidv4()); - dispatch(regionalGuidanceRecalled(clone)); - return; } - - if (layer.type === 'initial_image_layer') { - dispatch(iiLayerRecalled(layer)); - return; + clone.objects = clone.objects.filter(({ id }) => !invalidObjects.includes(id)); + for (const obj of clone.objects) { + if (obj.type === 'brush_line') { + obj.id = getBrushLineId(clone.id, uuidv4()); + } else if (obj.type === 'eraser_line') { + obj.id = getEraserLineId(clone.id, uuidv4()); + } else if (obj.type === 'image') { + obj.id = getImageObjectId(clone.id, uuidv4()); + } else if (obj.type === 'rect_shape') { + obj.id = getRectShapeId(clone.id, uuidv4()); + } else { + logger('metadata').error(`Unknown object type ${obj.type}`); + } } + clone.id = getRGId(uuidv4()); + dispatch(layerRecalled({ data: clone })); + return; }; const recallLayers: MetadataRecallFunc = (layers) => { const { dispatch } = getStore(); - dispatch(allLayersDeleted()); + dispatch(layerAllDeleted()); for (const l of layers) { recallLayer(l); } diff --git a/invokeai/frontend/web/src/features/metadata/util/validators.ts b/invokeai/frontend/web/src/features/metadata/util/validators.ts index 2d79854183a..6547e01ac44 100644 --- a/invokeai/frontend/web/src/features/metadata/util/validators.ts +++ b/invokeai/frontend/web/src/features/metadata/util/validators.ts @@ -22,7 +22,7 @@ const validateBaseCompatibility = (base?: BaseModelType, message?: string) => { if (!base) { throw new InvalidModelConfigError(message || 'Missing base'); } - const currentBase = getStore().getState().generation.model?.base; + const currentBase = getStore().getState().params.model?.base; if (currentBase && base !== currentBase) { throw new InvalidModelConfigError(message || `Incompatible base models: ${base} and ${currentBase}`); } diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts index 64170041982..7fd8ab10654 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts @@ -9,8 +9,7 @@ import { getHasMetadata, removeMetadata } from './canvas/metadata'; import { CANVAS_COHERENCE_NOISE, METADATA, NOISE, POSITIVE_CONDITIONING } from './constants'; export const prepareLinearUIBatch = (state: RootState, graph: NonNullableGraph, prepend: boolean): BatchConfig => { - const { iterations, model, shouldRandomizeSeed, seed } = state.canvasV2.params; - const { shouldConcatPrompts } = state.canvasV2; + const { iterations, model, shouldRandomizeSeed, seed, shouldConcatPrompts } = state.canvasV2.params; const { prompts, seedBehaviour } = state.dynamicPrompts; const data: Batch['data'] = []; From 0586c6bdf2c2e5d58156bef7f30d0c34de3ede3b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 16 Jun 2024 12:45:05 +1000 Subject: [PATCH 059/678] refactor(ui): fix more types --- .../frontend/web/.storybook/ReduxInit.tsx | 4 +- .../listeners/appConfigReceived.ts | 4 +- .../listeners/canvasImageToControlNet.ts | 11 +----- .../listeners/canvasMaskToControlNet.ts | 11 +----- .../listeners/modelSelected.ts | 12 +++--- .../listeners/modelsLoaded.ts | 39 +++++++++++++++---- .../listeners/setDefaultSettings.ts | 7 ++-- .../socketio/socketInvocationComplete.ts | 6 ++- .../controlLayers/store/paramsReducers.ts | 5 ++- .../parameters/hooks/usePreselectedImage.ts | 22 ++++------- .../api/hooks/useSelectedModelConfig.ts | 6 +-- 11 files changed, 67 insertions(+), 60 deletions(-) diff --git a/invokeai/frontend/web/.storybook/ReduxInit.tsx b/invokeai/frontend/web/.storybook/ReduxInit.tsx index ede6b5f34ad..6b1113c4be0 100644 --- a/invokeai/frontend/web/.storybook/ReduxInit.tsx +++ b/invokeai/frontend/web/.storybook/ReduxInit.tsx @@ -10,7 +10,9 @@ export const ReduxInit = memo((props: PropsWithChildren) => { const dispatch = useAppDispatch(); useGlobalModifiersInit(); useEffect(() => { - dispatch(modelChanged({ key: 'test_model', hash: 'some_hash', name: 'some name', base: 'sd-1', type: 'main' })); + dispatch( + modelChanged({ model: { key: 'test_model', hash: 'some_hash', name: 'some name', base: 'sd-1', type: 'main' } }) + ); }, []); return props.children; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appConfigReceived.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appConfigReceived.ts index 080c4dff92b..023bf73bc53 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appConfigReceived.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appConfigReceived.ts @@ -1,5 +1,5 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { setInfillMethod } from 'features/canvas/store/canvasSlice'; +import { setInfillMethod } from 'features/controlLayers/store/canvasV2Slice'; import { shouldUseNSFWCheckerChanged, shouldUseWatermarkerChanged } from 'features/system/store/systemSlice'; import { appInfoApi } from 'services/api/endpoints/appInfo'; @@ -8,7 +8,7 @@ export const addAppConfigReceivedListener = (startAppListening: AppStartListenin matcher: appInfoApi.endpoints.getAppConfig.matchFulfilled, effect: async (action, { getState, dispatch }) => { const { infill_methods = [], nsfw_methods = [], watermarking_methods = [] } = action.payload; - const infillMethod = getState().generation.infillMethod; + const infillMethod = getState().canvasV2.compositing.infillMethod; if (!infill_methods.includes(infillMethod)) { // if there is no infill method, set it to the first one diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts index 2aa1f52d6cb..eccf8b58895 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts @@ -2,7 +2,7 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { canvasImageToControlAdapter } from 'features/canvas/store/actions'; import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob'; -import { controlAdapterImageChanged } from 'features/controlAdapters/store/controlAdaptersSlice'; +import { caImageChanged } from 'features/controlLayers/store/canvasV2Slice'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { imagesApi } from 'services/api/endpoints/images'; @@ -47,14 +47,7 @@ export const addCanvasImageToControlNetListener = (startAppListening: AppStartLi }) ).unwrap(); - const { image_name } = imageDTO; - - dispatch( - controlAdapterImageChanged({ - id, - controlImage: image_name, - }) - ); + dispatch(caImageChanged({ id, imageDTO })); }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts index 2e6ca61d8ae..e124fd825c0 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts @@ -2,7 +2,7 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { canvasMaskToControlAdapter } from 'features/canvas/store/actions'; import { getCanvasData } from 'features/canvas/util/getCanvasData'; -import { controlAdapterImageChanged } from 'features/controlAdapters/store/controlAdaptersSlice'; +import { caImageChanged } from 'features/controlLayers/store/canvasV2Slice'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { imagesApi } from 'services/api/endpoints/images'; @@ -57,14 +57,7 @@ export const addCanvasMaskToControlNetListener = (startAppListening: AppStartLis }) ).unwrap(); - const { image_name } = imageDTO; - - dispatch( - controlAdapterImageChanged({ - id, - controlImage: image_name, - }) - ); + dispatch(caImageChanged({ id, imageDTO })); }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts index 3ec87b6225d..e3ebd277ff6 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts @@ -1,12 +1,8 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { - controlAdapterIsEnabledChanged, - selectControlAdapterAll, -} from 'features/controlAdapters/store/controlAdaptersSlice'; +import { caIsEnabledToggled, modelChanged, vaeSelected } from 'features/controlLayers/store/canvasV2Slice'; import { loraRemoved } from 'features/lora/store/loraSlice'; import { modelSelected } from 'features/parameters/store/actions'; -import { modelChanged, vaeSelected } from 'features/canvas/store/canvasSlice'; import { zParameterModel } from 'features/parameters/types/parameterSchemas'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; @@ -51,10 +47,12 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = } // handle incompatible controlnets - selectControlAdapterAll(state.controlAdapters).forEach((ca) => { + state.canvasV2.controlAdapters.forEach((ca) => { if (ca.model?.base !== newBaseModel) { - dispatch(controlAdapterIsEnabledChanged({ id: ca.id, isEnabled: false })); modelsCleared += 1; + if (ca.isEnabled) { + dispatch(caIsEnabledToggled({ id: ca.id })); + } } }); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts index a7723c57751..b3b4dacaa54 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts @@ -5,8 +5,10 @@ import type { JSONObject } from 'common/types'; import { caModelChanged, heightChanged, + ipaModelChanged, modelChanged, refinerModelChanged, + rgIPAdapterModelChanged, vaeSelected, widthChanged, } from 'features/controlLayers/store/canvasV2Slice'; @@ -20,6 +22,9 @@ import type { Logger } from 'roarr'; import { modelConfigsAdapterSelectors, modelsApi } from 'services/api/endpoints/models'; import type { AnyModelConfig } from 'services/api/types'; import { + isControlNetOrT2IAdapterModelConfig, + isIPAdapterModelConfig, + isLoRAModelConfig, isNonRefinerMainModelConfig, isRefinerMainModelModelConfig, isSpandrelImageToImageModelConfig, @@ -44,6 +49,7 @@ export const addModelsLoadedListener = (startAppListening: AppStartListening) => handleLoRAModels(models, state, dispatch, log); handleControlAdapterModels(models, state, dispatch, log); handleSpandrelImageToImageModels(models, state, dispatch, log); + handleIPAdapterModels(models, state, dispatch, log); }, }); }; @@ -75,7 +81,7 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => { if (defaultModelInList) { const result = zParameterModel.safeParse(defaultModelInList); if (result.success) { - dispatch(modelChanged({ model: defaultModelInList, previousModel: currentModel ?? undefined })); + dispatch(modelChanged({ model: defaultModelInList, previousModel: currentModel })); const optimalDimension = getOptimalDimension(defaultModelInList); if (getIsSizeOptimal(state.canvasV2.document.width, state.canvasV2.document.height, optimalDimension)) { @@ -99,7 +105,7 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => { return; } - dispatch(modelChanged({ model: result.data, previousModel: currentModel ?? undefined })); + dispatch(modelChanged({ model: result.data, previousModel: currentModel })); }; const handleRefinerModels: ModelHandler = (models, state, dispatch, _log) => { @@ -156,27 +162,46 @@ const handleVAEModels: ModelHandler = (models, state, dispatch, log) => { const handleLoRAModels: ModelHandler = (models, state, dispatch, _log) => { const loras = state.lora.loras; + const loraModels = models.filter(isLoRAModelConfig); forEach(loras, (lora, id) => { - const isLoRAAvailable = models.some((m) => m.key === lora.model.key); - + const isLoRAAvailable = loraModels.some((m) => m.key === lora.model.key); if (isLoRAAvailable) { return; } - dispatch(loraRemoved(id)); }); }; const handleControlAdapterModels: ModelHandler = (models, state, dispatch, _log) => { + const caModels = models.filter(isControlNetOrT2IAdapterModelConfig); state.canvasV2.controlAdapters.forEach((ca) => { - const isModelAvailable = models.some((m) => m.key === ca.model?.key); + const isModelAvailable = caModels.some((m) => m.key === ca.model?.key); + if (isModelAvailable) { + return; + } + dispatch(caModelChanged({ id: ca.id, modelConfig: null })); + }); +}; +const handleIPAdapterModels: ModelHandler = (models, state, dispatch, _log) => { + const ipaModels = models.filter(isIPAdapterModelConfig); + state.canvasV2.controlAdapters.forEach(({ id, model }) => { + const isModelAvailable = ipaModels.some((m) => m.key === model?.key); if (isModelAvailable) { return; } + dispatch(ipaModelChanged({ id, modelConfig: null })); + }); - dispatch(caModelChanged({ id: ca.id, modelConfig: null })); + state.canvasV2.regions.forEach(({ id, ipAdapters }) => { + ipAdapters.forEach(({ id: ipAdapterId, model }) => { + const isModelAvailable = ipaModels.some((m) => m.key === model?.key); + if (isModelAvailable) { + return; + } + dispatch(rgIPAdapterModelChanged({ id, ipAdapterId, modelConfig: null })); + }); }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts index a89a1351032..fa01ee9e650 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts @@ -1,14 +1,15 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { heightChanged, widthChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { setDefaultSettings } from 'features/parameters/store/actions'; import { + heightChanged, setCfgRescaleMultiplier, setCfgScale, setScheduler, setSteps, vaePrecisionChanged, vaeSelected, -} from 'features/canvas/store/canvasSlice'; + widthChanged, +} from 'features/controlLayers/store/canvasV2Slice'; +import { setDefaultSettings } from 'features/parameters/store/actions'; import { isParameterCFGRescaleMultiplier, isParameterCFGScale, diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts index 2fc6210397b..79b4bc69cdf 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts @@ -34,7 +34,7 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi // This complete event has an associated image output if (data.result.type === 'image_output' && !nodeTypeDenylist.includes(data.invocation.type)) { const { image_name } = data.result.image; - const { canvas, gallery } = getState(); + const { canvasV2, gallery } = getState(); // This populates the `getImageDTO` cache const imageDTORequest = dispatch( @@ -47,7 +47,9 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi imageDTORequest.unsubscribe(); // Add canvas images to the staging area - if (canvas.batchIds.includes(data.batch_id) && data.invocation_source_id === CANVAS_OUTPUT) { + // TODO(psyche): canvas batchid processing, [] -> canvas.batchIds + // if (canvas.batchIds.includes(data.batch_id) && data.invocation_source_id === CANVAS_OUTPUT) { + if ([].includes(data.batch_id) && data.invocation_source_id === CANVAS_OUTPUT) { dispatch(addImageToStagingArea(imageDTO)); } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts index 023ea78f021..2020a1b732d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts @@ -47,7 +47,10 @@ export const paramsReducers = { setShouldRandomizeSeed: (state, action: PayloadAction) => { state.params.shouldRandomizeSeed = action.payload; }, - modelChanged: (state, action: PayloadAction<{ model: ParameterModel | null; previousModel?: ParameterModel }>) => { + modelChanged: ( + state, + action: PayloadAction<{ model: ParameterModel | null; previousModel?: ParameterModel | null }> + ) => { const { model, previousModel } = action.payload; state.params.model = model; diff --git a/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts b/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts index 7769a7ee137..aecb812ceeb 100644 --- a/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts +++ b/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts @@ -1,11 +1,6 @@ import { skipToken } from '@reduxjs/toolkit/query'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; -import { iiLayerAdded } from 'features/controlLayers/store/canvasV2Slice'; -import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { parseAndRecallAllMetadata } from 'features/metadata/util/handlers'; import { toast } from 'features/toast/toast'; -import { setActiveTab } from 'features/ui/store/uiSlice'; import { t } from 'i18next'; import { useCallback, useEffect } from 'react'; import { useGetImageDTOQuery, useGetImageMetadataQuery } from 'services/api/endpoints/images'; @@ -14,31 +9,30 @@ export const usePreselectedImage = (selectedImage?: { imageName: string; action: 'sendToImg2Img' | 'sendToCanvas' | 'useAllParameters'; }) => { - const dispatch = useAppDispatch(); - const optimalDimension = useAppSelector(selectOptimalDimension); - const { currentData: selectedImageDto } = useGetImageDTOQuery(selectedImage?.imageName ?? skipToken); const { currentData: selectedImageMetadata } = useGetImageMetadataQuery(selectedImage?.imageName ?? skipToken); const handleSendToCanvas = useCallback(() => { if (selectedImageDto) { - dispatch(setInitialCanvasImage(selectedImageDto, optimalDimension)); - dispatch(setActiveTab('canvas')); + // TODO(psyche): handle send to canvas + // dispatch(setInitialCanvasImage(selectedImageDto, optimalDimension)); + // dispatch(setActiveTab('canvas')); toast({ id: 'SENT_TO_CANVAS', title: t('toast.sentToUnifiedCanvas'), status: 'info', }); } - }, [selectedImageDto, dispatch, optimalDimension]); + }, [selectedImageDto]); const handleSendToImg2Img = useCallback(() => { if (selectedImageDto) { - dispatch(iiLayerAdded(selectedImageDto)); - dispatch(setActiveTab('generation')); + // TODO(psyche): handle send to img2img + // dispatch(iiLayerAdded(selectedImageDto)); + // dispatch(setActiveTab('generation')); } - }, [dispatch, selectedImageDto]); + }, [selectedImageDto]); const handleUseAllMetadata = useCallback(() => { if (selectedImageMetadata) { diff --git a/invokeai/frontend/web/src/services/api/hooks/useSelectedModelConfig.ts b/invokeai/frontend/web/src/services/api/hooks/useSelectedModelConfig.ts index 8b169a9e174..3c43101d4c9 100644 --- a/invokeai/frontend/web/src/services/api/hooks/useSelectedModelConfig.ts +++ b/invokeai/frontend/web/src/services/api/hooks/useSelectedModelConfig.ts @@ -1,13 +1,9 @@ -import { createSelector } from '@reduxjs/toolkit'; import { skipToken } from '@reduxjs/toolkit/query'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectGenerationSlice } from 'features/canvas/store/canvasSlice'; import { useGetModelConfigQuery } from 'services/api/endpoints/models'; -const selectModelKey = createSelector(selectGenerationSlice, (generation) => generation.model?.key); - export const useSelectedModelConfig = () => { - const key = useAppSelector(selectModelKey); + const key = useAppSelector((s) => s.canvasV2.params.model?.key); const { currentData: modelConfig } = useGetModelConfigQuery(key ?? skipToken); return modelConfig; From b4319630b1adb49f5e740f7aa5daa386e0d88319 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 16 Jun 2024 20:04:53 +1000 Subject: [PATCH 060/678] refactor(ui): port remaining canvasV1 rendering logic to V2, remove old code --- .../listeners/enqueueRequestedCanvas.ts | 2 +- .../listeners/imageDeletionListeners.ts | 5 - .../socketio/socketInvocationComplete.ts | 9 +- .../ClearCanvasHistoryButtonModal.tsx | 35 - .../features/canvas/components/IAICanvas.tsx | 198 ----- .../IAICanvasBoundingBoxOverlay.tsx | 48 -- .../canvas/components/IAICanvasGrid.tsx | 126 --- .../canvas/components/IAICanvasImage.tsx | 26 - .../IAICanvasImageErrorFallback.tsx | 41 - .../components/IAICanvasIntermediateImage.tsx | 53 -- .../components/IAICanvasMaskCompositor.tsx | 92 --- .../canvas/components/IAICanvasMaskLines.tsx | 37 - .../components/IAICanvasObjectRenderer.tsx | 84 -- .../components/IAICanvasStagingArea.tsx | 75 -- .../IAICanvasStagingAreaToolbar.tsx | 237 ------ .../canvas/components/IAICanvasStatusText.tsx | 120 --- .../IAICanvasStatusTextCursorPos.tsx | 20 - .../components/IAICanvasToolPreview.tsx | 173 ----- .../IAICanvasToolbar/IAICanvasBoundingBox.tsx | 331 -------- .../IAICanvasToolbar/IAICanvasMaskOptions.tsx | 156 ---- .../IAICanvasToolbar/IAICanvasRedoButton.tsx | 44 -- .../IAICanvasSettingsButtonPopover.tsx | 187 ----- .../IAICanvasToolChooserOptions.tsx | 280 ------- .../IAICanvasToolbar/IAICanvasToolbar.tsx | 323 -------- .../IAICanvasToolbar/IAICanvasUndoButton.tsx | 43 -- .../canvas/hooks/useCanvasDragMove.ts | 46 -- .../canvas/hooks/useCanvasGenerationMode.ts | 49 -- .../features/canvas/hooks/useCanvasHotkeys.ts | 117 --- .../canvas/hooks/useCanvasMouseDown.ts | 59 -- .../canvas/hooks/useCanvasMouseMove.ts | 52 -- .../canvas/hooks/useCanvasMouseOut.ts | 12 - .../features/canvas/hooks/useCanvasMouseUp.ts | 46 -- .../features/canvas/hooks/useCanvasZoom.ts | 85 -- .../canvas/hooks/useColorUnderCursor.ts | 44 -- .../web/src/features/canvas/store/actions.ts | 18 - .../features/canvas/store/canvasNanostore.ts | 41 - .../features/canvas/store/canvasSelectors.ts | 8 - .../src/features/canvas/store/canvasSlice.ts | 728 ------------------ .../src/features/canvas/store/canvasTypes.ts | 138 ---- .../src/features/canvas/store/constants.ts | 3 - .../src/features/canvas/util/blobToDataURL.ts | 28 - .../canvas/util/calculateCoordinates.ts | 17 - .../features/canvas/util/calculateScale.ts | 14 - .../src/features/canvas/util/canvasToBlob.ts | 13 - .../src/features/canvas/util/colorToString.ts | 11 - .../web/src/features/canvas/util/constants.ts | 14 - .../features/canvas/util/createMaskStage.ts | 61 -- .../canvas/util/dataURLToImageData.ts | 25 - .../src/features/canvas/util/downloadBlob.ts | 11 - .../features/canvas/util/floorCoordinates.ts | 10 - .../features/canvas/util/getBaseLayerBlob.ts | 35 - .../src/features/canvas/util/getCanvasData.ts | 71 -- .../canvas/util/getCanvasGenerationMode.ts | 24 - .../features/canvas/util/getColoredMaskSVG.ts | 81 -- .../canvas/util/getFullBaseLayerBlob.ts | 20 - .../canvas/util/getScaledCursorPosition.ts | 20 - .../canvas/util/isInteractiveTarget.ts | 13 - .../features/canvas/util/konvaNodeToBlob.ts | 14 - .../canvas/util/konvaNodeToImageData.ts | 17 - .../features/canvas/util/roundToHundreth.ts | 5 - .../ControlLayersSettingsPopover.tsx | 8 +- .../components/HeadsUpDisplay.tsx | 7 +- .../{RGGlobalOpacity.tsx => MaskOpacity.tsx} | 10 +- .../components/RegionalGuidance/RG.tsx | 2 +- .../RGMaskFillColorPicker.tsx | 2 +- .../components/StageComponent.tsx | 39 +- .../features/controlLayers/konva/constants.ts | 12 +- .../features/controlLayers/konva/events.ts | 43 +- .../konva/renderers/background.ts | 115 +++ .../controlLayers/konva/renderers/bbox.ts | 2 +- .../controlLayers/konva/renderers/layers.ts | 2 +- .../controlLayers/konva/renderers/objects.ts | 10 +- .../konva/renderers/previewLayer.ts | 24 +- .../controlLayers/konva/renderers/rgLayer.ts | 2 +- .../src/features/controlLayers/konva/util.ts | 155 +++- .../controlLayers/store/bboxReducers.ts | 39 + .../controlLayers/store/canvasV2Slice.ts | 62 +- .../controlLayers/store/paramsReducers.ts | 8 +- .../controlLayers/store/regionsReducers.ts | 4 - .../features/controlLayers/store/selectors.ts | 6 +- .../controlLayers/store/settingsReducers.ts | 8 + .../src/features/controlLayers/store/test.ts | 57 -- .../controlLayers/store/toolReducers.ts | 23 + .../src/features/controlLayers/store/types.ts | 27 +- .../util/getScaledBoundingBoxDimensions.ts | 14 +- .../nodes/util/graph/generation/addRegions.ts | 2 +- .../ParamScaleBeforeProcessing.tsx | 17 +- .../InfillAndScaling/ParamScaledHeight.tsx | 8 +- .../InfillAndScaling/ParamScaledWidth.tsx | 8 +- 89 files changed, 497 insertions(+), 4914 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/canvas/components/ClearCanvasHistoryButtonModal.tsx delete mode 100644 invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx delete mode 100644 invokeai/frontend/web/src/features/canvas/components/IAICanvasBoundingBoxOverlay.tsx delete mode 100644 invokeai/frontend/web/src/features/canvas/components/IAICanvasGrid.tsx delete mode 100644 invokeai/frontend/web/src/features/canvas/components/IAICanvasImage.tsx delete mode 100644 invokeai/frontend/web/src/features/canvas/components/IAICanvasImageErrorFallback.tsx delete mode 100644 invokeai/frontend/web/src/features/canvas/components/IAICanvasIntermediateImage.tsx delete mode 100644 invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskCompositor.tsx delete mode 100644 invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskLines.tsx delete mode 100644 invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx delete mode 100644 invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx delete mode 100644 invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx delete mode 100644 invokeai/frontend/web/src/features/canvas/components/IAICanvasStatusText.tsx delete mode 100644 invokeai/frontend/web/src/features/canvas/components/IAICanvasStatusText/IAICanvasStatusTextCursorPos.tsx delete mode 100644 invokeai/frontend/web/src/features/canvas/components/IAICanvasToolPreview.tsx delete mode 100644 invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx delete mode 100644 invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskOptions.tsx delete mode 100644 invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasRedoButton.tsx delete mode 100644 invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx delete mode 100644 invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolChooserOptions.tsx delete mode 100644 invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx delete mode 100644 invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasUndoButton.tsx delete mode 100644 invokeai/frontend/web/src/features/canvas/hooks/useCanvasDragMove.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/hooks/useCanvasGenerationMode.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/hooks/useCanvasHotkeys.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseDown.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseMove.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseOut.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseUp.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/hooks/useColorUnderCursor.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/store/actions.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/store/canvasNanostore.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/store/canvasSelectors.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/store/constants.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/util/blobToDataURL.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/util/calculateCoordinates.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/util/calculateScale.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/util/canvasToBlob.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/util/colorToString.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/util/constants.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/util/createMaskStage.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/util/dataURLToImageData.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/util/downloadBlob.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/util/floorCoordinates.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/util/getBaseLayerBlob.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/util/getCanvasData.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/util/getCanvasGenerationMode.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/util/getColoredMaskSVG.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/util/getFullBaseLayerBlob.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/util/getScaledCursorPosition.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/util/isInteractiveTarget.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/util/konvaNodeToBlob.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/util/konvaNodeToImageData.ts delete mode 100644 invokeai/frontend/web/src/features/canvas/util/roundToHundreth.ts rename invokeai/frontend/web/src/features/controlLayers/components/{RGGlobalOpacity.tsx => MaskOpacity.tsx} (77%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/settingsReducers.ts delete mode 100644 invokeai/frontend/web/src/features/controlLayers/store/test.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts rename invokeai/frontend/web/src/features/{canvas => controlLayers}/util/getScaledBoundingBoxDimensions.ts (64%) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts index a7491ab01b8..ac32d6fa01b 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts @@ -4,9 +4,9 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware' import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; import { parseify } from 'common/util/serialize'; import { canvasBatchIdAdded, stagingAreaInitialized } from 'features/canvas/store/canvasSlice'; -import { blobToDataURL } from 'features/canvas/util/blobToDataURL'; import { getCanvasData } from 'features/canvas/util/getCanvasData'; import { getCanvasGenerationMode } from 'features/canvas/util/getCanvasGenerationMode'; +import { blobToDataURL } from "features/controlLayers/konva/util"; import { canvasGraphBuilt } from 'features/nodes/store/actions'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; import { buildCanvasGraph } from 'features/nodes/util/graph/canvas/buildCanvasGraph'; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts index 7b579d2a4ab..6f918f99595 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts @@ -1,7 +1,6 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import type { AppDispatch, RootState } from 'app/store/store'; -import { resetCanvas } from 'features/canvas/store/canvasSlice'; import { caImageChanged, caProcessedImageChanged, @@ -160,10 +159,6 @@ export const addImageDeletionListeners = (startAppListening: AppStartListening) // We need to reset the features where the image is in use - none of these work if their image(s) don't exist - if (imagesUsage.some((i) => i.isLayerImage)) { - dispatch(resetCanvas()); - } - imageDTOs.forEach((imageDTO) => { deleteNodesImages(state, dispatch, imageDTO); deleteControlAdapterImages(state, dispatch, imageDTO); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts index 79b4bc69cdf..a82f3f265c4 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts @@ -2,7 +2,6 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { deepClone } from 'common/util/deepClone'; import { parseify } from 'common/util/serialize'; -import { addImageToStagingArea } from 'features/canvas/store/canvasSlice'; import { boardIdSelected, galleryViewChanged, @@ -12,7 +11,6 @@ import { } from 'features/gallery/store/gallerySlice'; import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; import { zNodeStatus } from 'features/nodes/types/invocation'; -import { CANVAS_OUTPUT } from 'features/nodes/util/graph/constants'; import { boardsApi } from 'services/api/endpoints/boards'; import { imagesApi } from 'services/api/endpoints/images'; import { getCategories, getListImagesUrl } from 'services/api/util'; @@ -47,11 +45,10 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi imageDTORequest.unsubscribe(); // Add canvas images to the staging area - // TODO(psyche): canvas batchid processing, [] -> canvas.batchIds + // TODO(psyche): canvas batchid processing // if (canvas.batchIds.includes(data.batch_id) && data.invocation_source_id === CANVAS_OUTPUT) { - if ([].includes(data.batch_id) && data.invocation_source_id === CANVAS_OUTPUT) { - dispatch(addImageToStagingArea(imageDTO)); - } + // dispatch(addImageToStagingArea(imageDTO)); + // } if (!imageDTO.is_intermediate) { // update the total images for the board diff --git a/invokeai/frontend/web/src/features/canvas/components/ClearCanvasHistoryButtonModal.tsx b/invokeai/frontend/web/src/features/canvas/components/ClearCanvasHistoryButtonModal.tsx deleted file mode 100644 index e49976e532e..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/ClearCanvasHistoryButtonModal.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Button, ConfirmationAlertDialog, useDisclosure } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; -import { clearCanvasHistory } from 'features/canvas/store/canvasSlice'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiTrashSimpleFill } from 'react-icons/pi'; - -const ClearCanvasHistoryButtonModal = () => { - const isStaging = useAppSelector(isStagingSelector); - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - const { isOpen, onOpen, onClose } = useDisclosure(); - const acceptCallback = useCallback(() => dispatch(clearCanvasHistory()), [dispatch]); - - return ( - <> - - -

{t('unifiedCanvas.clearCanvasHistoryMessage')}

-
-

{t('unifiedCanvas.clearCanvasHistoryConfirm')}

-
- - ); -}; -export default memo(ClearCanvasHistoryButtonModal); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx deleted file mode 100644 index c08b70a7839..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import { Box, chakra, Flex } from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import useCanvasDragMove from 'features/canvas/hooks/useCanvasDragMove'; -import useCanvasHotkeys from 'features/canvas/hooks/useCanvasHotkeys'; -import useCanvasMouseDown from 'features/canvas/hooks/useCanvasMouseDown'; -import useCanvasMouseMove from 'features/canvas/hooks/useCanvasMouseMove'; -import useCanvasMouseOut from 'features/canvas/hooks/useCanvasMouseOut'; -import useCanvasMouseUp from 'features/canvas/hooks/useCanvasMouseUp'; -import useCanvasWheel from 'features/canvas/hooks/useCanvasZoom'; -import { - $canvasBaseLayer, - $canvasStage, - $isModifyingBoundingBox, - $isMouseOverBoundingBox, - $isMovingStage, - $isTransformingBoundingBox, - $tool, -} from 'features/canvas/store/canvasNanostore'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; -import { canvasResized, selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -import type Konva from 'konva'; -import type { KonvaEventObject } from 'konva/lib/Node'; -import type { Vector2d } from 'konva/lib/types'; -import { memo, useCallback, useEffect, useMemo, useRef } from 'react'; -import { Layer, Stage } from 'react-konva'; - -import IAICanvasBoundingBoxOverlay from './IAICanvasBoundingBoxOverlay'; -import IAICanvasGrid from './IAICanvasGrid'; -import IAICanvasIntermediateImage from './IAICanvasIntermediateImage'; -import IAICanvasMaskCompositor from './IAICanvasMaskCompositor'; -import IAICanvasMaskLines from './IAICanvasMaskLines'; -import IAICanvasObjectRenderer from './IAICanvasObjectRenderer'; -import IAICanvasStagingArea from './IAICanvasStagingArea'; -import IAICanvasStagingAreaToolbar from './IAICanvasStagingAreaToolbar'; -import IAICanvasStatusText from './IAICanvasStatusText'; -import IAICanvasBoundingBox from './IAICanvasToolbar/IAICanvasBoundingBox'; -import IAICanvasToolPreview from './IAICanvasToolPreview'; - -const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => { - return { - stageCoordinates: canvas.stageCoordinates, - stageDimensions: canvas.stageDimensions, - }; -}); - -const ChakraStage = chakra(Stage, { - shouldForwardProp: (prop) => !['sx'].includes(prop), -}); - -const IAICanvas = () => { - const isStaging = useAppSelector(isStagingSelector); - const isMaskEnabled = useAppSelector((s) => s.canvas.isMaskEnabled); - const shouldShowBoundingBox = useAppSelector((s) => s.canvas.shouldShowBoundingBox); - const shouldShowGrid = useAppSelector((s) => s.canvas.shouldShowGrid); - const stageScale = useAppSelector((s) => s.canvas.stageScale); - const shouldShowIntermediates = useAppSelector((s) => s.canvas.shouldShowIntermediates); - const shouldAntialias = useAppSelector((s) => s.canvas.shouldAntialias); - const shouldRestrictStrokesToBox = useAppSelector((s) => s.canvas.shouldRestrictStrokesToBox); - const { stageCoordinates, stageDimensions } = useAppSelector(selector); - const dispatch = useAppDispatch(); - const containerRef = useRef(null); - const stageRef = useRef(null); - const canvasBaseLayerRef = useRef(null); - const isModifyingBoundingBox = useStore($isModifyingBoundingBox); - const isMovingStage = useStore($isMovingStage); - const isTransformingBoundingBox = useStore($isTransformingBoundingBox); - const isMouseOverBoundingBox = useStore($isMouseOverBoundingBox); - const tool = useStore($tool); - useCanvasHotkeys(); - const canvasStageRefCallback = useCallback((stageElement: Konva.Stage) => { - $canvasStage.set(stageElement); - stageRef.current = stageElement; - }, []); - const stageCursor = useMemo(() => { - if (tool === 'move' || isStaging) { - if (isMovingStage) { - return 'grabbing'; - } else { - return 'grab'; - } - } else if (isTransformingBoundingBox) { - return undefined; - } else if (shouldRestrictStrokesToBox && !isMouseOverBoundingBox) { - return 'default'; - } - return 'none'; - }, [isMouseOverBoundingBox, isMovingStage, isStaging, isTransformingBoundingBox, shouldRestrictStrokesToBox, tool]); - - const canvasBaseLayerRefCallback = useCallback((layerElement: Konva.Layer) => { - $canvasBaseLayer.set(layerElement); - canvasBaseLayerRef.current = layerElement; - }, []); - - const lastCursorPositionRef = useRef({ x: 0, y: 0 }); - - // Use refs for values that do not affect rendering, other values in redux - const didMouseMoveRef = useRef(false); - - const handleWheel = useCanvasWheel(stageRef); - const handleMouseDown = useCanvasMouseDown(stageRef); - const handleMouseUp = useCanvasMouseUp(stageRef, didMouseMoveRef); - const handleMouseMove = useCanvasMouseMove(stageRef, didMouseMoveRef, lastCursorPositionRef); - const { handleDragStart, handleDragMove, handleDragEnd } = useCanvasDragMove(); - const handleMouseOut = useCanvasMouseOut(); - const handleContextMenu = useCallback((e: KonvaEventObject) => e.evt.preventDefault(), []); - - useEffect(() => { - if (!containerRef.current) { - return; - } - const resizeObserver = new ResizeObserver(() => { - if (!containerRef.current) { - return; - } - const { width, height } = containerRef.current.getBoundingClientRect(); - dispatch(canvasResized({ width, height })); - }); - - resizeObserver.observe(containerRef.current); - const { width, height } = containerRef.current.getBoundingClientRect(); - dispatch(canvasResized({ width, height })); - - return () => { - resizeObserver.disconnect(); - }; - }, [dispatch]); - - const stageStyles = useMemo( - () => ({ - outline: 'none', - overflow: 'hidden', - cursor: stageCursor ? stageCursor : undefined, - canvas: { - outline: 'none', - }, - }), - [stageCursor] - ); - - const scale = useMemo(() => ({ x: stageScale, y: stageScale }), [stageScale]); - - return ( - - - - - - - - - - - - - - - - - - - {!isStaging && } - - {shouldShowIntermediates && } - - - - - - - - ); -}; - -export default memo(IAICanvas); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasBoundingBoxOverlay.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasBoundingBoxOverlay.tsx deleted file mode 100644 index 3cb75c09c6e..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasBoundingBoxOverlay.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -import { memo } from 'react'; -import { Group, Rect } from 'react-konva'; - -const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => { - const { boundingBoxCoordinates, boundingBoxDimensions, stageDimensions, stageCoordinates } = canvas; - - return { - boundingBoxCoordinates, - boundingBoxDimensions, - stageCoordinates, - stageDimensions, - }; -}); - -const IAICanvasBoundingBoxOverlay = () => { - const { boundingBoxCoordinates, boundingBoxDimensions, stageCoordinates, stageDimensions } = useAppSelector(selector); - const shouldDarkenOutsideBoundingBox = useAppSelector((s) => s.canvas.shouldDarkenOutsideBoundingBox); - const stageScale = useAppSelector((s) => s.canvas.stageScale); - - return ( - - - - - ); -}; - -export default memo(IAICanvasBoundingBoxOverlay); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasGrid.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasGrid.tsx deleted file mode 100644 index b9105ce9dd7..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasGrid.tsx +++ /dev/null @@ -1,126 +0,0 @@ -// Grid drawing adapted from https://longviewcoder.com/2021/12/08/konva-a-better-grid/ -import { getArbitraryBaseColor } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -import type { ReactElement } from 'react'; -import { memo, useCallback, useMemo } from 'react'; -import { Group, Line as KonvaLine } from 'react-konva'; - -const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => { - return { - stageCoordinates: canvas.stageCoordinates, - stageDimensions: canvas.stageDimensions, - }; -}); - -const baseGridLineColor = getArbitraryBaseColor(27); -const fineGridLineColor = getArbitraryBaseColor(18); - -const IAICanvasGrid = () => { - const { stageCoordinates, stageDimensions } = useAppSelector(selector); - const stageScale = useAppSelector((s) => s.canvas.stageScale); - - const gridSpacing = useMemo(() => { - if (stageScale >= 2) { - return 8; - } - if (stageScale >= 1 && stageScale < 2) { - return 16; - } - if (stageScale >= 0.5 && stageScale < 1) { - return 32; - } - return 64; - }, [stageScale]); - - const unscale = useCallback( - (value: number) => { - return value / stageScale; - }, - [stageScale] - ); - - const gridLines = useMemo(() => { - const { width, height } = stageDimensions; - const { x, y } = stageCoordinates; - - const stageRect = { - x1: 0, - y1: 0, - x2: width, - y2: height, - offset: { - x: unscale(x), - y: unscale(y), - }, - }; - - const gridOffset = { - x: Math.ceil(unscale(x) / gridSpacing) * gridSpacing, - y: Math.ceil(unscale(y) / gridSpacing) * gridSpacing, - }; - - const gridRect = { - x1: -gridOffset.x, - y1: -gridOffset.y, - x2: unscale(width) - gridOffset.x + gridSpacing, - y2: unscale(height) - gridOffset.y + gridSpacing, - }; - - const gridFullRect = { - x1: Math.min(stageRect.x1, gridRect.x1), - y1: Math.min(stageRect.y1, gridRect.y1), - x2: Math.max(stageRect.x2, gridRect.x2), - y2: Math.max(stageRect.y2, gridRect.y2), - }; - - const // find the x & y size of the grid - xSize = gridFullRect.x2 - gridFullRect.x1; - const ySize = gridFullRect.y2 - gridFullRect.y1; - // compute the number of steps required on each axis. - const xSteps = Math.round(xSize / gridSpacing) + 1; - const ySteps = Math.round(ySize / gridSpacing) + 1; - - const strokeWidth = unscale(1); - - const gridLines: ReactElement[] = new Array(xSteps + ySteps); - let _x = 0; - let _y = 0; - for (let i = 0; i < xSteps; i++) { - _x = gridFullRect.x1 + i * gridSpacing; - gridLines.push( - - ); - } - - for (let i = 0; i < ySteps; i++) { - _y = gridFullRect.y1 + i * gridSpacing; - gridLines.push( - - ); - } - - return gridLines; - }, [stageDimensions, stageCoordinates, unscale, gridSpacing]); - - return {gridLines}; -}; - -export default memo(IAICanvasGrid); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasImage.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasImage.tsx deleted file mode 100644 index 75ae983f23d..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasImage.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { skipToken } from '@reduxjs/toolkit/query'; -import { $authToken } from 'app/store/nanostores/authToken'; -import type { CanvasImage } from 'features/canvas/store/canvasTypes'; -import { memo } from 'react'; -import { Image } from 'react-konva'; -import { useGetImageDTOQuery } from 'services/api/endpoints/images'; -import useImage from 'use-image'; - -import IAICanvasImageErrorFallback from './IAICanvasImageErrorFallback'; - -type IAICanvasImageProps = { - canvasImage: CanvasImage; -}; -const IAICanvasImage = (props: IAICanvasImageProps) => { - const { x, y, imageName } = props.canvasImage; - const { currentData: imageDTO, isError } = useGetImageDTOQuery(imageName ?? skipToken); - const [image, status] = useImage(imageDTO?.image_url ?? '', $authToken.get() ? 'use-credentials' : 'anonymous'); - - if (isError || status === 'failed') { - return ; - } - - return ; -}; - -export default memo(IAICanvasImage); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasImageErrorFallback.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasImageErrorFallback.tsx deleted file mode 100644 index 1606dfa8446..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasImageErrorFallback.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { useToken } from '@invoke-ai/ui-library'; -import type { CanvasImage } from 'features/canvas/store/canvasTypes'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Group, Rect, Text } from 'react-konva'; - -type IAICanvasImageErrorFallbackProps = { - canvasImage: CanvasImage; -}; -const IAICanvasImageErrorFallback = ({ canvasImage }: IAICanvasImageErrorFallbackProps) => { - const [rectFill, textFill] = useToken('colors', ['base.500', 'base.900']); - const { t } = useTranslation(); - return ( - - - - - ); -}; - -export default memo(IAICanvasImageErrorFallback); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasIntermediateImage.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasIntermediateImage.tsx deleted file mode 100644 index bd9b93997ec..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasIntermediateImage.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -import { selectSystemSlice } from 'features/system/store/systemSlice'; -import { memo, useEffect, useState } from 'react'; -import { Image as KonvaImage } from 'react-konva'; - -const progressImageSelector = createMemoizedSelector([selectSystemSlice, selectCanvasSlice], (system, canvas) => { - const { denoiseProgress } = system; - const { batchIds } = canvas; - - return { - progressImage: - denoiseProgress && batchIds.includes(denoiseProgress.batch_id) ? denoiseProgress.progress_image : undefined, - boundingBox: canvas.layerState.stagingArea.boundingBox, - }; -}); - -const IAICanvasIntermediateImage = () => { - const { progressImage, boundingBox } = useAppSelector(progressImageSelector); - const [loadedImageElement, setLoadedImageElement] = useState(null); - - useEffect(() => { - if (!progressImage) { - return; - } - - const tempImage = new Image(); - - tempImage.onload = () => { - setLoadedImageElement(tempImage); - }; - - tempImage.src = progressImage.dataURL; - }, [progressImage]); - - if (!(progressImage && boundingBox) || !loadedImageElement) { - return null; - } - - return ( - - ); -}; - -export default memo(IAICanvasIntermediateImage); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskCompositor.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskCompositor.tsx deleted file mode 100644 index a339cf5352e..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskCompositor.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -import { rgbaColorToString } from 'features/canvas/util/colorToString'; -import { getColoredMaskSVG } from 'features/canvas/util/getColoredMaskSVG'; -import type Konva from 'konva'; -import type { RectConfig } from 'konva/lib/shapes/Rect'; -import { isNumber } from 'lodash-es'; -import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { Rect } from 'react-konva'; - -const canvasMaskCompositerSelector = createMemoizedSelector(selectCanvasSlice, (canvas) => { - return { - stageCoordinates: canvas.stageCoordinates, - stageDimensions: canvas.stageDimensions, - }; -}); - -type IAICanvasMaskCompositorProps = RectConfig; - -const IAICanvasMaskCompositor = (props: IAICanvasMaskCompositorProps) => { - const { ...rest } = props; - - const { stageCoordinates, stageDimensions } = useAppSelector(canvasMaskCompositerSelector); - const stageScale = useAppSelector((s) => s.canvas.stageScale); - const maskColorString = useAppSelector((s) => rgbaColorToString(s.canvas.maskColor)); - const [fillPatternImage, setFillPatternImage] = useState(null); - - const [offset, setOffset] = useState(0); - - const rectRef = useRef(null); - const incrementOffset = useCallback(() => { - setOffset(offset + 1); - setTimeout(incrementOffset, 500); - }, [offset]); - - useEffect(() => { - if (fillPatternImage) { - return; - } - const image = new Image(); - - image.onload = () => { - setFillPatternImage(image); - }; - image.src = getColoredMaskSVG(maskColorString); - }, [fillPatternImage, maskColorString]); - - useEffect(() => { - if (!fillPatternImage) { - return; - } - fillPatternImage.src = getColoredMaskSVG(maskColorString); - }, [fillPatternImage, maskColorString]); - - useEffect(() => { - const timer = setInterval(() => setOffset((i) => (i + 1) % 5), 50); - return () => clearInterval(timer); - }, []); - - const fillPatternScale = useMemo(() => ({ x: 1 / stageScale, y: 1 / stageScale }), [stageScale]); - - if ( - !fillPatternImage || - !isNumber(stageCoordinates.x) || - !isNumber(stageCoordinates.y) || - !isNumber(stageScale) || - !isNumber(stageDimensions.width) || - !isNumber(stageDimensions.height) - ) { - return null; - } - - return ( - - ); -}; - -export default memo(IAICanvasMaskCompositor); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskLines.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskLines.tsx deleted file mode 100644 index 27a733cb5e5..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskLines.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { useAppSelector } from 'app/store/storeHooks'; -import { isCanvasMaskLine } from 'features/canvas/store/canvasTypes'; -import type { GroupConfig } from 'konva/lib/Group'; -import { memo } from 'react'; -import { Group, Line } from 'react-konva'; - -type InpaintingCanvasLinesProps = GroupConfig; - -/** - * Draws the lines which comprise the mask. - * - * Uses globalCompositeOperation to handle the brush and eraser tools. - */ -const IAICanvasLines = (props: InpaintingCanvasLinesProps) => { - const objects = useAppSelector((s) => s.canvas.layerState.objects); - - return ( - - {objects.filter(isCanvasMaskLine).map((line, i) => ( - 0 - strokeWidth={line.strokeWidth * 2} - tension={0} - lineCap="round" - lineJoin="round" - shadowForStrokeEnabled={false} - listening={false} - globalCompositeOperation={line.tool === 'brush' ? 'source-over' : 'destination-out'} - /> - ))} - - ); -}; - -export default memo(IAICanvasLines); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx deleted file mode 100644 index 23005a9d246..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { useAppSelector } from 'app/store/storeHooks'; -import { - isCanvasBaseImage, - isCanvasBaseLine, - isCanvasEraseRect, - isCanvasFillRect, -} from 'features/canvas/store/canvasTypes'; -import { rgbaColorToString } from 'features/canvas/util/colorToString'; -import { memo } from 'react'; -import { Group, Line, Rect } from 'react-konva'; - -import IAICanvasImage from './IAICanvasImage'; - -const IAICanvasObjectRenderer = () => { - const objects = useAppSelector((s) => s.canvas.layerState.objects); - - return ( - - {objects.map((obj, i) => { - if (isCanvasBaseImage(obj)) { - return ; - } else if (isCanvasBaseLine(obj)) { - const line = ( - 0 - strokeWidth={obj.strokeWidth * 2} - tension={0} - lineCap="round" - lineJoin="round" - shadowForStrokeEnabled={false} - listening={false} - globalCompositeOperation={obj.tool === 'brush' ? 'source-over' : 'destination-out'} - /> - ); - if (obj.clip) { - return ( - - {line} - - ); - } else { - return line; - } - } else if (isCanvasFillRect(obj)) { - return ( - - ); - } else if (isCanvasEraseRect(obj)) { - return ( - - ); - } - })} - - ); -}; - -export default memo(IAICanvasObjectRenderer); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx deleted file mode 100644 index 8b9b580e63c..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -import type { GroupConfig } from 'konva/lib/Group'; -import { memo } from 'react'; -import { Group, Rect } from 'react-konva'; - -import IAICanvasImage from './IAICanvasImage'; - -const dash = [4, 4]; - -const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => { - const { - layerState, - shouldShowStagingImage, - shouldShowStagingOutline, - boundingBoxCoordinates: stageBoundingBoxCoordinates, - boundingBoxDimensions: stageBoundingBoxDimensions, - } = canvas; - - const { selectedImageIndex, images, boundingBox } = layerState.stagingArea; - - return { - currentStagingAreaImage: - images.length > 0 && selectedImageIndex !== undefined ? images[selectedImageIndex] : undefined, - isOnFirstImage: selectedImageIndex === 0, - isOnLastImage: selectedImageIndex === images.length - 1, - shouldShowStagingImage, - shouldShowStagingOutline, - x: boundingBox?.x ?? stageBoundingBoxCoordinates.x, - y: boundingBox?.y ?? stageBoundingBoxCoordinates.y, - width: boundingBox?.width ?? stageBoundingBoxDimensions.width, - height: boundingBox?.height ?? stageBoundingBoxDimensions.height, - }; -}); - -type Props = GroupConfig; - -const IAICanvasStagingArea = (props: Props) => { - const { currentStagingAreaImage, shouldShowStagingImage, shouldShowStagingOutline, x, y, width, height } = - useAppSelector(selector); - - return ( - - {shouldShowStagingImage && currentStagingAreaImage && } - {shouldShowStagingOutline && ( - - - - - )} - - ); -}; - -export default memo(IAICanvasStagingArea); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx deleted file mode 100644 index ed394599761..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx +++ /dev/null @@ -1,237 +0,0 @@ -import { Button, ButtonGroup, Flex, IconButton } from '@invoke-ai/ui-library'; -import { skipToken } from '@reduxjs/toolkit/query'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { stagingAreaImageSaved } from 'features/canvas/store/actions'; -import { - commitStagingAreaImage, - discardStagedImage, - discardStagedImages, - nextStagingAreaImage, - prevStagingAreaImage, - selectCanvasSlice, - setShouldShowStagingImage, - setShouldShowStagingOutline, -} from 'features/canvas/store/canvasSlice'; -import { memo, useCallback } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { - PiArrowLeftBold, - PiArrowRightBold, - PiCheckBold, - PiEyeBold, - PiEyeSlashBold, - PiFloppyDiskBold, - PiTrashSimpleBold, - PiXBold, -} from 'react-icons/pi'; -import { useGetImageDTOQuery } from 'services/api/endpoints/images'; - -const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => { - const { - layerState: { - stagingArea: { images, selectedImageIndex }, - }, - shouldShowStagingOutline, - shouldShowStagingImage, - } = canvas; - - return { - currentIndex: selectedImageIndex, - total: images.length, - currentStagingAreaImage: images.length > 0 ? images[selectedImageIndex] : undefined, - shouldShowStagingImage, - shouldShowStagingOutline, - }; -}); - -const ClearStagingIntermediatesIconButton = () => { - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - const totalStagedImages = useAppSelector((s) => s.canvas.layerState.stagingArea.images.length); - - const handleDiscardStagingArea = useCallback(() => { - dispatch(discardStagedImages()); - }, [dispatch]); - - const handleDiscardStagingImage = useCallback(() => { - // Discarding all staged images triggers cancelation of all canvas batches. It's too easy to accidentally - // click the discard button, so to prevent accidental cancelation of all batches, we only discard the current - // image if there are more than one staged images. - if (totalStagedImages > 1) { - dispatch(discardStagedImage()); - } - }, [dispatch, totalStagedImages]); - - return ( - <> - } - onClick={handleDiscardStagingImage} - colorScheme="invokeBlue" - fontSize={16} - isDisabled={totalStagedImages <= 1} - /> - } - onClick={handleDiscardStagingArea} - colorScheme="error" - fontSize={16} - /> - - ); -}; - -const IAICanvasStagingAreaToolbar = () => { - const dispatch = useAppDispatch(); - const { currentStagingAreaImage, shouldShowStagingImage, currentIndex, total } = useAppSelector(selector); - - const { t } = useTranslation(); - - const handleMouseOver = useCallback(() => { - dispatch(setShouldShowStagingOutline(true)); - }, [dispatch]); - - const handleMouseOut = useCallback(() => { - dispatch(setShouldShowStagingOutline(false)); - }, [dispatch]); - - const handlePrevImage = useCallback(() => dispatch(prevStagingAreaImage()), [dispatch]); - - const handleNextImage = useCallback(() => dispatch(nextStagingAreaImage()), [dispatch]); - - const handleAccept = useCallback(() => dispatch(commitStagingAreaImage()), [dispatch]); - - useHotkeys(['left'], handlePrevImage, { - enabled: () => true, - preventDefault: true, - }); - - useHotkeys(['right'], handleNextImage, { - enabled: () => true, - preventDefault: true, - }); - - useHotkeys(['enter'], handleAccept, { - enabled: () => true, - preventDefault: true, - }); - - useHotkeys( - ['esc'], - () => { - handleDiscardStagingArea(); - }, - { - preventDefault: true, - } - ); - - const { data: imageDTO } = useGetImageDTOQuery(currentStagingAreaImage?.imageName ?? skipToken); - - const handleToggleShouldShowStagingImage = useCallback(() => { - dispatch(setShouldShowStagingImage(!shouldShowStagingImage)); - }, [dispatch, shouldShowStagingImage]); - - const handleSaveToGallery = useCallback(() => { - if (!imageDTO) { - return; - } - - dispatch( - stagingAreaImageSaved({ - imageDTO, - }) - ); - }, [dispatch, imageDTO]); - - useHotkeys( - ['shift+s'], - () => { - shouldShowStagingImage && handleSaveToGallery(); - }, - { - preventDefault: true, - }, - [shouldShowStagingImage, handleSaveToGallery] - ); - - const handleDiscardStagingArea = useCallback(() => { - dispatch(discardStagedImages()); - }, [dispatch]); - - if (!currentStagingAreaImage) { - return null; - } - - return ( - - - } - onClick={handlePrevImage} - colorScheme="invokeBlue" - isDisabled={!shouldShowStagingImage} - /> - - } - onClick={handleNextImage} - colorScheme="invokeBlue" - isDisabled={!shouldShowStagingImage} - /> - - - } - onClick={handleAccept} - colorScheme="invokeBlue" - /> - : } - onClick={handleToggleShouldShowStagingImage} - colorScheme="invokeBlue" - /> - } - onClick={handleSaveToGallery} - colorScheme="invokeBlue" - /> - - - - ); -}; - -export default memo(IAICanvasStagingAreaToolbar); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStatusText.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStatusText.tsx deleted file mode 100644 index 4c8153b83fe..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStatusText.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { Box, Flex } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -import roundToHundreth from 'features/canvas/util/roundToHundreth'; -import GenerationModeStatusText from 'features/parameters/components/Canvas/GenerationModeStatusText'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; - -import IAICanvasStatusTextCursorPos from './IAICanvasStatusText/IAICanvasStatusTextCursorPos'; - -const warningColor = 'var(--invoke-colors-warning-500)'; - -const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => { - const { - stageDimensions: { width: stageWidth, height: stageHeight }, - stageCoordinates: { x: stageX, y: stageY }, - boundingBoxDimensions: { width: boxWidth, height: boxHeight }, - scaledBoundingBoxDimensions: { width: scaledBoxWidth, height: scaledBoxHeight }, - boundingBoxCoordinates: { x: boxX, y: boxY }, - stageScale, - shouldShowCanvasDebugInfo, - layer, - boundingBoxScaleMethod, - shouldPreserveMaskedArea, - } = canvas; - - let boundingBoxColor = 'inherit'; - - if ( - (boundingBoxScaleMethod === 'none' && (boxWidth < 512 || boxHeight < 512)) || - (boundingBoxScaleMethod === 'manual' && scaledBoxWidth * scaledBoxHeight < 512 * 512) - ) { - boundingBoxColor = warningColor; - } - - const activeLayerColor = layer === 'mask' ? warningColor : 'inherit'; - - return { - activeLayerColor, - layer, - boundingBoxColor, - boundingBoxCoordinatesString: `(${roundToHundreth(boxX)}, ${roundToHundreth(boxY)})`, - boundingBoxDimensionsString: `${boxWidth}×${boxHeight}`, - scaledBoundingBoxDimensionsString: `${scaledBoxWidth}×${scaledBoxHeight}`, - canvasCoordinatesString: `${roundToHundreth(stageX)}×${roundToHundreth(stageY)}`, - canvasDimensionsString: `${stageWidth}×${stageHeight}`, - canvasScaleString: Math.round(stageScale * 100), - shouldShowCanvasDebugInfo, - shouldShowBoundingBox: boundingBoxScaleMethod !== 'auto', - shouldShowScaledBoundingBox: boundingBoxScaleMethod !== 'none', - shouldPreserveMaskedArea, - }; -}); - -const IAICanvasStatusText = () => { - const { - activeLayerColor, - layer, - boundingBoxColor, - boundingBoxCoordinatesString, - boundingBoxDimensionsString, - scaledBoundingBoxDimensionsString, - shouldShowScaledBoundingBox, - canvasCoordinatesString, - canvasDimensionsString, - canvasScaleString, - shouldShowCanvasDebugInfo, - shouldShowBoundingBox, - shouldPreserveMaskedArea, - } = useAppSelector(selector); - - const { t } = useTranslation(); - - return ( - - - {`${t('unifiedCanvas.activeLayer')}: ${t(`unifiedCanvas.${layer}`)}`} - {`${t('unifiedCanvas.canvasScale')}: ${canvasScaleString}%`} - {shouldPreserveMaskedArea && ( - - {t('unifiedCanvas.preserveMaskedArea')}: {t('common.on')} - - )} - {shouldShowBoundingBox && ( - {`${t('unifiedCanvas.boundingBox')}: ${boundingBoxDimensionsString}`} - )} - {shouldShowScaledBoundingBox && ( - {`${t( - 'unifiedCanvas.scaledBoundingBox' - )}: ${scaledBoundingBoxDimensionsString}`} - )} - {shouldShowCanvasDebugInfo && ( - <> - {`${t('unifiedCanvas.boundingBoxPosition')}: ${boundingBoxCoordinatesString}`} - {`${t('unifiedCanvas.canvasDimensions')}: ${canvasDimensionsString}`} - {`${t('unifiedCanvas.canvasPosition')}: ${canvasCoordinatesString}`} - - - )} - - ); -}; - -export default memo(IAICanvasStatusText); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStatusText/IAICanvasStatusTextCursorPos.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStatusText/IAICanvasStatusTextCursorPos.tsx deleted file mode 100644 index a7e9ecb1575..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStatusText/IAICanvasStatusTextCursorPos.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Box } from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; -import { $cursorPosition } from 'features/canvas/store/canvasNanostore'; -import roundToHundreth from 'features/canvas/util/roundToHundreth'; -import { memo, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -const IAICanvasStatusTextCursorPos = () => { - const { t } = useTranslation(); - const cursorPosition = useStore($cursorPosition); - const cursorCoordinatesString = useMemo(() => { - const x = cursorPosition?.x ?? -1; - const y = cursorPosition?.y ?? -1; - return `(${roundToHundreth(x)}, ${roundToHundreth(y)})`; - }, [cursorPosition?.x, cursorPosition?.y]); - - return {`${t('unifiedCanvas.cursorPosition')}: ${cursorCoordinatesString}`}; -}; - -export default memo(IAICanvasStatusTextCursorPos); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolPreview.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolPreview.tsx deleted file mode 100644 index be1a4c2d4cc..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolPreview.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { useStore } from '@nanostores/react'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - $cursorPosition, - $isMovingBoundingBox, - $isTransformingBoundingBox, - $tool, -} from 'features/canvas/store/canvasNanostore'; -import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -import { rgbaColorToString } from 'features/canvas/util/colorToString'; -import { COLOR_PICKER_SIZE, COLOR_PICKER_STROKE_RADIUS } from 'features/canvas/util/constants'; -import type { GroupConfig } from 'konva/lib/Group'; -import { memo, useMemo } from 'react'; -import { Circle, Group } from 'react-konva'; - -const canvasBrushPreviewSelector = createMemoizedSelector(selectCanvasSlice, (canvas) => { - const { stageDimensions, boundingBoxCoordinates, boundingBoxDimensions, shouldRestrictStrokesToBox } = canvas; - - const clip = shouldRestrictStrokesToBox - ? { - clipX: boundingBoxCoordinates.x, - clipY: boundingBoxCoordinates.y, - clipWidth: boundingBoxDimensions.width, - clipHeight: boundingBoxDimensions.height, - } - : {}; - - // // big brain time; this is the *inverse* of the clip that is needed for shouldRestrictStrokesToBox - // // it took some fiddling to work out, so I am leaving it here in case it is needed for something else... - // const clipFunc = shouldRestrictStrokesToBox - // ? (ctx: SceneContext) => { - // console.log( - // stageCoordinates.x / stageScale, - // stageCoordinates.y / stageScale, - // stageDimensions.height / stageScale, - // stageDimensions.width / stageScale - // ); - // ctx.fillStyle = 'red'; - // ctx.rect( - // -stageCoordinates.x / stageScale, - // -stageCoordinates.y / stageScale, - // stageDimensions.width / stageScale, - // stageCoordinates.y / stageScale + boundingBoxCoordinates.y - // ); - // ctx.rect( - // -stageCoordinates.x / stageScale, - // boundingBoxCoordinates.y + boundingBoxDimensions.height, - // stageDimensions.width / stageScale, - // stageDimensions.height / stageScale - // ); - // ctx.rect( - // -stageCoordinates.x / stageScale, - // -stageCoordinates.y / stageScale, - // stageCoordinates.x / stageScale + boundingBoxCoordinates.x, - // stageDimensions.height / stageScale - // ); - // ctx.rect( - // boundingBoxCoordinates.x + boundingBoxDimensions.width, - // -stageCoordinates.y / stageScale, - // stageDimensions.width / stageScale - - // (boundingBoxCoordinates.x + boundingBoxDimensions.width), - // stageDimensions.height / stageScale - // ); - // } - // : undefined; - - return { - clip, - stageDimensions, - }; -}); - -/** - * Draws a black circle around the canvas brush preview. - */ -const IAICanvasToolPreview = (props: GroupConfig) => { - const radius = useAppSelector((s) => s.canvas.brushSize / 2); - const maskColorString = useAppSelector((s) => rgbaColorToString({ ...s.canvas.maskColor, a: 0.5 })); - const tool = useStore($tool); - const layer = useAppSelector((s) => s.canvas.layer); - const dotRadius = useAppSelector((s) => 1.5 / s.canvas.stageScale); - const strokeWidth = useAppSelector((s) => 1.5 / s.canvas.stageScale); - const brushColorString = useAppSelector((s) => rgbaColorToString(s.canvas.brushColor)); - const colorPickerColorString = useAppSelector((s) => rgbaColorToString(s.canvas.colorPickerColor)); - const colorPickerInnerRadius = useAppSelector( - (s) => (COLOR_PICKER_SIZE - COLOR_PICKER_STROKE_RADIUS + 1) / s.canvas.stageScale - ); - const colorPickerOuterRadius = useAppSelector((s) => COLOR_PICKER_SIZE / s.canvas.stageScale); - const { clip, stageDimensions } = useAppSelector(canvasBrushPreviewSelector); - - const cursorPosition = useStore($cursorPosition); - const isMovingBoundingBox = useStore($isMovingBoundingBox); - const isTransformingBoundingBox = useStore($isTransformingBoundingBox); - - const brushX = useMemo( - () => (cursorPosition ? cursorPosition.x : stageDimensions.width / 2), - [cursorPosition, stageDimensions] - ); - const brushY = useMemo( - () => (cursorPosition ? cursorPosition.y : stageDimensions.height / 2), - [cursorPosition, stageDimensions] - ); - - const shouldDrawBrushPreview = useMemo( - () => !(isMovingBoundingBox || isTransformingBoundingBox || !cursorPosition), - [cursorPosition, isMovingBoundingBox, isTransformingBoundingBox] - ); - - if (!shouldDrawBrushPreview) { - return null; - } - - return ( - - {tool === 'colorPicker' ? ( - <> - - - - ) : ( - <> - - - - - )} - - - - ); -}; - -export default memo(IAICanvasToolPreview); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx deleted file mode 100644 index 4650506a429..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx +++ /dev/null @@ -1,331 +0,0 @@ -import { useShiftModifier } from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { roundDownToMultiple, roundDownToMultipleMin, roundToMultiple } from 'common/util/roundDownToMultiple'; -import { - $isDrawing, - $isMouseOverBoundingBox, - $isMouseOverBoundingBoxOutline, - $isMovingBoundingBox, - $isTransformingBoundingBox, - $tool, -} from 'features/canvas/store/canvasNanostore'; -import { - aspectRatioChanged, - setBoundingBoxCoordinates, - setBoundingBoxDimensions, - setShouldSnapToGrid, -} from 'features/canvas/store/canvasSlice'; -import { CANVAS_GRID_SIZE_COARSE, CANVAS_GRID_SIZE_FINE } from 'features/canvas/store/constants'; -import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; -import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; -import type Konva from 'konva'; -import type { GroupConfig } from 'konva/lib/Group'; -import type { KonvaEventObject } from 'konva/lib/Node'; -import type { Vector2d } from 'konva/lib/types'; -import { memo, useCallback, useEffect, useMemo, useRef } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { Group, Rect, Transformer } from 'react-konva'; - -const borderDash = [4, 4]; - -type IAICanvasBoundingBoxPreviewProps = GroupConfig; - -const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => { - const { ...rest } = props; - const dispatch = useAppDispatch(); - const boundingBoxCoordinates = useAppSelector((s) => s.canvas.boundingBoxCoordinates); - const boundingBoxDimensions = useAppSelector((s) => s.canvas.boundingBoxDimensions); - const stageScale = useAppSelector((s) => s.canvas.stageScale); - const shouldSnapToGrid = useAppSelector((s) => s.canvas.shouldSnapToGrid); - const hitStrokeWidth = useAppSelector((s) => 20 / s.canvas.stageScale); - const aspectRatio = useAppSelector((s) => s.canvas.aspectRatio); - const optimalDimension = useAppSelector(selectOptimalDimension); - const transformerRef = useRef(null); - const shapeRef = useRef(null); - const shift = useShiftModifier(); - const tool = useStore($tool); - const isDrawing = useStore($isDrawing); - const isMovingBoundingBox = useStore($isMovingBoundingBox); - const isTransformingBoundingBox = useStore($isTransformingBoundingBox); - const isMouseOverBoundingBoxOutline = useStore($isMouseOverBoundingBoxOutline); - - useEffect(() => { - if (!transformerRef.current || !shapeRef.current) { - return; - } - transformerRef.current.nodes([shapeRef.current]); - transformerRef.current.getLayer()?.batchDraw(); - }, []); - - const gridSize = useMemo(() => (shift ? CANVAS_GRID_SIZE_FINE : CANVAS_GRID_SIZE_COARSE), [shift]); - const scaledStep = useMemo(() => gridSize * stageScale, [gridSize, stageScale]); - - useHotkeys( - 'N', - () => { - dispatch(setShouldSnapToGrid(!shouldSnapToGrid)); - }, - [shouldSnapToGrid] - ); - - const handleOnDragMove = useCallback( - (e: KonvaEventObject) => { - if (!shouldSnapToGrid) { - dispatch( - setBoundingBoxCoordinates({ - x: Math.floor(e.target.x()), - y: Math.floor(e.target.y()), - }) - ); - return; - } - - const dragX = e.target.x(); - const dragY = e.target.y(); - - const newX = roundToMultiple(dragX, gridSize); - const newY = roundToMultiple(dragY, gridSize); - - e.target.x(newX); - e.target.y(newY); - - dispatch( - setBoundingBoxCoordinates({ - x: newX, - y: newY, - }) - ); - }, - [dispatch, gridSize, shouldSnapToGrid] - ); - - const handleOnTransform = useCallback( - (_e: KonvaEventObject) => { - /** - * The Konva Transformer changes the object's anchor point and scale factor, - * not its width and height. We need to un-scale the width and height before - * setting the values. - */ - if (!shapeRef.current) { - return; - } - - const rect = shapeRef.current; - - const scaleX = rect.scaleX(); - const scaleY = rect.scaleY(); - - // undo the scaling - const width = Math.round(rect.width() * scaleX); - const height = Math.round(rect.height() * scaleY); - - const x = Math.round(rect.x()); - const y = Math.round(rect.y()); - - if (aspectRatio.isLocked) { - const newDimensions = calculateNewSize(aspectRatio.value, width * height); - dispatch( - setBoundingBoxDimensions( - { - width: roundDownToMultipleMin(newDimensions.width, gridSize), - height: roundDownToMultipleMin(newDimensions.height, gridSize), - }, - optimalDimension - ) - ); - } else { - dispatch( - setBoundingBoxDimensions( - { - width: roundDownToMultipleMin(width, gridSize), - height: roundDownToMultipleMin(height, gridSize), - }, - optimalDimension - ) - ); - dispatch( - aspectRatioChanged({ - isLocked: false, - id: 'Free', - value: width / height, - }) - ); - } - - dispatch( - setBoundingBoxCoordinates({ - x: shouldSnapToGrid ? roundDownToMultiple(x, gridSize) : x, - y: shouldSnapToGrid ? roundDownToMultiple(y, gridSize) : y, - }) - ); - - // Reset the scale now that the coords/dimensions have been un-scaled - rect.scaleX(1); - rect.scaleY(1); - }, - [aspectRatio.isLocked, aspectRatio.value, dispatch, shouldSnapToGrid, gridSize, optimalDimension] - ); - - const anchorDragBoundFunc = useCallback( - ( - oldPos: Vector2d, // old absolute position of anchor point - newPos: Vector2d, // new absolute position (potentially) of anchor point - _e: MouseEvent - ) => { - /** - * Konva does not transform with width or height. It transforms the anchor point - * and scale factor. This is then sent to the shape's onTransform listeners. - * - * We need to snap the new dimensions to steps of 8 (or 64). But because the whole - * stage is scaled, our actual desired step is actually 8 (or 64) * the stage scale. - * - * Additionally, we need to ensure we offset the position so that we snap to a - * multiple of 8 (or 64) that is aligned with the grid, and not from the absolute zero - * coordinate. - */ - - // Calculate the offset of the grid. - const offsetX = oldPos.x % scaledStep; - const offsetY = oldPos.y % scaledStep; - - const newCoordinates = { - x: roundDownToMultiple(newPos.x, scaledStep) + offsetX, - y: roundDownToMultiple(newPos.y, scaledStep) + offsetY, - }; - - return newCoordinates; - }, - [scaledStep] - ); - - const handleStartedTransforming = useCallback(() => { - $isTransformingBoundingBox.set(true); - }, []); - - const handleEndedTransforming = useCallback(() => { - $isTransformingBoundingBox.set(false); - $isMovingBoundingBox.set(false); - $isMouseOverBoundingBox.set(false); - $isMouseOverBoundingBoxOutline.set(false); - }, []); - - const handleStartedMoving = useCallback(() => { - $isMovingBoundingBox.set(true); - }, []); - - const handleEndedModifying = useCallback(() => { - $isTransformingBoundingBox.set(false); - $isMovingBoundingBox.set(false); - $isMouseOverBoundingBox.set(false); - $isMouseOverBoundingBoxOutline.set(false); - }, []); - - const handleMouseOver = useCallback(() => { - $isMouseOverBoundingBoxOutline.set(true); - }, []); - - const handleMouseOut = useCallback(() => { - !isTransformingBoundingBox && !isMovingBoundingBox && $isMouseOverBoundingBoxOutline.set(false); - }, [isMovingBoundingBox, isTransformingBoundingBox]); - - const handleMouseEnterBoundingBox = useCallback(() => { - $isMouseOverBoundingBox.set(true); - }, []); - - const handleMouseLeaveBoundingBox = useCallback(() => { - $isMouseOverBoundingBox.set(false); - }, []); - - const stroke = useMemo(() => { - if (isMouseOverBoundingBoxOutline || isMovingBoundingBox || isTransformingBoundingBox) { - return 'rgba(255,255,255,0.5)'; - } - return 'white'; - }, [isMouseOverBoundingBoxOutline, isMovingBoundingBox, isTransformingBoundingBox]); - - const strokeWidth = useMemo(() => { - if (isMouseOverBoundingBoxOutline || isMovingBoundingBox || isTransformingBoundingBox) { - return 6 / stageScale; - } - return 1 / stageScale; - }, [isMouseOverBoundingBoxOutline, isMovingBoundingBox, isTransformingBoundingBox, stageScale]); - - const enabledAnchors = useMemo(() => { - if (tool !== 'move') { - return emptyArray; - } - if (aspectRatio.isLocked) { - // TODO: The math to resize the bbox when locked and using other handles is confusing. - // Workaround for now is to only allow resizing from the bottom-right handle. - return ['bottom-right']; - } - return undefined; - }, [aspectRatio.isLocked, tool]); - - return ( - - - - - - ); -}; - -export default memo(IAICanvasBoundingBox); - -const emptyArray: string[] = []; diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskOptions.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskOptions.tsx deleted file mode 100644 index 6dacb7c59df..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskOptions.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import type { FormLabelProps } from '@invoke-ai/ui-library'; -import { - Box, - Button, - ButtonGroup, - Checkbox, - Flex, - FormControl, - FormControlGroup, - FormLabel, - IconButton, - Popover, - PopoverBody, - PopoverContent, - PopoverTrigger, -} from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIColorPicker from 'common/components/IAIColorPicker'; -import { canvasMaskSavedToGallery } from 'features/canvas/store/actions'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; -import { - clearMask, - setIsMaskEnabled, - setLayer, - setMaskColor, - setShouldPreserveMaskedArea, -} from 'features/canvas/store/canvasSlice'; -import type { ChangeEvent } from 'react'; -import { memo, useCallback } from 'react'; -import type { RgbaColor } from 'react-colorful'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { PiExcludeBold, PiFloppyDiskBackFill, PiTrashSimpleFill } from 'react-icons/pi'; - -const formLabelProps: FormLabelProps = { - flexGrow: 1, -}; - -const IAICanvasMaskOptions = () => { - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - const layer = useAppSelector((s) => s.canvas.layer); - const maskColor = useAppSelector((s) => s.canvas.maskColor); - const isMaskEnabled = useAppSelector((s) => s.canvas.isMaskEnabled); - const shouldPreserveMaskedArea = useAppSelector((s) => s.canvas.shouldPreserveMaskedArea); - const isStaging = useAppSelector(isStagingSelector); - - useHotkeys( - ['q'], - () => { - handleToggleMaskLayer(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [layer] - ); - - useHotkeys( - ['shift+c'], - () => { - handleClearMask(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [] - ); - - useHotkeys( - ['h'], - () => { - handleToggleEnableMask(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [isMaskEnabled] - ); - - const handleToggleMaskLayer = useCallback(() => { - dispatch(setLayer(layer === 'mask' ? 'base' : 'mask')); - }, [dispatch, layer]); - - const handleClearMask = useCallback(() => { - dispatch(clearMask()); - }, [dispatch]); - - const handleToggleEnableMask = useCallback(() => { - dispatch(setIsMaskEnabled(!isMaskEnabled)); - }, [dispatch, isMaskEnabled]); - - const handleSaveMask = useCallback(async () => { - dispatch(canvasMaskSavedToGallery()); - }, [dispatch]); - - const handleChangePreserveMaskedArea = useCallback( - (e: ChangeEvent) => { - dispatch(setShouldPreserveMaskedArea(e.target.checked)); - }, - [dispatch] - ); - - const handleChangeMaskColor = useCallback( - (newColor: RgbaColor) => { - dispatch(setMaskColor(newColor)); - }, - [dispatch] - ); - - return ( - - - } - isChecked={layer === 'mask'} - isDisabled={isStaging} - /> - - - - - - - {`${t('unifiedCanvas.enableMask')} (H)`} - - - - {t('unifiedCanvas.preserveMaskedArea')} - - - - - - - - - - - - - - - ); -}; - -export default memo(IAICanvasMaskOptions); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasRedoButton.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasRedoButton.tsx deleted file mode 100644 index d156944a60b..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasRedoButton.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { redo } from 'features/canvas/store/canvasSlice'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { memo, useCallback } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { PiArrowClockwiseBold } from 'react-icons/pi'; - -const IAICanvasRedoButton = () => { - const dispatch = useAppDispatch(); - const canRedo = useAppSelector((s) => s.canvas.futureLayerStates.length > 0); - const activeTabName = useAppSelector(activeTabNameSelector); - - const { t } = useTranslation(); - - const handleRedo = useCallback(() => { - dispatch(redo()); - }, [dispatch]); - - useHotkeys( - ['meta+shift+z', 'ctrl+shift+z', 'control+y', 'meta+y'], - () => { - handleRedo(); - }, - { - enabled: () => canRedo, - preventDefault: true, - }, - [activeTabName, canRedo] - ); - - return ( - } - onClick={handleRedo} - isDisabled={!canRedo} - /> - ); -}; - -export default memo(IAICanvasRedoButton); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx deleted file mode 100644 index 83ee900a43d..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import type { FormLabelProps } from '@invoke-ai/ui-library'; -import { - Checkbox, - Flex, - FormControl, - FormControlGroup, - FormLabel, - IconButton, - Popover, - PopoverBody, - PopoverContent, - PopoverTrigger, -} from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import ClearCanvasHistoryButtonModal from 'features/canvas/components/ClearCanvasHistoryButtonModal'; -import { - setShouldAntialias, - setShouldAutoSave, - setShouldCropToBoundingBoxOnSave, - setShouldDarkenOutsideBoundingBox, - setShouldFitImageSize, - setShouldInvertBrushSizeScrollDirection, - setShouldRestrictStrokesToBox, - setShouldShowCanvasDebugInfo, - setShouldShowGrid, - setShouldShowIntermediates, - setShouldSnapToGrid, -} from 'features/canvas/store/canvasSlice'; -import type { ChangeEvent } from 'react'; -import { memo, useCallback } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { PiGearSixBold } from 'react-icons/pi'; - -const formLabelProps: FormLabelProps = { - flexGrow: 1, -}; - -const IAICanvasSettingsButtonPopover = () => { - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - const shouldAutoSave = useAppSelector((s) => s.canvas.shouldAutoSave); - const shouldCropToBoundingBoxOnSave = useAppSelector((s) => s.canvas.shouldCropToBoundingBoxOnSave); - const shouldDarkenOutsideBoundingBox = useAppSelector((s) => s.canvas.shouldDarkenOutsideBoundingBox); - const shouldInvertBrushSizeScrollDirection = useAppSelector((s) => s.canvas.shouldInvertBrushSizeScrollDirection); - const shouldShowCanvasDebugInfo = useAppSelector((s) => s.canvas.shouldShowCanvasDebugInfo); - const shouldShowGrid = useAppSelector((s) => s.canvas.shouldShowGrid); - const shouldShowIntermediates = useAppSelector((s) => s.canvas.shouldShowIntermediates); - const shouldSnapToGrid = useAppSelector((s) => s.canvas.shouldSnapToGrid); - const shouldRestrictStrokesToBox = useAppSelector((s) => s.canvas.shouldRestrictStrokesToBox); - const shouldAntialias = useAppSelector((s) => s.canvas.shouldAntialias); - const shouldFitImageSize = useAppSelector((s) => s.canvas.shouldFitImageSize); - - useHotkeys( - ['n'], - () => { - dispatch(setShouldSnapToGrid(!shouldSnapToGrid)); - }, - { - enabled: true, - preventDefault: true, - }, - [shouldSnapToGrid] - ); - - const handleChangeShouldSnapToGrid = useCallback( - (e: ChangeEvent) => dispatch(setShouldSnapToGrid(e.target.checked)), - [dispatch] - ); - - const handleChangeShouldShowIntermediates = useCallback( - (e: ChangeEvent) => dispatch(setShouldShowIntermediates(e.target.checked)), - [dispatch] - ); - const handleChangeShouldShowGrid = useCallback( - (e: ChangeEvent) => dispatch(setShouldShowGrid(e.target.checked)), - [dispatch] - ); - const handleChangeShouldDarkenOutsideBoundingBox = useCallback( - (e: ChangeEvent) => dispatch(setShouldDarkenOutsideBoundingBox(e.target.checked)), - [dispatch] - ); - const handleChangeShouldInvertBrushSizeScrollDirection = useCallback( - (e: ChangeEvent) => dispatch(setShouldInvertBrushSizeScrollDirection(e.target.checked)), - [dispatch] - ); - const handleChangeShouldAutoSave = useCallback( - (e: ChangeEvent) => dispatch(setShouldAutoSave(e.target.checked)), - [dispatch] - ); - const handleChangeShouldCropToBoundingBoxOnSave = useCallback( - (e: ChangeEvent) => dispatch(setShouldCropToBoundingBoxOnSave(e.target.checked)), - [dispatch] - ); - const handleChangeShouldRestrictStrokesToBox = useCallback( - (e: ChangeEvent) => dispatch(setShouldRestrictStrokesToBox(e.target.checked)), - [dispatch] - ); - const handleChangeShouldShowCanvasDebugInfo = useCallback( - (e: ChangeEvent) => dispatch(setShouldShowCanvasDebugInfo(e.target.checked)), - [dispatch] - ); - const handleChangeShouldAntialias = useCallback( - (e: ChangeEvent) => dispatch(setShouldAntialias(e.target.checked)), - [dispatch] - ); - const handleChangeShouldFitImageSize = useCallback( - (e: ChangeEvent) => dispatch(setShouldFitImageSize(e.target.checked)), - [dispatch] - ); - - return ( - - - } - /> - - - - - - - {t('unifiedCanvas.showIntermediates')} - - - - {t('unifiedCanvas.showGrid')} - - - - {t('unifiedCanvas.snapToGrid')} - - - - {t('unifiedCanvas.darkenOutsideSelection')} - - - - {t('unifiedCanvas.autoSaveToGallery')} - - - - {t('unifiedCanvas.saveBoxRegionOnly')} - - - - {t('unifiedCanvas.limitStrokesToBox')} - - - - {t('unifiedCanvas.invertBrushSizeScrollDirection')} - - - - {t('unifiedCanvas.showCanvasDebugInfo')} - - - - {t('unifiedCanvas.antialiasing')} - - - - {t('unifiedCanvas.initialFitImageSize')} - - - - - - - - - ); -}; - -export default memo(IAICanvasSettingsButtonPopover); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolChooserOptions.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolChooserOptions.tsx deleted file mode 100644 index 697434739ea..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolChooserOptions.tsx +++ /dev/null @@ -1,280 +0,0 @@ -import { - Box, - ButtonGroup, - CompositeNumberInput, - CompositeSlider, - Flex, - FormControl, - FormLabel, - IconButton, - Popover, - PopoverBody, - PopoverContent, - PopoverTrigger, -} from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIColorPicker from 'common/components/IAIColorPicker'; -import { $tool, resetToolInteractionState } from 'features/canvas/store/canvasNanostore'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; -import { addEraseRect, addFillRect, setBrushColor, setBrushSize } from 'features/canvas/store/canvasSlice'; -import { clamp } from 'lodash-es'; -import { memo, useCallback } from 'react'; -import type { RgbaColor } from 'react-colorful'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { - PiEraserBold, - PiEyedropperBold, - PiPaintBrushBold, - PiPaintBucketBold, - PiSlidersHorizontalBold, - PiXBold, -} from 'react-icons/pi'; - -const marks = [1, 25, 50, 75, 100]; - -const IAICanvasToolChooserOptions = () => { - const dispatch = useAppDispatch(); - const tool = useStore($tool); - const brushColor = useAppSelector((s) => s.canvas.brushColor); - const brushSize = useAppSelector((s) => s.canvas.brushSize); - const isStaging = useAppSelector(isStagingSelector); - const { t } = useTranslation(); - - useHotkeys( - ['b'], - () => { - handleSelectBrushTool(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [] - ); - - useHotkeys( - ['e'], - () => { - handleSelectEraserTool(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [tool] - ); - - useHotkeys( - ['c'], - () => { - handleSelectColorPickerTool(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [tool] - ); - - useHotkeys( - ['shift+f'], - () => { - handleFillRect(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - } - ); - - useHotkeys( - ['delete', 'backspace'], - () => { - handleEraseBoundingBox(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - } - ); - - useHotkeys( - ['BracketLeft'], - () => { - if (brushSize - 5 <= 5) { - dispatch(setBrushSize(Math.max(brushSize - 1, 1))); - } else { - dispatch(setBrushSize(Math.max(brushSize - 5, 1))); - } - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [brushSize] - ); - - useHotkeys( - ['BracketRight'], - () => { - dispatch(setBrushSize(Math.min(brushSize + 5, 500))); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [brushSize] - ); - - useHotkeys( - ['Shift+BracketLeft'], - () => { - dispatch( - setBrushColor({ - ...brushColor, - a: clamp(brushColor.a - 0.05, 0.05, 1), - }) - ); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [brushColor] - ); - - useHotkeys( - ['Shift+BracketRight'], - () => { - dispatch( - setBrushColor({ - ...brushColor, - a: clamp(brushColor.a + 0.05, 0.05, 1), - }) - ); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [brushColor] - ); - - const handleSelectBrushTool = useCallback(() => { - $tool.set('brush'); - resetToolInteractionState(); - }, []); - const handleSelectEraserTool = useCallback(() => { - $tool.set('eraser'); - resetToolInteractionState(); - }, []); - const handleSelectColorPickerTool = useCallback(() => { - $tool.set('colorPicker'); - resetToolInteractionState(); - }, []); - const handleFillRect = useCallback(() => { - dispatch(addFillRect()); - }, [dispatch]); - const handleEraseBoundingBox = useCallback(() => { - dispatch(addEraseRect()); - }, [dispatch]); - const handleChangeBrushSize = useCallback( - (newSize: number) => { - dispatch(setBrushSize(newSize)); - }, - [dispatch] - ); - const handleChangeBrushColor = useCallback( - (newColor: RgbaColor) => { - dispatch(setBrushColor(newColor)); - }, - [dispatch] - ); - - return ( - - } - isChecked={tool === 'brush' && !isStaging} - onClick={handleSelectBrushTool} - isDisabled={isStaging} - /> - } - isChecked={tool === 'eraser' && !isStaging} - isDisabled={isStaging} - onClick={handleSelectEraserTool} - /> - } - isDisabled={isStaging} - onClick={handleFillRect} - /> - } - isDisabled={isStaging} - onClick={handleEraseBoundingBox} - /> - } - isChecked={tool === 'colorPicker' && !isStaging} - isDisabled={isStaging} - onClick={handleSelectColorPickerTool} - /> - - - } - /> - - - - - - - {t('unifiedCanvas.brushSize')} - - - - - - - - - - - - - ); -}; - -export default memo(IAICanvasToolChooserOptions); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx deleted file mode 100644 index 5ed5ffe5730..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx +++ /dev/null @@ -1,323 +0,0 @@ -import type { ComboboxOnChange } from '@invoke-ai/ui-library'; -import { ButtonGroup, Combobox, Flex, FormControl, IconButton, Tooltip } from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard'; -import { useImageUploadButton } from 'common/hooks/useImageUploadButton'; -import { useSingleAndDoubleClick } from 'common/hooks/useSingleAndDoubleClick'; -import { - canvasCopiedToClipboard, - canvasDownloadedAsImage, - canvasMerged, - canvasSavedToGallery, -} from 'features/canvas/store/actions'; -import { $canvasBaseLayer, $tool } from 'features/canvas/store/canvasNanostore'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; -import { - resetCanvas, - resetCanvasView, - setIsMaskEnabled, - setLayer, - setShouldShowBoundingBox, -} from 'features/canvas/store/canvasSlice'; -import type { CanvasLayer } from 'features/canvas/store/canvasTypes'; -import { memo, useCallback, useMemo } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { - PiCopyBold, - PiCrosshairSimpleBold, - PiDownloadSimpleBold, - PiEyeBold, - PiEyeSlashBold, - PiFloppyDiskBold, - PiHandGrabbingBold, - PiStackBold, - PiTrashSimpleBold, - PiUploadSimpleBold, -} from 'react-icons/pi'; - -import IAICanvasMaskOptions from './IAICanvasMaskOptions'; -import IAICanvasRedoButton from './IAICanvasRedoButton'; -import IAICanvasSettingsButtonPopover from './IAICanvasSettingsButtonPopover'; -import IAICanvasToolChooserOptions from './IAICanvasToolChooserOptions'; -import IAICanvasUndoButton from './IAICanvasUndoButton'; - -const IAICanvasToolbar = () => { - const dispatch = useAppDispatch(); - const isMaskEnabled = useAppSelector((s) => s.canvas.isMaskEnabled); - const layer = useAppSelector((s) => s.canvas.layer); - const tool = useStore($tool); - const isStaging = useAppSelector(isStagingSelector); - const { t } = useTranslation(); - const { isClipboardAPIAvailable } = useCopyImageToClipboard(); - const shouldShowBoundingBox = useAppSelector((s) => s.canvas.shouldShowBoundingBox); - - const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({ - postUploadAction: { type: 'SET_CANVAS_INITIAL_IMAGE' }, - }); - - useHotkeys( - ['v'], - () => { - handleSelectMoveTool(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [] - ); - - useHotkeys( - 'shift+h', - () => { - dispatch(setShouldShowBoundingBox(!shouldShowBoundingBox)); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [shouldShowBoundingBox] - ); - - useHotkeys( - ['r'], - () => { - handleResetCanvasView(); - }, - { - enabled: () => true, - preventDefault: true, - }, - [] - ); - - useHotkeys( - ['shift+m'], - () => { - handleMergeVisible(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [] - ); - - useHotkeys( - ['shift+s'], - () => { - !isStaging && handleSaveToGallery(); - }, - { - enabled: true, - preventDefault: true, - }, - [isStaging] - ); - - useHotkeys( - ['meta+c', 'ctrl+c'], - () => { - handleCopyImageToClipboard(); - }, - { - enabled: () => !isStaging && isClipboardAPIAvailable, - preventDefault: true, - }, - [isClipboardAPIAvailable] - ); - - useHotkeys( - ['shift+d'], - () => { - handleDownloadAsImage(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [] - ); - - const handleSelectMoveTool = useCallback(() => { - $tool.set('move'); - }, []); - - const handleSetShouldShowBoundingBox = useCallback(() => { - dispatch(setShouldShowBoundingBox(!shouldShowBoundingBox)); - }, [dispatch, shouldShowBoundingBox]); - - const handleResetCanvasView = useCallback( - (shouldScaleTo1 = false) => { - const canvasBaseLayer = $canvasBaseLayer.get(); - if (!canvasBaseLayer) { - return; - } - const clientRect = canvasBaseLayer.getClientRect({ - skipTransform: true, - }); - dispatch( - resetCanvasView({ - contentRect: clientRect, - shouldScaleTo1, - }) - ); - }, - [dispatch] - ); - const onSingleClick = useCallback(() => { - handleResetCanvasView(false); - }, [handleResetCanvasView]); - const onDoubleClick = useCallback(() => { - handleResetCanvasView(true); - }, [handleResetCanvasView]); - - const handleClickResetCanvasView = useSingleAndDoubleClick({ - onSingleClick, - onDoubleClick, - }); - - const handleResetCanvas = useCallback(() => { - dispatch(resetCanvas()); - }, [dispatch]); - - const handleMergeVisible = useCallback(() => { - dispatch(canvasMerged()); - }, [dispatch]); - - const handleSaveToGallery = useCallback(() => { - dispatch(canvasSavedToGallery()); - }, [dispatch]); - - const handleCopyImageToClipboard = useCallback(() => { - if (!isClipboardAPIAvailable) { - return; - } - dispatch(canvasCopiedToClipboard()); - }, [dispatch, isClipboardAPIAvailable]); - - const handleDownloadAsImage = useCallback(() => { - dispatch(canvasDownloadedAsImage()); - }, [dispatch]); - - const handleChangeLayer = useCallback( - (v) => { - if (!v) { - return; - } - dispatch(setLayer(v.value as CanvasLayer)); - if (v.value === 'mask' && !isMaskEnabled) { - dispatch(setIsMaskEnabled(true)); - } - }, - [dispatch, isMaskEnabled] - ); - - const layerOptions = useMemo<{ label: string; value: CanvasLayer }[]>( - () => [ - { label: t('unifiedCanvas.base'), value: 'base' }, - { label: t('unifiedCanvas.mask'), value: 'mask' }, - ], - [t] - ); - const layerValue = useMemo(() => layerOptions.filter((o) => o.value === layer)[0] ?? null, [layer, layerOptions]); - - return ( - - - - - - - - - - - - } - isChecked={tool === 'move' || isStaging} - onClick={handleSelectMoveTool} - /> - : } - onClick={handleSetShouldShowBoundingBox} - isDisabled={isStaging} - /> - } - onClick={handleClickResetCanvasView} - /> - - - - } - onClick={handleMergeVisible} - isDisabled={isStaging} - /> - } - onClick={handleSaveToGallery} - isDisabled={isStaging} - /> - {isClipboardAPIAvailable && ( - } - onClick={handleCopyImageToClipboard} - isDisabled={isStaging} - /> - )} - } - onClick={handleDownloadAsImage} - isDisabled={isStaging} - /> - - - - - - - - } - isDisabled={isStaging} - {...getUploadButtonProps()} - /> - - } - onClick={handleResetCanvas} - colorScheme="error" - isDisabled={isStaging} - /> - - - - - - ); -}; - -export default memo(IAICanvasToolbar); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasUndoButton.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasUndoButton.tsx deleted file mode 100644 index f1fcdf96e57..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasUndoButton.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { undo } from 'features/canvas/store/canvasSlice'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { memo, useCallback } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { PiArrowCounterClockwiseBold } from 'react-icons/pi'; - -const IAICanvasUndoButton = () => { - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - const activeTabName = useAppSelector(activeTabNameSelector); - const canUndo = useAppSelector((s) => s.canvas.pastLayerStates.length > 0); - - const handleUndo = useCallback(() => { - dispatch(undo()); - }, [dispatch]); - - useHotkeys( - ['meta+z', 'ctrl+z'], - () => { - handleUndo(); - }, - { - enabled: () => canUndo, - preventDefault: true, - }, - [activeTabName, canUndo] - ); - - return ( - } - onClick={handleUndo} - isDisabled={!canUndo} - /> - ); -}; - -export default memo(IAICanvasUndoButton); diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasDragMove.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasDragMove.ts deleted file mode 100644 index d47b1c6ccdf..00000000000 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasDragMove.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { $isMovingBoundingBox, $isMovingStage, $tool } from 'features/canvas/store/canvasNanostore'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; -import { setStageCoordinates } from 'features/canvas/store/canvasSlice'; -import type { KonvaEventObject } from 'konva/lib/Node'; -import { useCallback } from 'react'; - -const useCanvasDrag = () => { - const dispatch = useAppDispatch(); - const isStaging = useAppSelector(isStagingSelector); - const handleDragStart = useCallback(() => { - if (!(($tool.get() === 'move' || isStaging) && !$isMovingBoundingBox.get())) { - return; - } - $isMovingStage.set(true); - }, [isStaging]); - - const handleDragMove = useCallback( - (e: KonvaEventObject) => { - const tool = $tool.get(); - if (!((tool === 'move' || isStaging) && !$isMovingBoundingBox.get())) { - return; - } - - const newCoordinates = { x: e.target.x(), y: e.target.y() }; - - dispatch(setStageCoordinates(newCoordinates)); - }, - [dispatch, isStaging] - ); - - const handleDragEnd = useCallback(() => { - if (!(($tool.get() === 'move' || isStaging) && !$isMovingBoundingBox.get())) { - return; - } - $isMovingStage.set(false); - }, [isStaging]); - - return { - handleDragStart, - handleDragMove, - handleDragEnd, - }; -}; - -export default useCanvasDrag; diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasGenerationMode.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasGenerationMode.ts deleted file mode 100644 index 45852cd1bc1..00000000000 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasGenerationMode.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { useAppSelector } from 'app/store/storeHooks'; -import type { GenerationMode } from 'features/canvas/store/canvasTypes'; -import { getCanvasData } from 'features/canvas/util/getCanvasData'; -import { getCanvasGenerationMode } from 'features/canvas/util/getCanvasGenerationMode'; -import { useEffect, useState } from 'react'; -import { useDebounce } from 'react-use'; - -export const useCanvasGenerationMode = () => { - const layerState = useAppSelector((s) => s.canvas.layerState); - - const boundingBoxCoordinates = useAppSelector((s) => s.canvas.boundingBoxCoordinates); - const boundingBoxDimensions = useAppSelector((s) => s.canvas.boundingBoxDimensions); - const isMaskEnabled = useAppSelector((s) => s.canvas.isMaskEnabled); - - const shouldPreserveMaskedArea = useAppSelector((s) => s.canvas.shouldPreserveMaskedArea); - const [generationMode, setGenerationMode] = useState(); - - useEffect(() => { - setGenerationMode(undefined); - }, [layerState, boundingBoxCoordinates, boundingBoxDimensions, isMaskEnabled, shouldPreserveMaskedArea]); - - useDebounce( - async () => { - // Build canvas blobs - const canvasBlobsAndImageData = await getCanvasData( - layerState, - boundingBoxCoordinates, - boundingBoxDimensions, - isMaskEnabled, - shouldPreserveMaskedArea - ); - - if (!canvasBlobsAndImageData) { - return; - } - - const { baseImageData, maskImageData } = canvasBlobsAndImageData; - - // Determine the generation mode - const generationMode = getCanvasGenerationMode(baseImageData, maskImageData); - - setGenerationMode(generationMode); - }, - 1000, - [layerState, boundingBoxCoordinates, boundingBoxDimensions, isMaskEnabled, shouldPreserveMaskedArea] - ); - - return generationMode; -}; diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasHotkeys.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasHotkeys.ts deleted file mode 100644 index ec833c5f3d5..00000000000 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasHotkeys.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { - $canvasStage, - $tool, - $toolStash, - resetCanvasInteractionState, - resetToolInteractionState, -} from 'features/canvas/store/canvasNanostore'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; -import { clearMask, setIsMaskEnabled, setShouldSnapToGrid } from 'features/canvas/store/canvasSlice'; -import { isInteractiveTarget } from 'features/canvas/util/isInteractiveTarget'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { useCallback, useEffect } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; - -const useInpaintingCanvasHotkeys = () => { - const dispatch = useAppDispatch(); - const activeTabName = useAppSelector(activeTabNameSelector); - const isStaging = useAppSelector(isStagingSelector); - const isMaskEnabled = useAppSelector((s) => s.canvas.isMaskEnabled); - const shouldSnapToGrid = useAppSelector((s) => s.canvas.shouldSnapToGrid); - - // Beta Keys - const handleClearMask = useCallback(() => dispatch(clearMask()), [dispatch]); - - useHotkeys( - ['shift+c'], - () => { - handleClearMask(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [] - ); - - const handleToggleEnableMask = () => dispatch(setIsMaskEnabled(!isMaskEnabled)); - - useHotkeys( - ['h'], - () => { - handleToggleEnableMask(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [isMaskEnabled] - ); - - useHotkeys( - ['n'], - () => { - dispatch(setShouldSnapToGrid(!shouldSnapToGrid)); - }, - { - enabled: true, - preventDefault: true, - }, - [shouldSnapToGrid] - ); - // - - useHotkeys( - 'esc', - () => { - resetCanvasInteractionState(); - }, - { - enabled: () => true, - preventDefault: true, - } - ); - - const onKeyDown = useCallback( - (e: KeyboardEvent) => { - if (e.repeat || e.key !== ' ' || isInteractiveTarget(e.target) || activeTabName !== 'canvas') { - return; - } - if ($toolStash.get() || $tool.get() === 'move') { - return; - } - $canvasStage.get()?.container().focus(); - $toolStash.set($tool.get()); - $tool.set('move'); - resetToolInteractionState(); - }, - [activeTabName] - ); - const onKeyUp = useCallback( - (e: KeyboardEvent) => { - if (e.repeat || e.key !== ' ' || isInteractiveTarget(e.target) || activeTabName !== 'canvas') { - return; - } - if (!$toolStash.get() || $tool.get() !== 'move') { - return; - } - $canvasStage.get()?.container().focus(); - $tool.set($toolStash.get() ?? 'move'); - $toolStash.set(null); - }, - [activeTabName] - ); - - useEffect(() => { - window.addEventListener('keydown', onKeyDown); - window.addEventListener('keyup', onKeyUp); - - return () => { - window.removeEventListener('keydown', onKeyDown); - window.removeEventListener('keyup', onKeyUp); - }; - }, [onKeyDown, onKeyUp]); -}; - -export default useInpaintingCanvasHotkeys; diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseDown.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseDown.ts deleted file mode 100644 index b392b72e3fb..00000000000 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseDown.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { $isDrawing, $isMovingStage, $tool } from 'features/canvas/store/canvasNanostore'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; -import { addLine } from 'features/canvas/store/canvasSlice'; -import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition'; -import type Konva from 'konva'; -import type { KonvaEventObject } from 'konva/lib/Node'; -import type { MutableRefObject } from 'react'; -import { useCallback } from 'react'; - -import useColorPicker from './useColorUnderCursor'; - -const useCanvasMouseDown = (stageRef: MutableRefObject) => { - const dispatch = useAppDispatch(); - const isStaging = useAppSelector(isStagingSelector); - const { commitColorUnderCursor } = useColorPicker(); - - return useCallback( - (e: KonvaEventObject) => { - if (!stageRef.current) { - return; - } - - stageRef.current.container().focus(); - const tool = $tool.get(); - - if (tool === 'move' || isStaging) { - $isMovingStage.set(true); - return; - } - - if (tool === 'colorPicker') { - commitColorUnderCursor(); - return; - } - - const scaledCursorPosition = getScaledCursorPosition(stageRef.current); - - if (!scaledCursorPosition) { - return; - } - - e.evt.preventDefault(); - - $isDrawing.set(true); - - // Add a new line starting from the current cursor position. - dispatch( - addLine({ - points: [scaledCursorPosition.x, scaledCursorPosition.y], - tool, - }) - ); - }, - [stageRef, isStaging, dispatch, commitColorUnderCursor] - ); -}; - -export default useCanvasMouseDown; diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseMove.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseMove.ts deleted file mode 100644 index 6d0a97031b9..00000000000 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseMove.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { $cursorPosition, $isDrawing, $tool } from 'features/canvas/store/canvasNanostore'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; -import { addPointToCurrentLine } from 'features/canvas/store/canvasSlice'; -import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition'; -import type Konva from 'konva'; -import type { Vector2d } from 'konva/lib/types'; -import type { MutableRefObject } from 'react'; -import { useCallback } from 'react'; - -import useColorPicker from './useColorUnderCursor'; - -const useCanvasMouseMove = ( - stageRef: MutableRefObject, - didMouseMoveRef: MutableRefObject, - lastCursorPositionRef: MutableRefObject -) => { - const dispatch = useAppDispatch(); - const isStaging = useAppSelector(isStagingSelector); - const { updateColorUnderCursor } = useColorPicker(); - - return useCallback(() => { - if (!stageRef.current) { - return; - } - - const scaledCursorPosition = getScaledCursorPosition(stageRef.current); - - if (!scaledCursorPosition) { - return; - } - - $cursorPosition.set(scaledCursorPosition); - - lastCursorPositionRef.current = scaledCursorPosition; - const tool = $tool.get(); - - if (tool === 'colorPicker') { - updateColorUnderCursor(); - return; - } - - if (!$isDrawing.get() || tool === 'move' || isStaging) { - return; - } - - didMouseMoveRef.current = true; - dispatch(addPointToCurrentLine([scaledCursorPosition.x, scaledCursorPosition.y])); - }, [didMouseMoveRef, dispatch, isStaging, lastCursorPositionRef, stageRef, updateColorUnderCursor]); -}; - -export default useCanvasMouseMove; diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseOut.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseOut.ts deleted file mode 100644 index 0b7220eb0b4..00000000000 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseOut.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { setCanvasInteractionStateMouseOut } from 'features/canvas/store/canvasNanostore'; -import { useCallback } from 'react'; - -const useCanvasMouseOut = () => { - const onMouseOut = useCallback(() => { - setCanvasInteractionStateMouseOut(); - }, []); - - return onMouseOut; -}; - -export default useCanvasMouseOut; diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseUp.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseUp.ts deleted file mode 100644 index e3c291f1e14..00000000000 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseUp.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { useStore } from '@nanostores/react'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { $isDrawing, $isMovingStage, $tool } from 'features/canvas/store/canvasNanostore'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; -import { addPointToCurrentLine } from 'features/canvas/store/canvasSlice'; -import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition'; -import type Konva from 'konva'; -import type { MutableRefObject } from 'react'; -import { useCallback } from 'react'; - -const useCanvasMouseUp = ( - stageRef: MutableRefObject, - didMouseMoveRef: MutableRefObject -) => { - const dispatch = useAppDispatch(); - const isDrawing = useStore($isDrawing); - const isStaging = useAppSelector(isStagingSelector); - - return useCallback(() => { - if ($tool.get() === 'move' || isStaging) { - $isMovingStage.set(false); - return; - } - - if (!didMouseMoveRef.current && isDrawing && stageRef.current) { - const scaledCursorPosition = getScaledCursorPosition(stageRef.current); - - if (!scaledCursorPosition) { - return; - } - - /** - * Extend the current line. - * In this case, the mouse didn't move, so we append the same point to - * the line's existing points. This allows the line to render as a circle - * centered on that point. - */ - dispatch(addPointToCurrentLine([scaledCursorPosition.x, scaledCursorPosition.y])); - } else { - didMouseMoveRef.current = false; - } - $isDrawing.set(false); - }, [didMouseMoveRef, dispatch, isDrawing, isStaging, stageRef]); -}; - -export default useCanvasMouseUp; diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts deleted file mode 100644 index 1434bc9afc5..00000000000 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { $ctrl, $meta } from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { $isMoveStageKeyHeld } from 'features/canvas/store/canvasNanostore'; -import { setBrushSize, setStageCoordinates, setStageScale } from 'features/canvas/store/canvasSlice'; -import { CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/canvas/util/constants'; -import type Konva from 'konva'; -import type { KonvaEventObject } from 'konva/lib/Node'; -import { clamp } from 'lodash-es'; -import type { MutableRefObject } from 'react'; -import { useCallback } from 'react'; - -export const calculateNewBrushSize = (brushSize: number, delta: number) => { - // This equation was derived by fitting a curve to the desired brush sizes and deltas - // see https://github.com/invoke-ai/InvokeAI/pull/5542#issuecomment-1915847565 - const targetDelta = Math.sign(delta) * 0.7363 * Math.pow(1.0394, brushSize); - // This needs to be clamped to prevent the delta from getting too large - const finalDelta = clamp(targetDelta, -20, 20); - // The new brush size is also clamped to prevent it from getting too large or small - const newBrushSize = clamp(brushSize + finalDelta, 1, 500); - - return newBrushSize; -}; - -const useCanvasWheel = (stageRef: MutableRefObject) => { - const dispatch = useAppDispatch(); - const stageScale = useAppSelector((s) => s.canvas.stageScale); - const isMoveStageKeyHeld = useStore($isMoveStageKeyHeld); - const brushSize = useAppSelector((s) => s.canvas.brushSize); - const shouldInvertBrushSizeScrollDirection = useAppSelector((s) => s.canvas.shouldInvertBrushSizeScrollDirection); - - return useCallback( - (e: KonvaEventObject) => { - // stop default scrolling - if (!stageRef.current || isMoveStageKeyHeld) { - return; - } - - e.evt.preventDefault(); - - // checking for ctrl key is pressed or not, - // so that brush size can be controlled using ctrl + scroll up/down - - // Invert the delta if the property is set to true - let delta = e.evt.deltaY; - if (shouldInvertBrushSizeScrollDirection) { - delta = -delta; - } - - if ($ctrl.get() || $meta.get()) { - dispatch(setBrushSize(calculateNewBrushSize(brushSize, delta))); - } else { - const cursorPos = stageRef.current.getPointerPosition(); - let delta = e.evt.deltaY; - - if (!cursorPos) { - return; - } - - const mousePointTo = { - x: (cursorPos.x - stageRef.current.x()) / stageScale, - y: (cursorPos.y - stageRef.current.y()) / stageScale, - }; - // when we zoom on trackpad, e.evt.ctrlKey is true - // in that case lets revert direction - if (e.evt.ctrlKey) { - delta = -delta; - } - - const newScale = clamp(stageScale * CANVAS_SCALE_BY ** delta, MIN_CANVAS_SCALE, MAX_CANVAS_SCALE); - - const newCoordinates = { - x: cursorPos.x - mousePointTo.x * newScale, - y: cursorPos.y - mousePointTo.y * newScale, - }; - - dispatch(setStageScale(newScale)); - dispatch(setStageCoordinates(newCoordinates)); - } - }, - [stageRef, isMoveStageKeyHeld, brushSize, dispatch, stageScale, shouldInvertBrushSizeScrollDirection] - ); -}; - -export default useCanvasWheel; diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useColorUnderCursor.ts b/invokeai/frontend/web/src/features/canvas/hooks/useColorUnderCursor.ts deleted file mode 100644 index f07433a3deb..00000000000 --- a/invokeai/frontend/web/src/features/canvas/hooks/useColorUnderCursor.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { useAppDispatch } from 'app/store/storeHooks'; -import { $canvasBaseLayer, $canvasStage, $tool } from 'features/canvas/store/canvasNanostore'; -import { commitColorPickerColor, setColorPickerColor } from 'features/canvas/store/canvasSlice'; -import Konva from 'konva'; -import { useCallback } from 'react'; - -const useColorPicker = () => { - const dispatch = useAppDispatch(); - - const updateColorUnderCursor = useCallback(() => { - const stage = $canvasStage.get(); - const canvasBaseLayer = $canvasBaseLayer.get(); - if (!stage || !canvasBaseLayer) { - return; - } - - const position = stage.getPointerPosition(); - - if (!position) { - return; - } - - const pixelRatio = Konva.pixelRatio; - - const [r, g, b, a] = canvasBaseLayer - .getContext() - .getImageData(position.x * pixelRatio, position.y * pixelRatio, 1, 1).data; - - if (r === undefined || g === undefined || b === undefined || a === undefined) { - return; - } - - dispatch(setColorPickerColor({ r, g, b, a })); - }, [dispatch]); - - const commitColorUnderCursor = useCallback(() => { - dispatch(commitColorPickerColor()); - $tool.set('brush'); - }, [dispatch]); - - return { updateColorUnderCursor, commitColorUnderCursor }; -}; - -export default useColorPicker; diff --git a/invokeai/frontend/web/src/features/canvas/store/actions.ts b/invokeai/frontend/web/src/features/canvas/store/actions.ts deleted file mode 100644 index b6483b7f3aa..00000000000 --- a/invokeai/frontend/web/src/features/canvas/store/actions.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { createAction } from '@reduxjs/toolkit'; -import type { ImageDTO } from 'services/api/types'; - -export const canvasSavedToGallery = createAction('canvas/canvasSavedToGallery'); - -export const canvasMaskSavedToGallery = createAction('canvas/canvasMaskSavedToGallery'); - -export const canvasCopiedToClipboard = createAction('canvas/canvasCopiedToClipboard'); - -export const canvasDownloadedAsImage = createAction('canvas/canvasDownloadedAsImage'); - -export const canvasMerged = createAction('canvas/canvasMerged'); - -export const stagingAreaImageSaved = createAction<{ imageDTO: ImageDTO }>('canvas/stagingAreaImageSaved'); - -export const canvasMaskToControlAdapter = createAction<{ id: string }>('canvas/canvasMaskToControlAdapter'); - -export const canvasImageToControlAdapter = createAction<{ id: string }>('canvas/canvasImageToControlAdapter'); diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasNanostore.ts b/invokeai/frontend/web/src/features/canvas/store/canvasNanostore.ts deleted file mode 100644 index b225f666773..00000000000 --- a/invokeai/frontend/web/src/features/canvas/store/canvasNanostore.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { CanvasTool } from 'features/canvas/store/canvasTypes'; -import type Konva from 'konva'; -import type { Vector2d } from 'konva/lib/types'; -import { atom, computed } from 'nanostores'; - -export const $cursorPosition = atom(null); -export const $tool = atom('move'); -export const $toolStash = atom(null); -export const $isDrawing = atom(false); -export const $isMouseOverBoundingBox = atom(false); -const $isMoveBoundingBoxKeyHeld = atom(false); -export const $isMoveStageKeyHeld = atom(false); -export const $isMovingBoundingBox = atom(false); -export const $isMovingStage = atom(false); -export const $isTransformingBoundingBox = atom(false); -export const $isMouseOverBoundingBoxOutline = atom(false); -export const $isModifyingBoundingBox = computed( - [$isTransformingBoundingBox, $isMovingBoundingBox], - (isTransformingBoundingBox, isMovingBoundingBox) => isTransformingBoundingBox || isMovingBoundingBox -); - -export const resetCanvasInteractionState = () => { - $cursorPosition.set(null); - $isDrawing.set(false); - $isMoveBoundingBoxKeyHeld.set(false); - $isMoveStageKeyHeld.set(false); - $isMovingBoundingBox.set(false); - $isMovingStage.set(false); -}; - -export const resetToolInteractionState = () => { - $isTransformingBoundingBox.set(false); - $isMovingBoundingBox.set(false); - $isMovingStage.set(false); -}; - -export const setCanvasInteractionStateMouseOut = () => { - $cursorPosition.set(null); -}; -export const $canvasBaseLayer = atom(null); -export const $canvasStage = atom(null); diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasSelectors.ts b/invokeai/frontend/web/src/features/canvas/store/canvasSelectors.ts deleted file mode 100644 index 29dc4c9fb8d..00000000000 --- a/invokeai/frontend/web/src/features/canvas/store/canvasSelectors.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; - -import { selectCanvasSlice } from './canvasSlice'; - -export const isStagingSelector = createSelector( - selectCanvasSlice, - (canvas) => canvas.batchIds.length > 0 || canvas.layerState.stagingArea.images.length > 0 -); diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts deleted file mode 100644 index e06caea3574..00000000000 --- a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts +++ /dev/null @@ -1,728 +0,0 @@ -import type { PayloadAction } from '@reduxjs/toolkit'; -import { createSlice } from '@reduxjs/toolkit'; -import type { PersistConfig, RootState } from 'app/store/store'; -import { deepClone } from 'common/util/deepClone'; -import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple'; -import { modelChanged } from 'features/canvas/store/canvasSlice'; -import calculateCoordinates from 'features/canvas/util/calculateCoordinates'; -import calculateScale from 'features/canvas/util/calculateScale'; -import { STAGE_PADDING_PERCENTAGE } from 'features/canvas/util/constants'; -import floorCoordinates from 'features/canvas/util/floorCoordinates'; -import getScaledBoundingBoxDimensions from 'features/canvas/util/getScaledBoundingBoxDimensions'; -import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; -import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants'; -import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; -import type { PayloadActionWithOptimalDimension } from 'features/parameters/store/types'; -import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension'; -import type { IRect, Vector2d } from 'konva/lib/types'; -import { clamp } from 'lodash-es'; -import type { RgbaColor } from 'react-colorful'; -import { queueApi } from 'services/api/endpoints/queue'; -import type { ImageDTO } from 'services/api/types'; -import { socketQueueItemStatusChanged } from 'services/events/actions'; - -import type { - BoundingBoxScaleMethod, - CanvasBaseLine, - CanvasImage, - CanvasLayer, - CanvasLayerState, - CanvasMaskLine, - CanvasState, - CanvasTool, - Dimensions, -} from './canvasTypes'; -import { isCanvasAnyLine, isCanvasMaskLine } from './canvasTypes'; -import { CANVAS_GRID_SIZE_FINE } from './constants'; - -/** - * The maximum history length to keep in the past/future layer states. - */ -const MAX_HISTORY = 100; - -const initialLayerState: CanvasLayerState = { - objects: [], - stagingArea: { - images: [], - selectedImageIndex: -1, - }, -}; - -const initialCanvasState: CanvasState = { - _version: 1, - boundingBoxCoordinates: { x: 0, y: 0 }, - boundingBoxDimensions: { width: 512, height: 512 }, - boundingBoxScaleMethod: 'auto', - brushColor: { r: 90, g: 90, b: 255, a: 1 }, - brushSize: 50, - colorPickerColor: { r: 90, g: 90, b: 255, a: 1 }, - futureLayerStates: [], - isMaskEnabled: true, - layer: 'base', - layerState: initialLayerState, - maskColor: { r: 255, g: 90, b: 90, a: 1 }, - pastLayerStates: [], - scaledBoundingBoxDimensions: { width: 512, height: 512 }, - shouldAntialias: true, - shouldAutoSave: false, - shouldCropToBoundingBoxOnSave: false, - shouldDarkenOutsideBoundingBox: false, - shouldFitImageSize: true, - shouldInvertBrushSizeScrollDirection: false, - shouldLockBoundingBox: false, - shouldPreserveMaskedArea: false, - shouldRestrictStrokesToBox: true, - shouldShowBoundingBox: true, - shouldShowCanvasDebugInfo: false, - shouldShowGrid: true, - shouldShowIntermediates: true, - shouldShowStagingImage: true, - shouldShowStagingOutline: true, - shouldSnapToGrid: true, - stageCoordinates: { x: 0, y: 0 }, - stageDimensions: { width: 0, height: 0 }, - stageScale: 1, - batchIds: [], - aspectRatio: { - id: '1:1', - value: 1, - isLocked: false, - }, -}; - -const setBoundingBoxDimensionsReducer = ( - state: CanvasState, - payload: Partial, - optimalDimension: number -) => { - const boundingBoxDimensions = payload; - const newDimensions = { - ...state.boundingBoxDimensions, - ...boundingBoxDimensions, - }; - state.boundingBoxDimensions = newDimensions; - if (state.boundingBoxScaleMethod === 'auto') { - const scaledDimensions = getScaledBoundingBoxDimensions(newDimensions, optimalDimension); - state.scaledBoundingBoxDimensions = scaledDimensions; - } -}; - -export const canvasSlice = createSlice({ - name: 'canvas', - initialState: initialCanvasState, - reducers: { - setLayer: (state, action: PayloadAction) => { - state.layer = action.payload; - }, - setMaskColor: (state, action: PayloadAction) => { - state.maskColor = action.payload; - }, - setBrushColor: (state, action: PayloadAction) => { - state.brushColor = action.payload; - }, - setBrushSize: (state, action: PayloadAction) => { - state.brushSize = action.payload; - }, - clearMask: (state) => { - pushToPrevLayerStates(state); - state.layerState.objects = state.layerState.objects.filter((obj) => !isCanvasMaskLine(obj)); - state.futureLayerStates = []; - state.shouldPreserveMaskedArea = false; - }, - toggleShouldInvertMask: (state) => { - state.shouldPreserveMaskedArea = !state.shouldPreserveMaskedArea; - }, - toggleShouldShowMask: (state) => { - state.isMaskEnabled = !state.isMaskEnabled; - }, - setShouldPreserveMaskedArea: (state, action: PayloadAction) => { - state.shouldPreserveMaskedArea = action.payload; - }, - setIsMaskEnabled: (state, action: PayloadAction) => { - state.isMaskEnabled = action.payload; - state.layer = action.payload ? 'mask' : 'base'; - }, - setInitialCanvasImage: { - reducer: (state, action: PayloadActionWithOptimalDimension) => { - const { width, height, image_name } = action.payload; - const { optimalDimension } = action.meta; - const { stageDimensions, shouldFitImageSize } = state; - - const newBoundingBoxDimensions = shouldFitImageSize - ? { - width: roundDownToMultiple(width, CANVAS_GRID_SIZE_FINE), - height: roundDownToMultiple(height, CANVAS_GRID_SIZE_FINE), - } - : { - width: roundDownToMultiple(clamp(width, CANVAS_GRID_SIZE_FINE, optimalDimension), CANVAS_GRID_SIZE_FINE), - height: roundDownToMultiple( - clamp(height, CANVAS_GRID_SIZE_FINE, optimalDimension), - CANVAS_GRID_SIZE_FINE - ), - }; - - const newBoundingBoxCoordinates = { - x: roundToMultiple(width / 2 - newBoundingBoxDimensions.width / 2, CANVAS_GRID_SIZE_FINE), - y: roundToMultiple(height / 2 - newBoundingBoxDimensions.height / 2, CANVAS_GRID_SIZE_FINE), - }; - - if (state.boundingBoxScaleMethod === 'auto') { - const scaledDimensions = getScaledBoundingBoxDimensions(newBoundingBoxDimensions, optimalDimension); - state.scaledBoundingBoxDimensions = scaledDimensions; - } - - state.boundingBoxDimensions = newBoundingBoxDimensions; - state.boundingBoxCoordinates = newBoundingBoxCoordinates; - - pushToPrevLayerStates(state); - - state.layerState = { - ...deepClone(initialLayerState), - objects: [ - { - kind: 'image', - layer: 'base', - x: 0, - y: 0, - width, - height, - imageName: image_name, - }, - ], - }; - state.futureLayerStates = []; - - const newScale = calculateScale( - stageDimensions.width, - stageDimensions.height, - width, - height, - STAGE_PADDING_PERCENTAGE - ); - - const newCoordinates = calculateCoordinates( - stageDimensions.width, - stageDimensions.height, - 0, - 0, - width, - height, - newScale - ); - state.stageScale = newScale; - state.stageCoordinates = newCoordinates; - }, - prepare: (payload: ImageDTO, optimalDimension: number) => ({ - payload, - meta: { - optimalDimension, - }, - }), - }, - setBoundingBoxCoordinates: (state, action: PayloadAction) => { - state.boundingBoxCoordinates = floorCoordinates(action.payload); - }, - setStageCoordinates: (state, action: PayloadAction) => { - state.stageCoordinates = action.payload; - }, - setStageScale: (state, action: PayloadAction) => { - state.stageScale = action.payload; - }, - setShouldDarkenOutsideBoundingBox: (state, action: PayloadAction) => { - state.shouldDarkenOutsideBoundingBox = action.payload; - }, - setShouldInvertBrushSizeScrollDirection: (state, action: PayloadAction) => { - state.shouldInvertBrushSizeScrollDirection = action.payload; - }, - clearCanvasHistory: (state) => { - state.pastLayerStates = []; - state.futureLayerStates = []; - }, - setShouldLockBoundingBox: (state, action: PayloadAction) => { - state.shouldLockBoundingBox = action.payload; - }, - setShouldShowBoundingBox: (state, action: PayloadAction) => { - state.shouldShowBoundingBox = action.payload; - }, - canvasBatchIdAdded: (state, action: PayloadAction) => { - state.batchIds.push(action.payload); - }, - canvasBatchIdsReset: (state) => { - state.batchIds = []; - }, - stagingAreaInitialized: ( - state, - action: PayloadAction<{ - boundingBox: IRect; - }> - ) => { - const { boundingBox } = action.payload; - - state.layerState.stagingArea = { - boundingBox, - images: [], - selectedImageIndex: -1, - }; - }, - addImageToStagingArea: (state, action: PayloadAction) => { - const image = action.payload; - - if (!image || !state.layerState.stagingArea.boundingBox) { - return; - } - - pushToPrevLayerStates(state); - - state.layerState.stagingArea.images.push({ - kind: 'image', - layer: 'base', - ...state.layerState.stagingArea.boundingBox, - imageName: image.image_name, - }); - - state.layerState.stagingArea.selectedImageIndex = state.layerState.stagingArea.images.length - 1; - - state.futureLayerStates = []; - }, - discardStagedImages: (state) => { - pushToPrevLayerStates(state); - resetStagingArea(state); - state.futureLayerStates = []; - }, - discardStagedImage: (state) => { - const { images, selectedImageIndex } = state.layerState.stagingArea; - pushToPrevLayerStates(state); - images.splice(selectedImageIndex, 1); - state.layerState.stagingArea.selectedImageIndex = Math.max(0, images.length - 1); - state.futureLayerStates = []; - }, - addFillRect: (state) => { - const { boundingBoxCoordinates, boundingBoxDimensions, brushColor } = state; - - pushToPrevLayerStates(state); - - state.layerState.objects.push({ - kind: 'fillRect', - layer: 'base', - ...boundingBoxCoordinates, - ...boundingBoxDimensions, - color: brushColor, - }); - - state.futureLayerStates = []; - }, - addEraseRect: (state) => { - const { boundingBoxCoordinates, boundingBoxDimensions } = state; - - pushToPrevLayerStates(state); - - state.layerState.objects.push({ - kind: 'eraseRect', - layer: 'base', - ...boundingBoxCoordinates, - ...boundingBoxDimensions, - }); - - state.futureLayerStates = []; - }, - addLine: (state, action: PayloadAction<{ points: number[]; tool: CanvasTool }>) => { - const { layer, brushColor, brushSize, shouldRestrictStrokesToBox } = state; - const { points, tool } = action.payload; - - if (tool === 'move' || tool === 'colorPicker') { - return; - } - - const newStrokeWidth = brushSize / 2; - - // set & then spread this to only conditionally add the "color" key - const newColor = layer === 'base' && tool === 'brush' ? { color: brushColor } : {}; - - pushToPrevLayerStates(state); - - const newLine: CanvasMaskLine | CanvasBaseLine = { - kind: 'line', - layer, - tool, - strokeWidth: newStrokeWidth, - points, - ...newColor, - }; - - if (shouldRestrictStrokesToBox) { - newLine.clip = { - ...state.boundingBoxCoordinates, - ...state.boundingBoxDimensions, - }; - } - - state.layerState.objects.push(newLine); - - state.futureLayerStates = []; - }, - addPointToCurrentLine: (state, action: PayloadAction) => { - const lastLine = state.layerState.objects.findLast(isCanvasAnyLine); - - if (!lastLine) { - return; - } - - lastLine.points.push(...action.payload); - }, - undo: (state) => { - const targetState = state.pastLayerStates.pop(); - - if (!targetState) { - return; - } - - pushToFutureLayerStates(state); - - state.layerState = targetState; - }, - redo: (state) => { - const targetState = state.futureLayerStates.shift(); - - if (!targetState) { - return; - } - - pushToPrevLayerStates(state); - - state.layerState = targetState; - }, - setShouldShowGrid: (state, action: PayloadAction) => { - state.shouldShowGrid = action.payload; - }, - setShouldSnapToGrid: (state, action: PayloadAction) => { - state.shouldSnapToGrid = action.payload; - }, - setShouldAutoSave: (state, action: PayloadAction) => { - state.shouldAutoSave = action.payload; - }, - setShouldShowIntermediates: (state, action: PayloadAction) => { - state.shouldShowIntermediates = action.payload; - }, - resetCanvas: (state) => { - pushToPrevLayerStates(state); - state.layerState = deepClone(initialLayerState); - state.futureLayerStates = []; - state.boundingBoxCoordinates = { - ...initialCanvasState.boundingBoxCoordinates, - }; - state.boundingBoxDimensions = { - ...initialCanvasState.boundingBoxDimensions, - }; - state.stageScale = calculateScale( - state.stageDimensions.width, - state.stageDimensions.height, - state.boundingBoxDimensions.width, - state.boundingBoxDimensions.height, - STAGE_PADDING_PERCENTAGE - ); - state.stageCoordinates = calculateCoordinates( - state.stageDimensions.width, - state.stageDimensions.height, - 0, - 0, - state.boundingBoxDimensions.width, - state.boundingBoxDimensions.height, - 1 - ); - }, - canvasResized: (state, action: PayloadAction<{ width: number; height: number }>) => { - state.stageDimensions = { - width: Math.floor(action.payload.width), - height: Math.floor(action.payload.height), - }; - }, - resetCanvasView: ( - state, - action: PayloadAction<{ - contentRect: IRect; - shouldScaleTo1?: boolean; - }> - ) => { - const { contentRect, shouldScaleTo1 } = action.payload; - const { - stageDimensions: { width: stageWidth, height: stageHeight }, - } = state; - - const newScale = shouldScaleTo1 - ? 1 - : calculateScale( - stageWidth, - stageHeight, - contentRect.width || state.boundingBoxDimensions.width, - contentRect.height || state.boundingBoxDimensions.height, - STAGE_PADDING_PERCENTAGE - ); - - const newCoordinates = calculateCoordinates( - stageWidth, - stageHeight, - contentRect.x || state.boundingBoxCoordinates.x, - contentRect.y || state.boundingBoxCoordinates.y, - contentRect.width || state.boundingBoxDimensions.width, - contentRect.height || state.boundingBoxDimensions.height, - newScale - ); - - state.stageScale = newScale; - state.stageCoordinates = newCoordinates; - }, - nextStagingAreaImage: (state) => { - if (!state.layerState.stagingArea.images.length) { - return; - } - - const nextIndex = state.layerState.stagingArea.selectedImageIndex + 1; - const lastIndex = state.layerState.stagingArea.images.length - 1; - - state.layerState.stagingArea.selectedImageIndex = nextIndex > lastIndex ? 0 : nextIndex; - }, - prevStagingAreaImage: (state) => { - if (!state.layerState.stagingArea.images.length) { - return; - } - - const prevIndex = state.layerState.stagingArea.selectedImageIndex - 1; - const lastIndex = state.layerState.stagingArea.images.length - 1; - - state.layerState.stagingArea.selectedImageIndex = prevIndex < 0 ? lastIndex : prevIndex; - }, - commitStagingAreaImage: (state) => { - if (!state.layerState.stagingArea.images.length) { - return; - } - - const { images, selectedImageIndex } = state.layerState.stagingArea; - - pushToPrevLayerStates(state); - - const imageToCommit = images[selectedImageIndex]; - - if (imageToCommit) { - state.layerState.objects.push({ - ...imageToCommit, - }); - } - - resetStagingArea(state); - state.futureLayerStates = []; - }, - setBoundingBoxScaleMethod: { - reducer: (state, action: PayloadActionWithOptimalDimension) => { - const boundingBoxScaleMethod = action.payload; - const { optimalDimension } = action.meta; - state.boundingBoxScaleMethod = boundingBoxScaleMethod; - - if (boundingBoxScaleMethod === 'auto') { - const scaledDimensions = getScaledBoundingBoxDimensions(state.boundingBoxDimensions, optimalDimension); - state.scaledBoundingBoxDimensions = scaledDimensions; - } - }, - prepare: (payload: BoundingBoxScaleMethod, optimalDimension: number) => ({ - payload, - meta: { - optimalDimension, - }, - }), - }, - setScaledBoundingBoxDimensions: (state, action: PayloadAction>) => { - state.scaledBoundingBoxDimensions = { - ...state.scaledBoundingBoxDimensions, - ...action.payload, - }; - }, - setBoundingBoxDimensions: { - reducer: (state, action: PayloadActionWithOptimalDimension>) => { - setBoundingBoxDimensionsReducer(state, action.payload, action.meta.optimalDimension); - }, - prepare: (payload: Partial, optimalDimension: number) => ({ - payload, - meta: { - optimalDimension, - }, - }), - }, - setShouldShowStagingImage: (state, action: PayloadAction) => { - state.shouldShowStagingImage = action.payload; - }, - setShouldShowStagingOutline: (state, action: PayloadAction) => { - state.shouldShowStagingOutline = action.payload; - }, - setShouldShowCanvasDebugInfo: (state, action: PayloadAction) => { - state.shouldShowCanvasDebugInfo = action.payload; - }, - setShouldRestrictStrokesToBox: (state, action: PayloadAction) => { - state.shouldRestrictStrokesToBox = action.payload; - }, - setShouldAntialias: (state, action: PayloadAction) => { - state.shouldAntialias = action.payload; - }, - setShouldFitImageSize: (state, action: PayloadAction) => { - state.shouldFitImageSize = action.payload; - }, - setShouldCropToBoundingBoxOnSave: (state, action: PayloadAction) => { - state.shouldCropToBoundingBoxOnSave = action.payload; - }, - setColorPickerColor: (state, action: PayloadAction) => { - state.colorPickerColor = action.payload; - }, - commitColorPickerColor: (state) => { - state.brushColor = { - ...state.colorPickerColor, - a: state.brushColor.a, - }; - }, - setMergedCanvas: (state, action: PayloadAction) => { - pushToPrevLayerStates(state); - - state.futureLayerStates = []; - - state.layerState.objects = [action.payload]; - }, - aspectRatioChanged: (state, action: PayloadAction) => { - state.aspectRatio = action.payload; - }, - }, - extraReducers: (builder) => { - builder.addCase(modelChanged, (state, action) => { - const newModel = action.payload; - if (!newModel || action.meta.previousModel?.base === newModel.base) { - // Model was cleared or the base didn't change - return; - } - const optimalDimension = getOptimalDimension(action.payload); - const { width, height } = state.boundingBoxDimensions; - if (getIsSizeOptimal(width, height, optimalDimension)) { - return; - } - const newSize = calculateNewSize(state.aspectRatio.value, optimalDimension * optimalDimension); - setBoundingBoxDimensionsReducer(state, newSize, optimalDimension); - }); - - builder.addCase(socketQueueItemStatusChanged, (state, action) => { - const batch_status = action.payload.data.batch_status; - if (!state.batchIds.includes(batch_status.batch_id)) { - return; - } - - if (batch_status.in_progress === 0 && batch_status.pending === 0) { - state.batchIds = state.batchIds.filter((id) => id !== batch_status.batch_id); - } - - const queueItemStatus = action.payload.data.status; - if (queueItemStatus === 'canceled' || queueItemStatus === 'failed') { - resetStagingAreaIfEmpty(state); - } - }); - builder.addMatcher(queueApi.endpoints.clearQueue.matchFulfilled, (state) => { - state.batchIds = []; - resetStagingAreaIfEmpty(state); - }); - builder.addMatcher(queueApi.endpoints.cancelByBatchIds.matchFulfilled, (state, action) => { - state.batchIds = state.batchIds.filter((id) => !action.meta.arg.originalArgs.batch_ids.includes(id)); - resetStagingAreaIfEmpty(state); - }); - }, -}); - -export const { - addEraseRect, - addFillRect, - addImageToStagingArea, - addLine, - addPointToCurrentLine, - clearCanvasHistory, - clearMask, - commitColorPickerColor, - commitStagingAreaImage, - discardStagedImages, - discardStagedImage, - nextStagingAreaImage, - prevStagingAreaImage, - redo, - resetCanvas, - resetCanvasView, - setBoundingBoxCoordinates, - setBoundingBoxDimensions, - setBoundingBoxScaleMethod, - setBrushColor, - setBrushSize, - setColorPickerColor, - setInitialCanvasImage, - setIsMaskEnabled, - setLayer, - setMaskColor, - setMergedCanvas, - setShouldAutoSave, - setShouldCropToBoundingBoxOnSave, - setShouldDarkenOutsideBoundingBox, - setShouldInvertBrushSizeScrollDirection, - setShouldPreserveMaskedArea, - setShouldShowBoundingBox, - setShouldShowCanvasDebugInfo, - setShouldShowGrid, - setShouldShowIntermediates, - setShouldShowStagingImage, - setShouldShowStagingOutline, - setShouldSnapToGrid, - setStageCoordinates, - setStageScale, - undo, - setScaledBoundingBoxDimensions, - setShouldRestrictStrokesToBox, - stagingAreaInitialized, - setShouldAntialias, - setShouldFitImageSize, - canvasResized, - canvasBatchIdAdded, - canvasBatchIdsReset, - aspectRatioChanged, -} = canvasSlice.actions; - -export const selectCanvasSlice = (state: RootState) => state.canvas; - -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrateCanvasState = (state: any): any => { - if (!('_version' in state)) { - state._version = 1; - state.aspectRatio = initialAspectRatioState; - } - return state; -}; - -export const canvasPersistConfig: PersistConfig = { - name: canvasSlice.name, - initialState: initialCanvasState, - migrate: migrateCanvasState, - persistDenylist: ['shouldShowStagingImage', 'shouldShowStagingOutline'], -}; - -const pushToPrevLayerStates = (state: CanvasState) => { - state.pastLayerStates.push(deepClone(state.layerState)); - if (state.pastLayerStates.length > MAX_HISTORY) { - state.pastLayerStates = state.pastLayerStates.slice(-MAX_HISTORY); - } -}; - -const pushToFutureLayerStates = (state: CanvasState) => { - state.futureLayerStates.unshift(deepClone(state.layerState)); - if (state.futureLayerStates.length > MAX_HISTORY) { - state.futureLayerStates = state.futureLayerStates.slice(0, MAX_HISTORY); - } -}; - -const resetStagingAreaIfEmpty = (state: CanvasState) => { - if (state.batchIds.length === 0 && state.layerState.stagingArea.images.length === 0) { - resetStagingArea(state); - } -}; - -const resetStagingArea = (state: CanvasState) => { - state.layerState.stagingArea = { ...initialCanvasState.layerState.stagingArea }; - state.shouldShowStagingImage = initialCanvasState.shouldShowStagingImage; - state.shouldShowStagingOutline = initialCanvasState.shouldShowStagingOutline; -}; diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts b/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts deleted file mode 100644 index c41c6f329f3..00000000000 --- a/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts +++ /dev/null @@ -1,138 +0,0 @@ -import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; -import type { IRect, Vector2d } from 'konva/lib/types'; -import type { RgbaColor } from 'react-colorful'; -import { z } from 'zod'; - -export type CanvasLayer = 'base' | 'mask'; - -const zBoundingBoxScaleMethod = z.enum(['none', 'auto', 'manual']); -export type BoundingBoxScaleMethod = z.infer; -export const isBoundingBoxScaleMethod = (v: unknown): v is BoundingBoxScaleMethod => - zBoundingBoxScaleMethod.safeParse(v).success; - -type CanvasDrawingTool = 'brush' | 'eraser'; - -export type CanvasTool = CanvasDrawingTool | 'move' | 'colorPicker'; - -export type Dimensions = { - width: number; - height: number; -}; - -export type CanvasImage = { - kind: 'image'; - layer: 'base'; - x: number; - y: number; - width: number; - height: number; - imageName: string; -}; - -export type CanvasMaskLine = { - layer: 'mask'; - kind: 'line'; - tool: CanvasDrawingTool; - strokeWidth: number; - points: number[]; - clip?: IRect; -}; - -export type CanvasBaseLine = { - layer: 'base'; - color?: RgbaColor; - kind: 'line'; - tool: CanvasDrawingTool; - strokeWidth: number; - points: number[]; - clip?: IRect; -}; - -type CanvasFillRect = { - kind: 'fillRect'; - layer: 'base'; - x: number; - y: number; - width: number; - height: number; - color: RgbaColor; -}; - -type CanvasEraseRect = { - kind: 'eraseRect'; - layer: 'base'; - x: number; - y: number; - width: number; - height: number; -}; - -type CanvasObject = CanvasImage | CanvasBaseLine | CanvasMaskLine | CanvasFillRect | CanvasEraseRect; - -export type CanvasLayerState = { - objects: CanvasObject[]; - stagingArea: { - images: CanvasImage[]; - selectedImageIndex: number; - boundingBox?: IRect; - }; -}; - -// type guards -export const isCanvasMaskLine = (obj: CanvasObject): obj is CanvasMaskLine => - obj.kind === 'line' && obj.layer === 'mask'; - -export const isCanvasBaseLine = (obj: CanvasObject): obj is CanvasBaseLine => - obj.kind === 'line' && obj.layer === 'base'; - -export const isCanvasBaseImage = (obj: CanvasObject): obj is CanvasImage => - obj.kind === 'image' && obj.layer === 'base'; - -export const isCanvasFillRect = (obj: CanvasObject): obj is CanvasFillRect => - obj.kind === 'fillRect' && obj.layer === 'base'; - -export const isCanvasEraseRect = (obj: CanvasObject): obj is CanvasEraseRect => - obj.kind === 'eraseRect' && obj.layer === 'base'; - -export const isCanvasAnyLine = (obj: CanvasObject): obj is CanvasMaskLine | CanvasBaseLine => obj.kind === 'line'; - -export interface CanvasState { - _version: 1; - boundingBoxCoordinates: Vector2d; - boundingBoxDimensions: Dimensions; - boundingBoxScaleMethod: BoundingBoxScaleMethod; - brushColor: RgbaColor; - brushSize: number; - colorPickerColor: RgbaColor; - futureLayerStates: CanvasLayerState[]; - isMaskEnabled: boolean; - layer: CanvasLayer; - layerState: CanvasLayerState; - maskColor: RgbaColor; - pastLayerStates: CanvasLayerState[]; - scaledBoundingBoxDimensions: Dimensions; - shouldAntialias: boolean; - shouldAutoSave: boolean; - shouldCropToBoundingBoxOnSave: boolean; - shouldDarkenOutsideBoundingBox: boolean; - shouldFitImageSize: boolean; - shouldInvertBrushSizeScrollDirection: boolean; - shouldLockBoundingBox: boolean; - shouldPreserveMaskedArea: boolean; - shouldRestrictStrokesToBox: boolean; - shouldShowBoundingBox: boolean; - shouldShowCanvasDebugInfo: boolean; - shouldShowGrid: boolean; - shouldShowIntermediates: boolean; - shouldShowStagingImage: boolean; - shouldShowStagingOutline: boolean; - shouldSnapToGrid: boolean; - stageCoordinates: Vector2d; - stageDimensions: Dimensions; - stageScale: number; - generationMode?: GenerationMode; - batchIds: string[]; - aspectRatio: AspectRatioState; -} - -export type GenerationMode = 'txt2img' | 'img2img' | 'inpaint' | 'outpaint'; diff --git a/invokeai/frontend/web/src/features/canvas/store/constants.ts b/invokeai/frontend/web/src/features/canvas/store/constants.ts deleted file mode 100644 index 450c2461944..00000000000 --- a/invokeai/frontend/web/src/features/canvas/store/constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const CANVAS_GRID_SIZE_COARSE = 64; -export const CANVAS_GRID_SIZE_FINE = 8; -export const CANVAS_TAB_TESTID = 'unified-canvas-tab'; diff --git a/invokeai/frontend/web/src/features/canvas/util/blobToDataURL.ts b/invokeai/frontend/web/src/features/canvas/util/blobToDataURL.ts deleted file mode 100644 index f29010c99c2..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/blobToDataURL.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const blobToDataURL = (blob: Blob): Promise => { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = (_e) => resolve(reader.result as string); - reader.onerror = (_e) => reject(reader.error); - reader.onabort = (_e) => reject(new Error('Read aborted')); - reader.readAsDataURL(blob); - }); -}; - -export function imageDataToDataURL(imageData: ImageData): string { - const { width, height } = imageData; - - // Create a canvas to transfer the ImageData to - const canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - - // Draw the ImageData onto the canvas - const ctx = canvas.getContext('2d'); - if (!ctx) { - throw new Error('Unable to get canvas context'); - } - ctx.putImageData(imageData, 0, 0); - - // Convert the canvas to a data URL (base64) - return canvas.toDataURL(); -} diff --git a/invokeai/frontend/web/src/features/canvas/util/calculateCoordinates.ts b/invokeai/frontend/web/src/features/canvas/util/calculateCoordinates.ts deleted file mode 100644 index fe9c14b2ba8..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/calculateCoordinates.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Vector2d } from 'konva/lib/types'; - -const calculateCoordinates = ( - containerWidth: number, - containerHeight: number, - containerX: number, - containerY: number, - contentWidth: number, - contentHeight: number, - scale: number -): Vector2d => { - const x = Math.floor(containerWidth / 2 - (containerX + contentWidth / 2) * scale); - const y = Math.floor(containerHeight / 2 - (containerY + contentHeight / 2) * scale); - return { x, y }; -}; - -export default calculateCoordinates; diff --git a/invokeai/frontend/web/src/features/canvas/util/calculateScale.ts b/invokeai/frontend/web/src/features/canvas/util/calculateScale.ts deleted file mode 100644 index 255cb2850b2..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/calculateScale.ts +++ /dev/null @@ -1,14 +0,0 @@ -const calculateScale = ( - containerWidth: number, - containerHeight: number, - contentWidth: number, - contentHeight: number, - padding = 0.95 -): number => { - const scaleX = (containerWidth * padding) / contentWidth; - const scaleY = (containerHeight * padding) / contentHeight; - const scaleFit = Math.min(1, Math.min(scaleX, scaleY)); - return scaleFit ? scaleFit : 1; -}; - -export default calculateScale; diff --git a/invokeai/frontend/web/src/features/canvas/util/canvasToBlob.ts b/invokeai/frontend/web/src/features/canvas/util/canvasToBlob.ts deleted file mode 100644 index 44220c8ba42..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/canvasToBlob.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Gets a Blob from a canvas. - */ -export const canvasToBlob = async (canvas: HTMLCanvasElement): Promise => - new Promise((resolve, reject) => { - canvas.toBlob((blob) => { - if (blob) { - resolve(blob); - return; - } - reject('Unable to create Blob'); - }); - }); diff --git a/invokeai/frontend/web/src/features/canvas/util/colorToString.ts b/invokeai/frontend/web/src/features/canvas/util/colorToString.ts deleted file mode 100644 index 25d79fed5aa..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/colorToString.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { RgbaColor, RgbColor } from 'react-colorful'; - -export const rgbaColorToString = (color: RgbaColor): string => { - const { r, g, b, a } = color; - return `rgba(${r}, ${g}, ${b}, ${a})`; -}; - -export const rgbColorToString = (color: RgbColor): string => { - const { r, g, b } = color; - return `rgba(${r}, ${g}, ${b})`; -}; diff --git a/invokeai/frontend/web/src/features/canvas/util/constants.ts b/invokeai/frontend/web/src/features/canvas/util/constants.ts deleted file mode 100644 index 3291732ecc4..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/constants.ts +++ /dev/null @@ -1,14 +0,0 @@ -// canvas wheel zoom exponential scale factor -export const CANVAS_SCALE_BY = 0.999; - -// minimum (furthest-zoomed-out) scale -export const MIN_CANVAS_SCALE = 0.1; - -// maximum (furthest-zoomed-in) scale -export const MAX_CANVAS_SCALE = 20; - -// padding given to initial image/bounding box when stage view is reset -export const STAGE_PADDING_PERCENTAGE = 0.95; - -export const COLOR_PICKER_SIZE = 30; -export const COLOR_PICKER_STROKE_RADIUS = 10; diff --git a/invokeai/frontend/web/src/features/canvas/util/createMaskStage.ts b/invokeai/frontend/web/src/features/canvas/util/createMaskStage.ts deleted file mode 100644 index d0a71dee40c..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/createMaskStage.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { CanvasMaskLine } from 'features/canvas/store/canvasTypes'; -import Konva from 'konva'; -import type { IRect } from 'konva/lib/types'; - -/** - * Creates a stage from array of mask objects. - * We cannot just convert the mask layer to a blob because it uses a texture with transparent areas. - * So instead we create a new stage with the mask layer and composite it onto a white background. - */ -const createMaskStage = async ( - lines: CanvasMaskLine[], - boundingBox: IRect, - shouldInvertMask: boolean -): Promise => { - // create an offscreen canvas and add the mask to it - const { width, height } = boundingBox; - - const offscreenContainer = document.createElement('div'); - - const maskStage = new Konva.Stage({ - container: offscreenContainer, - width: width, - height: height, - }); - - const baseLayer = new Konva.Layer(); - const maskLayer = new Konva.Layer(); - - // composite the image onto the mask layer - baseLayer.add( - new Konva.Rect({ - ...boundingBox, - fill: shouldInvertMask ? 'black' : 'white', - }) - ); - - lines.forEach((line) => - maskLayer.add( - new Konva.Line({ - points: line.points, - stroke: shouldInvertMask ? 'white' : 'black', - strokeWidth: line.strokeWidth * 2, - tension: 0, - lineCap: 'round', - lineJoin: 'round', - shadowForStrokeEnabled: false, - globalCompositeOperation: line.tool === 'brush' ? 'source-over' : 'destination-out', - }) - ) - ); - - maskStage.add(baseLayer); - maskStage.add(maskLayer); - - // you'd think we can't do this until we finish with the maskStage, but we can - offscreenContainer.remove(); - - return maskStage; -}; - -export default createMaskStage; diff --git a/invokeai/frontend/web/src/features/canvas/util/dataURLToImageData.ts b/invokeai/frontend/web/src/features/canvas/util/dataURLToImageData.ts deleted file mode 100644 index d19cbe46127..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/dataURLToImageData.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Gets an ImageData object from an image dataURL by drawing it to a canvas. - */ -export const dataURLToImageData = async (dataURL: string, width: number, height: number): Promise => - new Promise((resolve, reject) => { - const canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - const ctx = canvas.getContext('2d'); - const image = new Image(); - - if (!ctx) { - canvas.remove(); - reject('Unable to get context'); - return; - } - - image.onload = function () { - ctx.drawImage(image, 0, 0); - canvas.remove(); - resolve(ctx.getImageData(0, 0, width, height)); - }; - - image.src = dataURL; - }); diff --git a/invokeai/frontend/web/src/features/canvas/util/downloadBlob.ts b/invokeai/frontend/web/src/features/canvas/util/downloadBlob.ts deleted file mode 100644 index 837e76c9980..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/downloadBlob.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** Download a blob as a file */ -export const downloadBlob = (blob: Blob, fileName: string) => { - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = fileName; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - a.remove(); -}; diff --git a/invokeai/frontend/web/src/features/canvas/util/floorCoordinates.ts b/invokeai/frontend/web/src/features/canvas/util/floorCoordinates.ts deleted file mode 100644 index 49088683324..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/floorCoordinates.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Vector2d } from 'konva/lib/types'; - -const floorCoordinates = (coord: Vector2d): Vector2d => { - return { - x: Math.floor(coord.x), - y: Math.floor(coord.y), - }; -}; - -export default floorCoordinates; diff --git a/invokeai/frontend/web/src/features/canvas/util/getBaseLayerBlob.ts b/invokeai/frontend/web/src/features/canvas/util/getBaseLayerBlob.ts deleted file mode 100644 index d37d6440087..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/getBaseLayerBlob.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { RootState } from 'app/store/store'; -import { $canvasBaseLayer } from 'features/canvas/store/canvasNanostore'; - -import { konvaNodeToBlob } from './konvaNodeToBlob'; - -/** - * Get the canvas base layer blob, with or without bounding box according to `shouldCropToBoundingBoxOnSave` - */ -export const getBaseLayerBlob = async (state: RootState, alwaysUseBoundingBox: boolean = false) => { - const canvasBaseLayer = $canvasBaseLayer.get(); - - if (!canvasBaseLayer) { - throw new Error('Problem getting base layer blob'); - } - - const { shouldCropToBoundingBoxOnSave, boundingBoxCoordinates, boundingBoxDimensions } = state.canvas; - - const clonedBaseLayer = canvasBaseLayer.clone(); - - clonedBaseLayer.scale({ x: 1, y: 1 }); - - const absPos = clonedBaseLayer.getAbsolutePosition(); - - const boundingBox = - shouldCropToBoundingBoxOnSave || alwaysUseBoundingBox - ? { - x: boundingBoxCoordinates.x + absPos.x, - y: boundingBoxCoordinates.y + absPos.y, - width: boundingBoxDimensions.width, - height: boundingBoxDimensions.height, - } - : clonedBaseLayer.getClientRect(); - - return konvaNodeToBlob(clonedBaseLayer, boundingBox); -}; diff --git a/invokeai/frontend/web/src/features/canvas/util/getCanvasData.ts b/invokeai/frontend/web/src/features/canvas/util/getCanvasData.ts deleted file mode 100644 index d17096cb71b..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/getCanvasData.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { logger } from 'app/logging/logger'; -import { $canvasBaseLayer, $canvasStage } from 'features/canvas/store/canvasNanostore'; -import type { CanvasLayerState, Dimensions } from 'features/canvas/store/canvasTypes'; -import { isCanvasMaskLine } from 'features/canvas/store/canvasTypes'; -import { konvaNodeToImageData } from 'features/canvas/util/konvaNodeToImageData'; -import type { Vector2d } from 'konva/lib/types'; - -import createMaskStage from './createMaskStage'; -import { konvaNodeToBlob } from './konvaNodeToBlob'; - -/** - * Gets Blob and ImageData objects for the base and mask layers - */ -export const getCanvasData = async ( - layerState: CanvasLayerState, - boundingBoxCoordinates: Vector2d, - boundingBoxDimensions: Dimensions, - isMaskEnabled: boolean, - shouldPreserveMaskedArea: boolean -) => { - const log = logger('canvas'); - - const canvasBaseLayer = $canvasBaseLayer.get(); - const canvasStage = $canvasStage.get(); - - if (!canvasBaseLayer || !canvasStage) { - log.error('Unable to find canvas / stage'); - return; - } - - const boundingBox = { - ...boundingBoxCoordinates, - ...boundingBoxDimensions, - }; - - // Clone the base layer so we don't affect the visible base layer - const clonedBaseLayer = canvasBaseLayer.clone(); - - // Scale it to 100% so we get full resolution - clonedBaseLayer.scale({ x: 1, y: 1 }); - - // absolute position is needed to get the bounding box coords relative to the base layer - const absPos = clonedBaseLayer.getAbsolutePosition(); - - const offsetBoundingBox = { - x: boundingBox.x + absPos.x, - y: boundingBox.y + absPos.y, - width: boundingBox.width, - height: boundingBox.height, - }; - - // For the base layer, use the offset boundingBox - const baseBlob = await konvaNodeToBlob(clonedBaseLayer, offsetBoundingBox); - const baseImageData = await konvaNodeToImageData(clonedBaseLayer, offsetBoundingBox); - - // For the mask layer, use the normal boundingBox - const maskStage = await createMaskStage( - isMaskEnabled ? layerState.objects.filter(isCanvasMaskLine) : [], // only include mask lines, and only if mask is enabled - boundingBox, - shouldPreserveMaskedArea - ); - const maskBlob = await konvaNodeToBlob(maskStage, boundingBox); - const maskImageData = await konvaNodeToImageData(maskStage, boundingBox); - - return { - baseBlob, - baseImageData, - maskBlob, - maskImageData, - }; -}; diff --git a/invokeai/frontend/web/src/features/canvas/util/getCanvasGenerationMode.ts b/invokeai/frontend/web/src/features/canvas/util/getCanvasGenerationMode.ts deleted file mode 100644 index f0a34649862..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/getCanvasGenerationMode.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { areAnyPixelsBlack, getImageDataTransparency } from 'common/util/arrayBuffer'; -import type { GenerationMode } from 'features/canvas/store/canvasTypes'; - -export const getCanvasGenerationMode = (baseImageData: ImageData, maskImageData: ImageData): GenerationMode => { - const { isPartiallyTransparent: baseIsPartiallyTransparent, isFullyTransparent: baseIsFullyTransparent } = - getImageDataTransparency(baseImageData.data); - - // check mask for black - const doesMaskHaveBlackPixels = areAnyPixelsBlack(maskImageData.data); - - if (baseIsPartiallyTransparent) { - if (baseIsFullyTransparent) { - return 'txt2img'; - } - - return 'outpaint'; - } else { - if (doesMaskHaveBlackPixels) { - return 'inpaint'; - } - - return 'img2img'; - } -}; diff --git a/invokeai/frontend/web/src/features/canvas/util/getColoredMaskSVG.ts b/invokeai/frontend/web/src/features/canvas/util/getColoredMaskSVG.ts deleted file mode 100644 index 47e1100447a..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/getColoredMaskSVG.ts +++ /dev/null @@ -1,81 +0,0 @@ -export const getColoredMaskSVG = (color: string) => { - return `data:image/svg+xml;utf8, - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -`.replaceAll('black', color); -}; diff --git a/invokeai/frontend/web/src/features/canvas/util/getFullBaseLayerBlob.ts b/invokeai/frontend/web/src/features/canvas/util/getFullBaseLayerBlob.ts deleted file mode 100644 index a5fbc999222..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/getFullBaseLayerBlob.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { $canvasBaseLayer } from 'features/canvas/store/canvasNanostore'; - -import { konvaNodeToBlob } from './konvaNodeToBlob'; - -/** - * Gets the canvas base layer blob, without bounding box - */ -export const getFullBaseLayerBlob = async () => { - const canvasBaseLayer = $canvasBaseLayer.get(); - - if (!canvasBaseLayer) { - return; - } - - const clonedBaseLayer = canvasBaseLayer.clone(); - - clonedBaseLayer.scale({ x: 1, y: 1 }); - - return konvaNodeToBlob(clonedBaseLayer, clonedBaseLayer.getClientRect()); -}; diff --git a/invokeai/frontend/web/src/features/canvas/util/getScaledCursorPosition.ts b/invokeai/frontend/web/src/features/canvas/util/getScaledCursorPosition.ts deleted file mode 100644 index 1250f66d525..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/getScaledCursorPosition.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Stage } from 'konva/lib/Stage'; - -const getScaledCursorPosition = (stage: Stage) => { - const pointerPosition = stage.getPointerPosition(); - - const stageTransform = stage.getAbsoluteTransform().copy(); - - if (!pointerPosition || !stageTransform) { - return; - } - - const scaledCursorPosition = stageTransform.invert().point(pointerPosition); - - return { - x: scaledCursorPosition.x, - y: scaledCursorPosition.y, - }; -}; - -export default getScaledCursorPosition; diff --git a/invokeai/frontend/web/src/features/canvas/util/isInteractiveTarget.ts b/invokeai/frontend/web/src/features/canvas/util/isInteractiveTarget.ts deleted file mode 100644 index 74a09aa8e48..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/isInteractiveTarget.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { isInputElement } from 'common/util/isInputElement'; - -export const isInteractiveTarget = (target: EventTarget | null) => { - if (target instanceof HTMLElement) { - return ( - target.tabIndex > -1 || - isInputElement(target) || - ['dialog', 'alertdialog'].includes(target.getAttribute('role') ?? '') - ); - } - - return false; -}; diff --git a/invokeai/frontend/web/src/features/canvas/util/konvaNodeToBlob.ts b/invokeai/frontend/web/src/features/canvas/util/konvaNodeToBlob.ts deleted file mode 100644 index e16988ea23b..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/konvaNodeToBlob.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type Konva from 'konva'; -import type { IRect } from 'konva/lib/types'; - -import { canvasToBlob } from './canvasToBlob'; - -/** - * Converts a Konva node to a Blob - * @param node - The Konva node to convert to a Blob - * @param boundingBox - The bounding box to crop to - * @returns A Promise that resolves with Blob of the node cropped to the bounding box - */ -export const konvaNodeToBlob = async (node: Konva.Node, boundingBox: IRect): Promise => { - return await canvasToBlob(node.toCanvas(boundingBox)); -}; diff --git a/invokeai/frontend/web/src/features/canvas/util/konvaNodeToImageData.ts b/invokeai/frontend/web/src/features/canvas/util/konvaNodeToImageData.ts deleted file mode 100644 index 3b5780ae16a..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/konvaNodeToImageData.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type Konva from 'konva'; -import type { IRect } from 'konva/lib/types'; - -import { dataURLToImageData } from './dataURLToImageData'; - -/** - * Converts a Konva node to an ImageData object - * @param node - The Konva node to convert to an ImageData object - * @param boundingBox - The bounding box to crop to - * @returns A Promise that resolves with ImageData object of the node cropped to the bounding box - */ -export const konvaNodeToImageData = async (node: Konva.Node, boundingBox: IRect): Promise => { - // get a dataURL of the bbox'd region - const dataURL = node.toDataURL(boundingBox); - - return await dataURLToImageData(dataURL, boundingBox.width, boundingBox.height); -}; diff --git a/invokeai/frontend/web/src/features/canvas/util/roundToHundreth.ts b/invokeai/frontend/web/src/features/canvas/util/roundToHundreth.ts deleted file mode 100644 index 1b05e7f64dd..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/roundToHundreth.ts +++ /dev/null @@ -1,5 +0,0 @@ -const roundToHundreth = (val: number): number => { - return Math.round(val * 100) / 100; -}; - -export default roundToHundreth; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx index 9b9f957dfc8..4b4a9842a28 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx @@ -10,8 +10,8 @@ import { PopoverTrigger, } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { setShouldInvertBrushSizeScrollDirection } from 'features/canvas/store/canvasSlice'; -import { RGGlobalOpacity } from 'features/controlLayers/components/RGGlobalOpacity'; +import { MaskOpacity } from 'features/controlLayers/components/MaskOpacity'; +import { invertScrollChanged } from 'features/controlLayers/store/canvasV2Slice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -22,7 +22,7 @@ const ControlLayersSettingsPopover = () => { const dispatch = useAppDispatch(); const invertScroll = useAppSelector((s) => s.canvasV2.tool.invertScroll); const onChangeInvertScroll = useCallback( - (e: ChangeEvent) => dispatch(setShouldInvertBrushSizeScrollDirection(e.target.checked)), + (e: ChangeEvent) => dispatch(invertScrollChanged(e.target.checked)), [dispatch] ); return ( @@ -33,7 +33,7 @@ const ControlLayersSettingsPopover = () => { - + {t('unifiedCanvas.invertBrushSizeScrollDirection')} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx index f5ab9d1ba5c..d99bffcc61b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx @@ -8,12 +8,17 @@ import { memo } from 'react'; export const HeadsUpDisplay = memo(() => { const stageAttrs = useStore($stageAttrs); const bbox = useAppSelector((s) => s.canvasV2.bbox); + const document = useAppSelector((s) => s.canvasV2.document); return ( + - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGGlobalOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/MaskOpacity.tsx similarity index 77% rename from invokeai/frontend/web/src/features/controlLayers/components/RGGlobalOpacity.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/MaskOpacity.tsx index e20026f3ddb..58c659c65bc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGGlobalOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/MaskOpacity.tsx @@ -1,19 +1,19 @@ import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { rgGlobalOpacityChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { maskOpacityChanged } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const marks = [0, 25, 50, 75, 100]; const formatPct = (v: number | string) => `${v} %`; -export const RGGlobalOpacity = memo(() => { +export const MaskOpacity = memo(() => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const opacity = useAppSelector((s) => Math.round(s.canvasV2.maskFillOpacity * 100)); + const opacity = useAppSelector((s) => Math.round(s.canvasV2.settings.maskOpacity * 100)); const onChange = useCallback( (v: number) => { - dispatch(rgGlobalOpacityChanged({ opacity: v / 100 })); + dispatch(maskOpacityChanged(v / 100)); }, [dispatch] ); @@ -46,4 +46,4 @@ export const RGGlobalOpacity = memo(() => { ); }); -RGGlobalOpacity.displayName = 'RGGlobalOpacity'; +MaskOpacity.displayName = 'MaskOpacity'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx index 26b5cf624c9..12297ab5a1f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx @@ -1,6 +1,6 @@ import { useDisclosure } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { rgbColorToString } from 'features/canvas/util/colorToString'; +import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { RGHeader } from 'features/controlLayers/components/RegionalGuidance/RGHeader'; import { RGSettings } from 'features/controlLayers/components/RegionalGuidance/RGSettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx index 24df9dc5586..471febea280 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx @@ -1,8 +1,8 @@ import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import RgbColorPicker from 'common/components/RgbColorPicker'; +import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { stopPropagation } from 'common/util/stopPropagation'; -import { rgbColorToString } from 'features/canvas/util/colorToString'; import { rgFillChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import { memo, useCallback } from 'react'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index 055070717d3..a1c67ad86c6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -1,10 +1,10 @@ -import { $alt, $ctrl, $meta, $shift, Box, Flex, Heading } from '@invoke-ai/ui-library'; +import { $alt, $ctrl, $meta, $shift, Flex, Heading } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { logger } from 'app/logging/logger'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay'; -import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants'; import { setStageEventHandlers } from 'features/controlLayers/konva/events'; +import { renderBackgroundLayer } from 'features/controlLayers/konva/renderers/background'; import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers/layers'; import { $bbox, @@ -70,7 +70,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, const regions = useAppSelector((s) => s.canvasV2.regions); const tool = useAppSelector((s) => s.canvasV2.tool); const selectedEntityIdentifier = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier); - const maskFillOpacity = useAppSelector((s) => s.canvasV2.maskFillOpacity); + const maskOpacity = useAppSelector((s) => s.canvasV2.settings.maskOpacity); const bbox = useAppSelector((s) => s.canvasV2.bbox); const lastCursorPos = useStore($lastCursorPos); const lastMouseDownPos = useStore($lastMouseDownPos); @@ -95,10 +95,10 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, const currentFill = useMemo(() => { if (selectedEntity && selectedEntity.type === 'regional_guidance') { - return { ...selectedEntity.fill, a: maskFillOpacity }; + return { ...selectedEntity.fill, a: maskOpacity }; } return tool.fill; - }, [maskFillOpacity, selectedEntity, tool.fill]); + }, [maskOpacity, selectedEntity, tool.fill]); const renderers = useMemo(() => (asPreview ? debouncedRenderers : normalRenderers), [asPreview]); const dpr = useDevicePixelRatio({ round: false }); @@ -106,7 +106,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, useLayoutEffect(() => { $toolState.set(tool); $selectedEntity.set(selectedEntity); - $bbox.set(bbox); + $bbox.set({ x: bbox.x, y: bbox.y, width: bbox.width, height: bbox.height }); $currentFill.set(currentFill); }, [selectedEntity, tool, bbox, currentFill]); @@ -291,6 +291,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, const resizeObserver = new ResizeObserver(fitStageToContainer); resizeObserver.observe(container); fitStageToContainer(); + renderBackgroundLayer(stage); return () => { resizeObserver.disconnect(); @@ -352,23 +353,13 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, layers, controlAdapters, regions, - maskFillOpacity, + maskOpacity, tool.selected, selectedEntity, getImageDTO, onPosChanged ); - }, [ - controlAdapters, - layers, - maskFillOpacity, - onPosChanged, - regions, - renderers, - selectedEntity, - stage, - tool.selected, - ]); + }, [controlAdapters, layers, maskOpacity, onPosChanged, regions, renderers, selectedEntity, stage, tool.selected]); // useLayoutEffect(() => { // if (asPreview) { @@ -414,15 +405,6 @@ export const StageComponent = memo(({ asPreview = false }: Props) => { return ( - {!asPreview && } { ref={containerRef} tabIndex={-1} borderRadius="base" + border={1} + borderStyle="solid" + borderColor="base.700" overflow="hidden" data-testid="control-layers-canvas" /> diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts b/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts index 9fd691bbdae..e7a3e35d2b7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts @@ -23,7 +23,7 @@ export const BRUSH_BORDER_OUTER_COLOR = 'rgba(255,255,255,0.8)'; /** * The target spacing of individual points of brush strokes, as a percentage of the brush size. */ -export const BRUSH_SPACING_PCT = 10; +export const BRUSH_SPACING_TARGET_SCALE = 0.1; /** * The minimum brush spacing in pixels. @@ -54,3 +54,13 @@ export const MIN_CANVAS_SCALE = 0.1; * Maximum (furthest-zoomed-in) scale */ export const MAX_CANVAS_SCALE = 20; + +/** + * The fine grid size of the canvas + */ +export const CANVAS_GRID_SIZE_FINE = 8; + +/** + * The coarse grid size of the canvas + */ +export const CANVAS_GRID_SIZE_COARSE = 64; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index bc8f94d31e6..849a164bb76 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -1,5 +1,4 @@ -import { calculateNewBrushSize } from 'features/canvas/hooks/useCanvasZoom'; -import { CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/canvas/util/constants'; +import { renderBackgroundLayer } from 'features/controlLayers/konva/renderers/background'; import { getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util'; import type { BrushLineAddedArg, @@ -16,6 +15,14 @@ import type Konva from 'konva'; import type { Vector2d } from 'konva/lib/types'; import { clamp } from 'lodash-es'; +import { + BRUSH_SPACING_TARGET_SCALE, + CANVAS_SCALE_BY, + MAX_BRUSH_SPACING_PX, + MAX_CANVAS_SCALE, + MIN_BRUSH_SPACING_PX, + MIN_CANVAS_SCALE, +} from './constants'; import { PREVIEW_TOOL_GROUP_ID } from './naming'; type Arg = { @@ -60,6 +67,18 @@ const updateLastCursorPos = (stage: Konva.Stage, setLastCursorPos: Arg['setLastC return pos; }; +const calculateNewBrushSize = (brushSize: number, delta: number) => { + // This equation was derived by fitting a curve to the desired brush sizes and deltas + // see https://github.com/invoke-ai/InvokeAI/pull/5542#issuecomment-1915847565 + const targetDelta = Math.sign(delta) * 0.7363 * Math.pow(1.0394, brushSize); + // This needs to be clamped to prevent the delta from getting too large + const finalDelta = clamp(targetDelta, -20, 20); + // The new brush size is also clamped to prevent it from getting too large or small + const newBrushSize = clamp(brushSize + finalDelta, 1, 500); + + return newBrushSize; +}; + /** * Adds the next point to a line if the cursor has moved far enough from the last point. * @param layerId The layer to (maybe) add the point to @@ -82,7 +101,13 @@ const maybeAddNextPoint = ( // Continue the last line const lastAddedPoint = getLastAddedPoint(); const toolState = getToolState(); - const minSpacingPx = toolState.selected === 'brush' ? toolState.brush.width * 0.05 : toolState.eraser.width * 0.05; + const minSpacingPx = clamp( + toolState.selected === 'brush' + ? toolState.brush.width * BRUSH_SPACING_TARGET_SCALE + : toolState.eraser.width * BRUSH_SPACING_TARGET_SCALE, + MIN_BRUSH_SPACING_PX, + MAX_BRUSH_SPACING_PX + ); if (lastAddedPoint) { // Dispatching redux events impacts perf substantially - using brush spacing keeps dispatches to a reasonable number if (Math.hypot(lastAddedPoint.x - currentPos.x, lastAddedPoint.y - currentPos.y) < minSpacingPx) { @@ -354,6 +379,7 @@ export const setStageEventHandlers = ({ } }); + //#region wheel stage.on('wheel', (e) => { e.evt.preventDefault(); @@ -393,9 +419,11 @@ export const setStageEventHandlers = ({ stage.scaleY(newScale); stage.position(newPos); setStageAttrs({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale }); + renderBackgroundLayer(stage); } }); + //#region dragmove stage.on('dragmove', () => { setStageAttrs({ x: stage.x(), @@ -404,21 +432,22 @@ export const setStageEventHandlers = ({ height: stage.height(), scale: stage.scaleX(), }); + renderBackgroundLayer(stage); }); + //#region dragend stage.on('dragend', () => { // Stage position should always be an integer, else we get fractional pixels which are blurry - stage.x(Math.floor(stage.x())); - stage.y(Math.floor(stage.y())); setStageAttrs({ - x: stage.x(), - y: stage.y(), + x: Math.floor(stage.x()), + y: Math.floor(stage.y()), width: stage.width(), height: stage.height(), scale: stage.scaleX(), }); }); + //#region key const onKeyDown = (e: KeyboardEvent) => { if (e.repeat) { return; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts new file mode 100644 index 00000000000..4d898f31bdc --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts @@ -0,0 +1,115 @@ +import { getArbitraryBaseColor } from '@invoke-ai/ui-library'; +import Konva from 'konva'; + +const baseGridLineColor = getArbitraryBaseColor(27); +const fineGridLineColor = getArbitraryBaseColor(18); + +const getGridSpacing = (scale: number): number => { + if (scale >= 2) { + return 8; + } + if (scale >= 1 && scale < 2) { + return 16; + } + if (scale >= 0.5 && scale < 1) { + return 32; + } + if (scale >= 0.25 && scale < 0.5) { + return 64; + } + if (scale >= 0.125 && scale < 0.25) { + return 128; + } + return 256; +}; + +const getBackgroundLayer = (stage: Konva.Stage): Konva.Layer => { + let background = stage.findOne('#background'); + if (background) { + return background; + } + + background = new Konva.Layer({ id: 'background' }); + stage.add(background); + return background; +}; + +export const renderBackgroundLayer = (stage: Konva.Stage): void => { + const background = getBackgroundLayer(stage); + background.zIndex(0); + const scale = stage.scaleX(); + const gridSpacing = getGridSpacing(scale); + const x = stage.x(); + const y = stage.y(); + const width = stage.width(); + const height = stage.height(); + const stageRect = { + x1: 0, + y1: 0, + x2: width, + y2: height, + offset: { + x: x / scale, + y: y / scale, + }, + }; + + const gridOffset = { + x: Math.ceil(x / scale / gridSpacing) * gridSpacing, + y: Math.ceil(y / scale / gridSpacing) * gridSpacing, + }; + + const gridRect = { + x1: -gridOffset.x, + y1: -gridOffset.y, + x2: width / scale - gridOffset.x + gridSpacing, + y2: height / scale - gridOffset.y + gridSpacing, + }; + + const gridFullRect = { + x1: Math.min(stageRect.x1, gridRect.x1), + y1: Math.min(stageRect.y1, gridRect.y1), + x2: Math.max(stageRect.x2, gridRect.x2), + y2: Math.max(stageRect.y2, gridRect.y2), + }; + + const // find the x & y size of the grid + xSize = gridFullRect.x2 - gridFullRect.x1; + const ySize = gridFullRect.y2 - gridFullRect.y1; + // compute the number of steps required on each axis. + const xSteps = Math.round(xSize / gridSpacing) + 1; + const ySteps = Math.round(ySize / gridSpacing) + 1; + + const strokeWidth = 1 / scale; + let _x = 0; + let _y = 0; + + background.destroyChildren(); + + for (let i = 0; i < xSteps; i++) { + _x = gridFullRect.x1 + i * gridSpacing; + background.add( + new Konva.Line({ + x: _x, + y: gridFullRect.y1, + points: [0, 0, 0, ySize], + stroke: _x % 64 ? fineGridLineColor : baseGridLineColor, + strokeWidth, + listening: false, + }) + ); + } + for (let i = 0; i < ySteps; i++) { + _y = gridFullRect.y1 + i * gridSpacing; + background.add( + new Konva.Line({ + x: gridFullRect.x1, + y: _y, + points: [0, 0, xSize, 0], + stroke: _y % 64 ? fineGridLineColor : baseGridLineColor, + strokeWidth, + listening: false, + }) + ); + } +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts index 5b3df88aeae..a006f481c70 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts @@ -1,5 +1,4 @@ import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; -import { imageDataToDataURL } from 'features/canvas/util/blobToDataURL'; import { CA_LAYER_IMAGE_NAME, LAYER_BBOX_NAME, @@ -7,6 +6,7 @@ import { RG_LAYER_OBJECT_GROUP_NAME, } from 'features/controlLayers/konva/naming'; import { createBboxRect } from 'features/controlLayers/konva/renderers/objects'; +import { imageDataToDataURL } from "features/controlLayers/konva/util"; import type { ControlAdapterData, LayerData, RegionalGuidanceData } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts index d5dc53f3be6..43db3554ee7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -50,7 +50,7 @@ const renderLayers = ( } } // We'll need to ensure the tool preview layer is on top of the rest of the layers - let zIndex = 0; + let zIndex = 1; for (const layer of layers) { renderRasterLayer(stage, layer, tool, zIndex, onPosChanged); zIndex++; 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 9074f240460..dd4ba645e2a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -1,17 +1,11 @@ -import { rgbaColorToString } from 'features/canvas/util/colorToString'; +import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { getLayerBboxId, getObjectGroupId, LAYER_BBOX_NAME, PREVIEW_GENERATION_BBOX_DUMMY_RECT, } from 'features/controlLayers/konva/naming'; -import type { - BrushLine, - CanvasEntity, - EraserLine, - ImageObject, - RectShape, -} from 'features/controlLayers/store/types'; +import type { BrushLine, CanvasEntity, EraserLine, ImageObject, RectShape } from 'features/controlLayers/store/types'; import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types'; import { t } from 'i18next'; import Konva from 'konva'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts index 4ee22d71a6e..2ca1573041e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts @@ -1,10 +1,6 @@ +import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMultiple'; -import { rgbaColorToString } from 'features/canvas/util/colorToString'; -import { - BBOX_SELECTED_STROKE, - BRUSH_BORDER_INNER_COLOR, - BRUSH_BORDER_OUTER_COLOR, -} from 'features/controlLayers/konva/constants'; +import { BRUSH_BORDER_INNER_COLOR, BRUSH_BORDER_OUTER_COLOR } from 'features/controlLayers/konva/constants'; import { PREVIEW_BRUSH_BORDER_INNER_ID, PREVIEW_BRUSH_BORDER_OUTER_ID, @@ -206,9 +202,10 @@ export const getBboxPreviewGroup = ( height, }; - // Here we _could_ go ahead and update the bboxRect's attrs directly with the new transform, and reset its scale to 1. - // However, we have another function that renders the bbox when its internal state changes, so we will rely on that - // to set the new attrs. + // Update the bboxRect's attrs directly with the new transform, and reset its scale to 1. + // TODO(psyche): In `renderBboxPreview()` we also call setAttrs, need to do it twice to ensure it renders correctly. + // Gotta be a way to avoid setting it twice... + bboxRect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1 }); // Update the bbox in internal state. onBboxTransformed(bbox); @@ -281,7 +278,7 @@ export const getToolPreviewGroup = (stage: Konva.Stage): Konva.Group => { if (toolPreviewGroup) { return toolPreviewGroup; } - + const scale = stage.scaleX(); toolPreviewGroup = new Konva.Group({ id: PREVIEW_TOOL_GROUP_ID }); // Create the brush preview group & circles @@ -296,7 +293,7 @@ export const getToolPreviewGroup = (stage: Konva.Stage): Konva.Group => { id: PREVIEW_BRUSH_BORDER_INNER_ID, listening: false, stroke: BRUSH_BORDER_INNER_COLOR, - strokeWidth: 1, + strokeWidth: 1 / scale, strokeEnabled: true, }); brushPreviewGroup.add(brushPreviewBorderInner); @@ -304,7 +301,7 @@ export const getToolPreviewGroup = (stage: Konva.Stage): Konva.Group => { id: PREVIEW_BRUSH_BORDER_OUTER_ID, listening: false, stroke: BRUSH_BORDER_OUTER_COLOR, - strokeWidth: 1, + strokeWidth: 1 / scale, strokeEnabled: true, }); brushPreviewGroup.add(brushPreviewBorderOuter); @@ -313,8 +310,7 @@ export const getToolPreviewGroup = (stage: Konva.Stage): Konva.Group => { const rectPreview = new Konva.Rect({ id: PREVIEW_RECT_ID, listening: false, - stroke: BBOX_SELECTED_STROKE, - strokeWidth: 1, + strokeEnabled: false, }); toolPreviewGroup.add(rectPreview); 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 2f7f259baa1..b6033416cd1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts @@ -1,4 +1,4 @@ -import { rgbColorToString } from 'features/canvas/util/colorToString'; +import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { BBOX_SELECTED_STROKE } from 'features/controlLayers/konva/constants'; import { COMPOSITING_RECT_NAME, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index 1e28392f2af..ba0f3a33433 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -12,11 +12,11 @@ import { RG_LAYER_NAME, RG_LAYER_RECT_SHAPE_NAME, } from 'features/controlLayers/konva/naming'; -import type Konva from 'konva'; +import type { RgbaColor } from 'features/controlLayers/store/types'; +import Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; -import type { Vector2d } from 'konva/lib/types'; +import type { IRect, Vector2d } from 'konva/lib/types'; -//#region getScaledFlooredCursorPosition /** * Gets the scaled and floored cursor position on the stage. If the cursor is not currently over the stage, returns null. * @param stage The konva stage @@ -33,9 +33,7 @@ export const getScaledFlooredCursorPosition = (stage: Konva.Stage): Vector2d | n y: Math.floor(scaledCursorPosition.y), }; }; -//#endregion -//#region snapPosToStage /** * Snaps a position to the edge of the stage if within a threshold of the edge * @param pos The position to snap @@ -62,25 +60,19 @@ export const snapPosToStage = (pos: Vector2d, stage: Konva.Stage, snapPx = 10): } return snappedPos; }; -//#endregion -//#region getIsMouseDown /** * Checks if the left mouse button is currently pressed * @param e The konva event */ export const getIsMouseDown = (e: KonvaEventObject): boolean => e.evt.buttons === 1; -//#endregion -//#region getIsFocused /** * Checks if the stage is currently focused * @param stage The konva stage */ export const getIsFocused = (stage: Konva.Stage): boolean => stage.container().contains(document.activeElement); -//#endregion -//#region mapId /** * Simple util to map an object to its id property. Serves as a minor optimization to avoid recreating a map callback * every time we need to map an object to its id, which happens very often. @@ -88,9 +80,7 @@ export const getIsFocused = (stage: Konva.Stage): boolean => stage.container().c * @returns The object's id property */ export const mapId = (object: { id: string }): string => object.id; -//#endregion -//#region konva selector callbacks /** * 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. @@ -120,4 +110,141 @@ export const selectRasterObjects = (node: Konva.Node): boolean => node.name() === RASTER_LAYER_ERASER_LINE_NAME || node.name() === RASTER_LAYER_RECT_SHAPE_NAME || node.name() === RASTER_LAYER_IMAGE_NAME; -//#endregion + +/** + * Convert a Blob to a data URL. + */ +export const blobToDataURL = (blob: Blob): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (_e) => resolve(reader.result as string); + reader.onerror = (_e) => reject(reader.error); + reader.onabort = (_e) => reject(new Error('Read aborted')); + reader.readAsDataURL(blob); + }); +}; + +/** + * Convert an ImageData object to a data URL. + */ +export function imageDataToDataURL(imageData: ImageData): string { + const { width, height } = imageData; + + // Create a canvas to transfer the ImageData to + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + + // Draw the ImageData onto the canvas + const ctx = canvas.getContext('2d'); + if (!ctx) { + throw new Error('Unable to get canvas context'); + } + ctx.putImageData(imageData, 0, 0); + + // Convert the canvas to a data URL (base64) + return canvas.toDataURL(); +} + +/** + * Download a Blob as a file + */ +export const downloadBlob = (blob: Blob, fileName: string) => { + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = fileName; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + a.remove(); +}; + +/** + * Gets a Blob from a HTMLCanvasElement. + */ +export const canvasToBlob = async (canvas: HTMLCanvasElement): Promise => { + return new Promise((resolve, reject) => { + canvas.toBlob((blob) => { + if (blob) { + resolve(blob); + return; + } + reject('Unable to create Blob'); + }); + }); +}; + +/** + * Gets an ImageData object from an image dataURL by drawing it to a canvas. + */ +export const dataURLToImageData = async (dataURL: string, width: number, height: number): Promise => { + return new Promise((resolve, reject) => { + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + const image = new Image(); + + if (!ctx) { + canvas.remove(); + reject('Unable to get context'); + return; + } + + image.onload = function () { + ctx.drawImage(image, 0, 0); + canvas.remove(); + resolve(ctx.getImageData(0, 0, width, height)); + }; + + image.src = dataURL; + }); +}; + +/** + * Converts a Konva node to a Blob + * @param node - The Konva node to convert to a Blob + * @param boundingBox - The bounding box to crop to + * @returns A Promise that resolves with Blob of the node cropped to the bounding box + */ +export const konvaNodeToBlob = async (node: Konva.Node, boundingBox: IRect): Promise => { + return await canvasToBlob(node.toCanvas(boundingBox)); +}; + +/** + * Converts a Konva node to an ImageData object + * @param node - The Konva node to convert to an ImageData object + * @param boundingBox - The bounding box to crop to + * @returns A Promise that resolves with ImageData object of the node cropped to the bounding box + */ +export const konvaNodeToImageData = async (node: Konva.Node, boundingBox: IRect): Promise => { + // get a dataURL of the bbox'd region + const dataURL = node.toDataURL(boundingBox); + + return await dataURLToImageData(dataURL, boundingBox.width, boundingBox.height); +}; + +/** + * Gets the pixel under the cursor on the stage, or null if the cursor is not over the stage. + * @param stage The konva stage + */ +export const getPixelUnderCursor = (stage: Konva.Stage): RgbaColor | null => { + const cursorPos = stage.getPointerPosition(); + const pixelRatio = Konva.pixelRatio; + if (!cursorPos) { + return null; + } + const ctx = stage.toCanvas().getContext('2d'); + + if (!ctx) { + return null; + } + const [r, g, b, a] = ctx.getImageData(cursorPos.x * pixelRatio, cursorPos.y * pixelRatio, 1, 1).data; + + if (r === undefined || g === undefined || b === undefined || a === undefined) { + return null; + } + + return { r, g, b, a }; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts new file mode 100644 index 00000000000..dc75e2e8e66 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts @@ -0,0 +1,39 @@ +import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import type { BoundingBoxScaleMethod, CanvasV2State, Dimensions } from 'features/controlLayers/store/types'; +import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; +import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; +import type { IRect } from 'konva/lib/types'; + +export const bboxReducers = { + scaledBboxChanged: (state, action: PayloadAction>) => { + const { width, height } = action.payload; + state.bbox.scaledWidth = width ?? state.bbox.scaledWidth; + state.bbox.scaledHeight = height ?? state.bbox.scaledHeight; + }, + bboxScaleMethodChanged: (state, action: PayloadAction) => { + state.bbox.scaleMethod = action.payload; + + if (action.payload === 'auto') { + const bboxDims = { width: state.bbox.width, height: state.bbox.height }; + const optimalDimension = getOptimalDimension(state.params.model); + const scaledBboxDims = getScaledBoundingBoxDimensions(bboxDims, optimalDimension); + state.bbox.scaledWidth = scaledBboxDims.width; + state.bbox.scaledHeight = scaledBboxDims.height; + } + }, + bboxChanged: (state, action: PayloadAction) => { + const { x, y, width, height } = action.payload; + state.bbox.x = x; + state.bbox.y = y; + state.bbox.width = width; + state.bbox.height = height; + + if (state.bbox.scaleMethod === 'auto') { + const bboxDims = { width: state.bbox.width, height: state.bbox.height }; + const optimalDimension = getOptimalDimension(state.params.model); + const scaledBboxDims = getScaledBoundingBoxDimensions(bboxDims, optimalDimension); + state.bbox.scaledWidth = scaledBboxDims.width; + state.bbox.scaledHeight = scaledBboxDims.height; + } + }, +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 60abd47769c..4f86bb1fb4f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -3,23 +3,30 @@ import { createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { deepClone } from 'common/util/deepClone'; import { roundDownToMultiple } from 'common/util/roundDownToMultiple'; +import { bboxReducers } from 'features/controlLayers/store/bboxReducers'; import { compositingReducers } from 'features/controlLayers/store/compositingReducers'; import { controlAdaptersReducers } from 'features/controlLayers/store/controlAdaptersReducers'; import { ipAdaptersReducers } from 'features/controlLayers/store/ipAdaptersReducers'; import { layersReducers } from 'features/controlLayers/store/layersReducers'; import { paramsReducers } from 'features/controlLayers/store/paramsReducers'; import { regionsReducers } from 'features/controlLayers/store/regionsReducers'; +import { settingsReducers } from 'features/controlLayers/store/settingsReducers'; +import { toolReducers } from 'features/controlLayers/store/toolReducers'; import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; import type { IRect, Vector2d } from 'konva/lib/types'; import { atom } from 'nanostores'; -import type { CanvasEntity, CanvasEntityIdentifier, CanvasV2State, RgbaColor, StageAttrs, Tool } from './types'; +import type { CanvasEntity, CanvasEntityIdentifier, CanvasV2State, RgbaColor, StageAttrs } from './types'; import { DEFAULT_RGBA_COLOR } from './types'; const initialState: CanvasV2State = { _version: 3, selectedEntityIdentifier: null, + layers: [], + controlAdapters: [], + ipAdapters: [], + regions: [], tool: { selected: 'bbox', selectedBuffer: null, @@ -42,17 +49,20 @@ const initialState: CanvasV2State = { y: 0, width: 512, height: 512, - }, - scaledBbox: { - width: 512, - height: 512, scaleMethod: 'auto', + scaledWidth: 512, + scaledHeight: 512, + }, + settings: { + maskOpacity: 0.3, + // TODO(psyche): These are copied from old canvas state, need to be implemented + autoSave: false, + imageSmoothing: true, + preserveMaskedArea: false, + showHUD: true, + clipToBbox: false, + cropToBboxOnSave: false, }, - controlAdapters: [], - ipAdapters: [], - regions: [], - layers: [], - maskFillOpacity: 0.3, compositing: { maskBlur: 16, maskBlurMethod: 'box', @@ -105,6 +115,9 @@ export const canvasV2Slice = createSlice({ ...regionsReducers, ...paramsReducers, ...compositingReducers, + ...settingsReducers, + ...toolReducers, + ...bboxReducers, widthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }>) => { const { width, updateAspectRatio, clamp } = action.payload; state.document.width = clamp ? Math.max(roundDownToMultiple(width, 8), 64) : width; @@ -126,30 +139,6 @@ export const canvasV2Slice = createSlice({ aspectRatioChanged: (state, action: PayloadAction) => { state.document.aspectRatio = action.payload; }, - bboxChanged: (state, action: PayloadAction) => { - state.bbox = action.payload; - }, - brushWidthChanged: (state, action: PayloadAction) => { - state.tool.brush.width = Math.round(action.payload); - }, - eraserWidthChanged: (state, action: PayloadAction) => { - state.tool.eraser.width = Math.round(action.payload); - }, - fillChanged: (state, action: PayloadAction) => { - state.tool.fill = action.payload; - }, - invertScrollChanged: (state, action: PayloadAction) => { - state.tool.invertScroll = action.payload; - }, - toolChanged: (state, action: PayloadAction) => { - state.tool.selected = action.payload; - }, - toolBufferChanged: (state, action: PayloadAction) => { - state.tool.selectedBuffer = action.payload; - }, - maskFillOpacityChanged: (state, action: PayloadAction) => { - state.maskFillOpacity = action.payload; - }, entitySelected: (state, action: PayloadAction) => { state.selectedEntityIdentifier = action.payload; }, @@ -173,9 +162,11 @@ export const { invertScrollChanged, toolChanged, toolBufferChanged, - maskFillOpacityChanged, + maskOpacityChanged, entitySelected, allEntitiesDeleted, + scaledBboxChanged, + bboxScaleMethodChanged, // layers layerAdded, layerRecalled, @@ -238,7 +229,6 @@ export const { rgBboxChanged, rgDeleted, rgAllDeleted, - rgGlobalOpacityChanged, rgMovedForwardOne, rgMovedToFront, rgMovedBackwardOne, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts index 2020a1b732d..ba36deaa323 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts @@ -1,6 +1,6 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import getScaledBoundingBoxDimensions from 'features/canvas/util/getScaledBoundingBoxDimensions'; import type { CanvasV2State } from 'features/controlLayers/store/types'; +import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; import { CLIP_SKIP_MAP } from 'features/parameters/types/constants'; import type { @@ -67,10 +67,10 @@ export const paramsReducers = { state.bbox.width = bboxDims.width; state.bbox.height = bboxDims.height; - if (state.scaledBbox.scaleMethod === 'auto') { + if (state.bbox.scaleMethod === 'auto') { const scaledBboxDims = getScaledBoundingBoxDimensions(bboxDims, optimalDimension); - state.scaledBbox.width = scaledBboxDims.width; - state.scaledBbox.height = scaledBboxDims.height; + state.bbox.scaledWidth = scaledBboxDims.width; + state.bbox.scaledHeight = scaledBboxDims.height; } } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index 35b584adb74..9d75e0e93f4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -120,10 +120,6 @@ export const regionsReducers = { rgAllDeleted: (state) => { state.regions = []; }, - rgGlobalOpacityChanged: (state, action: PayloadAction<{ opacity: number }>) => { - const { opacity } = action.payload; - state.maskFillOpacity = opacity; - }, rgMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; const rg = selectRG(state, id); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts index d57fee3d49e..ebd59de4801 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts @@ -1,13 +1,13 @@ import { createSelector } from '@reduxjs/toolkit'; -import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; -export const selectEntityCount = createSelector(selectCanvasSlice, (canvasV2) => { +export const selectEntityCount = createSelector(selectCanvasV2Slice, (canvasV2) => { return ( canvasV2.regions.length + canvasV2.controlAdapters.length + canvasV2.ipAdapters.length + canvasV2.layers.length ); }); -export const selectOptimalDimension = createSelector(selectCanvasSlice, (canvasV2) => { +export const selectOptimalDimension = createSelector(selectCanvasV2Slice, (canvasV2) => { return getOptimalDimension(canvasV2.params.model); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/settingsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/settingsReducers.ts new file mode 100644 index 00000000000..d3b7dd40d9e --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/settingsReducers.ts @@ -0,0 +1,8 @@ +import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import type { CanvasV2State } from 'features/controlLayers/store/types'; + +export const settingsReducers = { + maskOpacityChanged: (state, action: PayloadAction) => { + state.settings.maskOpacity = action.payload; + }, +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/test.ts b/invokeai/frontend/web/src/features/controlLayers/store/test.ts deleted file mode 100644 index 2426f9af2ce..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/store/test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { ActionReducerMapBuilder, PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import { createSlice } from '@reduxjs/toolkit'; - -type MySlice = { - flavour: 'vanilla' | 'chocolate' | 'strawberry'; - sprinkles: boolean; - customers: { id: string; name: string }[]; -}; -const initialStateMySlice: MySlice = { flavour: 'vanilla', sprinkles: false, customers: [] }; - -const reducersInAnotherFile: SliceCaseReducers = { - sprinklesToggled: (state) => { - state.sprinkles = !state.sprinkles; - }, - customerAdded: { - reducer: (state, action: PayloadAction<{ id: string; name: string }>) => { - state.customers.push(action.payload); - }, - prepare: (name: string) => ({ payload: { name, id: crypto.randomUUID() } }), - }, -}; - -const extraReducersInAnotherFile = (builder: ActionReducerMapBuilder) => { - builder.addCase(otherSlice.actions.fooChanged, (state, action) => { - if (action.payload === 'bar') { - state.flavour = 'vanilla'; - } - }); -}; - -export const mySlice = createSlice({ - name: 'mySlice', - initialState: initialStateMySlice, - reducers: { - ...reducersInAnotherFile, - flavourChanged: (state, action: PayloadAction) => { - state.flavour = action.payload; - }, - }, - extraReducers: extraReducersInAnotherFile, -}); - -type OtherSlice = { - something: string; -}; - -const initialStateOtherSlice: OtherSlice = { something: 'foo' }; - -export const otherSlice = createSlice({ - name: 'otherSlice', - initialState: initialStateOtherSlice, - reducers: { - fooChanged: (state, action: PayloadAction) => { - state.something = action.payload; - }, - }, -}); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts new file mode 100644 index 00000000000..c1f14d7df40 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts @@ -0,0 +1,23 @@ +import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import type { CanvasV2State, RgbaColor, Tool } from 'features/controlLayers/store/types'; + +export const toolReducers = { + brushWidthChanged: (state, action: PayloadAction) => { + state.tool.brush.width = Math.round(action.payload); + }, + eraserWidthChanged: (state, action: PayloadAction) => { + state.tool.eraser.width = Math.round(action.payload); + }, + fillChanged: (state, action: PayloadAction) => { + state.tool.fill = action.payload; + }, + invertScrollChanged: (state, action: PayloadAction) => { + state.tool.invertScroll = action.payload; + }, + toolChanged: (state, action: PayloadAction) => { + state.tool.selected = action.payload; + }, + toolBufferChanged: (state, action: PayloadAction) => { + state.tool.selectedBuffer = action.payload; + }, +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index bce9d98fdf6..14d4725fb63 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -802,6 +802,10 @@ export type Dimensions = { export type CanvasV2State = { _version: 3; selectedEntityIdentifier: CanvasEntityIdentifier | null; + layers: LayerData[]; + controlAdapters: ControlAdapterData[]; + ipAdapters: IPAdapterData[]; + regions: RegionalGuidanceData[]; tool: { selected: Tool; selectedBuffer: Tool | null; @@ -815,17 +819,24 @@ export type CanvasV2State = { height: ParameterHeight; aspectRatio: AspectRatioState; }; - bbox: IRect; - scaledBbox: { - scaleMethod: BoundingBoxScaleMethod; + settings: { + imageSmoothing: boolean; + maskOpacity: number; + showHUD: boolean; + autoSave: boolean; + preserveMaskedArea: boolean; + cropToBboxOnSave: boolean; + clipToBbox: boolean; + }; + bbox: { + x: number; + y: number; width: ParameterWidth; height: ParameterHeight; + scaleMethod: BoundingBoxScaleMethod; + scaledWidth: ParameterWidth; + scaledHeight: ParameterHeight; }; - layers: LayerData[]; - controlAdapters: ControlAdapterData[]; - ipAdapters: IPAdapterData[]; - regions: RegionalGuidanceData[]; - maskFillOpacity: number; compositing: { maskBlur: number; maskBlurMethod: ParameterMaskBlurMethod; diff --git a/invokeai/frontend/web/src/features/canvas/util/getScaledBoundingBoxDimensions.ts b/invokeai/frontend/web/src/features/controlLayers/util/getScaledBoundingBoxDimensions.ts similarity index 64% rename from invokeai/frontend/web/src/features/canvas/util/getScaledBoundingBoxDimensions.ts rename to invokeai/frontend/web/src/features/controlLayers/util/getScaledBoundingBoxDimensions.ts index de38d12cf5f..d98d03f33ec 100644 --- a/invokeai/frontend/web/src/features/canvas/util/getScaledBoundingBoxDimensions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/getScaledBoundingBoxDimensions.ts @@ -1,8 +1,14 @@ import { roundToMultiple } from 'common/util/roundDownToMultiple'; -import type { Dimensions } from 'features/canvas/store/canvasTypes'; -import { CANVAS_GRID_SIZE_FINE } from 'features/canvas/store/constants'; +import { CANVAS_GRID_SIZE_FINE } from 'features/controlLayers/konva/constants'; +import type { Dimensions } from 'features/controlLayers/store/types'; -const getScaledBoundingBoxDimensions = (dimensions: Dimensions, optimalDimension: number) => { +/** + * Scales the bounding box dimensions to the optimal dimension. The optimal dimensions should be the trained dimension + * for the model. For example, 1024 for SDXL or 512 for SD1.5. + * @param dimensions The un-scaled bbox dimensions + * @param optimalDimension The optimal dimension to scale the bbox to + */ +export const getScaledBoundingBoxDimensions = (dimensions: Dimensions, optimalDimension: number): Dimensions => { const { width, height } = dimensions; const scaledDimensions = { width, height }; @@ -30,5 +36,3 @@ const getScaledBoundingBoxDimensions = (dimensions: Dimensions, optimalDimension return scaledDimensions; }; - -export default getScaledBoundingBoxDimensions; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts index 06eaf0be12a..b1037f0b1f8 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts @@ -1,7 +1,7 @@ import { getStore } from 'app/store/nanostores/store'; import { deepClone } from 'common/util/deepClone'; import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; -import { blobToDataURL } from 'features/canvas/util/blobToDataURL'; +import { blobToDataURL } from "features/controlLayers/konva/util"; import { RG_LAYER_NAME } from 'features/controlLayers/konva/naming'; import { renderers } from 'features/controlLayers/konva/renderers/layers'; import { rgMaskImageUploaded } from 'features/controlLayers/store/canvasV2Slice'; diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing.tsx index 5273b8c508f..1a0d95d57ad 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing.tsx @@ -2,17 +2,15 @@ import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setBoundingBoxScaleMethod } from 'features/canvas/store/canvasSlice'; -import { isBoundingBoxScaleMethod } from 'features/canvas/store/canvasTypes'; -import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; +import { bboxScaleMethodChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { isBoundingBoxScaleMethod } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamScaleBeforeProcessing = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const boundingBoxScaleMethod = useAppSelector((s) => s.canvasV2.scaledBbox.scaleMethod); - const optimalDimension = useAppSelector(selectOptimalDimension); + const scaleMethod = useAppSelector((s) => s.canvasV2.bbox.scaleMethod); const OPTIONS: ComboboxOption[] = useMemo( () => [ @@ -28,15 +26,12 @@ const ParamScaleBeforeProcessing = () => { if (!isBoundingBoxScaleMethod(v?.value)) { return; } - dispatch(setBoundingBoxScaleMethod(v.value, optimalDimension)); + dispatch(bboxScaleMethodChanged(v.value)); }, - [dispatch, optimalDimension] + [dispatch] ); - const value = useMemo( - () => OPTIONS.find((o) => o.value === boundingBoxScaleMethod), - [boundingBoxScaleMethod, OPTIONS] - ); + const value = useMemo(() => OPTIONS.find((o) => o.value === scaleMethod), [scaleMethod, OPTIONS]); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx index 0c6aa502ce1..71a5dc28c9f 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx @@ -1,6 +1,6 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { setScaledBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; +import { scaledBboxChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,8 +9,8 @@ const ParamScaledHeight = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const optimalDimension = useAppSelector(selectOptimalDimension); - const isManual = useAppSelector((s) => s.canvasV2.scaledBbox.scaleMethod === 'manual'); - const height = useAppSelector((s) => s.canvasV2.scaledBbox.height); + const isManual = useAppSelector((s) => s.canvasV2.bbox.scaleMethod === 'manual'); + const height = useAppSelector((s) => s.canvasV2.bbox.scaledHeight); const sliderMin = useAppSelector((s) => s.config.sd.scaledBoundingBoxHeight.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.scaledBoundingBoxHeight.sliderMax); const numberInputMin = useAppSelector((s) => s.config.sd.scaledBoundingBoxHeight.numberInputMin); @@ -20,7 +20,7 @@ const ParamScaledHeight = () => { const onChange = useCallback( (height: number) => { - dispatch(setScaledBoundingBoxDimensions({ height })); + dispatch(scaledBboxChanged({ height })); }, [dispatch] ); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx index 52bc567c183..ed09e4599aa 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx @@ -1,6 +1,6 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { setScaledBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; +import { scaledBboxChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,8 +9,8 @@ const ParamScaledWidth = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const optimalDimension = useAppSelector(selectOptimalDimension); - const isManual = useAppSelector((s) => s.canvasV2.scaledBbox.scaleMethod === 'manual'); - const width = useAppSelector((s) => s.canvasV2.scaledBbox.width); + const isManual = useAppSelector((s) => s.canvasV2.bbox.scaleMethod === 'manual'); + const width = useAppSelector((s) => s.canvasV2.bbox.scaledWidth); const sliderMin = useAppSelector((s) => s.config.sd.scaledBoundingBoxWidth.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.scaledBoundingBoxWidth.sliderMax); const numberInputMin = useAppSelector((s) => s.config.sd.scaledBoundingBoxWidth.numberInputMin); @@ -19,7 +19,7 @@ const ParamScaledWidth = () => { const fineStep = useAppSelector((s) => s.config.sd.scaledBoundingBoxWidth.fineStep); const onChange = useCallback( (width: number) => { - dispatch(setScaledBoundingBoxDimensions({ width })); + dispatch(scaledBboxChanged({ width })); }, [dispatch] ); From 1164763e1a162aa9c3eda977609ca4445179e52c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 16 Jun 2024 20:23:33 +1000 Subject: [PATCH 061/678] refactor(ui): scaled tool preview border --- .../features/controlLayers/konva/constants.ts | 5 ++++ .../features/controlLayers/konva/events.ts | 6 +++-- .../konva/renderers/previewLayer.ts | 27 ++++++++++++++++--- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts b/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts index e7a3e35d2b7..e526e2249e6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts @@ -20,6 +20,11 @@ export const BRUSH_BORDER_INNER_COLOR = 'rgba(0,0,0,1)'; */ export const BRUSH_BORDER_OUTER_COLOR = 'rgba(255,255,255,0.8)'; +/** + * The border width for the brush preview. + */ +export const BRUSH_ERASER_BORDER_WIDTH = 1.5; + /** * The target spacing of individual points of brush strokes, as a percentage of the brush size. */ diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 849a164bb76..f275bf415f5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -1,4 +1,5 @@ import { renderBackgroundLayer } from 'features/controlLayers/konva/renderers/background'; +import { scaleToolPreview } from 'features/controlLayers/konva/renderers/previewLayer'; import { getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util'; import type { BrushLineAddedArg, @@ -420,14 +421,15 @@ export const setStageEventHandlers = ({ stage.position(newPos); setStageAttrs({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale }); renderBackgroundLayer(stage); + scaleToolPreview(stage, getToolState()); } }); //#region dragmove stage.on('dragmove', () => { setStageAttrs({ - x: stage.x(), - y: stage.y(), + x: Math.floor(stage.x()), + y: Math.floor(stage.y()), width: stage.width(), height: stage.height(), scale: stage.scaleX(), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts index 2ca1573041e..eff1e48bc33 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts @@ -1,6 +1,10 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMultiple'; -import { BRUSH_BORDER_INNER_COLOR, BRUSH_BORDER_OUTER_COLOR } from 'features/controlLayers/konva/constants'; +import { + BRUSH_BORDER_INNER_COLOR, + BRUSH_BORDER_OUTER_COLOR, + BRUSH_ERASER_BORDER_WIDTH, +} from 'features/controlLayers/konva/constants'; import { PREVIEW_BRUSH_BORDER_INNER_ID, PREVIEW_BRUSH_BORDER_OUTER_ID, @@ -293,7 +297,7 @@ export const getToolPreviewGroup = (stage: Konva.Stage): Konva.Group => { id: PREVIEW_BRUSH_BORDER_INNER_ID, listening: false, stroke: BRUSH_BORDER_INNER_COLOR, - strokeWidth: 1 / scale, + strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, strokeEnabled: true, }); brushPreviewGroup.add(brushPreviewBorderInner); @@ -301,7 +305,7 @@ export const getToolPreviewGroup = (stage: Konva.Stage): Konva.Group => { id: PREVIEW_BRUSH_BORDER_OUTER_ID, listening: false, stroke: BRUSH_BORDER_OUTER_COLOR, - strokeWidth: 1 / scale, + strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, strokeEnabled: true, }); brushPreviewGroup.add(brushPreviewBorderOuter); @@ -387,6 +391,7 @@ export const renderToolPreview = ( // No need to render the brush preview if the cursor position or color is missing if (cursorPos && (tool === 'brush' || tool === 'eraser')) { + const scale = stage.scaleX(); // Update the fill circle const brushPreviewFill = brushPreviewGroup.findOne(`#${PREVIEW_BRUSH_FILL_ID}`); const radius = (tool === 'brush' ? toolState.brush.width : toolState.eraser.width) / 2; @@ -407,9 +412,11 @@ export const renderToolPreview = ( brushPreviewOuter?.setAttrs({ x: cursorPos.x, y: cursorPos.y, - radius: radius + 1, + radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, }); + scaleToolPreview(stage, toolState); + brushPreviewGroup.visible(true); } else { brushPreviewGroup.visible(false); @@ -430,3 +437,15 @@ export const renderToolPreview = ( } } }; + +export const scaleToolPreview = (stage: Konva.Stage, toolState: CanvasV2State['tool']): void => { + const scale = stage.scaleX(); + const radius = (toolState.selected === 'brush' ? toolState.brush.width : toolState.eraser.width) / 2; + const brushPreviewGroup = stage.findOne(`#${PREVIEW_BRUSH_GROUP_ID}`); + brushPreviewGroup + ?.findOne(`#${PREVIEW_BRUSH_BORDER_INNER_ID}`) + ?.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale); + brushPreviewGroup + ?.findOne(`#${PREVIEW_BRUSH_BORDER_OUTER_ID}`) + ?.setAttrs({ strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale }); +}; From 90b973f5292327c944d55e651e976ba71f423140 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 16 Jun 2024 20:23:42 +1000 Subject: [PATCH 062/678] refactor(ui): better hud --- .../controlLayers/components/HeadsUpDisplay.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx index d99bffcc61b..1172ec2ede6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx @@ -12,19 +12,19 @@ export const HeadsUpDisplay = memo(() => { return ( - + - - - - + + + + ); }); From 31c2b1af19c331faf691fd3e6b3f5e77a766ea28 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 16 Jun 2024 20:28:38 +1000 Subject: [PATCH 063/678] fix(ui): update bg on canvas resize --- .../src/features/controlLayers/components/StageComponent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index a1c67ad86c6..62fedc296d8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -286,12 +286,12 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, height: stage.height(), scale: stage.scaleX(), }); + renderBackgroundLayer(stage); }; const resizeObserver = new ResizeObserver(fitStageToContainer); resizeObserver.observe(container); fitStageToContainer(); - renderBackgroundLayer(stage); return () => { resizeObserver.disconnect(); From 86c785fdedd2371004ce997812e62a3fafb4b03f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 16 Jun 2024 20:29:37 +1000 Subject: [PATCH 064/678] feat(ui): hold shift w/ brush to draw straight line --- .../features/controlLayers/konva/events.ts | 78 ++++++++++++------- 1 file changed, 51 insertions(+), 27 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index f275bf415f5..1d98db35ec0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -187,36 +187,60 @@ export const setStageEventHandlers = ({ setLastMouseDownPos(pos); if (toolState.selected === 'brush') { - onBrushLineAdded( - { - id: selectedEntity.id, - points: [ - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - ], - color: getCurrentFill(), - width: toolState.brush.width, - }, - selectedEntity.type - ); + if (e.evt.shiftKey) { + // Extend the last line straight to the new point + setLastAddedPoint(pos); + onPointAddedToLine( + { + id: selectedEntity.id, + point: [pos.x - selectedEntity.x, pos.y - selectedEntity.y], + }, + selectedEntity.type + ); + } else { + onBrushLineAdded( + { + id: selectedEntity.id, + points: [ + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + ], + color: getCurrentFill(), + width: toolState.brush.width, + }, + selectedEntity.type + ); + } } if (toolState.selected === 'eraser') { - onEraserLineAdded( - { - id: selectedEntity.id, - points: [ - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - ], - width: toolState.eraser.width, - }, - selectedEntity.type - ); + if (e.evt.shiftKey) { + // Extend the last line straight to the new point + setLastAddedPoint(pos); + onPointAddedToLine( + { + id: selectedEntity.id, + point: [pos.x - selectedEntity.x, pos.y - selectedEntity.y], + }, + selectedEntity.type + ); + } else { + onEraserLineAdded( + { + id: selectedEntity.id, + points: [ + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + ], + width: toolState.eraser.width, + }, + selectedEntity.type + ); + } } }); From cc1995170e0a789b840bd2414d9c4ac15f6ce22e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 16 Jun 2024 20:50:57 +1000 Subject: [PATCH 065/678] fix(ui): always use current brush width when making straight lines --- .../features/controlLayers/konva/events.ts | 57 +++++++++++++------ 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 1d98db35ec0..36f03b7535b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -188,15 +188,24 @@ export const setStageEventHandlers = ({ if (toolState.selected === 'brush') { if (e.evt.shiftKey) { - // Extend the last line straight to the new point - setLastAddedPoint(pos); - onPointAddedToLine( - { - id: selectedEntity.id, - point: [pos.x - selectedEntity.x, pos.y - selectedEntity.y], - }, - selectedEntity.type - ); + const lastAddedPoint = getLastAddedPoint(); + // Create a straight line if holding shift + if (lastAddedPoint) { + onBrushLineAdded( + { + id: selectedEntity.id, + points: [ + lastAddedPoint.x - selectedEntity.x, + lastAddedPoint.y - selectedEntity.y, + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + ], + color: getCurrentFill(), + width: toolState.brush.width, + }, + selectedEntity.type + ); + } } else { onBrushLineAdded( { @@ -213,19 +222,28 @@ export const setStageEventHandlers = ({ selectedEntity.type ); } + setLastAddedPoint(pos); } if (toolState.selected === 'eraser') { if (e.evt.shiftKey) { - // Extend the last line straight to the new point - setLastAddedPoint(pos); - onPointAddedToLine( - { - id: selectedEntity.id, - point: [pos.x - selectedEntity.x, pos.y - selectedEntity.y], - }, - selectedEntity.type - ); + // Create a straight line if holding shift + const lastAddedPoint = getLastAddedPoint(); + if (lastAddedPoint) { + onEraserLineAdded( + { + id: selectedEntity.id, + points: [ + lastAddedPoint.x - selectedEntity.x, + lastAddedPoint.y - selectedEntity.y, + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + ], + width: toolState.eraser.width, + }, + selectedEntity.type + ); + } } else { onEraserLineAdded( { @@ -241,6 +259,7 @@ export const setStageEventHandlers = ({ selectedEntity.type ); } + setLastAddedPoint(pos); } }); @@ -341,6 +360,7 @@ export const setStageEventHandlers = ({ }, selectedEntity.type ); + setLastAddedPoint(pos); setIsDrawing(true); } } @@ -364,6 +384,7 @@ export const setStageEventHandlers = ({ }, selectedEntity.type ); + setLastAddedPoint(pos); setIsDrawing(true); } } From 261c24b704e1abb08922992a83fd686de841f410 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 16 Jun 2024 21:12:06 +1000 Subject: [PATCH 066/678] feat(ui): better HUD --- .../features/controlLayers/components/HeadsUpDisplay.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx index 1172ec2ede6..ba588583048 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx @@ -13,13 +13,13 @@ export const HeadsUpDisplay = memo(() => { return ( - + - + From ae81f4e20aec3dfaf462f89cdcf3262c2ee8ce42 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 16 Jun 2024 21:12:58 +1000 Subject: [PATCH 067/678] feat(ui): r to center & fit stage on document --- .../controlLayers/components/StageComponent.tsx | 6 +++++- .../src/features/controlLayers/konva/events.ts | 16 ++++++++++++++++ .../controlLayers/store/canvasV2Slice.ts | 1 + 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index 62fedc296d8..0cbb7fcf456 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -9,6 +9,7 @@ import { debouncedRenderers, renderers as normalRenderers } from 'features/contr import { $bbox, $currentFill, + $document, $isDrawing, $isMouseDown, $lastAddedPoint, @@ -72,6 +73,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, const selectedEntityIdentifier = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier); const maskOpacity = useAppSelector((s) => s.canvasV2.settings.maskOpacity); const bbox = useAppSelector((s) => s.canvasV2.bbox); + const document = useAppSelector((s) => s.canvasV2.document); const lastCursorPos = useStore($lastCursorPos); const lastMouseDownPos = useStore($lastMouseDownPos); const isMouseDown = useStore($isMouseDown); @@ -108,7 +110,8 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, $selectedEntity.set(selectedEntity); $bbox.set({ x: bbox.x, y: bbox.y, width: bbox.width, height: bbox.height }); $currentFill.set(currentFill); - }, [selectedEntity, tool, bbox, currentFill]); + $document.set(document); + }, [selectedEntity, tool, bbox, currentFill, document]); const onPosChanged = useCallback( (arg: PosChangedArg, entityType: CanvasEntity['type']) => { @@ -243,6 +246,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, setLastMouseDownPos: $lastMouseDownPos.set, getSpaceKey: $spaceKey.get, setStageAttrs: $stageAttrs.set, + getDocument: $document.get, onBrushLineAdded, onEraserLineAdded, onPointAddedToLine, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 36f03b7535b..de277d5675d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -45,6 +45,7 @@ type Arg = { setStageAttrs: (attrs: StageAttrs) => void; getSelectedEntity: () => CanvasEntity | null; getSpaceKey: () => boolean; + getDocument: () => CanvasV2State['document']; onBrushLineAdded: (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => void; onEraserLineAdded: (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => void; onPointAddedToLine: (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => void; @@ -144,6 +145,7 @@ export const setStageEventHandlers = ({ setStageAttrs, getSelectedEntity, getSpaceKey, + getDocument, onBrushLineAdded, onEraserLineAdded, onPointAddedToLine, @@ -506,6 +508,20 @@ export const setStageEventHandlers = ({ } else if (e.key === ' ') { setToolBuffer(getToolState().selected); setTool('view'); + } else if (e.key === 'r') { + // Fit & center the document on the stage + const width = stage.width(); + const height = stage.height(); + const document = getDocument(); + const docWidthWithBuffer = document.width + 20; + const docHeightWithBuffer = document.height + 20; + const scale = Math.min(Math.min(width / docWidthWithBuffer, height / docHeightWithBuffer), 1); + const x = (width - docWidthWithBuffer * scale) / 2; + const y = (height - docHeightWithBuffer * scale) / 2; + stage.setAttrs({ x, y, width, height, scaleX: scale, scaleY: scale }); + setStageAttrs({ x, y, width, height, scale }); + scaleToolPreview(stage, getToolState()); + renderBackgroundLayer(stage); } }; window.addEventListener('keydown', onKeyDown); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 4f86bb1fb4f..631cab64b32 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -318,6 +318,7 @@ export const $toolState = atom(deepClone(initialState.too export const $currentFill = atom(DEFAULT_RGBA_COLOR); export const $selectedEntity = atom(null); export const $bbox = atom({ x: 0, y: 0, width: 0, height: 0 }); +export const $document = atom(deepClone(initialState.document)); export const canvasV2PersistConfig: PersistConfig = { name: canvasV2Slice.name, From d2be9df7a7e1a636484ec068091b7e6e64c02235 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 17 Jun 2024 11:59:07 +1000 Subject: [PATCH 068/678] fix(ui): layer is selected when added --- .../features/controlLayers/store/controlAdaptersReducers.ts | 5 ++++- .../src/features/controlLayers/store/ipAdaptersReducers.ts | 5 ++++- .../web/src/features/controlLayers/store/layersReducers.ts | 5 ++++- .../web/src/features/controlLayers/store/regionsReducers.ts | 2 ++ 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts index 0a744c6c28d..5c2813c2d3c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts @@ -44,13 +44,16 @@ export const controlAdaptersReducers = { processorPendingBatchId: null, ...config, }); + state.selectedEntityIdentifier = { type: 'control_adapter', id }; }, prepare: (config: ControlNetConfig | T2IAdapterConfig) => ({ payload: { id: uuidv4(), config }, }), }, caRecalled: (state, action: PayloadAction<{ data: ControlAdapterData }>) => { - state.controlAdapters.push(action.payload.data); + const { data } = action.payload; + state.controlAdapters.push(data); + state.selectedEntityIdentifier = { type: 'control_adapter', id: data.id }; }, caIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts index fb6ff8d987f..a4e950767b6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts @@ -25,11 +25,14 @@ export const ipAdaptersReducers = { ...config, }; state.ipAdapters.push(layer); + state.selectedEntityIdentifier = { type: 'ip_adapter', id }; }, prepare: (config: IPAdapterConfig) => ({ payload: { id: uuidv4(), config } }), }, ipaRecalled: (state, action: PayloadAction<{ data: IPAdapterData }>) => { - state.ipAdapters.push(action.payload.data); + const { data } = action.payload; + state.ipAdapters.push(data); + state.selectedEntityIdentifier = { type: 'ip_adapter', id: data.id }; }, ipaIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index 6b3e58f8a2e..dc06f8d1884 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -38,11 +38,14 @@ export const layersReducers = { x: 0, y: 0, }); + state.selectedEntityIdentifier = { type: 'layer', id }; }, prepare: () => ({ payload: { id: uuidv4() } }), }, layerRecalled: (state, action: PayloadAction<{ data: LayerData }>) => { - state.layers.push(action.payload.data); + const { data } = action.payload; + state.layers.push(data); + state.selectedEntityIdentifier = { type: 'layer', id: data.id }; }, layerIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index 9d75e0e93f4..680eb35a824 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -72,6 +72,7 @@ export const regionsReducers = { imageCache: null, }; state.regions.push(rg); + state.selectedEntityIdentifier = { type: 'regional_guidance', id }; }, prepare: () => ({ payload: { id: uuidv4() } }), }, @@ -89,6 +90,7 @@ export const regionsReducers = { rgRecalled: (state, action: PayloadAction<{ data: RegionalGuidanceData }>) => { const { data } = action.payload; state.regions.push(data); + state.selectedEntityIdentifier = { type: 'regional_guidance', id: data.id }; }, rgIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; From c7af454bf5dc7478d72dbc04e2ba60ecfe7b546e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 17 Jun 2024 12:23:32 +1000 Subject: [PATCH 069/678] refactor(ui): move loras to canvas slice --- .../listeners/modelSelected.ts | 10 +-- .../listeners/modelsLoaded.ts | 9 +- invokeai/frontend/web/src/app/store/store.ts | 7 -- .../controlLayers/store/canvasV2Slice.ts | 10 +++ .../controlLayers/store/lorasReducers.ts | 56 ++++++++++++ .../src/features/controlLayers/store/types.ts | 9 ++ .../src/features/lora/components/LoRACard.tsx | 20 ++--- .../src/features/lora/components/LoRAList.tsx | 7 +- .../features/lora/components/LoRASelect.tsx | 18 ++-- .../web/src/features/lora/store/loraSlice.ts | 87 ------------------- .../metadata/components/MetadataLoRAs.tsx | 2 +- .../nodes/util/graph/generation/addLoRAs.ts | 7 +- .../util/graph/generation/addSDXLLoRAs.ts | 5 +- .../GenerationSettingsAccordion.tsx | 7 +- 14 files changed, 114 insertions(+), 140 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/lorasReducers.ts delete mode 100644 invokeai/frontend/web/src/features/lora/store/loraSlice.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts index e3ebd277ff6..38260b23a20 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts @@ -1,12 +1,10 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { caIsEnabledToggled, modelChanged, vaeSelected } from 'features/controlLayers/store/canvasV2Slice'; -import { loraRemoved } from 'features/lora/store/loraSlice'; +import { caIsEnabledToggled, loraDeleted, modelChanged, vaeSelected } from 'features/controlLayers/store/canvasV2Slice'; import { modelSelected } from 'features/parameters/store/actions'; import { zParameterModel } from 'features/parameters/types/parameterSchemas'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; -import { forEach } from 'lodash-es'; export const addModelSelectedListener = (startAppListening: AppStartListening) => { startAppListening({ @@ -32,9 +30,9 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = let modelsCleared = 0; // handle incompatible loras - forEach(state.lora.loras, (lora, id) => { + state.canvasV2.loras.forEach((lora) => { if (lora.model.base !== newBaseModel) { - dispatch(loraRemoved(id)); + dispatch(loraDeleted({ id: lora.id })); modelsCleared += 1; } }); @@ -68,7 +66,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = } } - dispatch(modelChanged(newModel, state.canvasV2.params.model)); + dispatch(modelChanged({ model: newModel, previousModel: state.canvasV2.params.model })); }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts index b3b4dacaa54..47555a0ef13 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts @@ -6,18 +6,17 @@ import { caModelChanged, heightChanged, ipaModelChanged, + loraDeleted, modelChanged, refinerModelChanged, rgIPAdapterModelChanged, vaeSelected, widthChanged, } from 'features/controlLayers/store/canvasV2Slice'; -import { loraRemoved } from 'features/lora/store/loraSlice'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; import { postProcessingModelChanged, upscaleModelChanged } from 'features/parameters/store/upscaleSlice'; import { zParameterModel, zParameterVAEModel } from 'features/parameters/types/parameterSchemas'; import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension'; -import { forEach } from 'lodash-es'; import type { Logger } from 'roarr'; import { modelConfigsAdapterSelectors, modelsApi } from 'services/api/endpoints/models'; import type { AnyModelConfig } from 'services/api/types'; @@ -161,15 +160,13 @@ const handleVAEModels: ModelHandler = (models, state, dispatch, log) => { }; const handleLoRAModels: ModelHandler = (models, state, dispatch, _log) => { - const loras = state.lora.loras; const loraModels = models.filter(isLoRAModelConfig); - - forEach(loras, (lora, id) => { + state.canvasV2.loras.forEach((lora) => { const isLoRAAvailable = loraModels.some((m) => m.key === lora.model.key); if (isLoRAAvailable) { return; } - dispatch(loraRemoved(id)); + dispatch(loraDeleted({ id: lora.id })); }); }; diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 95de810cdd7..f41d6273e9b 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -10,7 +10,6 @@ import { deleteImageModalSlice } from 'features/deleteImageModal/store/slice'; import { dynamicPromptsPersistConfig, dynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/gallerySlice'; import { hrfPersistConfig, hrfSlice } from 'features/hrf/store/hrfSlice'; -import { loraPersistConfig, loraSlice } from 'features/lora/store/loraSlice'; import { modelManagerV2PersistConfig, modelManagerV2Slice } from 'features/modelManagerV2/store/modelManagerV2Slice'; import { nodesPersistConfig, nodesSlice, nodesUndoableConfig } from 'features/nodes/store/nodesSlice'; import { workflowSettingsPersistConfig, workflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice'; @@ -40,7 +39,6 @@ import { listenerMiddleware } from './middleware/listenerMiddleware'; const allReducers = { [api.reducerPath]: api.reducer, [gallerySlice.name]: gallerySlice.reducer, - // [generationSlice.name]: generationSlice.reducer, [nodesSlice.name]: undoable(nodesSlice.reducer, nodesUndoableConfig), [systemSlice.name]: systemSlice.reducer, [configSlice.name]: configSlice.reducer, @@ -48,9 +46,7 @@ const allReducers = { [dynamicPromptsSlice.name]: dynamicPromptsSlice.reducer, [deleteImageModalSlice.name]: deleteImageModalSlice.reducer, [changeBoardModalSlice.name]: changeBoardModalSlice.reducer, - [loraSlice.name]: loraSlice.reducer, [modelManagerV2Slice.name]: modelManagerV2Slice.reducer, - // [sdxlSlice.name]: sdxlSlice.reducer, [queueSlice.name]: queueSlice.reducer, [workflowSlice.name]: workflowSlice.reducer, [hrfSlice.name]: hrfSlice.reducer, @@ -88,14 +84,11 @@ export type PersistConfig = { const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = { [galleryPersistConfig.name]: galleryPersistConfig, - // [generationPersistConfig.name]: generationPersistConfig, [nodesPersistConfig.name]: nodesPersistConfig, [systemPersistConfig.name]: systemPersistConfig, [workflowPersistConfig.name]: workflowPersistConfig, [uiPersistConfig.name]: uiPersistConfig, [dynamicPromptsPersistConfig.name]: dynamicPromptsPersistConfig, - // [sdxlPersistConfig.name]: sdxlPersistConfig, - [loraPersistConfig.name]: loraPersistConfig, [modelManagerV2PersistConfig.name]: modelManagerV2PersistConfig, [hrfPersistConfig.name]: hrfPersistConfig, [canvasV2PersistConfig.name]: canvasV2PersistConfig, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 631cab64b32..c6f6a2bd7a1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -8,6 +8,7 @@ import { compositingReducers } from 'features/controlLayers/store/compositingRed import { controlAdaptersReducers } from 'features/controlLayers/store/controlAdaptersReducers'; import { ipAdaptersReducers } from 'features/controlLayers/store/ipAdaptersReducers'; import { layersReducers } from 'features/controlLayers/store/layersReducers'; +import { lorasReducers } from 'features/controlLayers/store/lorasReducers'; import { paramsReducers } from 'features/controlLayers/store/paramsReducers'; import { regionsReducers } from 'features/controlLayers/store/regionsReducers'; import { settingsReducers } from 'features/controlLayers/store/settingsReducers'; @@ -27,6 +28,7 @@ const initialState: CanvasV2State = { controlAdapters: [], ipAdapters: [], regions: [], + loras: [], tool: { selected: 'bbox', selectedBuffer: null, @@ -113,6 +115,7 @@ export const canvasV2Slice = createSlice({ ...ipAdaptersReducers, ...controlAdaptersReducers, ...regionsReducers, + ...lorasReducers, ...paramsReducers, ...compositingReducers, ...settingsReducers, @@ -287,6 +290,13 @@ export const { setRefinerNegativeAestheticScore, setRefinerStart, modelChanged, + // LoRAs + loraAdded, + loraRecalled, + loraDeleted, + loraWeightChanged, + loraIsEnabledChanged, + loraAllDeleted, } = canvasV2Slice.actions; export const selectCanvasV2Slice = (state: RootState) => state.canvasV2; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/lorasReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/lorasReducers.ts new file mode 100644 index 00000000000..bca3ccdd9e2 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/lorasReducers.ts @@ -0,0 +1,56 @@ +import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import type { CanvasV2State, LoRA } from 'features/controlLayers/store/types'; +import { zModelIdentifierField } from 'features/nodes/types/common'; +import type { LoRAModelConfig } from 'services/api/types'; +import { assert } from 'tsafe'; +import { v4 as uuidv4 } from 'uuid'; + +export const defaultLoRAConfig: Pick = { + weight: 0.75, + isEnabled: true, +}; + +export const selectLoRA = (state: CanvasV2State, id: string) => state.loras.find((lora) => lora.id === id); +export const selectLoRAOrThrow = (state: CanvasV2State, id: string) => { + const lora = selectLoRA(state, id); + assert(lora, `LoRA with id ${id} not found`); + return lora; +}; + +export const lorasReducers = { + loraAdded: { + reducer: (state, action: PayloadAction<{ model: LoRAModelConfig; id: string }>) => { + const { model, id } = action.payload; + const parsedModel = zModelIdentifierField.parse(model); + state.loras.push({ ...defaultLoRAConfig, model: parsedModel, id }); + }, + prepare: (payload: { model: LoRAModelConfig }) => ({ payload: { ...payload, id: uuidv4() } }), + }, + loraRecalled: (state, action: PayloadAction<{ lora: LoRA }>) => { + const { lora } = action.payload; + state.loras.push(lora); + }, + loraDeleted: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + state.loras = state.loras.filter((lora) => lora.id !== id); + }, + loraWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { + const { id, weight } = action.payload; + const lora = selectLoRA(state, id); + if (!lora) { + return; + } + lora.weight = weight; + }, + loraIsEnabledChanged: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => { + const { id, isEnabled } = action.payload; + const lora = selectLoRA(state, id); + if (!lora) { + return; + } + lora.isEnabled = isEnabled; + }, + loraAllDeleted: (state) => { + state.loras = []; + }, +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 14d4725fb63..9038682c924 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -6,6 +6,7 @@ import type { ParameterCFGRescaleMultiplier, ParameterCFGScale, ParameterHeight, + ParameterLoRAModel, ParameterMaskBlurMethod, ParameterModel, ParameterNegativePrompt, @@ -799,6 +800,13 @@ export type Dimensions = { height: number; }; +export type LoRA = { + id: string; + isEnabled: boolean; + model: ParameterLoRAModel; + weight: number; +}; + export type CanvasV2State = { _version: 3; selectedEntityIdentifier: CanvasEntityIdentifier | null; @@ -806,6 +814,7 @@ export type CanvasV2State = { controlAdapters: ControlAdapterData[]; ipAdapters: IPAdapterData[]; regions: RegionalGuidanceData[]; + loras: LoRA[]; tool: { selected: Tool; selectedBuffer: Tool | null; diff --git a/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx b/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx index f7261b46089..54ad2cf9879 100644 --- a/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx +++ b/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx @@ -11,8 +11,8 @@ import { } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import type { LoRA } from 'features/lora/store/loraSlice'; -import { loraIsEnabledChanged, loraRemoved, loraWeightChanged } from 'features/lora/store/loraSlice'; +import { loraDeleted, loraIsEnabledChanged, loraWeightChanged } from 'features/controlLayers/store/canvasV2Slice'; +import type { LoRA } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { PiTrashSimpleBold } from 'react-icons/pi'; import { useGetModelConfigQuery } from 'services/api/endpoints/models'; @@ -21,6 +21,8 @@ type LoRACardProps = { lora: LoRA; }; +const marks = [-1, 0, 1, 2]; + export const LoRACard = memo((props: LoRACardProps) => { const { lora } = props; const dispatch = useAppDispatch(); @@ -28,18 +30,18 @@ export const LoRACard = memo((props: LoRACardProps) => { const handleChange = useCallback( (v: number) => { - dispatch(loraWeightChanged({ key: lora.model.key, weight: v })); + dispatch(loraWeightChanged({ id: lora.id, weight: v })); }, - [dispatch, lora.model.key] + [dispatch, lora.id] ); const handleSetLoraToggle = useCallback(() => { - dispatch(loraIsEnabledChanged({ key: lora.model.key, isEnabled: !lora.isEnabled })); - }, [dispatch, lora.model.key, lora.isEnabled]); + dispatch(loraIsEnabledChanged({ id: lora.id, isEnabled: !lora.isEnabled })); + }, [dispatch, lora.id, lora.isEnabled]); const handleRemoveLora = useCallback(() => { - dispatch(loraRemoved(lora.model.key)); - }, [dispatch, lora.model.key]); + dispatch(loraDeleted({ id: lora.id })); + }, [dispatch, lora.id]); return ( @@ -90,5 +92,3 @@ export const LoRACard = memo((props: LoRACardProps) => { }); LoRACard.displayName = 'LoRACard'; - -const marks = [-1, 0, 1, 2]; diff --git a/invokeai/frontend/web/src/features/lora/components/LoRAList.tsx b/invokeai/frontend/web/src/features/lora/components/LoRAList.tsx index 68d259a852c..b5a2b1bba97 100644 --- a/invokeai/frontend/web/src/features/lora/components/LoRAList.tsx +++ b/invokeai/frontend/web/src/features/lora/components/LoRAList.tsx @@ -1,12 +1,11 @@ import { Flex } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { LoRACard } from 'features/lora/components/LoRACard'; -import { selectLoraSlice } from 'features/lora/store/loraSlice'; -import { map } from 'lodash-es'; import { memo } from 'react'; -const selectLoRAsArray = createMemoizedSelector(selectLoraSlice, (lora) => map(lora.loras)); +const selectLoRAsArray = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => canvasV2.loras); export const LoRAList = memo(() => { const lorasArray = useAppSelector(selectLoRAsArray); @@ -18,7 +17,7 @@ export const LoRAList = memo(() => { return ( {lorasArray.map((lora) => ( - + ))} ); diff --git a/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx b/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx index 3c3e0375e24..8296031418f 100644 --- a/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx +++ b/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx @@ -4,34 +4,34 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; -import { loraAdded, selectLoraSlice } from 'features/lora/store/loraSlice'; +import { loraAdded, selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useLoRAModels } from 'services/api/hooks/modelsByType'; import type { LoRAModelConfig } from 'services/api/types'; -const selectAddedLoRAs = createMemoizedSelector(selectLoraSlice, (lora) => lora.loras); +const selectLoRAs = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => canvasV2.loras); const LoRASelect = () => { const dispatch = useAppDispatch(); const [modelConfigs, { isLoading }] = useLoRAModels(); const { t } = useTranslation(); - const addedLoRAs = useAppSelector(selectAddedLoRAs); + const addedLoRAs = useAppSelector(selectLoRAs); const currentBaseModel = useAppSelector((s) => s.canvasV2.params.model?.base); - const getIsDisabled = (lora: LoRAModelConfig): boolean => { - const isCompatible = currentBaseModel === lora.base; - const isAdded = Boolean(addedLoRAs[lora.key]); + const getIsDisabled = (model: LoRAModelConfig): boolean => { + const isCompatible = currentBaseModel === model.base; + const isAdded = Boolean(addedLoRAs.find((lora) => lora.model.key === model.key)); const hasMainModel = Boolean(currentBaseModel); return !hasMainModel || !isCompatible || isAdded; }; const _onChange = useCallback( - (lora: LoRAModelConfig | null) => { - if (!lora) { + (model: LoRAModelConfig | null) => { + if (!model) { return; } - dispatch(loraAdded(lora)); + dispatch(loraAdded({ model })); }, [dispatch] ); diff --git a/invokeai/frontend/web/src/features/lora/store/loraSlice.ts b/invokeai/frontend/web/src/features/lora/store/loraSlice.ts deleted file mode 100644 index 2382e9ffe4c..00000000000 --- a/invokeai/frontend/web/src/features/lora/store/loraSlice.ts +++ /dev/null @@ -1,87 +0,0 @@ -import type { PayloadAction } from '@reduxjs/toolkit'; -import { createSlice } from '@reduxjs/toolkit'; -import type { PersistConfig, RootState } from 'app/store/store'; -import { deepClone } from 'common/util/deepClone'; -import { zModelIdentifierField } from 'features/nodes/types/common'; -import type { ParameterLoRAModel } from 'features/parameters/types/parameterSchemas'; -import type { LoRAModelConfig } from 'services/api/types'; - -export type LoRA = { - model: ParameterLoRAModel; - weight: number; - isEnabled?: boolean; -}; - -export const defaultLoRAConfig: Pick = { - weight: 0.75, - isEnabled: true, -}; - -type LoraState = { - _version: 2; - loras: Record; -}; - -const initialLoraState: LoraState = { - _version: 2, - loras: {}, -}; - -export const loraSlice = createSlice({ - name: 'lora', - initialState: initialLoraState, - reducers: { - loraAdded: (state, action: PayloadAction) => { - const model = zModelIdentifierField.parse(action.payload); - state.loras[model.key] = { ...defaultLoRAConfig, model }; - }, - loraRecalled: (state, action: PayloadAction) => { - state.loras[action.payload.model.key] = action.payload; - }, - loraRemoved: (state, action: PayloadAction) => { - const key = action.payload; - delete state.loras[key]; - }, - loraWeightChanged: (state, action: PayloadAction<{ key: string; weight: number }>) => { - const { key, weight } = action.payload; - const lora = state.loras[key]; - if (!lora) { - return; - } - lora.weight = weight; - }, - loraIsEnabledChanged: (state, action: PayloadAction<{ key: string; isEnabled: boolean }>) => { - const { key, isEnabled } = action.payload; - const lora = state.loras[key]; - if (!lora) { - return; - } - lora.isEnabled = isEnabled; - }, - lorasReset: () => deepClone(initialLoraState), - }, -}); - -export const { loraAdded, loraRemoved, loraWeightChanged, loraIsEnabledChanged, loraRecalled, lorasReset } = - loraSlice.actions; - -export const selectLoraSlice = (state: RootState) => state.lora; - -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrateLoRAState = (state: any): any => { - if (!('_version' in state)) { - state._version = 1; - } - if (state._version === 1) { - // Model type has changed, so we need to reset the state - too risky to migrate - state = deepClone(initialLoraState); - } - return state; -}; - -export const loraPersistConfig: PersistConfig = { - name: loraSlice.name, - initialState: initialLoraState, - migrate: migrateLoRAState, - persistDenylist: [], -}; diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataLoRAs.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataLoRAs.tsx index 7e78985c497..921306fc93c 100644 --- a/invokeai/frontend/web/src/features/metadata/components/MetadataLoRAs.tsx +++ b/invokeai/frontend/web/src/features/metadata/components/MetadataLoRAs.tsx @@ -1,4 +1,4 @@ -import type { LoRA } from 'features/lora/store/loraSlice'; +import type { LoRA } from 'features/controlLayers/store/types'; import { MetadataItemView } from 'features/metadata/components/MetadataItemView'; import type { MetadataHandlers } from 'features/metadata/types'; import { handlers } from 'features/metadata/util/handlers'; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLoRAs.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLoRAs.ts index 3335e0f80d6..b078dfcdfcc 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLoRAs.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLoRAs.ts @@ -2,7 +2,6 @@ import type { RootState } from 'app/store/store'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { LORA_LOADER } from 'features/nodes/util/graph/constants'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; -import { filter, size } from 'lodash-es'; import type { Invocation, S } from 'services/api/types'; export const addLoRAs = ( @@ -15,8 +14,10 @@ export const addLoRAs = ( posCond: Invocation<'compel'>, negCond: Invocation<'compel'> ): void => { - const enabledLoRAs = filter(state.lora.loras, (l) => l.isEnabled ?? false); - const loraCount = size(enabledLoRAs); + const enabledLoRAs = state.canvasV2.loras.filter( + (l) => l.isEnabled && (l.model.base === 'sd-1' || l.model.base === 'sd-2') + ); + const loraCount = enabledLoRAs.length; if (loraCount === 0) { return; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts index 3125ab5ac3e..d7377da4b06 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts @@ -2,7 +2,6 @@ import type { RootState } from 'app/store/store'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { LORA_LOADER } from 'features/nodes/util/graph/constants'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; -import { filter, size } from 'lodash-es'; import type { Invocation, S } from 'services/api/types'; export const addSDXLLoRas = ( @@ -14,8 +13,8 @@ export const addSDXLLoRas = ( posCond: Invocation<'sdxl_compel_prompt'>, negCond: Invocation<'sdxl_compel_prompt'> ): void => { - const enabledLoRAs = filter(state.lora.loras, (l) => l.isEnabled ?? false); - const loraCount = size(enabledLoRAs); + const enabledLoRAs = state.canvasV2.loras.filter((l) => l.isEnabled && l.model.base === 'sdxl'); + const loraCount = enabledLoRAs.length; if (loraCount === 0) { return; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx index a809a355873..634b8a6bd3b 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx @@ -3,9 +3,9 @@ import { Box, Expander, Flex, FormControlGroup, StandaloneAccordion } from '@inv import { EMPTY_ARRAY } from 'app/store/constants'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { LoRAList } from 'features/lora/components/LoRAList'; import LoRASelect from 'features/lora/components/LoRASelect'; -import { selectLoraSlice } from 'features/lora/store/loraSlice'; import ParamCFGScale from 'features/parameters/components/Core/ParamCFGScale'; import ParamScheduler from 'features/parameters/components/Core/ParamScheduler'; import ParamSteps from 'features/parameters/components/Core/ParamSteps'; @@ -15,7 +15,6 @@ import { UseDefaultSettingsButton } from 'features/parameters/components/MainMod import { useExpanderToggle } from 'features/settingsAccordions/hooks/useExpanderToggle'; import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { filter } from 'lodash-es'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelectedModelConfig } from 'services/api/hooks/useSelectedModelConfig'; @@ -30,8 +29,8 @@ export const GenerationSettingsAccordion = memo(() => { const activeTabName = useAppSelector(activeTabNameSelector); const selectBadges = useMemo( () => - createMemoizedSelector(selectLoraSlice, (lora) => { - const enabledLoRAsCount = filter(lora.loras, (l) => !!l.isEnabled).length; + createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { + const enabledLoRAsCount = canvasV2.loras.filter((l) => l.isEnabled).length; const loraTabBadges = enabledLoRAsCount ? [`${enabledLoRAsCount} ${t('models.concepts')}`] : EMPTY_ARRAY; const accordionBadges = modelConfig ? [modelConfig.name, modelConfig.base] : EMPTY_ARRAY; return { loraTabBadges, accordionBadges }; From 033e2b27a9e40cedd7468bef3cbb689a7ad25371 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 17 Jun 2024 12:26:45 +1000 Subject: [PATCH 070/678] chore(ui): lint --- .../controlLayers/components/IPAdapter/IPAImagePreview.tsx | 2 +- .../features/gallery/components/ImageViewer/ImageComparison.tsx | 2 +- .../gallery/components/ImageViewer/ImageComparisonHover.tsx | 2 +- .../gallery/components/ImageViewer/ImageComparisonSlider.tsx | 2 +- .../web/src/features/gallery/components/ImageViewer/common.ts | 2 +- .../web/src/features/nodes/util/graph/generation/addRegions.ts | 2 +- .../components/Canvas/BoundingBox/ParamBoundingBoxWidth.tsx | 2 +- .../web/src/features/parameters/components/Core/ParamHeight.tsx | 2 +- .../web/src/features/parameters/components/Core/ParamWidth.tsx | 2 +- .../parameters/components/ImageSize/ImageSizeContext.ts | 2 +- .../parameters/components/ImageSize/SetOptimalSizeButton.tsx | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx index c2b3ec48d9c..9de19ea3efe 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx @@ -4,10 +4,10 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; import { heightChanged, widthChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import type { ImageWithDims } from 'features/controlLayers/store/types'; import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; -import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { memo, useCallback, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparison.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparison.tsx index 5607d7dd4fd..ff97a5a687a 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparison.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparison.tsx @@ -1,6 +1,6 @@ import { useAppSelector } from 'app/store/storeHooks'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; -import type { Dimensions } from 'features/canvas/store/canvasTypes'; +import type { Dimensions } from 'features/controlLayers/store/types'; import { selectComparisonImages } from 'features/gallery/components/ImageViewer/common'; import { ImageComparisonHover } from 'features/gallery/components/ImageViewer/ImageComparisonHover'; import { ImageComparisonSideBySide } from 'features/gallery/components/ImageViewer/ImageComparisonSideBySide'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonHover.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonHover.tsx index 9bff769cf03..11f9da928be 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonHover.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonHover.tsx @@ -2,8 +2,8 @@ import { Box, Flex, Image } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { useBoolean } from 'common/hooks/useBoolean'; import { preventDefault } from 'common/util/stopPropagation'; -import type { Dimensions } from 'features/canvas/store/canvasTypes'; import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants'; +import type { Dimensions } from 'features/controlLayers/store/types'; import { ImageComparisonLabel } from 'features/gallery/components/ImageViewer/ImageComparisonLabel'; import { memo, useMemo, useRef } from 'react'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx index 3cdf7c48d55..00b25b1b326 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx @@ -1,8 +1,8 @@ import { Box, Flex, Icon, Image } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { preventDefault } from 'common/util/stopPropagation'; -import type { Dimensions } from 'features/canvas/store/canvasTypes'; import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants'; +import type { Dimensions } from 'features/controlLayers/store/types'; import { ImageComparisonLabel } from 'features/gallery/components/ImageViewer/ImageComparisonLabel'; import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { PiCaretLeftBold, PiCaretRightBold } from 'react-icons/pi'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/common.ts b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/common.ts index 7f29e906efe..ac3d7b172b4 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/common.ts +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/common.ts @@ -1,5 +1,5 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import type { Dimensions } from 'features/canvas/store/canvasTypes'; +import type { Dimensions } from 'features/controlLayers/store/types'; import { selectGallerySlice } from 'features/gallery/store/gallerySlice'; import type { ComparisonFit } from 'features/gallery/store/types'; import type { ImageDTO } from 'services/api/types'; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts index b1037f0b1f8..772770c0213 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts @@ -1,9 +1,9 @@ import { getStore } from 'app/store/nanostores/store'; import { deepClone } from 'common/util/deepClone'; import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; -import { blobToDataURL } from "features/controlLayers/konva/util"; import { RG_LAYER_NAME } from 'features/controlLayers/konva/naming'; import { renderers } from 'features/controlLayers/konva/renderers/layers'; +import { blobToDataURL } from "features/controlLayers/konva/util"; import { rgMaskImageUploaded } from 'features/controlLayers/store/canvasV2Slice'; import type { Dimensions, IPAdapterData, RegionalGuidanceData } from 'features/controlLayers/store/types'; import { diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxWidth.tsx index 64a99175c79..ae03d448b86 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxWidth.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxWidth.tsx @@ -1,8 +1,8 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; -import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext'; import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; +import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx index 552402a8bcd..63fc4ed632a 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx @@ -1,8 +1,8 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext'; import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; +import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamWidth.tsx index 36fb4f849b5..f8dea1f2b57 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamWidth.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamWidth.tsx @@ -1,8 +1,8 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext'; import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; +import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/ImageSizeContext.ts b/invokeai/frontend/web/src/features/parameters/components/ImageSize/ImageSizeContext.ts index e66a1b83326..fb2a3d9eeb8 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/ImageSizeContext.ts +++ b/invokeai/frontend/web/src/features/parameters/components/ImageSize/ImageSizeContext.ts @@ -1,9 +1,9 @@ import { useAppSelector } from 'app/store/storeHooks'; import { roundToMultiple } from 'common/util/roundDownToMultiple'; +import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; import { ASPECT_RATIO_MAP, initialAspectRatioState } from 'features/parameters/components/ImageSize/constants'; import type { AspectRatioID, AspectRatioState } from 'features/parameters/components/ImageSize/types'; -import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { createContext, useCallback, useContext, useMemo } from 'react'; export type ImageSizeContextInnerValue = { diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/SetOptimalSizeButton.tsx b/invokeai/frontend/web/src/features/parameters/components/ImageSize/SetOptimalSizeButton.tsx index dcad2918f17..4ebdf583d05 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/SetOptimalSizeButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ImageSize/SetOptimalSizeButton.tsx @@ -1,7 +1,7 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; -import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext'; import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; +import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext'; import { getIsSizeTooLarge, getIsSizeTooSmall } from 'features/parameters/util/optimalDimension'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; From 2a0d16d57d7b09dbffc539978411df03ffe035e8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 17 Jun 2024 12:33:35 +1000 Subject: [PATCH 071/678] fix(ui): move lora followup fixes --- .../web/src/features/metadata/util/handlers.ts | 3 +-- .../web/src/features/metadata/util/parsers.ts | 5 ++--- .../web/src/features/metadata/util/recallers.ts | 11 ++++++----- .../web/src/features/metadata/util/validators.ts | 3 +-- .../web/src/features/prompt/PromptTriggerSelect.tsx | 2 +- .../components/SettingsModal/useClearIntermediates.ts | 1 + 6 files changed, 12 insertions(+), 13 deletions(-) diff --git a/invokeai/frontend/web/src/features/metadata/util/handlers.ts b/invokeai/frontend/web/src/features/metadata/util/handlers.ts index d0f151118c5..8c6f5170059 100644 --- a/invokeai/frontend/web/src/features/metadata/util/handlers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/handlers.ts @@ -2,8 +2,7 @@ import { getStore } from 'app/store/nanostores/store'; import { deepClone } from 'common/util/deepClone'; import { objectKeys } from 'common/util/objectKeys'; import { shouldConcatPromptsChanged } from 'features/controlLayers/store/canvasV2Slice'; -import type { LayerData } from 'features/controlLayers/store/types'; -import type { LoRA } from 'features/lora/store/loraSlice'; +import type { LayerData, LoRA } from 'features/controlLayers/store/types'; import type { AnyControlAdapterConfigMetadata, BuildMetadataHandlers, diff --git a/invokeai/frontend/web/src/features/metadata/util/parsers.ts b/invokeai/frontend/web/src/features/metadata/util/parsers.ts index 734b457c82e..909296c5121 100644 --- a/invokeai/frontend/web/src/features/metadata/util/parsers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/parsers.ts @@ -1,5 +1,6 @@ import { getCAId, getImageObjectId, getIPAId, getLayerId } from 'features/controlLayers/konva/naming'; -import type { ControlAdapterData, IPAdapterData, LayerData } from 'features/controlLayers/store/types'; +import { defaultLoRAConfig } from 'features/controlLayers/store/lorasReducers'; +import type { ControlAdapterData, IPAdapterData, LayerData, LoRA } from 'features/controlLayers/store/types'; import { CA_PROCESSOR_DATA, imageDTOToImageWithDims, @@ -9,8 +10,6 @@ import { isProcessorTypeV2, zLayerData, } from 'features/controlLayers/store/types'; -import type { LoRA } from 'features/lora/store/loraSlice'; -import { defaultLoRAConfig } from 'features/lora/store/loraSlice'; import type { ControlNetConfigMetadata, IPAdapterConfigMetadata, diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index 0f3399833ef..1ab3da59ae4 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -16,6 +16,8 @@ import { ipaRecalled, layerAllDeleted, layerRecalled, + loraAllDeleted, + loraRecalled, negativePrompt2Changed, negativePromptChanged, positivePrompt2Changed, @@ -41,11 +43,10 @@ import type { ControlAdapterData, IPAdapterData, LayerData, + LoRA, RegionalGuidanceData, } from 'features/controlLayers/store/types'; import { setHrfEnabled, setHrfMethod, setHrfStrength } from 'features/hrf/store/hrfSlice'; -import type { LoRA } from 'features/lora/store/loraSlice'; -import { loraRecalled, lorasReset } from 'features/lora/store/loraSlice'; import type { ControlNetConfigMetadata, IPAdapterConfigMetadata, @@ -186,17 +187,17 @@ const recallVAE: MetadataRecallFunc = (vae }; const recallLoRA: MetadataRecallFunc = (lora) => { - getStore().dispatch(loraRecalled(lora)); + getStore().dispatch(loraRecalled({ lora })); }; const recallAllLoRAs: MetadataRecallFunc = (loras) => { const { dispatch } = getStore(); - dispatch(lorasReset()); + dispatch(loraAllDeleted()); if (!loras.length) { return; } loras.forEach((lora) => { - dispatch(loraRecalled(lora)); + dispatch(loraRecalled({ lora })); }); }; diff --git a/invokeai/frontend/web/src/features/metadata/util/validators.ts b/invokeai/frontend/web/src/features/metadata/util/validators.ts index 6547e01ac44..f0e5c84f397 100644 --- a/invokeai/frontend/web/src/features/metadata/util/validators.ts +++ b/invokeai/frontend/web/src/features/metadata/util/validators.ts @@ -1,6 +1,5 @@ import { getStore } from 'app/store/nanostores/store'; -import type { LayerData } from 'features/controlLayers/store/types'; -import type { LoRA } from 'features/lora/store/loraSlice'; +import type { LayerData, LoRA } from 'features/controlLayers/store/types'; import type { ControlNetConfigMetadata, IPAdapterConfigMetadata, diff --git a/invokeai/frontend/web/src/features/prompt/PromptTriggerSelect.tsx b/invokeai/frontend/web/src/features/prompt/PromptTriggerSelect.tsx index e8f0032d101..493979f5047 100644 --- a/invokeai/frontend/web/src/features/prompt/PromptTriggerSelect.tsx +++ b/invokeai/frontend/web/src/features/prompt/PromptTriggerSelect.tsx @@ -18,7 +18,7 @@ export const PromptTriggerSelect = memo(({ onSelect, onClose }: PromptTriggerSel const { t } = useTranslation(); const mainModel = useAppSelector((s) => s.canvasV2.params.model); - const addedLoRAs = useAppSelector((s) => s.lora.loras); + const addedLoRAs = useAppSelector((s) => s.canvasV2.loras); const { data: mainModelConfig, isLoading: isLoadingMainModelConfig } = useGetModelConfigQuery( mainModel?.key ?? skipToken ); diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/useClearIntermediates.ts b/invokeai/frontend/web/src/features/system/components/SettingsModal/useClearIntermediates.ts index 6302a16ba55..4df8d20e294 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/useClearIntermediates.ts +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/useClearIntermediates.ts @@ -38,6 +38,7 @@ export const useClearIntermediates = (shouldShowClearIntermediates: boolean): Us _clearIntermediates() .unwrap() .then((clearedCount) => { + // TODO(psyche): Do we need to reset things w/ canvas v2? // dispatch(controlAdaptersReset()); // dispatch(resetCanvas()); toast({ From 9ea7c18d666be9ad06b18b1a82a463781a3ed8d5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 17 Jun 2024 14:16:09 +1000 Subject: [PATCH 072/678] fix(ui): canvas entity ids getting clobbered --- .../controlLayers/hooks/addLayerHooks.ts | 33 ++++++++----------- .../store/controlAdaptersReducers.ts | 4 +-- .../controlLayers/store/ipAdaptersReducers.ts | 2 +- .../src/features/controlLayers/store/types.ts | 14 -------- 4 files changed, 17 insertions(+), 36 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts index 7c2b1813d7e..803369ad15c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts @@ -1,10 +1,11 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { deepClone } from 'common/util/deepClone'; import { caAdded, ipaAdded, rgIPAdapterAdded } from 'features/controlLayers/store/canvasV2Slice'; import { - buildControlNet, - buildIPAdapter, - buildT2IAdapter, CA_PROCESSOR_DATA, + initialControlNetV2, + initialIPAdapterV2, + initialT2IAdapterV2, isProcessorTypeV2, } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; @@ -28,19 +29,15 @@ export const useAddCALayer = () => { return; } - const id = uuidv4(); const defaultPreprocessor = model.default_settings?.preprocessor; const processorConfig = isProcessorTypeV2(defaultPreprocessor) ? CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults(baseModel) : null; - const builder = model.type === 'controlnet' ? buildControlNet : buildT2IAdapter; - const controlAdapter = builder(id, { - model: zModelIdentifierField.parse(model), - processorConfig, - }); + const initialConfig = deepClone(model.type === 'controlnet' ? initialControlNetV2 : initialT2IAdapterV2); + const config = { ...initialConfig, model: zModelIdentifierField.parse(model), processorConfig }; - dispatch(caAdded(controlAdapter)); + dispatch(caAdded({ config })); }, [dispatch, model, baseModel]); return [addCALayer, isDisabled] as const; @@ -60,11 +57,10 @@ export const useAddIPALayer = () => { if (!model) { return; } - const id = uuidv4(); - const ipAdapter = buildIPAdapter(id, { - model: zModelIdentifierField.parse(model), - }); - dispatch(ipaAdded(ipAdapter)); + + const initialConfig = deepClone(initialIPAdapterV2); + const config = { ...initialConfig, model: zModelIdentifierField.parse(model) }; + dispatch(ipaAdded({ config })); }, [dispatch, model]); return [addIPALayer, isDisabled] as const; @@ -84,10 +80,9 @@ export const useAddIPAdapterToRGLayer = (id: string) => { if (!model) { return; } - const ipAdapter = buildIPAdapter(uuidv4(), { - model: zModelIdentifierField.parse(model), - }); - dispatch(rgIPAdapterAdded({ id, ipAdapter: { ...ipAdapter, id: uuidv4(), type: 'ip_adapter', isEnabled: true } })); + const initialConfig = deepClone(initialIPAdapterV2); + const config = { ...initialConfig, model: zModelIdentifierField.parse(model) }; + dispatch(rgIPAdapterAdded({ id, ipAdapter: { ...config, id: uuidv4(), type: 'ip_adapter', isEnabled: true } })); }, [model, dispatch, id]); return [addIPAdapter, isDisabled] as const; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts index 5c2813c2d3c..c48d023a0d4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts @@ -46,8 +46,8 @@ export const controlAdaptersReducers = { }); state.selectedEntityIdentifier = { type: 'control_adapter', id }; }, - prepare: (config: ControlNetConfig | T2IAdapterConfig) => ({ - payload: { id: uuidv4(), config }, + prepare: (payload: { config: ControlNetConfig | T2IAdapterConfig }) => ({ + payload: { id: uuidv4(), ...payload }, }), }, caRecalled: (state, action: PayloadAction<{ data: ControlAdapterData }>) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts index a4e950767b6..66761c52eb2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts @@ -27,7 +27,7 @@ export const ipAdaptersReducers = { state.ipAdapters.push(layer); state.selectedEntityIdentifier = { type: 'ip_adapter', id }; }, - prepare: (config: IPAdapterConfig) => ({ payload: { id: uuidv4(), config } }), + prepare: (payload: { config: IPAdapterConfig }) => ({ payload: { id: uuidv4(), ...payload } }), }, ipaRecalled: (state, action: PayloadAction<{ data: IPAdapterData }>) => { const { data } = action.payload; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 9038682c924..f92b842cbb2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,4 +1,3 @@ -import { deepClone } from 'common/util/deepClone'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; import type { @@ -28,7 +27,6 @@ import { zParameterPositivePrompt, } from 'features/parameters/types/parameterSchemas'; import type { IRect } from 'konva/lib/types'; -import { merge } from 'lodash-es'; import type { AnyInvocation, BaseModelType, @@ -758,18 +756,6 @@ export const initialIPAdapterV2: IPAdapterConfig = { weight: 1, }; -export const buildControlNet = (id: string, overrides?: Partial): ControlNetConfig => { - return merge(deepClone(initialControlNetV2), { id, ...overrides }); -}; - -export const buildT2IAdapter = (id: string, overrides?: Partial): T2IAdapterConfig => { - return merge(deepClone(initialT2IAdapterV2), { id, ...overrides }); -}; - -export const buildIPAdapter = (id: string, overrides?: Partial): IPAdapterConfig => { - return merge(deepClone(initialIPAdapterV2), { id, ...overrides }); -}; - export const buildControlAdapterProcessorV2 = ( modelConfig: ControlNetModelConfig | T2IAdapterModelConfig ): ProcessorConfig | null => { From 322790bfdb63b8bedeb682dad056d23b3f685ecf Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 17 Jun 2024 16:47:52 +1000 Subject: [PATCH 073/678] fix(ui): ignore keyboard shortcuts in input/textarea elements --- .../web/src/features/controlLayers/konva/events.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index de277d5675d..825a2dcb0cb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -501,11 +501,15 @@ export const setStageEventHandlers = ({ if (e.repeat) { return; } - // Cancel shape drawing on escape + if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { + return; + } if (e.key === 'Escape') { + // Cancel shape drawing on escape setIsDrawing(false); setLastMouseDownPos(null); } else if (e.key === ' ') { + // Select the view tool on space key down setToolBuffer(getToolState().selected); setTool('view'); } else if (e.key === 'r') { @@ -527,11 +531,14 @@ export const setStageEventHandlers = ({ window.addEventListener('keydown', onKeyDown); const onKeyUp = (e: KeyboardEvent) => { - // Cancel shape drawing on escape if (e.repeat) { return; } + if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { + return; + } if (e.key === ' ') { + // Revert the tool to the previous tool on space key up const toolBuffer = getToolState().selectedBuffer; setTool(toolBuffer ?? 'move'); setToolBuffer(null); From fa447b28139e81f16243198ac5ceb5929daf4fec Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 17 Jun 2024 16:48:09 +1000 Subject: [PATCH 074/678] fix(ui): delete all layers button --- .../features/controlLayers/components/DeleteAllLayersButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx index 647a8fba2c8..fbe9c22f680 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx @@ -26,7 +26,7 @@ export const DeleteAllLayersButton = memo(() => { leftIcon={} variant="ghost" colorScheme="error" - isDisabled={entityCount > 0} + isDisabled={entityCount === 0} data-testid="control-layers-delete-all-layers-button" > {t('controlLayers.deleteAll')} From 478324ea62bb5f6265f2338714595750529bf15c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 17 Jun 2024 16:49:07 +1000 Subject: [PATCH 075/678] refactor(ui): split up canvas entity renderers, temp disable preview --- .../components/StageComponent.tsx | 37 ++++++++++++------- .../features/controlLayers/konva/naming.ts | 2 + .../konva/renderers/background.ts | 7 ++-- .../controlLayers/konva/renderers/caLayer.ts | 27 ++++++++++---- .../controlLayers/konva/renderers/layers.ts | 22 ++++++++++- .../konva/renderers/rasterLayer.ts | 19 +++++++++- .../controlLayers/konva/renderers/rgLayer.ts | 21 ++++++++++- .../ImageSize/AspectRatioCanvasPreview.tsx | 7 ++-- 8 files changed, 109 insertions(+), 33 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index 0cbb7fcf456..a99f1366adf 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -5,7 +5,14 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay'; import { setStageEventHandlers } from 'features/controlLayers/konva/events'; import { renderBackgroundLayer } from 'features/controlLayers/konva/renderers/background'; -import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers/layers'; +import { renderControlAdapters } from 'features/controlLayers/konva/renderers/caLayer'; +import { + arrangeEntities, + debouncedRenderers, + renderers as normalRenderers, +} from 'features/controlLayers/konva/renderers/layers'; +import { renderLayers } from 'features/controlLayers/konva/renderers/rasterLayer'; +import { renderRegions } from 'features/controlLayers/konva/renderers/rgLayer'; import { $bbox, $currentFill, @@ -352,18 +359,22 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, useLayoutEffect(() => { log.trace('Rendering layers'); - renderers.renderLayers( - stage, - layers, - controlAdapters, - regions, - maskOpacity, - tool.selected, - selectedEntity, - getImageDTO, - onPosChanged - ); - }, [controlAdapters, layers, maskOpacity, onPosChanged, regions, renderers, selectedEntity, stage, tool.selected]); + renderLayers(stage, layers, tool.selected, onPosChanged); + }, [layers, onPosChanged, stage, tool.selected]); + + useLayoutEffect(() => { + log.trace('Rendering regions'); + renderRegions(stage, regions, maskOpacity, tool.selected, selectedEntity, onPosChanged); + }, [maskOpacity, onPosChanged, regions, selectedEntity, stage, tool.selected]); + + useLayoutEffect(() => { + log.trace('Rendering layers'); + renderControlAdapters(stage, controlAdapters, getImageDTO); + }, [controlAdapters, stage]); + + useLayoutEffect(() => { + arrangeEntities(stage, layers, controlAdapters, regions); + }, [layers, controlAdapters, regions, stage]); // useLayoutEffect(() => { // if (asPreview) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts index 3b897b86798..63abe4d799b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts @@ -43,6 +43,8 @@ export const RASTER_LAYER_IMAGE_NAME = 'raster_layer.image'; export const INPAINT_MASK_LAYER_NAME = 'inpaint_mask_layer'; +export const BACKGROUND_LAYER_ID = 'background_layer'; + // Getters for non-singleton layer and object IDs export const getRGId = (entityId: string) => `${RG_LAYER_NAME}_${entityId}`; export const getLayerId = (entityId: string) => `${RASTER_LAYER_NAME}_${entityId}`; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts index 4d898f31bdc..48333909b69 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts @@ -1,4 +1,5 @@ import { getArbitraryBaseColor } from '@invoke-ai/ui-library'; +import { BACKGROUND_LAYER_ID } from 'features/controlLayers/konva/naming'; import Konva from 'konva'; const baseGridLineColor = getArbitraryBaseColor(27); @@ -23,13 +24,13 @@ const getGridSpacing = (scale: number): number => { return 256; }; -const getBackgroundLayer = (stage: Konva.Stage): Konva.Layer => { - let background = stage.findOne('#background'); +export const getBackgroundLayer = (stage: Konva.Stage): Konva.Layer => { + let background = stage.findOne(`#${BACKGROUND_LAYER_ID}`); if (background) { return background; } - background = new Konva.Layer({ id: 'background' }); + background = new Konva.Layer({ id: BACKGROUND_LAYER_ID }); stage.add(background); return background; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts index 4170b87ffe5..d0e611e859a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts @@ -92,10 +92,9 @@ const updateCALayerImageSource = async ( const updateCALayerImageAttrs = (stage: Konva.Stage, konvaImage: Konva.Image, ca: ControlAdapterData): void => { let needsCache = false; - // 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. - // TODO(psyche): Investigate and report upstream. - const filter = konvaImage.filters()[0] ?? null; + // TODO(psyche): `node.filters()` returns null if no filters; report upstream + const filters = konvaImage.filters() ?? []; + const filter = filters[0] ?? null; const filterNeedsUpdate = (filter === null && ca.filter !== 'none') || (filter && filter.name !== ca.filter); if ( konvaImage.x() !== ca.x || @@ -130,13 +129,9 @@ const updateCALayerImageAttrs = (stage: Konva.Stage, konvaImage: Konva.Image, ca export const renderCALayer = ( stage: Konva.Stage, ca: ControlAdapterData, - zIndex: number, getImageDTO: (imageName: string) => Promise ): void => { const konvaLayer = stage.findOne(`#${ca.id}`) ?? createCALayer(stage, ca); - - konvaLayer.zIndex(zIndex); - const konvaImage = konvaLayer.findOne(`.${CA_LAYER_IMAGE_NAME}`); const canvasImageSource = konvaImage?.image(); @@ -159,3 +154,19 @@ export const renderCALayer = ( updateCALayerImageAttrs(stage, konvaImage, ca); } }; + +export const renderControlAdapters = ( + stage: Konva.Stage, + controlAdapters: ControlAdapterData[], + getImageDTO: (imageName: string) => Promise +): void => { + // Destroy nonexistent layers + for (const konvaLayer of stage.find(`.${CA_LAYER_NAME}`)) { + if (!controlAdapters.find((ca) => ca.id === konvaLayer.id())) { + konvaLayer.destroy(); + } + } + for (const ca of controlAdapters) { + renderCALayer(stage, ca, getImageDTO); + } +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts index 43db3554ee7..4945af07064 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -1,5 +1,5 @@ import { DEBOUNCE_MS } from 'features/controlLayers/konva/constants'; -import { PREVIEW_LAYER_ID } from 'features/controlLayers/konva/naming'; +import { BACKGROUND_LAYER_ID, PREVIEW_LAYER_ID } from 'features/controlLayers/konva/naming'; import { updateBboxes } from 'features/controlLayers/konva/renderers/bbox'; import { renderCALayer } from 'features/controlLayers/konva/renderers/caLayer'; import { renderBboxPreview, renderToolPreview } from 'features/controlLayers/konva/renderers/previewLayer'; @@ -93,3 +93,23 @@ const getDebouncedRenderers = (ms = DEBOUNCE_MS): typeof renderers => ({ * All the renderers for the Konva stage, debounced. */ export const debouncedRenderers: typeof renderers = getDebouncedRenderers(); + +export const arrangeEntities = ( + stage: Konva.Stage, + layers: LayerData[], + controlAdapters: ControlAdapterData[], + regions: RegionalGuidanceData[] +): void => { + let zIndex = 0; + stage.findOne(`#${BACKGROUND_LAYER_ID}`)?.zIndex(++zIndex); + for (const layer of layers) { + stage.findOne(`#${layer.id}`)?.zIndex(++zIndex); + } + for (const ca of controlAdapters) { + stage.findOne(`#${ca.id}`)?.zIndex(++zIndex); + } + for (const rg of regions) { + stage.findOne(`#${rg.id}`)?.zIndex(++zIndex); + } + stage.findOne(`#${PREVIEW_LAYER_ID}`)?.zIndex(++zIndex); +}; 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 d34d48063cf..2606a26e736 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts @@ -64,7 +64,6 @@ export const renderRasterLayer = async ( stage: Konva.Stage, layerState: LayerData, tool: Tool, - zIndex: number, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ) => { const konvaLayer = @@ -75,7 +74,6 @@ export const renderRasterLayer = async ( listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events x: Math.floor(layerState.x), y: Math.floor(layerState.y), - zIndex, }); const konvaObjectGroup = @@ -145,3 +143,20 @@ export const renderRasterLayer = async ( konvaObjectGroup.opacity(layerState.opacity); }; + +export const renderLayers = ( + stage: Konva.Stage, + layers: LayerData[], + tool: Tool, + onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void +): void => { + // Destroy nonexistent layers + for (const konvaLayer of stage.find(`.${RASTER_LAYER_NAME}`)) { + if (!layers.find((l) => l.id === konvaLayer.id())) { + konvaLayer.destroy(); + } + } + for (const layer of layers) { + renderRasterLayer(stage, layer, tool, onPosChanged); + } +}; 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 b6033416cd1..4bb255317e1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts @@ -83,7 +83,6 @@ export const renderRGLayer = ( rg: RegionalGuidanceData, globalMaskLayerOpacity: number, tool: Tool, - zIndex: number, selectedEntity: CanvasEntity | null, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): void => { @@ -94,7 +93,6 @@ export const renderRGLayer = ( listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events x: Math.floor(rg.x), y: Math.floor(rg.y), - zIndex, }); // Convert the color to a string, stripping the alpha - the object group will handle opacity. @@ -233,3 +231,22 @@ export const renderRGLayer = ( bboxRect.visible(false); } }; + +export const renderRegions = ( + stage: Konva.Stage, + regions: RegionalGuidanceData[], + maskOpacity: number, + tool: Tool, + selectedEntity: CanvasEntity | null, + onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void +): void => { + // Destroy nonexistent layers + for (const konvaLayer of stage.find(`.${RG_LAYER_NAME}`)) { + if (!regions.find((rg) => rg.id === konvaLayer.id())) { + konvaLayer.destroy(); + } + } + for (const rg of regions) { + renderRGLayer(stage, rg, maskOpacity, tool, selectedEntity, onPosChanged); + } +}; diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioCanvasPreview.tsx b/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioCanvasPreview.tsx index 56b188c3bfa..b901cc494e8 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioCanvasPreview.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioCanvasPreview.tsx @@ -2,15 +2,14 @@ import { Flex } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { StageComponent } from 'features/controlLayers/components/StageComponent'; import { $isPreviewVisible } from 'features/controlLayers/store/canvasV2Slice'; -import { AspectRatioIconPreview } from 'features/parameters/components/ImageSize/AspectRatioIconPreview'; import { memo } from 'react'; export const AspectRatioCanvasPreview = memo(() => { const isPreviewVisible = useStore($isPreviewVisible); - if (!isPreviewVisible) { - return ; - } + // if (!isPreviewVisible) { + // return ; + // } return ( From 26b971978d893bcf599c1ad7dc6dce41d3c03a64 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 17 Jun 2024 17:03:29 +1000 Subject: [PATCH 076/678] fix(ui): canvas HUD doesn't interrupt tool --- .../src/features/controlLayers/components/StageComponent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index a99f1366adf..c634a355c16 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -437,7 +437,7 @@ export const StageComponent = memo(({ asPreview = false }: Props) => { data-testid="control-layers-canvas" /> {!asPreview && ( - + )} From 352a651ac0d6b248626d81b300d340ae5a120a4e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 17 Jun 2024 17:14:11 +1000 Subject: [PATCH 077/678] feat(ui): brush size border radius = 1 --- .../frontend/web/src/features/controlLayers/konva/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts b/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts index e526e2249e6..a6d852f0125 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts @@ -23,7 +23,7 @@ export const BRUSH_BORDER_OUTER_COLOR = 'rgba(255,255,255,0.8)'; /** * The border width for the brush preview. */ -export const BRUSH_ERASER_BORDER_WIDTH = 1.5; +export const BRUSH_ERASER_BORDER_WIDTH = 1; /** * The target spacing of individual points of brush strokes, as a percentage of the brush size. From 2d123fa11c7b06a79daa2803814a6e6f9f1b2fb5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 17 Jun 2024 17:16:13 +1000 Subject: [PATCH 078/678] refactor(ui): use "entity" instead of "data" for canvas --- .../ControlAdapter/CAImagePreview.tsx | 4 +- .../controlLayers/konva/renderers/bbox.ts | 4 +- .../controlLayers/konva/renderers/caLayer.ts | 12 +++--- .../controlLayers/konva/renderers/layers.ts | 18 ++++----- .../konva/renderers/rasterLayer.ts | 8 ++-- .../controlLayers/konva/renderers/rgLayer.ts | 8 ++-- .../store/controlAdaptersReducers.ts | 4 +- .../controlLayers/store/ipAdaptersReducers.ts | 6 +-- .../controlLayers/store/layersReducers.ts | 4 +- .../controlLayers/store/regionsReducers.ts | 10 ++--- .../src/features/controlLayers/store/types.ts | 40 +++++++++---------- .../metadata/components/MetadataLayers.tsx | 8 ++-- .../src/features/metadata/util/handlers.ts | 6 +-- .../web/src/features/metadata/util/parsers.ts | 34 ++++++++-------- .../src/features/metadata/util/recallers.ts | 18 ++++----- .../src/features/metadata/util/validators.ts | 8 ++-- .../graph/generation/addControlAdapters.ts | 8 ++-- .../util/graph/generation/addIPAdapters.ts | 10 ++--- .../nodes/util/graph/generation/addRegions.ts | 14 +++---- 19 files changed, 112 insertions(+), 112 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx index 2fe84170b32..f9d4a26fe98 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx @@ -5,7 +5,7 @@ import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; import { heightChanged, widthChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; -import type { ControlAdapterData } from 'features/controlLayers/store/types'; +import type { ControlAdapterEntity } from 'features/controlLayers/store/types'; import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; import { memo, useCallback, useEffect, useMemo, useState } from 'react'; @@ -20,7 +20,7 @@ import { import type { ImageDTO, PostUploadAction } from 'services/api/types'; type Props = { - controlAdapter: ControlAdapterData; + controlAdapter: ControlAdapterEntity; onChangeImage: (imageDTO: ImageDTO | null) => void; droppableData: TypesafeDroppableData; postUploadAction: PostUploadAction; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts index a006f481c70..cd17862f3ad 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts @@ -7,7 +7,7 @@ import { } from 'features/controlLayers/konva/naming'; import { createBboxRect } from 'features/controlLayers/konva/renderers/objects'; import { imageDataToDataURL } from "features/controlLayers/konva/util"; -import type { ControlAdapterData, LayerData, RegionalGuidanceData } from 'features/controlLayers/store/types'; +import type { ControlAdapterEntity, LayerEntity, RegionEntity } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; import { assert } from 'tsafe'; @@ -186,7 +186,7 @@ const filterCAChildren = (node: Konva.Node): boolean => node.name() === CA_LAYER */ export const updateBboxes = ( stage: Konva.Stage, - entityStates: (ControlAdapterData | LayerData | RegionalGuidanceData)[], + entityStates: (ControlAdapterEntity | LayerEntity | RegionEntity)[], onBboxChanged: (layerId: string, bbox: IRect | null) => void ): void => { for (const entityState of entityStates) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts index d0e611e859a..91c6db83bb9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts @@ -1,6 +1,6 @@ import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; import { CA_LAYER_IMAGE_NAME, CA_LAYER_NAME, getCAImageId } from 'features/controlLayers/konva/naming'; -import type { ControlAdapterData } from 'features/controlLayers/store/types'; +import type { ControlAdapterEntity } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { ImageDTO } from 'services/api/types'; @@ -14,7 +14,7 @@ import type { ImageDTO } from 'services/api/types'; * @param stage The konva stage * @param ca The control adapter layer state */ -const createCALayer = (stage: Konva.Stage, ca: ControlAdapterData): Konva.Layer => { +const createCALayer = (stage: Konva.Stage, ca: ControlAdapterEntity): Konva.Layer => { const konvaLayer = new Konva.Layer({ id: ca.id, name: CA_LAYER_NAME, @@ -50,7 +50,7 @@ const createCALayerImage = (konvaLayer: Konva.Layer, imageEl: HTMLImageElement): const updateCALayerImageSource = async ( stage: Konva.Stage, konvaLayer: Konva.Layer, - ca: ControlAdapterData, + ca: ControlAdapterEntity, getImageDTO: (imageName: string) => Promise ): Promise => { const image = ca.processedImage ?? ca.image; @@ -90,7 +90,7 @@ const updateCALayerImageSource = async ( * @param ca The control adapter layer state */ -const updateCALayerImageAttrs = (stage: Konva.Stage, konvaImage: Konva.Image, ca: ControlAdapterData): void => { +const updateCALayerImageAttrs = (stage: Konva.Stage, konvaImage: Konva.Image, ca: ControlAdapterEntity): void => { let needsCache = false; // TODO(psyche): `node.filters()` returns null if no filters; report upstream const filters = konvaImage.filters() ?? []; @@ -128,7 +128,7 @@ const updateCALayerImageAttrs = (stage: Konva.Stage, konvaImage: Konva.Image, ca */ export const renderCALayer = ( stage: Konva.Stage, - ca: ControlAdapterData, + ca: ControlAdapterEntity, getImageDTO: (imageName: string) => Promise ): void => { const konvaLayer = stage.findOne(`#${ca.id}`) ?? createCALayer(stage, ca); @@ -157,7 +157,7 @@ export const renderCALayer = ( export const renderControlAdapters = ( stage: Konva.Stage, - controlAdapters: ControlAdapterData[], + controlAdapters: ControlAdapterEntity[], getImageDTO: (imageName: string) => Promise ): void => { // Destroy nonexistent layers diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts index 4945af07064..7ed3fa31708 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -8,10 +8,10 @@ import { renderRGLayer } from 'features/controlLayers/konva/renderers/rgLayer'; import { mapId, selectRenderableLayers } from 'features/controlLayers/konva/util'; import type { CanvasEntity, - ControlAdapterData, - LayerData, + ControlAdapterEntity, + LayerEntity, PosChangedArg, - RegionalGuidanceData, + RegionEntity, Tool, } from 'features/controlLayers/store/types'; import type Konva from 'konva'; @@ -33,9 +33,9 @@ import type { ImageDTO } from 'services/api/types'; */ const renderLayers = ( stage: Konva.Stage, - layers: LayerData[], - controlAdapters: ControlAdapterData[], - regions: RegionalGuidanceData[], + layers: LayerEntity[], + controlAdapters: ControlAdapterEntity[], + regions: RegionEntity[], rgGlobalOpacity: number, tool: Tool, selectedEntity: CanvasEntity | null, @@ -96,9 +96,9 @@ export const debouncedRenderers: typeof renderers = getDebouncedRenderers(); export const arrangeEntities = ( stage: Konva.Stage, - layers: LayerData[], - controlAdapters: ControlAdapterData[], - regions: RegionalGuidanceData[] + layers: LayerEntity[], + controlAdapters: ControlAdapterEntity[], + regions: RegionEntity[] ): void => { let zIndex = 0; stage.findOne(`#${BACKGROUND_LAYER_ID}`)?.zIndex(++zIndex); 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 2606a26e736..772475b9533 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts @@ -14,7 +14,7 @@ import { createRectShape, } from 'features/controlLayers/konva/renderers/objects'; import { mapId, selectRasterObjects } from 'features/controlLayers/konva/util'; -import type { CanvasEntity, LayerData, PosChangedArg, Tool } from 'features/controlLayers/store/types'; +import type { CanvasEntity, LayerEntity, PosChangedArg, Tool } from 'features/controlLayers/store/types'; import Konva from 'konva'; /** @@ -29,7 +29,7 @@ import Konva from 'konva'; */ const createRasterLayer = ( stage: Konva.Stage, - layerState: LayerData, + layerState: LayerEntity, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): Konva.Layer => { // This layer hasn't been added to the konva state yet @@ -62,7 +62,7 @@ const createRasterLayer = ( */ export const renderRasterLayer = async ( stage: Konva.Stage, - layerState: LayerData, + layerState: LayerEntity, tool: Tool, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ) => { @@ -146,7 +146,7 @@ export const renderRasterLayer = async ( export const renderLayers = ( stage: Konva.Stage, - layers: LayerData[], + layers: LayerEntity[], tool: Tool, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): void => { 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 4bb255317e1..11a096487ef 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts @@ -18,7 +18,7 @@ import { createRectShape, } from 'features/controlLayers/konva/renderers/objects'; import { mapId, selectVectorMaskObjects } from 'features/controlLayers/konva/util'; -import type { CanvasEntity, PosChangedArg, RegionalGuidanceData, Tool } from 'features/controlLayers/store/types'; +import type { CanvasEntity, PosChangedArg, RegionEntity, Tool } from 'features/controlLayers/store/types'; import Konva from 'konva'; /** @@ -46,7 +46,7 @@ const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => { */ const createRGLayer = ( stage: Konva.Stage, - rg: RegionalGuidanceData, + rg: RegionEntity, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): Konva.Layer => { // This layer hasn't been added to the konva state yet @@ -80,7 +80,7 @@ const createRGLayer = ( */ export const renderRGLayer = ( stage: Konva.Stage, - rg: RegionalGuidanceData, + rg: RegionEntity, globalMaskLayerOpacity: number, tool: Tool, selectedEntity: CanvasEntity | null, @@ -234,7 +234,7 @@ export const renderRGLayer = ( export const renderRegions = ( stage: Konva.Stage, - regions: RegionalGuidanceData[], + regions: RegionEntity[], maskOpacity: number, tool: Tool, selectedEntity: CanvasEntity | null, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts index c48d023a0d4..4d4787c9cfc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts @@ -9,7 +9,7 @@ import { v4 as uuidv4 } from 'uuid'; import type { CanvasV2State, - ControlAdapterData, + ControlAdapterEntity, ControlModeV2, ControlNetConfig, ControlNetData, @@ -50,7 +50,7 @@ export const controlAdaptersReducers = { payload: { id: uuidv4(), ...payload }, }), }, - caRecalled: (state, action: PayloadAction<{ data: ControlAdapterData }>) => { + caRecalled: (state, action: PayloadAction<{ data: ControlAdapterEntity }>) => { const { data } = action.payload; state.controlAdapters.push(data); state.selectedEntityIdentifier = { type: 'control_adapter', id: data.id }; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts index 66761c52eb2..3bd5a9c47b3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts @@ -4,7 +4,7 @@ import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; -import type { CanvasV2State, CLIPVisionModelV2, IPAdapterConfig, IPAdapterData, IPMethodV2 } from './types'; +import type { CanvasV2State, CLIPVisionModelV2, IPAdapterConfig, IPAdapterEntity, IPMethodV2 } from './types'; import { imageDTOToImageWithDims } from './types'; export const selectIPA = (state: CanvasV2State, id: string) => state.ipAdapters.find((ipa) => ipa.id === id); @@ -18,7 +18,7 @@ export const ipAdaptersReducers = { ipaAdded: { reducer: (state, action: PayloadAction<{ id: string; config: IPAdapterConfig }>) => { const { id, config } = action.payload; - const layer: IPAdapterData = { + const layer: IPAdapterEntity = { id, type: 'ip_adapter', isEnabled: true, @@ -29,7 +29,7 @@ export const ipAdaptersReducers = { }, prepare: (payload: { config: IPAdapterConfig }) => ({ payload: { id: uuidv4(), ...payload } }), }, - ipaRecalled: (state, action: PayloadAction<{ data: IPAdapterData }>) => { + ipaRecalled: (state, action: PayloadAction<{ data: IPAdapterEntity }>) => { const { data } = action.payload; state.ipAdapters.push(data); state.selectedEntityIdentifier = { type: 'ip_adapter', id: data.id }; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index dc06f8d1884..9ba7df25892 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -10,7 +10,7 @@ import type { CanvasV2State, EraserLineAddedArg, ImageObjectAddedArg, - LayerData, + LayerEntity, PointAddedToLineArg, RectShapeAddedArg, } from './types'; @@ -42,7 +42,7 @@ export const layersReducers = { }, prepare: () => ({ payload: { id: uuidv4() } }), }, - layerRecalled: (state, action: PayloadAction<{ data: LayerData }>) => { + layerRecalled: (state, action: PayloadAction<{ data: LayerEntity }>) => { const { data } = action.payload; state.layers.push(data); state.selectedEntityIdentifier = { type: 'layer', id: data.id }; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index 680eb35a824..af586c4d836 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -14,10 +14,10 @@ import { v4 as uuidv4 } from 'uuid'; import type { BrushLineAddedArg, EraserLineAddedArg, - IPAdapterData, + IPAdapterEntity, PointAddedToLineArg, RectShapeAddedArg, - RegionalGuidanceData, + RegionEntity, RgbColor, } from './types'; import { isLine } from './types'; @@ -55,7 +55,7 @@ export const regionsReducers = { rgAdded: { reducer: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; - const rg: RegionalGuidanceData = { + const rg: RegionEntity = { id, type: 'regional_guidance', isEnabled: true, @@ -87,7 +87,7 @@ export const regionsReducers = { rg.bboxNeedsUpdate = false; rg.imageCache = null; }, - rgRecalled: (state, action: PayloadAction<{ data: RegionalGuidanceData }>) => { + rgRecalled: (state, action: PayloadAction<{ data: RegionEntity }>) => { const { data } = action.payload; state.regions.push(data); state.selectedEntityIdentifier = { type: 'regional_guidance', id: data.id }; @@ -194,7 +194,7 @@ export const regionsReducers = { } rg.autoNegative = autoNegative; }, - rgIPAdapterAdded: (state, action: PayloadAction<{ id: string; ipAdapter: IPAdapterData }>) => { + rgIPAdapterAdded: (state, action: PayloadAction<{ id: string; ipAdapter: IPAdapterEntity }>) => { const { id, ipAdapter } = action.payload; const rg = selectRG(state, id); if (!rg) { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index f92b842cbb2..f4c594c3bb8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -573,7 +573,7 @@ const zRect = z.object({ height: z.number().min(1), }); -export const zLayerData = z.object({ +export const zLayerEntity = z.object({ id: zId, type: z.literal('layer'), isEnabled: z.boolean(), @@ -584,9 +584,9 @@ export const zLayerData = z.object({ opacity: zOpacity, objects: z.array(zLayerObject), }); -export type LayerData = z.infer; +export type LayerEntity = z.infer; -export const zIPAdapterData = z.object({ +export const zIPAdapterEntity = z.object({ id: zId, type: z.literal('ip_adapter'), isEnabled: z.boolean(), @@ -597,9 +597,9 @@ export const zIPAdapterData = z.object({ clipVisionModel: zCLIPVisionModelV2, beginEndStepPct: zBeginEndStepPct, }); -export type IPAdapterData = z.infer; +export type IPAdapterEntity = z.infer; export type IPAdapterConfig = Pick< - IPAdapterData, + IPAdapterEntity, 'weight' | 'image' | 'beginEndStepPct' | 'model' | 'clipVisionModel' | 'method' >; @@ -636,7 +636,7 @@ const zMaskObject = z }) .pipe(z.discriminatedUnion('type', [zBrushLine, zEraserline, zRectShape])); -export const zRegionalGuidanceData = z.object({ +export const zRegionEntity = z.object({ id: zId, type: z.literal('regional_guidance'), isEnabled: z.boolean(), @@ -647,12 +647,12 @@ export const zRegionalGuidanceData = z.object({ objects: z.array(zMaskObject), positivePrompt: zParameterPositivePrompt.nullable(), negativePrompt: zParameterNegativePrompt.nullable(), - ipAdapters: z.array(zIPAdapterData), + ipAdapters: z.array(zIPAdapterEntity), fill: zRgbColor, autoNegative: zAutoNegative, imageCache: zImageWithDims.nullable(), }); -export type RegionalGuidanceData = z.infer; +export type RegionEntity = z.infer; const zColorFill = z.object({ type: z.literal('color_fill'), @@ -680,7 +680,7 @@ export type InpaintMaskData = z.infer; const zFilter = z.enum(['none', 'LightnessToAlphaFilter']); export type Filter = z.infer; -const zControlAdapterDataBase = z.object({ +const zControlAdapterEntityBase = z.object({ id: zId, type: z.literal('control_adapter'), isEnabled: z.boolean(), @@ -698,18 +698,18 @@ const zControlAdapterDataBase = z.object({ beginEndStepPct: zBeginEndStepPct, model: zModelIdentifierField.nullable(), }); -const zControlNetData = zControlAdapterDataBase.extend({ +const zControlNetEntity = zControlAdapterEntityBase.extend({ adapterType: z.literal('controlnet'), controlMode: zControlModeV2, }); -export type ControlNetData = z.infer; -const zT2IAdapterData = zControlAdapterDataBase.extend({ +export type ControlNetData = z.infer; +const zT2IAdapterEntity = zControlAdapterEntityBase.extend({ adapterType: z.literal('t2i_adapter'), }); -export type T2IAdapterData = z.infer; +export type T2IAdapterData = z.infer; -export const zControlAdapterData = z.discriminatedUnion('adapterType', [zControlNetData, zT2IAdapterData]); -export type ControlAdapterData = z.infer; +export const zControlAdapterEntity = z.discriminatedUnion('adapterType', [zControlNetEntity, zT2IAdapterEntity]); +export type ControlAdapterEntity = z.infer; export type ControlNetConfig = Pick< ControlNetData, | 'adapterType' @@ -778,7 +778,7 @@ export type BoundingBoxScaleMethod = z.infer; export const isBoundingBoxScaleMethod = (v: unknown): v is BoundingBoxScaleMethod => zBoundingBoxScaleMethod.safeParse(v).success; -export type CanvasEntity = LayerData | IPAdapterData | ControlAdapterData | RegionalGuidanceData | InpaintMaskData; +export type CanvasEntity = LayerEntity | IPAdapterEntity | ControlAdapterEntity | RegionEntity | InpaintMaskData; export type CanvasEntityIdentifier = Pick; export type Dimensions = { @@ -796,10 +796,10 @@ export type LoRA = { export type CanvasV2State = { _version: 3; selectedEntityIdentifier: CanvasEntityIdentifier | null; - layers: LayerData[]; - controlAdapters: ControlAdapterData[]; - ipAdapters: IPAdapterData[]; - regions: RegionalGuidanceData[]; + layers: LayerEntity[]; + controlAdapters: ControlAdapterEntity[]; + ipAdapters: IPAdapterEntity[]; + regions: RegionEntity[]; loras: LoRA[]; tool: { selected: Tool; diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx index 7bc60580374..38db77b9974 100644 --- a/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx +++ b/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx @@ -1,4 +1,4 @@ -import type { LayerData } from 'features/controlLayers/store/types'; +import type { LayerEntity } from 'features/controlLayers/store/types'; import { MetadataItemView } from 'features/metadata/components/MetadataItemView'; import type { MetadataHandlers } from 'features/metadata/types'; import { handlers } from 'features/metadata/util/handlers'; @@ -9,7 +9,7 @@ type Props = { }; export const MetadataLayers = ({ metadata }: Props) => { - const [layers, setLayers] = useState([]); + const [layers, setLayers] = useState([]); useEffect(() => { const parse = async () => { @@ -40,8 +40,8 @@ const MetadataViewLayer = ({ handlers, }: { label: string; - layer: LayerData; - handlers: MetadataHandlers; + layer: LayerEntity; + handlers: MetadataHandlers; }) => { const onRecall = useCallback(() => { if (!handlers.recallItem) { diff --git a/invokeai/frontend/web/src/features/metadata/util/handlers.ts b/invokeai/frontend/web/src/features/metadata/util/handlers.ts index 8c6f5170059..187ad8f9695 100644 --- a/invokeai/frontend/web/src/features/metadata/util/handlers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/handlers.ts @@ -2,7 +2,7 @@ import { getStore } from 'app/store/nanostores/store'; import { deepClone } from 'common/util/deepClone'; import { objectKeys } from 'common/util/objectKeys'; import { shouldConcatPromptsChanged } from 'features/controlLayers/store/canvasV2Slice'; -import type { LayerData, LoRA } from 'features/controlLayers/store/types'; +import type { LayerEntity, LoRA } from 'features/controlLayers/store/types'; import type { AnyControlAdapterConfigMetadata, BuildMetadataHandlers, @@ -48,7 +48,7 @@ const renderControlAdapterValue: MetadataRenderValueFunc = async (layer) => { +const renderLayerValue: MetadataRenderValueFunc = async (layer) => { if (layer.type === 'initial_image_layer') { let rendered = t('controlLayers.globalInitialImageLayer'); if (layer.image) { @@ -88,7 +88,7 @@ const renderLayerValue: MetadataRenderValueFunc = async (layer) => { } assert(false, 'Unknown layer type'); }; -const renderLayersValue: MetadataRenderValueFunc = async (layers) => { +const renderLayersValue: MetadataRenderValueFunc = async (layers) => { return `${layers.length} ${t('controlLayers.layers', { count: layers.length })}`; }; diff --git a/invokeai/frontend/web/src/features/metadata/util/parsers.ts b/invokeai/frontend/web/src/features/metadata/util/parsers.ts index 909296c5121..f864accd45a 100644 --- a/invokeai/frontend/web/src/features/metadata/util/parsers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/parsers.ts @@ -1,6 +1,6 @@ import { getCAId, getImageObjectId, getIPAId, getLayerId } from 'features/controlLayers/konva/naming'; import { defaultLoRAConfig } from 'features/controlLayers/store/lorasReducers'; -import type { ControlAdapterData, IPAdapterData, LayerData, LoRA } from 'features/controlLayers/store/types'; +import type { ControlAdapterEntity, IPAdapterEntity, LayerEntity, LoRA } from 'features/controlLayers/store/types'; import { CA_PROCESSOR_DATA, imageDTOToImageWithDims, @@ -8,7 +8,7 @@ import { initialIPAdapterV2, initialT2IAdapterV2, isProcessorTypeV2, - zLayerData, + zLayerEntity, } from 'features/controlLayers/store/types'; import type { ControlNetConfigMetadata, @@ -424,22 +424,22 @@ const parseAllIPAdapters: MetadataParseFunc = async ( }; //#region Control Layers -const parseLayer: MetadataParseFunc = async (metadataItem) => zLayerData.parseAsync(metadataItem); +const parseLayer: MetadataParseFunc = async (metadataItem) => zLayerEntity.parseAsync(metadataItem); -const parseLayers: MetadataParseFunc = async (metadata) => { +const parseLayers: MetadataParseFunc = async (metadata) => { // We need to support recalling pre-Control Layers metadata into Control Layers. A separate set of parsers handles // taking pre-CL metadata and parsing it into layers. It doesn't always map 1-to-1, so this is best-effort. For // example, CL Control Adapters don't support resize mode, so we simply omit that property. try { - const layers: LayerData[] = []; + const layers: LayerEntity[] = []; try { const control_layers = await getProperty(metadata, 'control_layers'); const controlLayersRaw = await getProperty(control_layers, 'layers', isArray); const controlLayersParseResults = await Promise.allSettled(controlLayersRaw.map(parseLayer)); const controlLayers = controlLayersParseResults - .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') .map((result) => result.value); layers.push(...controlLayers); } catch { @@ -452,7 +452,7 @@ const parseLayers: MetadataParseFunc = async (metadata) => { controlNetsRaw.map(async (cn) => await parseControlNetToControlAdapterLayer(cn)) ); const controlNetsAsLayers = controlNetsParseResults - .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') .map((result) => result.value); layers.push(...controlNetsAsLayers); } catch { @@ -465,7 +465,7 @@ const parseLayers: MetadataParseFunc = async (metadata) => { t2iAdaptersRaw.map(async (cn) => await parseT2IAdapterToControlAdapterLayer(cn)) ); const t2iAdaptersAsLayers = t2iAdaptersParseResults - .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') .map((result) => result.value); layers.push(...t2iAdaptersAsLayers); } catch { @@ -478,7 +478,7 @@ const parseLayers: MetadataParseFunc = async (metadata) => { ipAdaptersRaw.map(async (cn) => await parseIPAdapterToIPAdapterLayer(cn)) ); const ipAdaptersAsLayers = ipAdaptersParseResults - .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') .map((result) => result.value); layers.push(...ipAdaptersAsLayers); } catch { @@ -498,14 +498,14 @@ const parseLayers: MetadataParseFunc = async (metadata) => { } }; -const parseInitialImageToInitialImageLayer: MetadataParseFunc = async (metadata) => { +const parseInitialImageToInitialImageLayer: MetadataParseFunc = async (metadata) => { // TODO(psyche): recall denoise strength // const denoisingStrength = await getProperty(metadata, 'strength', isParameterStrength); const imageName = await getProperty(metadata, 'init_image', isString); const imageDTO = await getImageDTO(imageName); assert(imageDTO, 'ImageDTO is null'); const id = getLayerId(uuidv4()); - const layer: LayerData = { + const layer: LayerEntity = { id, type: 'layer', bbox: null, @@ -529,7 +529,7 @@ const parseInitialImageToInitialImageLayer: MetadataParseFunc = async return layer; }; -const parseControlNetToControlAdapterLayer: MetadataParseFunc = async (metadataItem) => { +const parseControlNetToControlAdapterLayer: MetadataParseFunc = async (metadataItem) => { const control_model = await getProperty(metadataItem, 'control_model'); const key = await getModelKey(control_model, 'controlnet'); const controlNetModel = await fetchModelConfigWithTypeGuard(key, isControlNetModelConfig); @@ -569,7 +569,7 @@ const parseControlNetToControlAdapterLayer: MetadataParseFunc = async (metadataItem) => { +const parseT2IAdapterToControlAdapterLayer: MetadataParseFunc = async (metadataItem) => { const t2i_adapter_model = await getProperty(metadataItem, 't2i_adapter_model'); const key = await getModelKey(t2i_adapter_model, 't2i_adapter'); const t2iAdapterModel = await fetchModelConfigWithTypeGuard(key, isT2IAdapterModelConfig); @@ -630,7 +630,7 @@ const parseT2IAdapterToControlAdapterLayer: MetadataParseFunc = async (metadataItem) => { +const parseIPAdapterToIPAdapterLayer: MetadataParseFunc = async (metadataItem) => { const ip_adapter_model = await getProperty(metadataItem, 'ip_adapter_model'); const key = await getModelKey(ip_adapter_model, 'ip_adapter'); const ipAdapterModel = await fetchModelConfigWithTypeGuard(key, isIPAdapterModelConfig); @@ -685,7 +685,7 @@ const parseIPAdapterToIPAdapterLayer: MetadataParseFunc = async ( ]; const imageDTO = image ? await getImageDTO(image.image_name) : null; - const layer: IPAdapterData = { + const layer: IPAdapterEntity = { id: getIPAId(uuidv4()), type: 'ip_adapter', isEnabled: true, diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index 1ab3da59ae4..00662e3b7f5 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -40,11 +40,11 @@ import { widthChanged, } from 'features/controlLayers/store/canvasV2Slice'; import type { - ControlAdapterData, - IPAdapterData, - LayerData, + ControlAdapterEntity, + IPAdapterEntity, + LayerEntity, LoRA, - RegionalGuidanceData, + RegionEntity, } from 'features/controlLayers/store/types'; import { setHrfEnabled, setHrfMethod, setHrfStrength } from 'features/hrf/store/hrfSlice'; import type { @@ -246,7 +246,7 @@ const recallIPAdapters: MetadataRecallFunc = (ipAdapt }); }; -const recallCA: MetadataRecallFunc = async (ca) => { +const recallCA: MetadataRecallFunc = async (ca) => { const { dispatch } = getStore(); const clone = deepClone(ca); if (clone.image) { @@ -275,7 +275,7 @@ const recallCA: MetadataRecallFunc = async (ca) => { return; }; -const recallIPA: MetadataRecallFunc = async (ipa) => { +const recallIPA: MetadataRecallFunc = async (ipa) => { const { dispatch } = getStore(); const clone = deepClone(ipa); if (clone.image) { @@ -298,7 +298,7 @@ const recallIPA: MetadataRecallFunc = async (ipa) => { return; }; -const recallRG: MetadataRecallFunc = async (rg) => { +const recallRG: MetadataRecallFunc = async (rg) => { const { dispatch } = getStore(); const clone = deepClone(rg); // Strip out the uploaded mask image property - this is an intermediate image @@ -328,7 +328,7 @@ const recallRG: MetadataRecallFunc = async (rg) => { }; //#region Control Layers -const recallLayer: MetadataRecallFunc = async (layer) => { +const recallLayer: MetadataRecallFunc = async (layer) => { const { dispatch } = getStore(); const clone = deepClone(layer); const invalidObjects: string[] = []; @@ -359,7 +359,7 @@ const recallLayer: MetadataRecallFunc = async (layer) => { return; }; -const recallLayers: MetadataRecallFunc = (layers) => { +const recallLayers: MetadataRecallFunc = (layers) => { const { dispatch } = getStore(); dispatch(layerAllDeleted()); for (const l of layers) { diff --git a/invokeai/frontend/web/src/features/metadata/util/validators.ts b/invokeai/frontend/web/src/features/metadata/util/validators.ts index f0e5c84f397..f3eff83b377 100644 --- a/invokeai/frontend/web/src/features/metadata/util/validators.ts +++ b/invokeai/frontend/web/src/features/metadata/util/validators.ts @@ -1,5 +1,5 @@ import { getStore } from 'app/store/nanostores/store'; -import type { LayerData, LoRA } from 'features/controlLayers/store/types'; +import type { LayerEntity, LoRA } from 'features/controlLayers/store/types'; import type { ControlNetConfigMetadata, IPAdapterConfigMetadata, @@ -109,7 +109,7 @@ const validateIPAdapters: MetadataValidateFunc = (ipA return new Promise((resolve) => resolve(validatedIPAdapters)); }; -const validateLayer: MetadataValidateFunc = async (layer) => { +const validateLayer: MetadataValidateFunc = async (layer) => { if (layer.type === 'control_adapter_layer') { const model = layer.controlAdapter.model; assert(model, 'Control Adapter layer missing model'); @@ -131,8 +131,8 @@ const validateLayer: MetadataValidateFunc = async (layer) => { return layer; }; -const validateLayers: MetadataValidateFunc = async (layers) => { - const validatedLayers: LayerData[] = []; +const validateLayers: MetadataValidateFunc = async (layers) => { + const validatedLayers: LayerEntity[] = []; for (const l of layers) { try { const validated = await validateLayer(l); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts index ba74bfc1b3f..3cd1a727f87 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts @@ -1,5 +1,5 @@ import type { - ControlAdapterData, + ControlAdapterEntity, ControlNetData, ImageWithDims, ProcessorConfig, @@ -12,11 +12,11 @@ import type { BaseModelType, Invocation } from 'services/api/types'; import { assert } from 'tsafe'; export const addControlAdapters = ( - controlAdapters: ControlAdapterData[], + controlAdapters: ControlAdapterEntity[], g: Graph, denoise: Invocation<'denoise_latents'>, base: BaseModelType -): ControlAdapterData[] => { +): ControlAdapterEntity[] => { const validControlAdapters = controlAdapters.filter((ca) => isValidControlAdapter(ca, base)); for (const ca of validControlAdapters) { if (ca.adapterType === 'controlnet') { @@ -122,7 +122,7 @@ const buildControlImage = ( assert(false, 'Attempted to add unprocessed control image'); }; -const isValidControlAdapter = (ca: ControlAdapterData, base: BaseModelType): boolean => { +const isValidControlAdapter = (ca: ControlAdapterEntity, base: BaseModelType): boolean => { // Must be have a model that matches the current base and must have a control image const hasModel = Boolean(ca.model); const modelMatchesBase = ca.model?.base === base; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts index dfbd4668ab4..07f3c019ac7 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts @@ -1,15 +1,15 @@ -import type { IPAdapterData } from 'features/controlLayers/store/types'; +import type { IPAdapterEntity } from 'features/controlLayers/store/types'; import { IP_ADAPTER_COLLECT } from 'features/nodes/util/graph/constants'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import type { BaseModelType, Invocation } from 'services/api/types'; import { assert } from 'tsafe'; export const addIPAdapters = ( - ipAdapters: IPAdapterData[], + ipAdapters: IPAdapterEntity[], g: Graph, denoise: Invocation<'denoise_latents'>, base: BaseModelType -): IPAdapterData[] => { +): IPAdapterEntity[] => { const validIPAdapters = ipAdapters.filter((ipa) => isValidIPAdapter(ipa, base)); for (const ipa of validIPAdapters) { addIPAdapter(ipa, g, denoise); @@ -33,7 +33,7 @@ export const addIPAdapterCollectorSafe = (g: Graph, denoise: Invocation<'denoise } }; -const addIPAdapter = (ipa: IPAdapterData, g: Graph, denoise: Invocation<'denoise_latents'>) => { +const addIPAdapter = (ipa: IPAdapterEntity, g: Graph, denoise: Invocation<'denoise_latents'>) => { const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipa; assert(image, 'IP Adapter image is required'); assert(model, 'IP Adapter model is required'); @@ -55,7 +55,7 @@ const addIPAdapter = (ipa: IPAdapterData, g: Graph, denoise: Invocation<'denoise g.addEdge(ipAdapter, 'ip_adapter', ipAdapterCollect, 'item'); }; -export const isValidIPAdapter = (ipa: IPAdapterData, base: BaseModelType): boolean => { +export const isValidIPAdapter = (ipa: IPAdapterEntity, base: BaseModelType): boolean => { // Must be have a model that matches the current base and must have a control image const hasModel = Boolean(ipa.model); const modelMatchesBase = ipa.model?.base === base; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts index 772770c0213..a245d2c8546 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts @@ -5,7 +5,7 @@ import { RG_LAYER_NAME } from 'features/controlLayers/konva/naming'; import { renderers } from 'features/controlLayers/konva/renderers/layers'; import { blobToDataURL } from "features/controlLayers/konva/util"; import { rgMaskImageUploaded } from 'features/controlLayers/store/canvasV2Slice'; -import type { Dimensions, IPAdapterData, RegionalGuidanceData } from 'features/controlLayers/store/types'; +import type { Dimensions, IPAdapterEntity, RegionEntity } from 'features/controlLayers/store/types'; import { PROMPT_REGION_INVERT_TENSOR_MASK_PREFIX, PROMPT_REGION_MASK_TO_TENSOR_PREFIX, @@ -36,7 +36,7 @@ import { assert } from 'tsafe'; */ export const addRegions = async ( - regions: RegionalGuidanceData[], + regions: RegionEntity[], g: Graph, documentSize: Dimensions, bbox: IRect, @@ -46,7 +46,7 @@ export const addRegions = async ( negCond: Invocation<'compel'> | Invocation<'sdxl_compel_prompt'>, posCondCollect: Invocation<'collect'>, negCondCollect: Invocation<'collect'> -): Promise => { +): Promise => { const isSDXL = base === 'sdxl'; const validRegions = regions.filter((rg) => isValidRegion(rg, base)); @@ -186,7 +186,7 @@ export const addRegions = async ( } } - const validRGIPAdapters: IPAdapterData[] = rg.ipAdapters.filter((ipa) => isValidIPAdapter(ipa, base)); + const validRGIPAdapters: IPAdapterEntity[] = rg.ipAdapters.filter((ipa) => isValidIPAdapter(ipa, base)); for (const ipa of validRGIPAdapters) { const ipAdapterCollect = addIPAdapterCollectorSafe(g, denoise); @@ -218,13 +218,13 @@ export const addRegions = async ( return validRegions; }; -export const isValidRegion = (rg: RegionalGuidanceData, base: BaseModelType) => { +export const isValidRegion = (rg: RegionEntity, base: BaseModelType) => { const hasTextPrompt = Boolean(rg.positivePrompt || rg.negativePrompt); const hasIPAdapter = rg.ipAdapters.filter((ipa) => isValidIPAdapter(ipa, base)).length > 0; return hasTextPrompt || hasIPAdapter; }; -export const getMaskImage = async (rg: RegionalGuidanceData, blob: Blob): Promise => { +export const getMaskImage = async (rg: RegionEntity, blob: Blob): Promise => { const { id, imageCache } = rg; if (imageCache) { const imageDTO = await getImageDTO(imageCache.name); @@ -253,7 +253,7 @@ export const getMaskImage = async (rg: RegionalGuidanceData, blob: Blob): Promis */ export const getRGMaskBlobs = async ( - regions: RegionalGuidanceData[], + regions: RegionEntity[], documentSize: Dimensions, bbox: IRect, preview: boolean = false From 30b46b0f9b22aeb9881820a8e35485ab6a7eb19d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 17 Jun 2024 17:19:28 +1000 Subject: [PATCH 079/678] tidy(ui): background layer --- .../features/controlLayers/konva/renderers/background.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts index 48333909b69..0f5c4ceaa54 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts @@ -49,10 +49,6 @@ export const renderBackgroundLayer = (stage: Konva.Stage): void => { y1: 0, x2: width, y2: height, - offset: { - x: x / scale, - y: y / scale, - }, }; const gridOffset = { @@ -74,8 +70,8 @@ export const renderBackgroundLayer = (stage: Konva.Stage): void => { y2: Math.max(stageRect.y2, gridRect.y2), }; - const // find the x & y size of the grid - xSize = gridFullRect.x2 - gridFullRect.x1; + // find the x & y size of the grid + const xSize = gridFullRect.x2 - gridFullRect.x1; const ySize = gridFullRect.y2 - gridFullRect.y1; // compute the number of steps required on each axis. const xSteps = Math.round(xSize / gridSpacing) + 1; From 398d6d1efd399569b1f9ae372e181478bbcc0446 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 17 Jun 2024 17:53:30 +1000 Subject: [PATCH 080/678] feat(ui): document bounds overlay --- .../components/StageComponent.tsx | 7 +++ .../features/controlLayers/konva/events.ts | 5 +- .../konva/renderers/previewLayer.ts | 54 +++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index c634a355c16..8b56b9598f7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -11,6 +11,7 @@ import { debouncedRenderers, renderers as normalRenderers, } from 'features/controlLayers/konva/renderers/layers'; +import { renderDocumentBoundsOverlay } from 'features/controlLayers/konva/renderers/previewLayer'; import { renderLayers } from 'features/controlLayers/konva/renderers/rasterLayer'; import { renderRegions } from 'features/controlLayers/konva/renderers/rgLayer'; import { @@ -298,6 +299,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, scale: stage.scaleX(), }); renderBackgroundLayer(stage); + renderDocumentBoundsOverlay(stage, $document.get); }; const resizeObserver = new ResizeObserver(fitStageToContainer); @@ -328,6 +330,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, }, [ asPreview, currentFill, + document, isDrawing, isMouseDown, lastCursorPos, @@ -372,6 +375,10 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, renderControlAdapters(stage, controlAdapters, getImageDTO); }, [controlAdapters, stage]); + useLayoutEffect(() => { + renderDocumentBoundsOverlay(stage, $document.get); + }, [stage, document]); + useLayoutEffect(() => { arrangeEntities(stage, layers, controlAdapters, regions); }, [layers, controlAdapters, regions, stage]); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 825a2dcb0cb..80edb1fcbde 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -1,5 +1,5 @@ import { renderBackgroundLayer } from 'features/controlLayers/konva/renderers/background'; -import { scaleToolPreview } from 'features/controlLayers/konva/renderers/previewLayer'; +import { renderDocumentBoundsOverlay, scaleToolPreview } from 'features/controlLayers/konva/renderers/previewLayer'; import { getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util'; import type { BrushLineAddedArg, @@ -469,6 +469,7 @@ export const setStageEventHandlers = ({ setStageAttrs({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale }); renderBackgroundLayer(stage); scaleToolPreview(stage, getToolState()); + renderDocumentBoundsOverlay(stage, getDocument); } }); @@ -482,6 +483,7 @@ export const setStageEventHandlers = ({ scale: stage.scaleX(), }); renderBackgroundLayer(stage); + renderDocumentBoundsOverlay(stage, getDocument); }); //#region dragend @@ -526,6 +528,7 @@ export const setStageEventHandlers = ({ setStageAttrs({ x, y, width, height, scale }); scaleToolPreview(stage, getToolState()); renderBackgroundLayer(stage); + renderDocumentBoundsOverlay(stage, getDocument); } }; window.addEventListener('keydown', onKeyDown); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts index eff1e48bc33..a3b8f7a8931 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts @@ -1,3 +1,4 @@ +import { getArbitraryBaseColor } from '@invoke-ai/ui-library'; import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMultiple'; import { @@ -449,3 +450,56 @@ export const scaleToolPreview = (stage: Konva.Stage, toolState: CanvasV2State['t ?.findOne(`#${PREVIEW_BRUSH_BORDER_OUTER_ID}`) ?.setAttrs({ strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale }); }; + +const getDocumentOverlayGroup = (stage: Konva.Stage): Konva.Group => { + const previewLayer = getPreviewLayer(stage); + let documentOverlayGroup = previewLayer.findOne('#document_overlay_group'); + if (documentOverlayGroup) { + return documentOverlayGroup; + } + + documentOverlayGroup = new Konva.Group({ id: 'document_overlay_group', listening: false }); + const documentOverlayOuterRect = new Konva.Rect({ + id: 'document_overlay_outer_rect', + listening: false, + fill: getArbitraryBaseColor(10), + opacity: 0.7, + }); + const documentOverlayInnerRect = new Konva.Rect({ + id: 'document_overlay_inner_rect', + listening: false, + fill: 'white', + globalCompositeOperation: 'destination-out', + }); + documentOverlayGroup.add(documentOverlayOuterRect); + documentOverlayGroup.add(documentOverlayInnerRect); + previewLayer.add(documentOverlayGroup); + return documentOverlayGroup; +}; + +export const renderDocumentBoundsOverlay = (stage: Konva.Stage, getDocument: () => CanvasV2State['document']): void => { + const document = getDocument(); + const documentOverlayGroup = getDocumentOverlayGroup(stage); + + documentOverlayGroup.zIndex(0); + + const x = stage.x(); + const y = stage.y(); + const width = stage.width(); + const height = stage.height(); + const scale = stage.scaleX(); + + documentOverlayGroup.findOne('#document_overlay_outer_rect')?.setAttrs({ + offsetX: x / scale, + offsetY: y / scale, + width: width / scale, + height: height / scale, + }); + + documentOverlayGroup.findOne('#document_overlay_inner_rect')?.setAttrs({ + x: 0, + y: 0, + width: document.width, + height: document.height, + }); +}; From 17c864dba3c8260cc8f97667fa223feee926f77b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 17 Jun 2024 18:10:50 +1000 Subject: [PATCH 081/678] fix(ui): document fit positioning --- .../web/src/features/controlLayers/konva/constants.ts | 5 +++++ .../web/src/features/controlLayers/konva/events.ts | 9 +++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts b/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts index a6d852f0125..5f9e2c52e1b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts @@ -69,3 +69,8 @@ export const CANVAS_GRID_SIZE_FINE = 8; * The coarse grid size of the canvas */ export const CANVAS_GRID_SIZE_COARSE = 64; + +/** + * Document fit padding + */ +export const DOCUMENT_FIT_PADDING_PX = 50; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 80edb1fcbde..548baa5c8f7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -19,6 +19,7 @@ import { clamp } from 'lodash-es'; import { BRUSH_SPACING_TARGET_SCALE, CANVAS_SCALE_BY, + DOCUMENT_FIT_PADDING_PX, MAX_BRUSH_SPACING_PX, MAX_CANVAS_SCALE, MIN_BRUSH_SPACING_PX, @@ -519,11 +520,11 @@ export const setStageEventHandlers = ({ const width = stage.width(); const height = stage.height(); const document = getDocument(); - const docWidthWithBuffer = document.width + 20; - const docHeightWithBuffer = document.height + 20; + const docWidthWithBuffer = document.width + DOCUMENT_FIT_PADDING_PX * 2; + const docHeightWithBuffer = document.height + DOCUMENT_FIT_PADDING_PX * 2; const scale = Math.min(Math.min(width / docWidthWithBuffer, height / docHeightWithBuffer), 1); - const x = (width - docWidthWithBuffer * scale) / 2; - const y = (height - docHeightWithBuffer * scale) / 2; + const x = (width - docWidthWithBuffer * scale) / 2 + DOCUMENT_FIT_PADDING_PX * scale; + const y = (height - docHeightWithBuffer * scale) / 2 + DOCUMENT_FIT_PADDING_PX * scale; stage.setAttrs({ x, y, width, height, scaleX: scale, scaleY: scale }); setStageAttrs({ x, y, width, height, scale }); scaleToolPreview(stage, getToolState()); From d4ae40fec477db03ae8859ff63ae780eb489abe3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 17 Jun 2024 21:10:26 +1000 Subject: [PATCH 082/678] feat(ui): clip lines to bbox --- .../components/StageComponent.tsx | 3 +- .../features/controlLayers/konva/events.ts | 42 +++++++++++++++++++ .../controlLayers/konva/renderers/objects.ts | 21 +++++++--- .../konva/renderers/previewLayer.ts | 2 +- .../konva/renderers/rasterLayer.ts | 10 ++--- .../controlLayers/konva/renderers/rgLayer.ts | 8 ++-- .../controlLayers/store/canvasV2Slice.ts | 4 +- .../controlLayers/store/layersReducers.ts | 6 ++- .../controlLayers/store/regionsReducers.ts | 6 ++- .../src/features/controlLayers/store/types.ts | 20 +++++---- 10 files changed, 92 insertions(+), 30 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index 8b56b9598f7..6d7ce825d66 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -116,7 +116,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, useLayoutEffect(() => { $toolState.set(tool); $selectedEntity.set(selectedEntity); - $bbox.set({ x: bbox.x, y: bbox.y, width: bbox.width, height: bbox.height }); + $bbox.set(bbox); $currentFill.set(currentFill); $document.set(document); }, [selectedEntity, tool, bbox, currentFill, document]); @@ -255,6 +255,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, getSpaceKey: $spaceKey.get, setStageAttrs: $stageAttrs.set, getDocument: $document.get, + getBbox: $bbox.get, onBrushLineAdded, onEraserLineAdded, onPointAddedToLine, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 548baa5c8f7..cb1f83a3340 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -47,6 +47,7 @@ type Arg = { getSelectedEntity: () => CanvasEntity | null; getSpaceKey: () => boolean; getDocument: () => CanvasV2State['document']; + getBbox: () => CanvasV2State['bbox']; onBrushLineAdded: (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => void; onEraserLineAdded: (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => void; onPointAddedToLine: (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => void; @@ -147,6 +148,7 @@ export const setStageEventHandlers = ({ getSelectedEntity, getSpaceKey, getDocument, + getBbox, onBrushLineAdded, onEraserLineAdded, onPointAddedToLine, @@ -190,6 +192,7 @@ export const setStageEventHandlers = ({ setLastMouseDownPos(pos); if (toolState.selected === 'brush') { + const bbox = getBbox(); if (e.evt.shiftKey) { const lastAddedPoint = getLastAddedPoint(); // Create a straight line if holding shift @@ -205,6 +208,12 @@ export const setStageEventHandlers = ({ ], color: getCurrentFill(), width: toolState.brush.width, + clip: { + x: bbox.x, + y: bbox.y, + width: bbox.width, + height: bbox.height, + }, }, selectedEntity.type ); @@ -221,6 +230,12 @@ export const setStageEventHandlers = ({ ], color: getCurrentFill(), width: toolState.brush.width, + clip: { + x: bbox.x, + y: bbox.y, + width: bbox.width, + height: bbox.height, + }, }, selectedEntity.type ); @@ -229,6 +244,7 @@ export const setStageEventHandlers = ({ } if (toolState.selected === 'eraser') { + const bbox = getBbox(); if (e.evt.shiftKey) { // Create a straight line if holding shift const lastAddedPoint = getLastAddedPoint(); @@ -243,6 +259,12 @@ export const setStageEventHandlers = ({ pos.y - selectedEntity.y, ], width: toolState.eraser.width, + clip: { + x: bbox.x, + y: bbox.y, + width: bbox.width, + height: bbox.height, + }, }, selectedEntity.type ); @@ -258,6 +280,12 @@ export const setStageEventHandlers = ({ pos.y - selectedEntity.y, ], width: toolState.eraser.width, + clip: { + x: bbox.x, + y: bbox.y, + width: bbox.width, + height: bbox.height, + }, }, selectedEntity.type ); @@ -348,6 +376,7 @@ export const setStageEventHandlers = ({ // Continue the last line maybeAddNextPoint(selectedEntity, pos, getToolState, getLastAddedPoint, setLastAddedPoint, onPointAddedToLine); } else { + const bbox = getBbox(); // Start a new line onBrushLineAdded( { @@ -360,6 +389,12 @@ export const setStageEventHandlers = ({ ], width: toolState.brush.width, color: getCurrentFill(), + clip: { + x: bbox.x, + y: bbox.y, + width: bbox.width, + height: bbox.height, + }, }, selectedEntity.type ); @@ -373,6 +408,7 @@ export const setStageEventHandlers = ({ // Continue the last line maybeAddNextPoint(selectedEntity, pos, getToolState, getLastAddedPoint, setLastAddedPoint, onPointAddedToLine); } else { + const bbox = getBbox(); // Start a new line onEraserLineAdded( { @@ -384,6 +420,12 @@ export const setStageEventHandlers = ({ pos.y - selectedEntity.y, ], width: toolState.eraser.width, + clip: { + x: bbox.x, + y: bbox.y, + width: bbox.width, + height: bbox.height, + }, }, selectedEntity.type ); 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 dd4ba645e2a..ce626e99523 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -23,10 +23,19 @@ import { v4 as uuidv4 } from 'uuid'; * @param layerObjectGroup The konva layer's object group to add the line to * @param name The konva name for the line */ -export const createBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Group, name: string): Konva.Line => { - const konvaLine = new Konva.Line({ +export const getBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Group, name: string): Konva.Line => { + let konvaLineGroup = layerObjectGroup.findOne(`#${brushLine.id}_group`); + let konvaLine = konvaLineGroup?.findOne(`#${brushLine.id}`); + if (konvaLine) { + return konvaLine; + } + + konvaLineGroup = new Konva.Group({ + id: `${brushLine.id}_group`, + // clip: brushLine.clip, + }); + konvaLine = new Konva.Line({ id: brushLine.id, - key: brushLine.id, name, strokeWidth: brushLine.strokeWidth, tension: 0, @@ -37,7 +46,8 @@ export const createBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Gr listening: false, stroke: rgbaColorToString(brushLine.color), }); - layerObjectGroup.add(konvaLine); + konvaLineGroup.add(konvaLine); + layerObjectGroup.add(konvaLineGroup); return konvaLine; }; @@ -47,7 +57,7 @@ export const createBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Gr * @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, name: string): Konva.Line => { +export const getEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva.Group, name: string): Konva.Line => { const konvaLine = new Konva.Line({ id: eraserLine.id, key: eraserLine.id, @@ -60,6 +70,7 @@ export const createEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva globalCompositeOperation: 'destination-out', listening: false, stroke: rgbaColorToString(DEFAULT_RGBA_COLOR), + clip: eraserLine.clip, }); layerObjectGroup.add(konvaLine); return konvaLine; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts index a3b8f7a8931..7416c3db201 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts @@ -253,7 +253,7 @@ export const renderBboxPreview = ( stage: Konva.Stage, bbox: IRect, tool: Tool, - getBbox: () => IRect, + getBbox: () => CanvasV2State['bbox'], onBboxTransformed: (bbox: IRect) => void, getShiftKey: () => boolean, getCtrlKey: () => boolean, 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 772475b9533..4f5ebfc7201 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts @@ -7,11 +7,11 @@ import { RASTER_LAYER_RECT_SHAPE_NAME, } from 'features/controlLayers/konva/naming'; import { - createBrushLine, - createEraserLine, createImageObjectGroup, createObjectGroup, createRectShape, + getBrushLine, + getEraserLine, } from 'features/controlLayers/konva/renderers/objects'; import { mapId, selectRasterObjects } from 'features/controlLayers/konva/util'; import type { CanvasEntity, LayerEntity, PosChangedArg, Tool } from 'features/controlLayers/store/types'; @@ -92,9 +92,7 @@ export const renderRasterLayer = async ( for (const obj of layerState.objects) { if (obj.type === 'brush_line') { - const konvaBrushLine = - konvaObjectGroup.findOne(`#${obj.id}`) ?? - createBrushLine(obj, konvaObjectGroup, RASTER_LAYER_BRUSH_LINE_NAME); + const konvaBrushLine = getBrushLine(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); @@ -102,7 +100,7 @@ export const renderRasterLayer = async ( } else if (obj.type === 'eraser_line') { const konvaEraserLine = konvaObjectGroup.findOne(`#${obj.id}`) ?? - createEraserLine(obj, konvaObjectGroup, RASTER_LAYER_ERASER_LINE_NAME); + getEraserLine(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); 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 11a096487ef..916c6c9c47a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts @@ -12,10 +12,10 @@ import { import { getLayerBboxFast } from 'features/controlLayers/konva/renderers/bbox'; import { createBboxRect, - createBrushLine, - createEraserLine, createObjectGroup, createRectShape, + getBrushLine, + getEraserLine, } from 'features/controlLayers/konva/renderers/objects'; import { mapId, selectVectorMaskObjects } from 'features/controlLayers/konva/util'; import type { CanvasEntity, PosChangedArg, RegionEntity, Tool } from 'features/controlLayers/store/types'; @@ -117,7 +117,7 @@ export const renderRGLayer = ( for (const obj of rg.objects) { if (obj.type === 'brush_line') { const konvaBrushLine = - stage.findOne(`#${obj.id}`) ?? createBrushLine(obj, konvaObjectGroup, RG_LAYER_BRUSH_LINE_NAME); + stage.findOne(`#${obj.id}`) ?? getBrushLine(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. @@ -132,7 +132,7 @@ export const renderRGLayer = ( } } else if (obj.type === 'eraser_line') { const konvaEraserLine = - stage.findOne(`#${obj.id}`) ?? createEraserLine(obj, konvaObjectGroup, RG_LAYER_ERASER_LINE_NAME); + stage.findOne(`#${obj.id}`) ?? getEraserLine(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. diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index c6f6a2bd7a1..652366ca1e0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -15,7 +15,7 @@ import { settingsReducers } from 'features/controlLayers/store/settingsReducers' import { toolReducers } from 'features/controlLayers/store/toolReducers'; import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; -import type { IRect, Vector2d } from 'konva/lib/types'; +import type { Vector2d } from 'konva/lib/types'; import { atom } from 'nanostores'; import type { CanvasEntity, CanvasEntityIdentifier, CanvasV2State, RgbaColor, StageAttrs } from './types'; @@ -327,7 +327,7 @@ export const $stageAttrs = atom({ export const $toolState = atom(deepClone(initialState.tool)); export const $currentFill = atom(DEFAULT_RGBA_COLOR); export const $selectedEntity = atom(null); -export const $bbox = atom({ x: 0, y: 0, width: 0, height: 0 }); +export const $bbox = atom(deepClone(initialState.bbox)); export const $document = atom(deepClone(initialState.document)); export const canvasV2PersistConfig: PersistConfig = { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index 9ba7df25892..2fef5abc4a6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -136,7 +136,7 @@ export const layersReducers = { }, layerBrushLineAdded: { reducer: (state, action: PayloadAction) => { - const { id, points, lineId, color, width } = action.payload; + const { id, points, lineId, color, width, clip } = action.payload; const layer = selectLayer(state, id); if (!layer) { return; @@ -148,6 +148,7 @@ export const layersReducers = { points, strokeWidth: width, color, + clip, }); layer.bboxNeedsUpdate = true; }, @@ -157,7 +158,7 @@ export const layersReducers = { }, layerEraserLineAdded: { reducer: (state, action: PayloadAction) => { - const { id, points, lineId, width } = action.payload; + const { id, points, lineId, width, clip } = action.payload; const layer = selectLayer(state, id); if (!layer) { return; @@ -168,6 +169,7 @@ export const layersReducers = { type: 'eraser_line', points, strokeWidth: width, + clip, }); layer.bboxNeedsUpdate = true; }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index af586c4d836..f2009ab15ca 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -304,7 +304,7 @@ export const regionsReducers = { }, rgBrushLineAdded: { reducer: (state, action: PayloadAction) => { - const { id, points, lineId, color, width } = action.payload; + const { id, points, lineId, color, width, clip } = action.payload; const rg = selectRG(state, id); if (!rg) { return; @@ -315,6 +315,7 @@ export const regionsReducers = { points, strokeWidth: width, color, + clip, }); rg.bboxNeedsUpdate = true; rg.imageCache = null; @@ -325,7 +326,7 @@ export const regionsReducers = { }, rgEraserLineAdded: { reducer: (state, action: PayloadAction) => { - const { id, points, lineId, width } = action.payload; + const { id, points, lineId, width, clip } = action.payload; const rg = selectRG(state, id); if (!rg) { return; @@ -335,6 +336,7 @@ export const regionsReducers = { type: 'eraser_line', points, strokeWidth: width, + clip, }); rg.bboxNeedsUpdate = true; rg.imageCache = null; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index f4c594c3bb8..6841e23d83b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -498,12 +498,21 @@ export const DEFAULT_RGBA_COLOR: RgbaColor = { r: 255, g: 255, b: 255, a: 1 }; const zOpacity = z.number().gte(0).lte(1); +const zRect = z.object({ + x: z.number(), + y: z.number(), + width: z.number().min(1), + height: z.number().min(1), +}); +export type Rect = z.infer; + const zBrushLine = z.object({ id: zId, type: z.literal('brush_line'), strokeWidth: z.number().min(1), points: zPoints, color: zRgbaColor, + clip: zRect.nullable(), }); export type BrushLine = z.infer; @@ -512,6 +521,7 @@ const zEraserline = z.object({ type: z.literal('eraser_line'), strokeWidth: z.number().min(1), points: zPoints, + clip: zRect.nullable(), }); export type EraserLine = z.infer; @@ -566,13 +576,6 @@ const zLayerObject = z.discriminatedUnion('type', [ ]); export type LayerObject = z.infer; -const zRect = z.object({ - x: z.number(), - y: z.number(), - width: z.number().min(1), - height: z.number().min(1), -}); - export const zLayerEntity = z.object({ id: zId, type: z.literal('layer'), @@ -614,12 +617,14 @@ const zMaskObject = z ...rest, type: 'brush_line', color: { r: 255, g: 255, b: 255, a: 1 }, + clip: null, }; return asBrushline; } else if (tool === 'eraser') { const asEraserLine: EraserLine = { ...rest, type: 'eraser_line', + clip: null, }; return asEraserLine; } @@ -881,6 +886,7 @@ export type EraserLineAddedArg = { id: string; points: [number, number, number, number]; width: number; + clip: Rect | null; }; export type BrushLineAddedArg = EraserLineAddedArg & { color: RgbaColor }; export type PointAddedToLineArg = { id: string; point: [number, number] }; From ed130366dbdd3a0a1262412fecd4214492d23bb8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 18 Jun 2024 17:24:13 +1000 Subject: [PATCH 083/678] refactor(ui): decouple konva renderer from react Subscribe to redux store directly, skipping all the react overhead. With react in dev mode, a typical frame while using the brush tool on almost-empty canvas is reduced from ~7.5ms to ~3.5ms. All things considered, this still feels slow, but it's a massive improvement. --- .../frontend/web/src/app/logging/logger.ts | 3 +- .../components/StageComponent.tsx | 420 +------------- .../features/controlLayers/konva/events.ts | 546 ++++++++++-------- .../controlLayers/konva/renderers/bbox.ts | 38 +- .../controlLayers/konva/renderers/renderer.ts | 393 +++++++++++++ .../controlLayers/konva/renderers/stage.ts | 16 + .../src/features/controlLayers/store/types.ts | 2 +- 7 files changed, 772 insertions(+), 646 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/renderers/stage.ts diff --git a/invokeai/frontend/web/src/app/logging/logger.ts b/invokeai/frontend/web/src/app/logging/logger.ts index c0a3089fe45..3af19af2efd 100644 --- a/invokeai/frontend/web/src/app/logging/logger.ts +++ b/invokeai/frontend/web/src/app/logging/logger.ts @@ -28,7 +28,8 @@ export type LoggerNamespace = | 'queue' | 'dnd' | 'controlLayers' - | 'metadata'; + | 'metadata' + | 'konva'; export const logger = (namespace: LoggerNamespace) => $logger.get().child({ namespace }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index 6d7ce825d66..0be12933a1e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -1,408 +1,27 @@ -import { $alt, $ctrl, $meta, $shift, Flex, Heading } from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; -import { logger } from 'app/logging/logger'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { Flex } from '@invoke-ai/ui-library'; +import { useAppStore } from 'app/store/storeHooks'; import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay'; -import { setStageEventHandlers } from 'features/controlLayers/konva/events'; -import { renderBackgroundLayer } from 'features/controlLayers/konva/renderers/background'; -import { renderControlAdapters } from 'features/controlLayers/konva/renderers/caLayer'; -import { - arrangeEntities, - debouncedRenderers, - renderers as normalRenderers, -} from 'features/controlLayers/konva/renderers/layers'; -import { renderDocumentBoundsOverlay } from 'features/controlLayers/konva/renderers/previewLayer'; -import { renderLayers } from 'features/controlLayers/konva/renderers/rasterLayer'; -import { renderRegions } from 'features/controlLayers/konva/renderers/rgLayer'; -import { - $bbox, - $currentFill, - $document, - $isDrawing, - $isMouseDown, - $lastAddedPoint, - $lastCursorPos, - $lastMouseDownPos, - $selectedEntity, - $spaceKey, - $stageAttrs, - $toolState, - bboxChanged, - brushWidthChanged, - caBboxChanged, - caTranslated, - eraserWidthChanged, - layerBboxChanged, - layerBrushLineAdded, - layerEraserLineAdded, - layerLinePointAdded, - layerRectAdded, - layerTranslated, - rgBboxChanged, - rgBrushLineAdded, - rgEraserLineAdded, - rgLinePointAdded, - rgRectAdded, - rgTranslated, - toolBufferChanged, - toolChanged, -} from 'features/controlLayers/store/canvasV2Slice'; -import { selectEntityCount } from 'features/controlLayers/store/selectors'; -import type { - BboxChangedArg, - BrushLineAddedArg, - CanvasEntity, - EraserLineAddedArg, - PointAddedToLineArg, - PosChangedArg, - RectShapeAddedArg, - Tool, -} from 'features/controlLayers/store/types'; +import { initializeRenderer } from 'features/controlLayers/konva/renderers/renderer'; import Konva from 'konva'; -import type { IRect } from 'konva/lib/types'; -import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { getImageDTO } from 'services/api/endpoints/images'; +import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react'; import { useDevicePixelRatio } from 'use-device-pixel-ratio'; import { v4 as uuidv4 } from 'uuid'; // This will log warnings when layers > 5 - maybe use `import.meta.env.MODE === 'development'` instead? Konva.showWarnings = false; -const log = logger('controlLayers'); - const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, asPreview: boolean) => { - const dispatch = useAppDispatch(); - const controlAdapters = useAppSelector((s) => s.canvasV2.controlAdapters); - const ipAdapters = useAppSelector((s) => s.canvasV2.ipAdapters); - const layers = useAppSelector((s) => s.canvasV2.layers); - const regions = useAppSelector((s) => s.canvasV2.regions); - const tool = useAppSelector((s) => s.canvasV2.tool); - const selectedEntityIdentifier = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier); - const maskOpacity = useAppSelector((s) => s.canvasV2.settings.maskOpacity); - const bbox = useAppSelector((s) => s.canvasV2.bbox); - const document = useAppSelector((s) => s.canvasV2.document); - const lastCursorPos = useStore($lastCursorPos); - const lastMouseDownPos = useStore($lastMouseDownPos); - const isMouseDown = useStore($isMouseDown); - const isDrawing = useStore($isDrawing); - const selectedEntity = useMemo(() => { - const identifier = selectedEntityIdentifier; - if (!identifier) { - return null; - } else if (identifier.type === 'layer') { - return layers.find((i) => i.id === identifier.id) ?? null; - } else if (identifier.type === 'control_adapter') { - return controlAdapters.find((i) => i.id === identifier.id) ?? null; - } else if (identifier.type === 'ip_adapter') { - return ipAdapters.find((i) => i.id === identifier.id) ?? null; - } else if (identifier.type === 'regional_guidance') { - return regions.find((i) => i.id === identifier.id) ?? null; - } else { - return null; - } - }, [controlAdapters, ipAdapters, layers, regions, selectedEntityIdentifier]); - - const currentFill = useMemo(() => { - if (selectedEntity && selectedEntity.type === 'regional_guidance') { - return { ...selectedEntity.fill, a: maskOpacity }; - } - return tool.fill; - }, [maskOpacity, selectedEntity, tool.fill]); - - const renderers = useMemo(() => (asPreview ? debouncedRenderers : normalRenderers), [asPreview]); + const store = useAppStore(); const dpr = useDevicePixelRatio({ round: false }); useLayoutEffect(() => { - $toolState.set(tool); - $selectedEntity.set(selectedEntity); - $bbox.set(bbox); - $currentFill.set(currentFill); - $document.set(document); - }, [selectedEntity, tool, bbox, currentFill, document]); - - const onPosChanged = useCallback( - (arg: PosChangedArg, entityType: CanvasEntity['type']) => { - if (entityType === 'layer') { - dispatch(layerTranslated(arg)); - } else if (entityType === 'control_adapter') { - dispatch(caTranslated(arg)); - } else if (entityType === 'regional_guidance') { - dispatch(rgTranslated(arg)); - } - }, - [dispatch] - ); - - const onBboxChanged = useCallback( - (arg: BboxChangedArg, entityType: CanvasEntity['type']) => { - if (entityType === 'layer') { - dispatch(layerBboxChanged(arg)); - } else if (entityType === 'control_adapter') { - dispatch(caBboxChanged(arg)); - } else if (entityType === 'regional_guidance') { - dispatch(rgBboxChanged(arg)); - } - }, - [dispatch] - ); - - const onBrushLineAdded = useCallback( - (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => { - if (entityType === 'layer') { - dispatch(layerBrushLineAdded(arg)); - } else if (entityType === 'regional_guidance') { - dispatch(rgBrushLineAdded(arg)); - } - }, - [dispatch] - ); - const onEraserLineAdded = useCallback( - (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => { - if (entityType === 'layer') { - dispatch(layerEraserLineAdded(arg)); - } else if (entityType === 'regional_guidance') { - dispatch(rgEraserLineAdded(arg)); - } - }, - [dispatch] - ); - const onPointAddedToLine = useCallback( - (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => { - if (entityType === 'layer') { - dispatch(layerLinePointAdded(arg)); - } else if (entityType === 'regional_guidance') { - dispatch(rgLinePointAdded(arg)); - } - }, - [dispatch] - ); - const onRectShapeAdded = useCallback( - (arg: RectShapeAddedArg, entityType: CanvasEntity['type']) => { - if (entityType === 'layer') { - dispatch(layerRectAdded(arg)); - } else if (entityType === 'regional_guidance') { - dispatch(rgRectAdded(arg)); - } - }, - [dispatch] - ); - const onBboxTransformed = useCallback( - (bbox: IRect) => { - dispatch(bboxChanged(bbox)); - }, - [dispatch] - ); - const onBrushWidthChanged = useCallback( - (width: number) => { - dispatch(brushWidthChanged(width)); - }, - [dispatch] - ); - const onEraserWidthChanged = useCallback( - (width: number) => { - dispatch(eraserWidthChanged(width)); - }, - [dispatch] - ); - const setTool = useCallback( - (tool: Tool) => { - dispatch(toolChanged(tool)); - }, - [dispatch] - ); - const setToolBuffer = useCallback( - (toolBuffer: Tool | null) => { - dispatch(toolBufferChanged(toolBuffer)); - }, - [dispatch] - ); - - useLayoutEffect(() => { - log.trace('Initializing stage'); - if (!container) { - return; - } - stage.container(container); - return () => { - log.trace('Cleaning up stage'); - stage.destroy(); - }; - }, [container, stage]); - - useLayoutEffect(() => { - log.trace('Adding stage listeners'); - if (asPreview || !container) { - return; - } - - const cleanup = setStageEventHandlers({ - stage, - getToolState: $toolState.get, - setTool, - setToolBuffer, - getIsDrawing: $isDrawing.get, - setIsDrawing: $isDrawing.set, - getIsMouseDown: $isMouseDown.get, - setIsMouseDown: $isMouseDown.set, - getSelectedEntity: $selectedEntity.get, - getLastAddedPoint: $lastAddedPoint.get, - setLastAddedPoint: $lastAddedPoint.set, - getLastCursorPos: $lastCursorPos.get, - setLastCursorPos: $lastCursorPos.set, - getLastMouseDownPos: $lastMouseDownPos.get, - setLastMouseDownPos: $lastMouseDownPos.set, - getSpaceKey: $spaceKey.get, - setStageAttrs: $stageAttrs.set, - getDocument: $document.get, - getBbox: $bbox.get, - onBrushLineAdded, - onEraserLineAdded, - onPointAddedToLine, - onRectShapeAdded, - onBrushWidthChanged, - onEraserWidthChanged, - getCurrentFill: $currentFill.get, - }); - - return () => { - log.trace('Removing stage listeners'); - cleanup(); - }; - }, [ - asPreview, - onBrushLineAdded, - onBrushWidthChanged, - onEraserLineAdded, - onPointAddedToLine, - onRectShapeAdded, - stage, - container, - onEraserWidthChanged, - setTool, - setToolBuffer, - ]); - - useLayoutEffect(() => { - log.trace('Updating stage dimensions'); - if (!container) { - return; - } - - const fitStageToContainer = () => { - stage.width(container.offsetWidth); - stage.height(container.offsetHeight); - $stageAttrs.set({ - x: stage.x(), - y: stage.y(), - width: stage.width(), - height: stage.height(), - scale: stage.scaleX(), - }); - renderBackgroundLayer(stage); - renderDocumentBoundsOverlay(stage, $document.get); - }; - - const resizeObserver = new ResizeObserver(fitStageToContainer); - resizeObserver.observe(container); - fitStageToContainer(); - - return () => { - resizeObserver.disconnect(); - }; - }, [stage, container]); - - useLayoutEffect(() => { - if (asPreview) { - // Preview should not display tool - return; - } - log.trace('Rendering tool preview'); - renderers.renderToolPreview( - stage, - tool, - currentFill, - selectedEntity, - lastCursorPos, - lastMouseDownPos, - isDrawing, - isMouseDown - ); - }, [ - asPreview, - currentFill, - document, - isDrawing, - isMouseDown, - lastCursorPos, - lastMouseDownPos, - renderers, - selectedEntity, - stage, - tool, - ]); - - useLayoutEffect(() => { - if (asPreview) { - // Preview should not display tool - return; - } - log.trace('Rendering bbox preview'); - renderers.renderBboxPreview( - stage, - bbox, - tool.selected, - $bbox.get, - onBboxTransformed, - $shift.get, - $ctrl.get, - $meta.get, - $alt.get - ); - }, [asPreview, bbox, onBboxTransformed, renderers, stage, tool.selected]); - - useLayoutEffect(() => { - log.trace('Rendering layers'); - renderLayers(stage, layers, tool.selected, onPosChanged); - }, [layers, onPosChanged, stage, tool.selected]); - - useLayoutEffect(() => { - log.trace('Rendering regions'); - renderRegions(stage, regions, maskOpacity, tool.selected, selectedEntity, onPosChanged); - }, [maskOpacity, onPosChanged, regions, selectedEntity, stage, tool.selected]); - - useLayoutEffect(() => { - log.trace('Rendering layers'); - renderControlAdapters(stage, controlAdapters, getImageDTO); - }, [controlAdapters, stage]); - - useLayoutEffect(() => { - renderDocumentBoundsOverlay(stage, $document.get); - }, [stage, document]); - - useLayoutEffect(() => { - arrangeEntities(stage, layers, controlAdapters, regions); - }, [layers, controlAdapters, regions, stage]); - - // useLayoutEffect(() => { - // if (asPreview) { - // // Preview should not check for transparency - // return; - // } - // log.trace('Updating bboxes'); - // debouncedRenderers.updateBboxes(stage, state.layers, onBboxChanged); - // }, [stage, asPreview, state.layers, onBboxChanged]); + const cleanup = initializeRenderer(store, stage, container); + return cleanup; + }, [asPreview, container, stage, store]); useLayoutEffect(() => { Konva.pixelRatio = dpr; }, [dpr]); - - useEffect( - () => () => { - stage.destroy(); - }, - [stage] - ); }; type Props = { @@ -426,9 +45,15 @@ export const StageComponent = memo(({ asPreview = false }: Props) => { useStageRenderer(stage, container, asPreview); + useEffect( + () => () => { + stage.destroy(); + }, + [stage] + ); + return ( - {!asPreview && } { }); StageComponent.displayName = 'StageComponent'; - -const NoEntitiesFallback = () => { - const { t } = useTranslation(); - const entityCount = useAppSelector(selectEntityCount); - - if (entityCount) { - return null; - } - - return ( - - {t('controlLayers.noLayersAdded')} - - ); -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index cb1f83a3340..6614d1c9dc8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -1,5 +1,10 @@ import { renderBackgroundLayer } from 'features/controlLayers/konva/renderers/background'; -import { renderDocumentBoundsOverlay, scaleToolPreview } from 'features/controlLayers/konva/renderers/previewLayer'; +import { + renderDocumentBoundsOverlay, + renderToolPreview, + scaleToolPreview, +} from 'features/controlLayers/konva/renderers/previewLayer'; +import { fitDocumentToStage } from 'features/controlLayers/konva/renderers/stage'; import { getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util'; import type { BrushLineAddedArg, @@ -19,7 +24,6 @@ import { clamp } from 'lodash-es'; import { BRUSH_SPACING_TARGET_SCALE, CANVAS_SCALE_BY, - DOCUMENT_FIT_PADDING_PX, MAX_BRUSH_SPACING_PX, MAX_CANVAS_SCALE, MIN_BRUSH_SPACING_PX, @@ -164,6 +168,16 @@ export const setStageEventHandlers = ({ } const tool = getToolState().selected; stage.findOne(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); + renderToolPreview( + stage, + getToolState(), + getCurrentFill(), + getSelectedEntity(), + getLastCursorPos(), + getLastAddedPoint(), + getIsDrawing(), + getIsMouseDown() + ); }); //#region mousedown @@ -176,33 +190,50 @@ export const setStageEventHandlers = ({ const toolState = getToolState(); const pos = updateLastCursorPos(stage, setLastCursorPos); const selectedEntity = getSelectedEntity(); - if (!pos || !selectedEntity) { - return; - } - if (selectedEntity.type !== 'regional_guidance' && selectedEntity.type !== 'layer') { - return; - } - - if (getSpaceKey()) { - // No drawing when space is down - we are panning the stage - return; - } - setIsDrawing(true); - setLastMouseDownPos(pos); + if ( + pos && + selectedEntity && + (selectedEntity?.type === 'regional_guidance' || selectedEntity?.type === 'layer') && + !getSpaceKey() + ) { + setIsDrawing(true); + setLastMouseDownPos(pos); - if (toolState.selected === 'brush') { - const bbox = getBbox(); - if (e.evt.shiftKey) { - const lastAddedPoint = getLastAddedPoint(); - // Create a straight line if holding shift - if (lastAddedPoint) { + if (toolState.selected === 'brush') { + const bbox = getBbox(); + if (e.evt.shiftKey) { + const lastAddedPoint = getLastAddedPoint(); + // Create a straight line if holding shift + if (lastAddedPoint) { + onBrushLineAdded( + { + id: selectedEntity.id, + points: [ + lastAddedPoint.x - selectedEntity.x, + lastAddedPoint.y - selectedEntity.y, + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + ], + color: getCurrentFill(), + width: toolState.brush.width, + clip: { + x: bbox.x, + y: bbox.y, + width: bbox.width, + height: bbox.height, + }, + }, + selectedEntity.type + ); + } + } else { onBrushLineAdded( { id: selectedEntity.id, points: [ - lastAddedPoint.x - selectedEntity.x, - lastAddedPoint.y - selectedEntity.y, + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, pos.x - selectedEntity.x, pos.y - selectedEntity.y, ], @@ -218,43 +249,42 @@ export const setStageEventHandlers = ({ selectedEntity.type ); } - } else { - onBrushLineAdded( - { - id: selectedEntity.id, - points: [ - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - ], - color: getCurrentFill(), - width: toolState.brush.width, - clip: { - x: bbox.x, - y: bbox.y, - width: bbox.width, - height: bbox.height, - }, - }, - selectedEntity.type - ); + setLastAddedPoint(pos); } - setLastAddedPoint(pos); - } - if (toolState.selected === 'eraser') { - const bbox = getBbox(); - if (e.evt.shiftKey) { - // Create a straight line if holding shift - const lastAddedPoint = getLastAddedPoint(); - if (lastAddedPoint) { + if (toolState.selected === 'eraser') { + const bbox = getBbox(); + if (e.evt.shiftKey) { + // Create a straight line if holding shift + const lastAddedPoint = getLastAddedPoint(); + if (lastAddedPoint) { + onEraserLineAdded( + { + id: selectedEntity.id, + points: [ + lastAddedPoint.x - selectedEntity.x, + lastAddedPoint.y - selectedEntity.y, + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + ], + width: toolState.eraser.width, + clip: { + x: bbox.x, + y: bbox.y, + width: bbox.width, + height: bbox.height, + }, + }, + selectedEntity.type + ); + } + } else { onEraserLineAdded( { id: selectedEntity.id, points: [ - lastAddedPoint.x - selectedEntity.x, - lastAddedPoint.y - selectedEntity.y, + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, pos.x - selectedEntity.x, pos.y - selectedEntity.y, ], @@ -269,29 +299,19 @@ export const setStageEventHandlers = ({ selectedEntity.type ); } - } else { - onEraserLineAdded( - { - id: selectedEntity.id, - points: [ - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - ], - width: toolState.eraser.width, - clip: { - x: bbox.x, - y: bbox.y, - width: bbox.width, - height: bbox.height, - }, - }, - selectedEntity.type - ); + setLastAddedPoint(pos); } - setLastAddedPoint(pos); } + renderToolPreview( + stage, + getToolState(), + getCurrentFill(), + getSelectedEntity(), + getLastCursorPos(), + getLastAddedPoint(), + getIsDrawing(), + getIsMouseDown() + ); }); //#region mouseup @@ -304,41 +324,47 @@ export const setStageEventHandlers = ({ const pos = getLastCursorPos(); const selectedEntity = getSelectedEntity(); - if (!pos || !selectedEntity) { - return; - } - if (selectedEntity.type !== 'regional_guidance' && selectedEntity.type !== 'layer') { - return; - } - - if (getSpaceKey()) { - // No drawing when space is down - we are panning the stage - return; - } - - const toolState = getToolState(); + if ( + pos && + selectedEntity && + (selectedEntity?.type === 'regional_guidance' || selectedEntity?.type === 'layer') && + !getSpaceKey() + ) { + const toolState = getToolState(); - if (toolState.selected === 'rect') { - const lastMouseDownPos = getLastMouseDownPos(); - if (lastMouseDownPos) { - onRectShapeAdded( - { - id: selectedEntity.id, - rect: { - x: Math.min(pos.x, lastMouseDownPos.x), - y: Math.min(pos.y, lastMouseDownPos.y), - width: Math.abs(pos.x - lastMouseDownPos.x), - height: Math.abs(pos.y - lastMouseDownPos.y), + if (toolState.selected === 'rect') { + const lastMouseDownPos = getLastMouseDownPos(); + if (lastMouseDownPos) { + onRectShapeAdded( + { + id: selectedEntity.id, + rect: { + x: Math.min(pos.x, lastMouseDownPos.x), + y: Math.min(pos.y, lastMouseDownPos.y), + width: Math.abs(pos.x - lastMouseDownPos.x), + height: Math.abs(pos.y - lastMouseDownPos.y), + }, + color: getCurrentFill(), }, - color: getCurrentFill(), - }, - selectedEntity.type - ); + selectedEntity.type + ); + } } + + setIsDrawing(false); + setLastMouseDownPos(null); } - setIsDrawing(false); - setLastMouseDownPos(null); + renderToolPreview( + stage, + getToolState(), + getCurrentFill(), + getSelectedEntity(), + getLastCursorPos(), + getLastAddedPoint(), + getIsDrawing(), + getIsMouseDown() + ); }); //#region mousemove @@ -355,84 +381,101 @@ export const setStageEventHandlers = ({ .findOne(`#${PREVIEW_TOOL_GROUP_ID}`) ?.visible(toolState.selected === 'brush' || toolState.selected === 'eraser'); - if (!pos || !selectedEntity) { - return; - } - if (selectedEntity.type !== 'regional_guidance' && selectedEntity.type !== 'layer') { - return; - } - - if (getSpaceKey()) { - // No drawing when space is down - we are panning the stage - return; - } - - if (!getIsMouseDown()) { - return; - } - - if (toolState.selected === 'brush') { - if (getIsDrawing()) { - // Continue the last line - maybeAddNextPoint(selectedEntity, pos, getToolState, getLastAddedPoint, setLastAddedPoint, onPointAddedToLine); - } else { - const bbox = getBbox(); - // Start a new line - onBrushLineAdded( - { - id: selectedEntity.id, - points: [ - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - ], - width: toolState.brush.width, - color: getCurrentFill(), - clip: { - x: bbox.x, - y: bbox.y, - width: bbox.width, - height: bbox.height, + if ( + pos && + selectedEntity && + (selectedEntity.type === 'regional_guidance' || selectedEntity.type === 'layer') && + !getSpaceKey() && + getIsMouseDown() + ) { + if (toolState.selected === 'brush') { + if (getIsDrawing()) { + // Continue the last line + maybeAddNextPoint( + selectedEntity, + pos, + getToolState, + getLastAddedPoint, + setLastAddedPoint, + onPointAddedToLine + ); + } else { + const bbox = getBbox(); + // Start a new line + onBrushLineAdded( + { + id: selectedEntity.id, + points: [ + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + ], + width: toolState.brush.width, + color: getCurrentFill(), + clip: { + x: bbox.x, + y: bbox.y, + width: bbox.width, + height: bbox.height, + }, }, - }, - selectedEntity.type - ); - setLastAddedPoint(pos); - setIsDrawing(true); + selectedEntity.type + ); + setLastAddedPoint(pos); + setIsDrawing(true); + } } - } - if (toolState.selected === 'eraser') { - if (getIsDrawing()) { - // Continue the last line - maybeAddNextPoint(selectedEntity, pos, getToolState, getLastAddedPoint, setLastAddedPoint, onPointAddedToLine); - } else { - const bbox = getBbox(); - // Start a new line - onEraserLineAdded( - { - id: selectedEntity.id, - points: [ - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - ], - width: toolState.eraser.width, - clip: { - x: bbox.x, - y: bbox.y, - width: bbox.width, - height: bbox.height, + if (toolState.selected === 'eraser') { + if (getIsDrawing()) { + // Continue the last line + maybeAddNextPoint( + selectedEntity, + pos, + getToolState, + getLastAddedPoint, + setLastAddedPoint, + onPointAddedToLine + ); + } else { + const bbox = getBbox(); + // Start a new line + onEraserLineAdded( + { + id: selectedEntity.id, + points: [ + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + ], + width: toolState.eraser.width, + clip: { + x: bbox.x, + y: bbox.y, + width: bbox.width, + height: bbox.height, + }, }, - }, - selectedEntity.type - ); - setLastAddedPoint(pos); - setIsDrawing(true); + selectedEntity.type + ); + setLastAddedPoint(pos); + setIsDrawing(true); + } } } + + renderToolPreview( + stage, + getToolState(), + getCurrentFill(), + getSelectedEntity(), + getLastCursorPos(), + getLastAddedPoint(), + getIsDrawing(), + getIsMouseDown() + ); }); //#region mouseleave @@ -450,24 +493,33 @@ export const setStageEventHandlers = ({ stage.findOne(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(false); - if (!pos || !selectedEntity) { - return; - } - if (selectedEntity.type !== 'regional_guidance' && selectedEntity.type !== 'layer') { - return; - } - if (getSpaceKey()) { - // No drawing when space is down - we are panning the stage - return; - } - if (getIsMouseDown()) { - if (toolState.selected === 'brush') { - onPointAddedToLine({ id: selectedEntity.id, point: [pos.x, pos.y] }, selectedEntity.type); - } - if (toolState.selected === 'eraser') { - onPointAddedToLine({ id: selectedEntity.id, point: [pos.x, pos.y] }, selectedEntity.type); + if ( + pos && + selectedEntity && + (selectedEntity.type === 'regional_guidance' || selectedEntity.type === 'layer') && + !getSpaceKey() && + getIsMouseDown() + ) { + if (getIsMouseDown()) { + if (toolState.selected === 'brush') { + onPointAddedToLine({ id: selectedEntity.id, point: [pos.x, pos.y] }, selectedEntity.type); + } + if (toolState.selected === 'eraser') { + onPointAddedToLine({ id: selectedEntity.id, point: [pos.x, pos.y] }, selectedEntity.type); + } } } + + renderToolPreview( + stage, + getToolState(), + getCurrentFill(), + getSelectedEntity(), + getLastCursorPos(), + getLastAddedPoint(), + getIsDrawing(), + getIsMouseDown() + ); }); //#region wheel @@ -489,31 +541,40 @@ export const setStageEventHandlers = ({ } else { // We need the absolute cursor position - not the scaled position const cursorPos = stage.getPointerPosition(); - if (!cursorPos) { - return; + if (cursorPos) { + // Stage's x and y scale are always the same + const stageScale = stage.scaleX(); + // When wheeling on trackpad, e.evt.ctrlKey is true - in that case, let's reverse the direction + const delta = e.evt.ctrlKey ? -e.evt.deltaY : e.evt.deltaY; + const mousePointTo = { + x: (cursorPos.x - stage.x()) / stageScale, + y: (cursorPos.y - stage.y()) / stageScale, + }; + const newScale = clamp(stageScale * CANVAS_SCALE_BY ** delta, MIN_CANVAS_SCALE, MAX_CANVAS_SCALE); + const newPos = { + x: cursorPos.x - mousePointTo.x * newScale, + y: cursorPos.y - mousePointTo.y * newScale, + }; + + stage.scaleX(newScale); + stage.scaleY(newScale); + stage.position(newPos); + setStageAttrs({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale }); + renderBackgroundLayer(stage); + scaleToolPreview(stage, getToolState()); + renderDocumentBoundsOverlay(stage, getDocument); } - // Stage's x and y scale are always the same - const stageScale = stage.scaleX(); - // When wheeling on trackpad, e.evt.ctrlKey is true - in that case, let's reverse the direction - const delta = e.evt.ctrlKey ? -e.evt.deltaY : e.evt.deltaY; - const mousePointTo = { - x: (cursorPos.x - stage.x()) / stageScale, - y: (cursorPos.y - stage.y()) / stageScale, - }; - const newScale = clamp(stageScale * CANVAS_SCALE_BY ** delta, MIN_CANVAS_SCALE, MAX_CANVAS_SCALE); - const newPos = { - x: cursorPos.x - mousePointTo.x * newScale, - y: cursorPos.y - mousePointTo.y * newScale, - }; - - stage.scaleX(newScale); - stage.scaleY(newScale); - stage.position(newPos); - setStageAttrs({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale }); - renderBackgroundLayer(stage); - scaleToolPreview(stage, getToolState()); - renderDocumentBoundsOverlay(stage, getDocument); } + renderToolPreview( + stage, + getToolState(), + getCurrentFill(), + getSelectedEntity(), + getLastCursorPos(), + getLastAddedPoint(), + getIsDrawing(), + getIsMouseDown() + ); }); //#region dragmove @@ -527,6 +588,16 @@ export const setStageEventHandlers = ({ }); renderBackgroundLayer(stage); renderDocumentBoundsOverlay(stage, getDocument); + renderToolPreview( + stage, + getToolState(), + getCurrentFill(), + getSelectedEntity(), + getLastCursorPos(), + getLastAddedPoint(), + getIsDrawing(), + getIsMouseDown() + ); }); //#region dragend @@ -539,6 +610,16 @@ export const setStageEventHandlers = ({ height: stage.height(), scale: stage.scaleX(), }); + renderToolPreview( + stage, + getToolState(), + getCurrentFill(), + getSelectedEntity(), + getLastCursorPos(), + getLastAddedPoint(), + getIsDrawing(), + getIsMouseDown() + ); }); //#region key @@ -558,21 +639,22 @@ export const setStageEventHandlers = ({ setToolBuffer(getToolState().selected); setTool('view'); } else if (e.key === 'r') { - // Fit & center the document on the stage - const width = stage.width(); - const height = stage.height(); - const document = getDocument(); - const docWidthWithBuffer = document.width + DOCUMENT_FIT_PADDING_PX * 2; - const docHeightWithBuffer = document.height + DOCUMENT_FIT_PADDING_PX * 2; - const scale = Math.min(Math.min(width / docWidthWithBuffer, height / docHeightWithBuffer), 1); - const x = (width - docWidthWithBuffer * scale) / 2 + DOCUMENT_FIT_PADDING_PX * scale; - const y = (height - docHeightWithBuffer * scale) / 2 + DOCUMENT_FIT_PADDING_PX * scale; - stage.setAttrs({ x, y, width, height, scaleX: scale, scaleY: scale }); - setStageAttrs({ x, y, width, height, scale }); + const stageAttrs = fitDocumentToStage(stage, getDocument()); + setStageAttrs(stageAttrs); scaleToolPreview(stage, getToolState()); renderBackgroundLayer(stage); renderDocumentBoundsOverlay(stage, getDocument); } + renderToolPreview( + stage, + getToolState(), + getCurrentFill(), + getSelectedEntity(), + getLastCursorPos(), + getLastAddedPoint(), + getIsDrawing(), + getIsMouseDown() + ); }; window.addEventListener('keydown', onKeyDown); @@ -589,6 +671,16 @@ export const setStageEventHandlers = ({ setTool(toolBuffer ?? 'move'); setToolBuffer(null); } + renderToolPreview( + stage, + getToolState(), + getCurrentFill(), + getSelectedEntity(), + getLastCursorPos(), + getLastAddedPoint(), + getIsDrawing(), + getIsMouseDown() + ); }; window.addEventListener('keyup', onKeyUp); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts index cd17862f3ad..c14d643657d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts @@ -6,8 +6,14 @@ import { RG_LAYER_OBJECT_GROUP_NAME, } from 'features/controlLayers/konva/naming'; import { createBboxRect } from 'features/controlLayers/konva/renderers/objects'; -import { imageDataToDataURL } from "features/controlLayers/konva/util"; -import type { ControlAdapterEntity, LayerEntity, RegionEntity } from 'features/controlLayers/store/types'; +import { imageDataToDataURL } from 'features/controlLayers/konva/util'; +import type { + BboxChangedArg, + CanvasEntity, + ControlAdapterEntity, + LayerEntity, + RegionEntity, +} from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; import { assert } from 'tsafe'; @@ -186,10 +192,12 @@ const filterCAChildren = (node: Konva.Node): boolean => node.name() === CA_LAYER */ export const updateBboxes = ( stage: Konva.Stage, - entityStates: (ControlAdapterEntity | LayerEntity | RegionEntity)[], - onBboxChanged: (layerId: string, bbox: IRect | null) => void + layers: LayerEntity[], + controlAdapters: ControlAdapterEntity[], + regions: RegionEntity[], + onBboxChanged: (arg: BboxChangedArg, entityType: CanvasEntity['type']) => void ): void => { - for (const entityState of entityStates) { + for (const entityState of [...layers, ...controlAdapters, ...regions]) { const konvaLayer = stage.findOne(`#${entityState.id}`); assert(konvaLayer, `Layer ${entityState.id} not found in stage`); // We only need to recalculate the bbox if the layer has changed @@ -202,24 +210,30 @@ export const updateBboxes = ( if (entityState.type === 'layer') { if (entityState.objects.length === 0) { - // No objects - no bbox to calculate - onBboxChanged(entityState.id, null); + // No objects - no bbox to calculate + onBboxChanged({ id: entityState.id, bbox: null }, 'layer'); } else { - onBboxChanged(entityState.id, getLayerBboxPixels(konvaLayer, filterLayerChildren)); + onBboxChanged({ id: entityState.id, bbox: getLayerBboxPixels(konvaLayer, filterLayerChildren) }, 'layer'); } } else if (entityState.type === 'control_adapter') { if (!entityState.image && !entityState.processedImage) { // No objects - no bbox to calculate - onBboxChanged(entityState.id, null); + onBboxChanged({ id: entityState.id, bbox: null }, 'control_adapter'); } else { - onBboxChanged(entityState.id, getLayerBboxPixels(konvaLayer, filterCAChildren)); + onBboxChanged( + { id: entityState.id, bbox: getLayerBboxPixels(konvaLayer, filterCAChildren) }, + 'control_adapter' + ); } } else if (entityState.type === 'regional_guidance') { if (entityState.objects.length === 0) { // No objects - no bbox to calculate - onBboxChanged(entityState.id, null); + onBboxChanged({ id: entityState.id, bbox: null }, 'regional_guidance'); } else { - onBboxChanged(entityState.id, getLayerBboxPixels(konvaLayer, filterRGChildren)); + onBboxChanged( + { id: entityState.id, bbox: getLayerBboxPixels(konvaLayer, filterRGChildren) }, + 'regional_guidance' + ); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts new file mode 100644 index 00000000000..d339de9d5bc --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -0,0 +1,393 @@ +import { $alt, $ctrl, $meta, $shift } from '@invoke-ai/ui-library'; +import type { Store } from '@reduxjs/toolkit'; +import { logger } from 'app/logging/logger'; +import { $isDebugging } from 'app/store/nanostores/isDebugging'; +import type { RootState } from 'app/store/store'; +import { setStageEventHandlers } from 'features/controlLayers/konva/events'; +import { renderBackgroundLayer } from 'features/controlLayers/konva/renderers/background'; +import { updateBboxes } from 'features/controlLayers/konva/renderers/bbox'; +import { renderControlAdapters } from 'features/controlLayers/konva/renderers/caLayer'; +import { arrangeEntities } from 'features/controlLayers/konva/renderers/layers'; +import { + renderBboxPreview, + renderDocumentBoundsOverlay, + scaleToolPreview, +} from 'features/controlLayers/konva/renderers/previewLayer'; +import { renderLayers } from 'features/controlLayers/konva/renderers/rasterLayer'; +import { renderRegions } from 'features/controlLayers/konva/renderers/rgLayer'; +import { fitDocumentToStage } from 'features/controlLayers/konva/renderers/stage'; +import { + $isDrawing, + $isMouseDown, + $lastAddedPoint, + $lastCursorPos, + $lastMouseDownPos, + $spaceKey, + $stageAttrs, + bboxChanged, + brushWidthChanged, + caBboxChanged, + caTranslated, + eraserWidthChanged, + layerBboxChanged, + layerBrushLineAdded, + layerEraserLineAdded, + layerLinePointAdded, + layerRectAdded, + layerTranslated, + rgBboxChanged, + rgBrushLineAdded, + rgEraserLineAdded, + rgLinePointAdded, + rgRectAdded, + rgTranslated, + toolBufferChanged, + toolChanged, +} from 'features/controlLayers/store/canvasV2Slice'; +import type { + BboxChangedArg, + BrushLineAddedArg, + CanvasEntity, + CanvasEntityIdentifier, + CanvasV2State, + EraserLineAddedArg, + PointAddedToLineArg, + PosChangedArg, + RectShapeAddedArg, + Tool, +} from 'features/controlLayers/store/types'; +import type Konva from 'konva'; +import type { IRect } from 'konva/lib/types'; +import { debounce } from 'lodash-es'; +import type { RgbaColor } from 'react-colorful'; +import { getImageDTO } from 'services/api/endpoints/images'; +/** + * Initializes the canvas renderer. It subscribes to the redux store and listens for changes directly, bypassing the + * react rendering cycle entirely, improving canvas performance. + * @param store The Redux store + * @param stage The Konva stage + * @param container The stage's target container element + * @returns A cleanup function + */ +export const initializeRenderer = ( + store: Store, + stage: Konva.Stage, + container: HTMLDivElement | null +): (() => void) => { + const _log = logger('konva'); + /** + * Logs a message to the console if debugging is enabled. + */ + const logIfDebugging = (message: string) => { + if ($isDebugging.get()) { + _log.trace(message); + } + }; + + logIfDebugging('Initializing renderer'); + if (!container) { + // Nothing to clean up + logIfDebugging('No stage container, skipping initialization'); + return () => {}; + } + + stage.container(container); + + // Set up callbacks for various events + const onPosChanged = (arg: PosChangedArg, entityType: CanvasEntity['type']) => { + logIfDebugging('Position changed'); + if (entityType === 'layer') { + dispatch(layerTranslated(arg)); + } else if (entityType === 'control_adapter') { + dispatch(caTranslated(arg)); + } else if (entityType === 'regional_guidance') { + dispatch(rgTranslated(arg)); + } + }; + const onBboxChanged = (arg: BboxChangedArg, entityType: CanvasEntity['type']) => { + logIfDebugging('Entity bbox changed'); + if (entityType === 'layer') { + dispatch(layerBboxChanged(arg)); + } else if (entityType === 'control_adapter') { + dispatch(caBboxChanged(arg)); + } else if (entityType === 'regional_guidance') { + dispatch(rgBboxChanged(arg)); + } + }; + const onBrushLineAdded = (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => { + logIfDebugging('Brush line added'); + if (entityType === 'layer') { + dispatch(layerBrushLineAdded(arg)); + } else if (entityType === 'regional_guidance') { + dispatch(rgBrushLineAdded(arg)); + } + }; + const onEraserLineAdded = (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => { + logIfDebugging('Eraser line added'); + if (entityType === 'layer') { + dispatch(layerEraserLineAdded(arg)); + } else if (entityType === 'regional_guidance') { + dispatch(rgEraserLineAdded(arg)); + } + }; + const onPointAddedToLine = (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => { + logIfDebugging('Point added to line'); + if (entityType === 'layer') { + dispatch(layerLinePointAdded(arg)); + } else if (entityType === 'regional_guidance') { + dispatch(rgLinePointAdded(arg)); + } + }; + const onRectShapeAdded = (arg: RectShapeAddedArg, entityType: CanvasEntity['type']) => { + logIfDebugging('Rect shape added'); + if (entityType === 'layer') { + dispatch(layerRectAdded(arg)); + } else if (entityType === 'regional_guidance') { + dispatch(rgRectAdded(arg)); + } + }; + const onBboxTransformed = (bbox: IRect) => { + logIfDebugging('Generation bbox transformed'); + dispatch(bboxChanged(bbox)); + }; + const onBrushWidthChanged = (width: number) => { + logIfDebugging('Brush width changed'); + dispatch(brushWidthChanged(width)); + }; + const onEraserWidthChanged = (width: number) => { + logIfDebugging('Eraser width changed'); + dispatch(eraserWidthChanged(width)); + }; + const setTool = (tool: Tool) => { + logIfDebugging('Tool selection changed'); + dispatch(toolChanged(tool)); + }; + const setToolBuffer = (toolBuffer: Tool | null) => { + logIfDebugging('Tool buffer changed'); + dispatch(toolBufferChanged(toolBuffer)); + }; + + const _getSelectedEntity = (canvasV2: CanvasV2State): CanvasEntity | null => { + const identifier = canvasV2.selectedEntityIdentifier; + let selectedEntity: CanvasEntity | null = null; + if (!identifier) { + selectedEntity = null; + } else if (identifier.type === 'layer') { + selectedEntity = canvasV2.layers.find((i) => i.id === identifier.id) ?? null; + } else if (identifier.type === 'control_adapter') { + selectedEntity = canvasV2.controlAdapters.find((i) => i.id === identifier.id) ?? null; + } else if (identifier.type === 'ip_adapter') { + selectedEntity = canvasV2.ipAdapters.find((i) => i.id === identifier.id) ?? null; + } else if (identifier.type === 'regional_guidance') { + selectedEntity = canvasV2.regions.find((i) => i.id === identifier.id) ?? null; + } else { + selectedEntity = null; + } + logIfDebugging('Selected entity changed'); + return selectedEntity; + }; + + const _getCurrentFill = (canvasV2: CanvasV2State, selectedEntity: CanvasEntity | null) => { + let currentFill: RgbaColor = canvasV2.tool.fill; + if (selectedEntity && selectedEntity.type === 'regional_guidance') { + currentFill = { ...selectedEntity.fill, a: canvasV2.settings.maskOpacity }; + } else { + currentFill = canvasV2.tool.fill; + } + logIfDebugging('Current fill changed'); + return currentFill; + }; + + const { getState, subscribe, dispatch } = store; + + // Create closures for the rendering functions, used to check if specific parts of state have changed so we only + // render what needs to be rendered. + let prevCanvasV2 = getState().canvasV2; + let selectedEntityIdentifier: CanvasEntityIdentifier | null = prevCanvasV2.selectedEntityIdentifier; + let selectedEntity: CanvasEntity | null = _getSelectedEntity(prevCanvasV2); + let currentFill: RgbaColor = _getCurrentFill(prevCanvasV2, selectedEntity); + let didSelectedEntityChange: boolean = false; + + // On the first render, we need to render everything. + let isFirstRender = true; + + // Stage event listeners use a fully imperative approach to event handling, using these helpers to get state. + const getBbox = () => getState().canvasV2.bbox; + const getDocument = () => getState().canvasV2.document; + const getToolState = () => getState().canvasV2.tool; + const getSelectedEntity = () => selectedEntity; + const getCurrentFill = () => currentFill; + + // Calculating bounding boxes is expensive, must be debounced to not block the UI thread. + // TODO(psyche): Figure out how to do this in a worker. Probably means running the renderer in a worker and sending + // the entire state over when needed. + const debouncedUpdateBboxes = debounce(updateBboxes, 300); + + const cleanupListeners = setStageEventHandlers({ + stage, + getToolState, + setTool, + setToolBuffer, + getIsDrawing: $isDrawing.get, + setIsDrawing: $isDrawing.set, + getIsMouseDown: $isMouseDown.get, + setIsMouseDown: $isMouseDown.set, + getSelectedEntity, + getLastAddedPoint: $lastAddedPoint.get, + setLastAddedPoint: $lastAddedPoint.set, + getLastCursorPos: $lastCursorPos.get, + setLastCursorPos: $lastCursorPos.set, + getLastMouseDownPos: $lastMouseDownPos.get, + setLastMouseDownPos: $lastMouseDownPos.set, + getSpaceKey: $spaceKey.get, + setStageAttrs: $stageAttrs.set, + getDocument, + getBbox, + onBrushLineAdded, + onEraserLineAdded, + onPointAddedToLine, + onRectShapeAdded, + onBrushWidthChanged, + onEraserWidthChanged, + getCurrentFill, + }); + + const renderCanvas = () => { + const { canvasV2 } = store.getState(); + + if (prevCanvasV2 === canvasV2 && !isFirstRender) { + logIfDebugging('No changes detected, skipping render'); + return; + } + + // We can save some cycles for specific renderers if we track whether the selected entity has changed. + if (canvasV2.selectedEntityIdentifier !== selectedEntityIdentifier) { + selectedEntityIdentifier = canvasV2.selectedEntityIdentifier; + selectedEntity = _getSelectedEntity(canvasV2); + didSelectedEntityChange = true; + } else { + didSelectedEntityChange = false; + } + + // The current fill is either the tool fill or, if a regional guidance region is selected, the mask fill for that + // region. We need to manually sync this state. + if (isFirstRender || canvasV2.tool.fill !== prevCanvasV2.tool.fill || didSelectedEntityChange) { + currentFill = _getCurrentFill(canvasV2, selectedEntity); + } + + if ( + isFirstRender || + canvasV2.layers !== prevCanvasV2.layers || + canvasV2.tool.selected !== prevCanvasV2.tool.selected + ) { + logIfDebugging('Rendering layers'); + renderLayers(stage, canvasV2.layers, canvasV2.tool.selected, onPosChanged); + } + + if ( + isFirstRender || + canvasV2.regions !== prevCanvasV2.regions || + canvasV2.settings.maskOpacity !== prevCanvasV2.settings.maskOpacity || + canvasV2.tool.selected !== prevCanvasV2.tool.selected || + didSelectedEntityChange + ) { + logIfDebugging('Rendering regions'); + renderRegions( + stage, + canvasV2.regions, + canvasV2.settings.maskOpacity, + canvasV2.tool.selected, + selectedEntity, + onPosChanged + ); + } + + if (isFirstRender || canvasV2.controlAdapters !== prevCanvasV2.controlAdapters) { + logIfDebugging('Rendering control adapters'); + renderControlAdapters(stage, canvasV2.controlAdapters, getImageDTO); + } + + if (isFirstRender || canvasV2.document !== prevCanvasV2.document) { + logIfDebugging('Rendering document bounds overlay'); + renderDocumentBoundsOverlay(stage, getDocument); + } + + if (isFirstRender || canvasV2.bbox !== prevCanvasV2.bbox || canvasV2.tool.selected !== prevCanvasV2.tool.selected) { + logIfDebugging('Rendering generation bbox'); + renderBboxPreview( + stage, + canvasV2.bbox, + canvasV2.tool.selected, + getBbox, + onBboxTransformed, + $shift.get, + $ctrl.get, + $meta.get, + $alt.get + ); + } + + if ( + isFirstRender || + canvasV2.layers !== prevCanvasV2.layers || + canvasV2.controlAdapters !== prevCanvasV2.controlAdapters || + canvasV2.regions !== prevCanvasV2.regions + ) { + logIfDebugging('Updating entity bboxes'); + debouncedUpdateBboxes(stage, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions, onBboxChanged); + } + + if ( + isFirstRender || + canvasV2.layers !== prevCanvasV2.layers || + canvasV2.controlAdapters !== prevCanvasV2.controlAdapters || + canvasV2.regions !== prevCanvasV2.regions + ) { + logIfDebugging('Arranging entities'); + arrangeEntities(stage, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions); + } + + prevCanvasV2 = canvasV2; + + if (isFirstRender) { + isFirstRender = false; + } + }; + + // We can use a resize observer to ensure the stage always fits the container. We also need to re-render the bg and + // document bounds overlay when the stage is resized. + const fitStageToContainer = () => { + stage.width(container.offsetWidth); + stage.height(container.offsetHeight); + $stageAttrs.set({ + x: stage.x(), + y: stage.y(), + width: stage.width(), + height: stage.height(), + scale: stage.scaleX(), + }); + renderBackgroundLayer(stage); + renderDocumentBoundsOverlay(stage, getDocument); + }; + + const resizeObserver = new ResizeObserver(fitStageToContainer); + resizeObserver.observe(container); + fitStageToContainer(); + + const unsubscribeRenderer = subscribe(renderCanvas); + + logIfDebugging('First render of konva stage'); + // On first render, the document should be fit to the stage. + const stageAttrs = fitDocumentToStage(stage, prevCanvasV2.document); + // The HUD displays some of the stage attributes, so we need to update it here. + $stageAttrs.set(stageAttrs); + scaleToolPreview(stage, getToolState()); + renderCanvas(); + + return () => { + logIfDebugging('Cleaning up konva renderer'); + unsubscribeRenderer(); + cleanupListeners(); + resizeObserver.disconnect(); + }; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stage.ts new file mode 100644 index 00000000000..3af86041ed5 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stage.ts @@ -0,0 +1,16 @@ +import { DOCUMENT_FIT_PADDING_PX } from 'features/controlLayers/konva/constants'; +import type { CanvasV2State, StageAttrs } from 'features/controlLayers/store/types'; +import type Konva from 'konva'; + +export const fitDocumentToStage = (stage: Konva.Stage, document: CanvasV2State['document']): StageAttrs => { + // Fit & center the document on the stage + const width = stage.width(); + const height = stage.height(); + const docWidthWithBuffer = document.width + DOCUMENT_FIT_PADDING_PX * 2; + const docHeightWithBuffer = document.height + DOCUMENT_FIT_PADDING_PX * 2; + const scale = Math.min(Math.min(width / docWidthWithBuffer, height / docHeightWithBuffer), 1); + const x = (width - docWidthWithBuffer * scale) / 2 + DOCUMENT_FIT_PADDING_PX * scale; + const y = (height - docHeightWithBuffer * scale) / 2 + DOCUMENT_FIT_PADDING_PX * scale; + stage.setAttrs({ x, y, width, height, scaleX: scale, scaleY: scale }); + return { x, y, width, height, scale }; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 6841e23d83b..16d82845408 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -881,7 +881,7 @@ export type CanvasV2State = { export type StageAttrs = { x: number; y: number; width: number; height: number; scale: number }; export type PosChangedArg = { id: string; x: number; y: number }; -export type BboxChangedArg = { id: string; bbox: IRect }; +export type BboxChangedArg = { id: string; bbox: Rect | null }; export type EraserLineAddedArg = { id: string; points: [number, number, number, number]; From d0903430834e7281868c96cb6e8801405767ef18 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 18 Jun 2024 17:24:35 +1000 Subject: [PATCH 084/678] perf(ui): memoize layeractionsmenu valid actions --- .../controlLayers/components/Layer/LayerActionsMenu.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx index a9ebc599bef..5498ba6ade6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx @@ -1,5 +1,5 @@ import { Menu, MenuItem, MenuList } from '@invoke-ai/ui-library'; -import { createAppSelector } from 'app/store/createMemoizedSelector'; +import { createMemoizedAppSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; import { @@ -25,7 +25,7 @@ type Props = { id: string; }; -const selectValidActions = createAppSelector([selectCanvasV2Slice, (canvasV2, id: string) => id], (canvasV2, id) => { +const selectValidActions = createMemoizedAppSelector([selectCanvasV2Slice, (canvasV2, id: string) => id], (canvasV2, id) => { const layer = selectLayerOrThrow(canvasV2, id); const layerIndex = canvasV2.layers.indexOf(layer); const layerCount = canvasV2.layers.length; From 4067927a2311fc35c94f5615c7b05a1712a856bf Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 18 Jun 2024 17:24:49 +1000 Subject: [PATCH 085/678] tweak(ui): canvas editor layout --- .../features/controlLayers/components/ControlLayersEditor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx index e9275426fee..41ee961b590 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx @@ -11,7 +11,7 @@ export const ControlLayersEditor = memo(() => { flexDirection="column" height="100%" width="100%" - rowGap={4} + gap={2} alignItems="center" justifyContent="center" > From 159031a0717bd12c6ceedb2c14d7644bd4b47096 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 18 Jun 2024 17:25:39 +1000 Subject: [PATCH 086/678] refactor(ui): disable the preview renderer for now --- .../ImageSize/AspectRatioCanvasPreview.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioCanvasPreview.tsx b/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioCanvasPreview.tsx index b901cc494e8..e9a6d687422 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioCanvasPreview.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioCanvasPreview.tsx @@ -1,21 +1,21 @@ -import { Flex } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; -import { StageComponent } from 'features/controlLayers/components/StageComponent'; import { $isPreviewVisible } from 'features/controlLayers/store/canvasV2Slice'; +import { AspectRatioIconPreview } from 'features/parameters/components/ImageSize/AspectRatioIconPreview'; import { memo } from 'react'; export const AspectRatioCanvasPreview = memo(() => { const isPreviewVisible = useStore($isPreviewVisible); + return ; // if (!isPreviewVisible) { // return ; // } - return ( - - - - ); + // return ( + // + // + // + // ); }); AspectRatioCanvasPreview.displayName = 'AspectRatioCanvasPreview'; From bbacfe403c01b44cba3315b30f5a17d75ee103ec Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 18 Jun 2024 17:25:51 +1000 Subject: [PATCH 087/678] refactor(ui): enable global debugging flag --- invokeai/frontend/web/src/main.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/main.tsx b/invokeai/frontend/web/src/main.tsx index acf94917780..0479a70fbff 100644 --- a/invokeai/frontend/web/src/main.tsx +++ b/invokeai/frontend/web/src/main.tsx @@ -2,4 +2,4 @@ import ReactDOM from 'react-dom/client'; import InvokeAIUI from './app/components/InvokeAIUI'; -ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(); +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(); From c3cada6bd522af0bda97fd0ebe3c91ed6c19f5e8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 18 Jun 2024 19:21:48 +1000 Subject: [PATCH 088/678] fix(ui): incorrect rect/brush/eraser positions --- .../features/controlLayers/konva/events.ts | 29 +++-- .../controlLayers/konva/renderers/layers.ts | 94 +------------- .../controlLayers/konva/renderers/renderer.ts | 120 ++++++++++-------- .../controlLayers/konva/renderers/rgLayer.ts | 16 ++- .../controlLayers/store/canvasV2Slice.ts | 19 +-- .../nodes/util/graph/generation/addRegions.ts | 6 +- 6 files changed, 102 insertions(+), 182 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 6614d1c9dc8..19178a66518 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -50,6 +50,7 @@ type Arg = { setStageAttrs: (attrs: StageAttrs) => void; getSelectedEntity: () => CanvasEntity | null; getSpaceKey: () => boolean; + setSpaceKey: (val: boolean) => void; getDocument: () => CanvasV2State['document']; getBbox: () => CanvasV2State['bbox']; onBrushLineAdded: (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => void; @@ -151,6 +152,7 @@ export const setStageEventHandlers = ({ setStageAttrs, getSelectedEntity, getSpaceKey, + setSpaceKey, getDocument, getBbox, onBrushLineAdded, @@ -174,7 +176,7 @@ export const setStageEventHandlers = ({ getCurrentFill(), getSelectedEntity(), getLastCursorPos(), - getLastAddedPoint(), + getLastMouseDownPos(), getIsDrawing(), getIsMouseDown() ); @@ -190,7 +192,6 @@ export const setStageEventHandlers = ({ const toolState = getToolState(); const pos = updateLastCursorPos(stage, setLastCursorPos); const selectedEntity = getSelectedEntity(); - if ( pos && selectedEntity && @@ -308,7 +309,7 @@ export const setStageEventHandlers = ({ getCurrentFill(), getSelectedEntity(), getLastCursorPos(), - getLastAddedPoint(), + getLastMouseDownPos(), getIsDrawing(), getIsMouseDown() ); @@ -339,8 +340,8 @@ export const setStageEventHandlers = ({ { id: selectedEntity.id, rect: { - x: Math.min(pos.x, lastMouseDownPos.x), - y: Math.min(pos.y, lastMouseDownPos.y), + x: Math.min(pos.x - selectedEntity.x, lastMouseDownPos.x - selectedEntity.x), + y: Math.min(pos.y - selectedEntity.y, lastMouseDownPos.y - selectedEntity.y), width: Math.abs(pos.x - lastMouseDownPos.x), height: Math.abs(pos.y - lastMouseDownPos.y), }, @@ -361,7 +362,7 @@ export const setStageEventHandlers = ({ getCurrentFill(), getSelectedEntity(), getLastCursorPos(), - getLastAddedPoint(), + getLastMouseDownPos(), getIsDrawing(), getIsMouseDown() ); @@ -472,7 +473,7 @@ export const setStageEventHandlers = ({ getCurrentFill(), getSelectedEntity(), getLastCursorPos(), - getLastAddedPoint(), + getLastMouseDownPos(), getIsDrawing(), getIsMouseDown() ); @@ -516,7 +517,7 @@ export const setStageEventHandlers = ({ getCurrentFill(), getSelectedEntity(), getLastCursorPos(), - getLastAddedPoint(), + getLastMouseDownPos(), getIsDrawing(), getIsMouseDown() ); @@ -571,7 +572,7 @@ export const setStageEventHandlers = ({ getCurrentFill(), getSelectedEntity(), getLastCursorPos(), - getLastAddedPoint(), + getLastMouseDownPos(), getIsDrawing(), getIsMouseDown() ); @@ -594,7 +595,7 @@ export const setStageEventHandlers = ({ getCurrentFill(), getSelectedEntity(), getLastCursorPos(), - getLastAddedPoint(), + getLastMouseDownPos(), getIsDrawing(), getIsMouseDown() ); @@ -616,7 +617,7 @@ export const setStageEventHandlers = ({ getCurrentFill(), getSelectedEntity(), getLastCursorPos(), - getLastAddedPoint(), + getLastMouseDownPos(), getIsDrawing(), getIsMouseDown() ); @@ -638,6 +639,7 @@ export const setStageEventHandlers = ({ // Select the view tool on space key down setToolBuffer(getToolState().selected); setTool('view'); + setSpaceKey(true); } else if (e.key === 'r') { const stageAttrs = fitDocumentToStage(stage, getDocument()); setStageAttrs(stageAttrs); @@ -651,7 +653,7 @@ export const setStageEventHandlers = ({ getCurrentFill(), getSelectedEntity(), getLastCursorPos(), - getLastAddedPoint(), + getLastMouseDownPos(), getIsDrawing(), getIsMouseDown() ); @@ -670,6 +672,7 @@ export const setStageEventHandlers = ({ const toolBuffer = getToolState().selectedBuffer; setTool(toolBuffer ?? 'move'); setToolBuffer(null); + setSpaceKey(false); } renderToolPreview( stage, @@ -677,7 +680,7 @@ export const setStageEventHandlers = ({ getCurrentFill(), getSelectedEntity(), getLastCursorPos(), - getLastAddedPoint(), + getLastMouseDownPos(), getIsDrawing(), getIsMouseDown() ); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts index 7ed3fa31708..d0b148da4dc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -1,98 +1,6 @@ -import { DEBOUNCE_MS } from 'features/controlLayers/konva/constants'; import { BACKGROUND_LAYER_ID, PREVIEW_LAYER_ID } from 'features/controlLayers/konva/naming'; -import { updateBboxes } from 'features/controlLayers/konva/renderers/bbox'; -import { renderCALayer } from 'features/controlLayers/konva/renderers/caLayer'; -import { renderBboxPreview, renderToolPreview } from 'features/controlLayers/konva/renderers/previewLayer'; -import { renderRasterLayer } from 'features/controlLayers/konva/renderers/rasterLayer'; -import { renderRGLayer } from 'features/controlLayers/konva/renderers/rgLayer'; -import { mapId, selectRenderableLayers } from 'features/controlLayers/konva/util'; -import type { - CanvasEntity, - ControlAdapterEntity, - LayerEntity, - PosChangedArg, - RegionEntity, - Tool, -} from 'features/controlLayers/store/types'; +import type { ControlAdapterEntity, LayerEntity, RegionEntity } from 'features/controlLayers/store/types'; import type Konva from 'konva'; -import { debounce } from 'lodash-es'; -import type { ImageDTO } from 'services/api/types'; - -/** - * Logic for rendering arranging and rendering all layers. - */ - -/** - * Renders the layers on the stage. - * @param stage The konva stage - * @param layers Array of all layer states - * @param rgGlobalOpacity The global mask layer opacity - * @param tool The current tool - * @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source - * @param onPosChanged Callback for when the layer's position changes - */ -const renderLayers = ( - stage: Konva.Stage, - layers: LayerEntity[], - controlAdapters: ControlAdapterEntity[], - regions: RegionEntity[], - rgGlobalOpacity: number, - tool: Tool, - selectedEntity: CanvasEntity | null, - getImageDTO: (imageName: string) => Promise, - onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void -): void => { - const renderableIds = [...layers.map(mapId), ...controlAdapters.map(mapId), ...regions.map(mapId)]; - // Remove un-rendered layers - for (const konvaLayer of stage.find(selectRenderableLayers)) { - if (!renderableIds.includes(konvaLayer.id())) { - konvaLayer.destroy(); - } - } - // We'll need to ensure the tool preview layer is on top of the rest of the layers - let zIndex = 1; - for (const layer of layers) { - renderRasterLayer(stage, layer, tool, zIndex, onPosChanged); - zIndex++; - } - for (const ca of controlAdapters) { - renderCALayer(stage, ca, zIndex, getImageDTO); - zIndex++; - } - for (const rg of regions) { - renderRGLayer(stage, rg, rgGlobalOpacity, tool, zIndex, selectedEntity, onPosChanged); - zIndex++; - } - // Arrange the tool preview layer - stage.findOne(`#${PREVIEW_LAYER_ID}`)?.zIndex(zIndex); -}; - -/** - * All the renderers for the Konva stage. - */ -export const renderers = { - renderToolPreview, - renderBboxPreview, - renderLayers, - updateBboxes, -}; - -/** - * Gets the renderers with debouncing applied. - * @param ms The debounce time in milliseconds - * @returns The renderers with debouncing applied - */ -const getDebouncedRenderers = (ms = DEBOUNCE_MS): typeof renderers => ({ - renderToolPreview: debounce(renderToolPreview, ms), - renderBboxPreview: debounce(renderBboxPreview, ms), - renderLayers: debounce(renderLayers, ms), - updateBboxes: debounce(updateBboxes, ms), -}); - -/** - * All the renderers for the Konva stage, debounced. - */ -export const debouncedRenderers: typeof renderers = getDebouncedRenderers(); export const arrangeEntities = ( stage: Konva.Stage, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts index d339de9d5bc..0e9c8fc53ec 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -17,12 +17,6 @@ import { renderLayers } from 'features/controlLayers/konva/renderers/rasterLayer import { renderRegions } from 'features/controlLayers/konva/renderers/rgLayer'; import { fitDocumentToStage } from 'features/controlLayers/konva/renderers/stage'; import { - $isDrawing, - $isMouseDown, - $lastAddedPoint, - $lastCursorPos, - $lastMouseDownPos, - $spaceKey, $stageAttrs, bboxChanged, brushWidthChanged, @@ -48,7 +42,6 @@ import type { BboxChangedArg, BrushLineAddedArg, CanvasEntity, - CanvasEntityIdentifier, CanvasV2State, EraserLineAddedArg, PointAddedToLineArg, @@ -57,7 +50,7 @@ import type { Tool, } from 'features/controlLayers/store/types'; import type Konva from 'konva'; -import type { IRect } from 'konva/lib/types'; +import type { IRect, Vector2d } from 'konva/lib/types'; import { debounce } from 'lodash-es'; import type { RgbaColor } from 'react-colorful'; import { getImageDTO } from 'services/api/endpoints/images'; @@ -200,46 +193,77 @@ export const initializeRenderer = ( const { getState, subscribe, dispatch } = store; - // Create closures for the rendering functions, used to check if specific parts of state have changed so we only - // render what needs to be rendered. - let prevCanvasV2 = getState().canvasV2; - let selectedEntityIdentifier: CanvasEntityIdentifier | null = prevCanvasV2.selectedEntityIdentifier; - let selectedEntity: CanvasEntity | null = _getSelectedEntity(prevCanvasV2); - let currentFill: RgbaColor = _getCurrentFill(prevCanvasV2, selectedEntity); - let didSelectedEntityChange: boolean = false; - // On the first render, we need to render everything. let isFirstRender = true; - // Stage event listeners use a fully imperative approach to event handling, using these helpers to get state. + // Stage interaction listeners need helpers to get and update current state. Some of the state is read-only, like + // bbox, document and tool state, while interaction state is read-write. + + // Read-only state, derived from redux + let prevCanvasV2 = getState().canvasV2; + let prevSelectedEntity: CanvasEntity | null = _getSelectedEntity(prevCanvasV2); + let prevCurrentFill: RgbaColor = _getCurrentFill(prevCanvasV2, prevSelectedEntity); + const getSelectedEntity = () => prevSelectedEntity; + const getCurrentFill = () => prevCurrentFill; const getBbox = () => getState().canvasV2.bbox; const getDocument = () => getState().canvasV2.document; const getToolState = () => getState().canvasV2.tool; - const getSelectedEntity = () => selectedEntity; - const getCurrentFill = () => currentFill; - // Calculating bounding boxes is expensive, must be debounced to not block the UI thread. - // TODO(psyche): Figure out how to do this in a worker. Probably means running the renderer in a worker and sending - // the entire state over when needed. - const debouncedUpdateBboxes = debounce(updateBboxes, 300); + // Read-write state, ephemeral interaction state + let isDrawing = false; + const getIsDrawing = () => isDrawing; + const setIsDrawing = (val: boolean) => { + isDrawing = val; + }; + + let isMouseDown = false; + const getIsMouseDown = () => isMouseDown; + const setIsMouseDown = (val: boolean) => { + isMouseDown = val; + }; + + let lastAddedPoint: Vector2d | null = null; + const getLastAddedPoint = () => lastAddedPoint; + const setLastAddedPoint = (val: Vector2d | null) => { + lastAddedPoint = val; + }; + + let lastMouseDownPos: Vector2d | null = null; + const getLastMouseDownPos = () => lastMouseDownPos; + const setLastMouseDownPos = (val: Vector2d | null) => { + lastMouseDownPos = val; + }; + + let lastCursorPos: Vector2d | null = null; + const getLastCursorPos = () => lastCursorPos; + const setLastCursorPos = (val: Vector2d | null) => { + lastCursorPos = val; + }; + + let spaceKey = false; + const getSpaceKey = () => spaceKey; + const setSpaceKey = (val: boolean) => { + spaceKey = val; + }; const cleanupListeners = setStageEventHandlers({ stage, getToolState, setTool, setToolBuffer, - getIsDrawing: $isDrawing.get, - setIsDrawing: $isDrawing.set, - getIsMouseDown: $isMouseDown.get, - setIsMouseDown: $isMouseDown.set, + getIsDrawing, + setIsDrawing, + getIsMouseDown, + setIsMouseDown, getSelectedEntity, - getLastAddedPoint: $lastAddedPoint.get, - setLastAddedPoint: $lastAddedPoint.set, - getLastCursorPos: $lastCursorPos.get, - setLastCursorPos: $lastCursorPos.set, - getLastMouseDownPos: $lastMouseDownPos.get, - setLastMouseDownPos: $lastMouseDownPos.set, - getSpaceKey: $spaceKey.get, + getLastAddedPoint, + setLastAddedPoint, + getLastCursorPos, + setLastCursorPos, + getLastMouseDownPos, + setLastMouseDownPos, + getSpaceKey, + setSpaceKey, setStageAttrs: $stageAttrs.set, getDocument, getBbox, @@ -252,6 +276,11 @@ export const initializeRenderer = ( getCurrentFill, }); + // Calculating bounding boxes is expensive, must be debounced to not block the UI thread during a user interaction. + // TODO(psyche): Figure out how to do this in a worker. Probably means running the renderer in a worker and sending + // the entire state over when needed. + const debouncedUpdateBboxes = debounce(updateBboxes, 300); + const renderCanvas = () => { const { canvasV2 } = store.getState(); @@ -260,20 +289,8 @@ export const initializeRenderer = ( return; } - // We can save some cycles for specific renderers if we track whether the selected entity has changed. - if (canvasV2.selectedEntityIdentifier !== selectedEntityIdentifier) { - selectedEntityIdentifier = canvasV2.selectedEntityIdentifier; - selectedEntity = _getSelectedEntity(canvasV2); - didSelectedEntityChange = true; - } else { - didSelectedEntityChange = false; - } - - // The current fill is either the tool fill or, if a regional guidance region is selected, the mask fill for that - // region. We need to manually sync this state. - if (isFirstRender || canvasV2.tool.fill !== prevCanvasV2.tool.fill || didSelectedEntityChange) { - currentFill = _getCurrentFill(canvasV2, selectedEntity); - } + const selectedEntity = _getSelectedEntity(canvasV2); + const currentFill = _getCurrentFill(canvasV2, selectedEntity); if ( isFirstRender || @@ -288,8 +305,7 @@ export const initializeRenderer = ( isFirstRender || canvasV2.regions !== prevCanvasV2.regions || canvasV2.settings.maskOpacity !== prevCanvasV2.settings.maskOpacity || - canvasV2.tool.selected !== prevCanvasV2.tool.selected || - didSelectedEntityChange + canvasV2.tool.selected !== prevCanvasV2.tool.selected ) { logIfDebugging('Rendering regions'); renderRegions( @@ -297,7 +313,7 @@ export const initializeRenderer = ( canvasV2.regions, canvasV2.settings.maskOpacity, canvasV2.tool.selected, - selectedEntity, + canvasV2.selectedEntityIdentifier, onPosChanged ); } @@ -348,6 +364,8 @@ export const initializeRenderer = ( } prevCanvasV2 = canvasV2; + prevSelectedEntity = selectedEntity; + prevCurrentFill = currentFill; if (isFirstRender) { isFirstRender = false; 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 916c6c9c47a..148f6925e6f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts @@ -18,7 +18,13 @@ import { getEraserLine, } from 'features/controlLayers/konva/renderers/objects'; import { mapId, selectVectorMaskObjects } from 'features/controlLayers/konva/util'; -import type { CanvasEntity, PosChangedArg, RegionEntity, Tool } from 'features/controlLayers/store/types'; +import type { + CanvasEntity, + CanvasEntityIdentifier, + PosChangedArg, + RegionEntity, + Tool, +} from 'features/controlLayers/store/types'; import Konva from 'konva'; /** @@ -83,7 +89,7 @@ export const renderRGLayer = ( rg: RegionEntity, globalMaskLayerOpacity: number, tool: Tool, - selectedEntity: CanvasEntity | null, + selectedEntityIdentifier: CanvasEntityIdentifier | null, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): void => { const konvaLayer = stage.findOne(`#${rg.id}`) ?? createRGLayer(stage, rg, onPosChanged); @@ -171,7 +177,7 @@ export const renderRGLayer = ( const compositingRect = konvaLayer.findOne(`.${COMPOSITING_RECT_NAME}`) ?? createCompositingRect(konvaLayer); - const isSelected = selectedEntity?.id === rg.id; + const isSelected = selectedEntityIdentifier?.id === rg.id; /** * When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows @@ -237,7 +243,7 @@ export const renderRegions = ( regions: RegionEntity[], maskOpacity: number, tool: Tool, - selectedEntity: CanvasEntity | null, + selectedEntityIdentifier: CanvasEntityIdentifier | null, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): void => { // Destroy nonexistent layers @@ -247,6 +253,6 @@ export const renderRegions = ( } } for (const rg of regions) { - renderRGLayer(stage, rg, maskOpacity, tool, selectedEntity, onPosChanged); + renderRGLayer(stage, rg, maskOpacity, tool, selectedEntityIdentifier, onPosChanged); } }; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 652366ca1e0..70efc6186c6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -15,10 +15,9 @@ import { settingsReducers } from 'features/controlLayers/store/settingsReducers' import { toolReducers } from 'features/controlLayers/store/toolReducers'; import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; -import type { Vector2d } from 'konva/lib/types'; import { atom } from 'nanostores'; -import type { CanvasEntity, CanvasEntityIdentifier, CanvasV2State, RgbaColor, StageAttrs } from './types'; +import type { CanvasEntityIdentifier, CanvasV2State, StageAttrs } from './types'; import { DEFAULT_RGBA_COLOR } from './types'; const initialState: CanvasV2State = { @@ -306,14 +305,8 @@ const migrate = (state: any): any => { return state; }; -// Ephemeral interaction state -export const $isDrawing = atom(false); -export const $isMouseDown = atom(false); -export const $lastMouseDownPos = atom(null); -export const $lastCursorPos = atom(null); +// Ephemeral state that does not need to be in redux export const $isPreviewVisible = atom(true); -export const $lastAddedPoint = atom(null); -export const $spaceKey = atom(false); export const $stageAttrs = atom({ x: 0, y: 0, @@ -322,14 +315,6 @@ export const $stageAttrs = atom({ scale: 0, }); -// Some nanostores that are manually synced to redux state to provide imperative access -// TODO(psyche): -export const $toolState = atom(deepClone(initialState.tool)); -export const $currentFill = atom(DEFAULT_RGBA_COLOR); -export const $selectedEntity = atom(null); -export const $bbox = atom(deepClone(initialState.bbox)); -export const $document = atom(deepClone(initialState.document)); - export const canvasV2PersistConfig: PersistConfig = { name: canvasV2Slice.name, initialState, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts index a245d2c8546..168c941033c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts @@ -2,8 +2,8 @@ import { getStore } from 'app/store/nanostores/store'; import { deepClone } from 'common/util/deepClone'; import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; import { RG_LAYER_NAME } from 'features/controlLayers/konva/naming'; -import { renderers } from 'features/controlLayers/konva/renderers/layers'; -import { blobToDataURL } from "features/controlLayers/konva/util"; +import { renderRegions } from 'features/controlLayers/konva/renderers/rgLayer'; +import { blobToDataURL } from 'features/controlLayers/konva/util'; import { rgMaskImageUploaded } from 'features/controlLayers/store/canvasV2Slice'; import type { Dimensions, IPAdapterEntity, RegionEntity } from 'features/controlLayers/store/types'; import { @@ -260,7 +260,7 @@ export const getRGMaskBlobs = async ( ): Promise> => { const container = document.createElement('div'); const stage = new Konva.Stage({ container, ...documentSize }); - renderers.renderLayers(stage, [], [], regions, 1, 'brush', null, getImageDTO); + renderRegions(stage, regions, 1, 'brush', null); const konvaLayers = stage.find(`.${RG_LAYER_NAME}`); const blobs: Record = {}; From ce497fff276835ac685072f49ba5871cbd712758 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 18 Jun 2024 20:47:47 +1000 Subject: [PATCH 089/678] refactor(ui): remove unused ellipse & polygon objects --- .../src/features/controlLayers/store/types.ts | 34 +++---------------- 1 file changed, 4 insertions(+), 30 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 16d82845408..0bf9f3bdfe6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -536,25 +536,6 @@ const zRectShape = z.object({ }); export type RectShape = z.infer; -const zEllipseShape = z.object({ - id: zId, - type: z.literal('ellipse_shape'), - x: z.number(), - y: z.number(), - width: z.number().min(1), - height: z.number().min(1), - color: zRgbaColor, -}); -export type EllipseShape = z.infer; - -const zPolygonShape = z.object({ - id: zId, - type: z.literal('polygon_shape'), - points: zPoints, - color: zRgbaColor, -}); -export type PolygonShape = z.infer; - const zImageObject = z.object({ id: zId, type: z.literal('image'), @@ -566,15 +547,8 @@ const zImageObject = z.object({ }); export type ImageObject = z.infer; -const zLayerObject = z.discriminatedUnion('type', [ - zImageObject, - zBrushLine, - zEraserline, - zRectShape, - zEllipseShape, - zPolygonShape, -]); -export type LayerObject = z.infer; +const zRenderableObject = z.discriminatedUnion('type', [zImageObject, zBrushLine, zEraserline, zRectShape]); +export type RenderableObject = z.infer; export const zLayerEntity = z.object({ id: zId, @@ -585,7 +559,7 @@ export const zLayerEntity = z.object({ bbox: zRect.nullable(), bboxNeedsUpdate: z.boolean(), opacity: zOpacity, - objects: z.array(zLayerObject), + objects: z.array(zRenderableObject), }); export type LayerEntity = z.infer; @@ -894,6 +868,6 @@ export type RectShapeAddedArg = { id: string; rect: IRect; color: RgbaColor }; export type ImageObjectAddedArg = { id: string; imageDTO: ImageDTO }; //#region Type guards -export const isLine = (obj: LayerObject): obj is BrushLine | EraserLine => { +export const isLine = (obj: RenderableObject): obj is BrushLine | EraserLine => { return obj.type === 'brush_line' || obj.type === 'eraser_line'; }; From 8c9a5c4ab57a5e1e7f7f0879d10a5638cc79c7b8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 18 Jun 2024 23:48:01 +1000 Subject: [PATCH 090/678] feat(ui): move canvas fill color picker to right --- .../controlLayers/components/ControlLayersToolbar.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index df2e911c506..9313a312ae6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -16,7 +16,7 @@ export const ControlLayersToolbar = memo(() => { return ( - + @@ -24,10 +24,10 @@ export const ControlLayersToolbar = memo(() => { {tool === 'brush' && } {tool === 'eraser' && } - - + + From 77d840593bde22b2b1493eb014cdb56cac06d1e9 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 19 Jun 2024 00:06:06 +1000 Subject: [PATCH 091/678] perf(ui): fix lag w/ region rendering Needed to memoize these selectors --- .../ControlAdapter/CAActionsMenu.tsx | 33 ++++++++------- .../components/Layer/LayerActionsMenu.tsx | 33 ++++++++------- .../RegionalGuidance/RGActionsMenu.tsx | 40 +++++++++---------- 3 files changed, 56 insertions(+), 50 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx index 1219a1d226e..2bf75373778 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx @@ -1,5 +1,5 @@ import { Menu, MenuItem, MenuList } from '@invoke-ai/ui-library'; -import { createAppSelector } from 'app/store/createMemoizedSelector'; +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; import { @@ -11,7 +11,7 @@ import { selectCanvasV2Slice, } from 'features/controlLayers/store/canvasV2Slice'; import { selectCAOrThrow } from 'features/controlLayers/store/controlAdaptersReducers'; -import { memo, useCallback } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowDownBold, @@ -25,22 +25,25 @@ type Props = { id: string; }; -const selectValidActions = createAppSelector([selectCanvasV2Slice, (canvasV2, id: string) => id], (canvasV2, id) => { - const ca = selectCAOrThrow(canvasV2, id); - const caIndex = canvasV2.controlAdapters.indexOf(ca); - const caCount = canvasV2.controlAdapters.length; - return { - canMoveForward: caIndex < caCount - 1, - canMoveBackward: caIndex > 0, - canMoveToFront: caIndex < caCount - 1, - canMoveToBack: caIndex > 0, - }; -}); - export const CAActionsMenu = memo(({ id }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const validActions = useAppSelector((s) => selectValidActions(s, id)); + const selectValidActions = useMemo( + () => + createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { + const ca = selectCAOrThrow(canvasV2, id); + const caIndex = canvasV2.controlAdapters.indexOf(ca); + const caCount = canvasV2.controlAdapters.length; + return { + canMoveForward: caIndex < caCount - 1, + canMoveBackward: caIndex > 0, + canMoveToFront: caIndex < caCount - 1, + canMoveToBack: caIndex > 0, + }; + }), + [id] + ); + const validActions = useAppSelector(selectValidActions); const onDelete = useCallback(() => { dispatch(caDeleted({ id })); }, [dispatch, id]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx index 5498ba6ade6..6c161531dd2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx @@ -1,5 +1,5 @@ import { Menu, MenuItem, MenuList } from '@invoke-ai/ui-library'; -import { createMemoizedAppSelector } from 'app/store/createMemoizedSelector'; +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; import { @@ -11,7 +11,7 @@ import { selectCanvasV2Slice, } from 'features/controlLayers/store/canvasV2Slice'; import { selectLayerOrThrow } from 'features/controlLayers/store/layersReducers'; -import { memo, useCallback } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowDownBold, @@ -25,22 +25,25 @@ type Props = { id: string; }; -const selectValidActions = createMemoizedAppSelector([selectCanvasV2Slice, (canvasV2, id: string) => id], (canvasV2, id) => { - const layer = selectLayerOrThrow(canvasV2, id); - const layerIndex = canvasV2.layers.indexOf(layer); - const layerCount = canvasV2.layers.length; - return { - canMoveForward: layerIndex < layerCount - 1, - canMoveBackward: layerIndex > 0, - canMoveToFront: layerIndex < layerCount - 1, - canMoveToBack: layerIndex > 0, - }; -}); - export const LayerActionsMenu = memo(({ id }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const validActions = useAppSelector((s) => selectValidActions(s, id)); + const selectValidActions = useMemo( + () => + createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { + const layer = selectLayerOrThrow(canvasV2, id); + const layerIndex = canvasV2.layers.indexOf(layer); + const layerCount = canvasV2.layers.length; + return { + canMoveForward: layerIndex < layerCount - 1, + canMoveBackward: layerIndex > 0, + canMoveToFront: layerIndex < layerCount - 1, + canMoveToBack: layerIndex > 0, + }; + }), + [id] + ); + const validActions = useAppSelector(selectValidActions); const onDelete = useCallback(() => { dispatch(layerDeleted({ id })); }, [dispatch, id]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx index 94107c9c0fa..1df34679596 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx @@ -1,5 +1,5 @@ import { Menu, MenuDivider, MenuItem, MenuList } from '@invoke-ai/ui-library'; -import { createMemoizedAppSelector } from 'app/store/createMemoizedSelector'; +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; import { useAddIPAdapterToRGLayer } from 'features/controlLayers/hooks/addLayerHooks'; @@ -15,7 +15,7 @@ import { selectCanvasV2Slice, } from 'features/controlLayers/store/canvasV2Slice'; import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; -import { memo, useCallback } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowCounterClockwiseBold, @@ -31,28 +31,28 @@ type Props = { id: string; }; -const selectActionsValidity = createMemoizedAppSelector( - [selectCanvasV2Slice, (canvasV2, id: string) => id], - (canvasV2, id) => { - const rg = selectRGOrThrow(canvasV2, id); - const rgIndex = canvasV2.regions.indexOf(rg); - const rgCount = canvasV2.regions.length; - return { - isMoveForwardOneDisabled: rgIndex < rgCount - 1, - isMoveBackardOneDisabled: rgIndex > 0, - isMoveToFrontDisabled: rgIndex < rgCount - 1, - isMoveToBackDisabled: rgIndex > 0, - isAddPositivePromptDisabled: rg.positivePrompt === null, - isAddNegativePromptDisabled: rg.negativePrompt === null, - }; - } -); - export const RGActionsMenu = memo(({ id }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const [onAddIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToRGLayer(id); - const actions = useAppSelector((s) => selectActionsValidity(s, id)); + const selectActionsValidity = useMemo( + () => + createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { + const rg = selectRGOrThrow(canvasV2, id); + const rgIndex = canvasV2.regions.indexOf(rg); + const rgCount = canvasV2.regions.length; + return { + isMoveForwardOneDisabled: rgIndex < rgCount - 1, + isMoveBackardOneDisabled: rgIndex > 0, + isMoveToFrontDisabled: rgIndex < rgCount - 1, + isMoveToBackDisabled: rgIndex > 0, + isAddPositivePromptDisabled: rg.positivePrompt === null, + isAddNegativePromptDisabled: rg.negativePrompt === null, + }; + }), + [id] + ); + const actions = useAppSelector(selectActionsValidity); const onDelete = useCallback(() => { dispatch(rgDeleted({ id })); }, [dispatch, id]); From 510249d2821b82aa2bb6f69a4f2c12fabefa3592 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 19 Jun 2024 00:13:52 +1000 Subject: [PATCH 092/678] refactor(ui): create entity to konva node map abstraction (wip) Instead of chaining konva `find` and `findOne` methods, all konva nodes are added to a mapping object. Finding and manipulating them is much simpler. Done for regions and layers, wip for control adapters. --- .../features/controlLayers/konva/konvaMap.ts | 104 ++++++++++++ .../controlLayers/konva/renderers/caLayer.ts | 12 +- .../controlLayers/konva/renderers/objects.ts | 74 +++++--- .../konva/renderers/rasterLayer.ts | 77 ++++----- .../controlLayers/konva/renderers/renderer.ts | 10 +- .../controlLayers/konva/renderers/rgLayer.ts | 160 +++++++++--------- 6 files changed, 290 insertions(+), 147 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/konvaMap.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/konvaMap.ts b/invokeai/frontend/web/src/features/controlLayers/konva/konvaMap.ts new file mode 100644 index 00000000000..c820027f96a --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/konvaMap.ts @@ -0,0 +1,104 @@ +import type { BrushLine, EraserLine, ImageObject, RectShape } from 'features/controlLayers/store/types'; +import type Konva from 'konva'; + +export type BrushLineEntry = { + id: string; + type: BrushLine['type']; + konvaLine: Konva.Line; + konvaLineGroup: Konva.Group; +}; + +export type EraserLineEntry = { + id: string; + type: EraserLine['type']; + konvaLine: Konva.Line; + konvaLineGroup: Konva.Group; +}; + +export type RectShapeEntry = { + id: string; + type: RectShape['type']; + konvaRect: Konva.Rect; +}; + +export type ImageEntry = { + id: string; + type: ImageObject['type']; + konvaImage: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately + konvaGroup: Konva.Group; +}; + +type Entry = BrushLineEntry | EraserLineEntry | RectShapeEntry | ImageEntry; + +export class EntityToKonvaMap { + mappings: Record; + + constructor() { + this.mappings = {}; + } + + addMapping(id: string, konvaLayer: Konva.Layer, konvaObjectGroup: Konva.Group): EntityToKonvaMapping { + const mapping = new EntityToKonvaMapping(id, konvaLayer, konvaObjectGroup); + this.mappings[id] = mapping; + return mapping; + } + + getMapping(id: string): EntityToKonvaMapping | undefined { + return this.mappings[id]; + } + + getMappings(): EntityToKonvaMapping[] { + return Object.values(this.mappings); + } + + destroyMapping(id: string): void { + const mapping = this.getMapping(id); + if (!mapping) { + return; + } + mapping.konvaObjectGroup.destroy(); + delete this.mappings[id]; + } +} + +export class EntityToKonvaMapping { + id: string; + konvaLayer: Konva.Layer; + konvaObjectGroup: Konva.Group; + konvaNodeEntries: Record; + + constructor(id: string, konvaLayer: Konva.Layer, konvaObjectGroup: Konva.Group) { + this.id = id; + this.konvaLayer = konvaLayer; + this.konvaObjectGroup = konvaObjectGroup; + this.konvaNodeEntries = {}; + } + + addEntry(entry: T): T { + this.konvaNodeEntries[entry.id] = entry; + return entry; + } + + getEntry(id: string): T | undefined { + return this.konvaNodeEntries[id] as T | undefined; + } + + getEntries(): Entry[] { + return Object.values(this.konvaNodeEntries); + } + + destroyEntry(id: string): void { + const entry = this.getEntry(id); + if (!entry) { + return; + } + if (entry.type === 'brush_line' || entry.type === 'eraser_line') { + entry.konvaLineGroup.destroy(); + } else if (entry.type === 'rect_shape') { + entry.konvaRect.destroy(); + } else if (entry.type === 'image') { + entry.konvaGroup.destroy(); + } + delete this.konvaNodeEntries[id]; + } +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts index 91c6db83bb9..71ed5623f4d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts @@ -1,4 +1,5 @@ import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; +import type { EntityToKonvaMap } from 'features/controlLayers/konva/konvaMap'; import { CA_LAYER_IMAGE_NAME, CA_LAYER_NAME, getCAImageId } from 'features/controlLayers/konva/naming'; import type { ControlAdapterEntity } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -34,6 +35,7 @@ const createCALayerImage = (konvaLayer: Konva.Layer, imageEl: HTMLImageElement): const konvaImage = new Konva.Image({ name: CA_LAYER_IMAGE_NAME, image: imageEl, + listening: false, }); konvaLayer.add(konvaImage); return konvaImage; @@ -128,6 +130,7 @@ const updateCALayerImageAttrs = (stage: Konva.Stage, konvaImage: Konva.Image, ca */ export const renderCALayer = ( stage: Konva.Stage, + controlAdapterMap: EntityToKonvaMap, ca: ControlAdapterEntity, getImageDTO: (imageName: string) => Promise ): void => { @@ -157,16 +160,17 @@ export const renderCALayer = ( export const renderControlAdapters = ( stage: Konva.Stage, + controlAdapterMap: EntityToKonvaMap, controlAdapters: ControlAdapterEntity[], getImageDTO: (imageName: string) => Promise ): void => { // Destroy nonexistent layers - for (const konvaLayer of stage.find(`.${CA_LAYER_NAME}`)) { - if (!controlAdapters.find((ca) => ca.id === konvaLayer.id())) { - konvaLayer.destroy(); + for (const mapping of controlAdapterMap.getMappings()) { + if (!controlAdapters.find((ca) => ca.id === mapping.id)) { + controlAdapterMap.destroyMapping(mapping.id); } } for (const ca of controlAdapters) { - renderCALayer(stage, ca, getImageDTO); + renderCALayer(stage, controlAdapterMap, ca, getImageDTO); } }; 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 ce626e99523..2c21aeb120b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -1,4 +1,11 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; +import type { + BrushLineEntry, + EntityToKonvaMapping, + EraserLineEntry, + ImageEntry, + RectShapeEntry, +} from 'features/controlLayers/konva/konvaMap'; import { getLayerBboxId, getObjectGroupId, @@ -23,18 +30,17 @@ import { v4 as uuidv4 } from 'uuid'; * @param layerObjectGroup The konva layer's object group to add the line to * @param name The konva name for the line */ -export const getBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Group, name: string): Konva.Line => { - let konvaLineGroup = layerObjectGroup.findOne(`#${brushLine.id}_group`); - let konvaLine = konvaLineGroup?.findOne(`#${brushLine.id}`); - if (konvaLine) { - return konvaLine; +export const getBrushLine = (mapping: EntityToKonvaMapping, brushLine: BrushLine, name: string): BrushLineEntry => { + let entry = mapping.getEntry(brushLine.id); + if (entry) { + return entry; } - konvaLineGroup = new Konva.Group({ - id: `${brushLine.id}_group`, - // clip: brushLine.clip, + const konvaLineGroup = new Konva.Group({ + clip: brushLine.clip, + listening: false, }); - konvaLine = new Konva.Line({ + const konvaLine = new Konva.Line({ id: brushLine.id, name, strokeWidth: brushLine.strokeWidth, @@ -47,8 +53,9 @@ export const getBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Group stroke: rgbaColorToString(brushLine.color), }); konvaLineGroup.add(konvaLine); - layerObjectGroup.add(konvaLineGroup); - return konvaLine; + mapping.konvaObjectGroup.add(konvaLineGroup); + entry = mapping.addEntry({ id: brushLine.id, type: 'brush_line', konvaLine, konvaLineGroup }); + return entry; }; /** @@ -57,10 +64,18 @@ export const getBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Group * @param layerObjectGroup The konva layer's object group to add the line to * @param name The konva name for the line */ -export const getEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva.Group, name: string): Konva.Line => { +export const getEraserLine = (mapping: EntityToKonvaMapping, eraserLine: EraserLine, name: string): EraserLineEntry => { + let entry = mapping.getEntry(eraserLine.id); + if (entry) { + return entry; + } + + const konvaLineGroup = new Konva.Group({ + clip: eraserLine.clip, + listening: false, + }); const konvaLine = new Konva.Line({ id: eraserLine.id, - key: eraserLine.id, name, strokeWidth: eraserLine.strokeWidth, tension: 0, @@ -72,8 +87,10 @@ export const getEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva.Gr stroke: rgbaColorToString(DEFAULT_RGBA_COLOR), clip: eraserLine.clip, }); - layerObjectGroup.add(konvaLine); - return konvaLine; + konvaLineGroup.add(konvaLine); + mapping.konvaObjectGroup.add(konvaLineGroup); + entry = mapping.addEntry({ id: eraserLine.id, type: 'eraser_line', konvaLine, konvaLineGroup }); + return entry; }; /** @@ -82,7 +99,11 @@ export const getEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva.Gr * @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, name: string): Konva.Rect => { +export const getRectShape = (mapping: EntityToKonvaMapping, rectShape: RectShape, name: string): RectShapeEntry => { + let entry = mapping.getEntry(rectShape.id); + if (entry) { + return entry; + } const konvaRect = new Konva.Rect({ id: rectShape.id, key: rectShape.id, @@ -94,8 +115,9 @@ export const createRectShape = (rectShape: RectShape, layerObjectGroup: Konva.Gr listening: false, fill: rgbaColorToString(rectShape.color), }); - layerObjectGroup.add(konvaRect); - return konvaRect; + mapping.konvaObjectGroup.add(konvaRect); + entry = mapping.addEntry({ id: rectShape.id, type: 'rect_shape', konvaRect }); + return entry; }; /** @@ -112,6 +134,7 @@ const createImagePlaceholderGroup = ( fill: 'hsl(220 12% 45% / 1)', // 'base.500' width, height, + listening: false, }); const konvaPlaceholderText = new Konva.Text({ name: 'image-placeholder-text', @@ -150,14 +173,20 @@ const createImagePlaceholderGroup = ( * @returns A promise that resolves to the konva group for the image object */ export const createImageObjectGroup = async ( + mapping: EntityToKonvaMapping, imageObject: ImageObject, - layerObjectGroup: Konva.Group, name: string -): Promise => { +): Promise => { + let entry = mapping.getEntry(imageObject.id); + if (entry) { + return entry; + } const konvaImageGroup = new Konva.Group({ id: imageObject.id, name, listening: false }); const placeholder = createImagePlaceholderGroup(imageObject); konvaImageGroup.add(placeholder.konvaPlaceholderGroup); - layerObjectGroup.add(konvaImageGroup); + mapping.konvaObjectGroup.add(konvaImageGroup); + + entry = mapping.addEntry({ id: imageObject.id, type: 'image', konvaGroup: konvaImageGroup, konvaImage: null }); getImageDTO(imageObject.image.name).then((imageDTO) => { if (!imageDTO) { placeholder.onError(); @@ -173,6 +202,7 @@ export const createImageObjectGroup = async ( }); placeholder.onLoaded(); konvaImageGroup.add(konvaImage); + entry.konvaImage = konvaImage; }; imageEl.onerror = () => { placeholder.onError(); @@ -180,7 +210,7 @@ export const createImageObjectGroup = async ( imageEl.id = imageObject.id; imageEl.src = imageDTO.image_url; }); - return konvaImageGroup; + return entry; }; /** 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 4f5ebfc7201..13829d4dae0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts @@ -1,3 +1,4 @@ +import type { EntityToKonvaMap, EntityToKonvaMapping } from 'features/controlLayers/konva/konvaMap'; import { RASTER_LAYER_BRUSH_LINE_NAME, RASTER_LAYER_ERASER_LINE_NAME, @@ -9,11 +10,11 @@ import { import { createImageObjectGroup, createObjectGroup, - createRectShape, getBrushLine, getEraserLine, + getRectShape, } from 'features/controlLayers/konva/renderers/objects'; -import { mapId, selectRasterObjects } from 'features/controlLayers/konva/util'; +import { mapId } from 'features/controlLayers/konva/util'; import type { CanvasEntity, LayerEntity, PosChangedArg, Tool } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -27,11 +28,16 @@ import Konva from 'konva'; * @param layerState The raster layer state * @param onPosChanged Callback for when the layer's position changes */ -const createRasterLayer = ( +const getLayer = ( stage: Konva.Stage, + layerMap: EntityToKonvaMap, layerState: LayerEntity, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void -): Konva.Layer => { +): EntityToKonvaMapping => { + let mapping = layerMap.getMapping(layerState.id); + if (mapping) { + return mapping; + } // This layer hasn't been added to the konva state yet const konvaLayer = new Konva.Layer({ id: layerState.id, @@ -48,9 +54,11 @@ const createRasterLayer = ( }); } + const konvaObjectGroup = createObjectGroup(konvaLayer, RASTER_LAYER_OBJECT_GROUP_NAME); + konvaLayer.add(konvaObjectGroup); stage.add(konvaLayer); - - return konvaLayer; + mapping = layerMap.addMapping(layerState.id, konvaLayer, konvaObjectGroup); + return mapping; }; /** @@ -62,63 +70,51 @@ const createRasterLayer = ( */ export const renderRasterLayer = async ( stage: Konva.Stage, + layerMap: EntityToKonvaMap, layerState: LayerEntity, tool: Tool, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ) => { - const konvaLayer = - stage.findOne(`#${layerState.id}`) ?? createRasterLayer(stage, layerState, onPosChanged); + const mapping = getLayer(stage, layerMap, layerState, onPosChanged); // Update the layer's position and listening state - konvaLayer.setAttrs({ + mapping.konvaLayer.setAttrs({ listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events x: Math.floor(layerState.x), y: Math.floor(layerState.y), }); - 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 - // 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(); + // Destroy any objects that are no longer in state + for (const entry of mapping.getEntries()) { + if (!objectIds.includes(entry.id)) { + mapping.destroyEntry(entry.id); } } for (const obj of layerState.objects) { if (obj.type === 'brush_line') { - const konvaBrushLine = getBrushLine(obj, konvaObjectGroup, RASTER_LAYER_BRUSH_LINE_NAME); + const entry = getBrushLine(mapping, obj, RASTER_LAYER_BRUSH_LINE_NAME); // Only update the points if they have changed. - if (konvaBrushLine.points().length !== obj.points.length) { - konvaBrushLine.points(obj.points); + if (entry.konvaLine.points().length !== obj.points.length) { + entry.konvaLine.points(obj.points); } } else if (obj.type === 'eraser_line') { - const konvaEraserLine = - konvaObjectGroup.findOne(`#${obj.id}`) ?? - getEraserLine(obj, konvaObjectGroup, RASTER_LAYER_ERASER_LINE_NAME); + const entry = getEraserLine(mapping, obj, RASTER_LAYER_ERASER_LINE_NAME); // Only update the points if they have changed. - if (konvaEraserLine.points().length !== obj.points.length) { - konvaEraserLine.points(obj.points); + if (entry.konvaLine.points().length !== obj.points.length) { + entry.konvaLine.points(obj.points); } } else if (obj.type === 'rect_shape') { - if (!konvaObjectGroup.findOne(`#${obj.id}`)) { - createRectShape(obj, konvaObjectGroup, RASTER_LAYER_RECT_SHAPE_NAME); - } + getRectShape(mapping, obj, RASTER_LAYER_RECT_SHAPE_NAME); } else if (obj.type === 'image') { - if (!konvaObjectGroup.findOne(`#${obj.id}`)) { - createImageObjectGroup(obj, konvaObjectGroup, RASTER_LAYER_IMAGE_NAME); - } + createImageObjectGroup(mapping, obj, RASTER_LAYER_IMAGE_NAME); } } // Only update layer visibility if it has changed. - if (konvaLayer.visible() !== layerState.isEnabled) { - konvaLayer.visible(layerState.isEnabled); + if (mapping.konvaLayer.visible() !== layerState.isEnabled) { + mapping.konvaLayer.visible(layerState.isEnabled); } // const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer); @@ -139,22 +135,23 @@ export const renderRasterLayer = async ( // bboxRect.visible(false); // } - konvaObjectGroup.opacity(layerState.opacity); + mapping.konvaObjectGroup.opacity(layerState.opacity); }; export const renderLayers = ( stage: Konva.Stage, + layerMap: EntityToKonvaMap, layers: LayerEntity[], tool: Tool, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): void => { // Destroy nonexistent layers - for (const konvaLayer of stage.find(`.${RASTER_LAYER_NAME}`)) { - if (!layers.find((l) => l.id === konvaLayer.id())) { - konvaLayer.destroy(); + for (const mapping of layerMap.getMappings()) { + if (!layers.find((l) => l.id === mapping.id)) { + layerMap.destroyMapping(mapping.id); } } for (const layer of layers) { - renderRasterLayer(stage, layer, tool, onPosChanged); + renderRasterLayer(stage, layerMap, layer, tool, onPosChanged); } }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts index 0e9c8fc53ec..1fb844dcc6e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -4,6 +4,7 @@ import { logger } from 'app/logging/logger'; import { $isDebugging } from 'app/store/nanostores/isDebugging'; import type { RootState } from 'app/store/store'; import { setStageEventHandlers } from 'features/controlLayers/konva/events'; +import { EntityToKonvaMap } from 'features/controlLayers/konva/konvaMap'; import { renderBackgroundLayer } from 'features/controlLayers/konva/renderers/background'; import { updateBboxes } from 'features/controlLayers/konva/renderers/bbox'; import { renderControlAdapters } from 'features/controlLayers/konva/renderers/caLayer'; @@ -281,6 +282,10 @@ export const initializeRenderer = ( // the entire state over when needed. const debouncedUpdateBboxes = debounce(updateBboxes, 300); + const regionMap = new EntityToKonvaMap(); + const layerMap = new EntityToKonvaMap(); + const controlAdapterMap = new EntityToKonvaMap(); + const renderCanvas = () => { const { canvasV2 } = store.getState(); @@ -298,7 +303,7 @@ export const initializeRenderer = ( canvasV2.tool.selected !== prevCanvasV2.tool.selected ) { logIfDebugging('Rendering layers'); - renderLayers(stage, canvasV2.layers, canvasV2.tool.selected, onPosChanged); + renderLayers(stage, layerMap, canvasV2.layers, canvasV2.tool.selected, onPosChanged); } if ( @@ -310,6 +315,7 @@ export const initializeRenderer = ( logIfDebugging('Rendering regions'); renderRegions( stage, + regionMap, canvasV2.regions, canvasV2.settings.maskOpacity, canvasV2.tool.selected, @@ -320,7 +326,7 @@ export const initializeRenderer = ( if (isFirstRender || canvasV2.controlAdapters !== prevCanvasV2.controlAdapters) { logIfDebugging('Rendering control adapters'); - renderControlAdapters(stage, canvasV2.controlAdapters, getImageDTO); + renderControlAdapters(stage, controlAdapterMap, canvasV2.controlAdapters, getImageDTO); } if (isFirstRender || canvasV2.document !== prevCanvasV2.document) { 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 148f6925e6f..c25e0e64868 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts @@ -1,8 +1,7 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; -import { BBOX_SELECTED_STROKE } from 'features/controlLayers/konva/constants'; +import type { EntityToKonvaMap, EntityToKonvaMapping } from 'features/controlLayers/konva/konvaMap'; import { COMPOSITING_RECT_NAME, - LAYER_BBOX_NAME, RG_LAYER_BRUSH_LINE_NAME, RG_LAYER_ERASER_LINE_NAME, RG_LAYER_NAME, @@ -11,13 +10,12 @@ import { } from 'features/controlLayers/konva/naming'; import { getLayerBboxFast } from 'features/controlLayers/konva/renderers/bbox'; import { - createBboxRect, createObjectGroup, - createRectShape, getBrushLine, getEraserLine, + getRectShape, } from 'features/controlLayers/konva/renderers/objects'; -import { mapId, selectVectorMaskObjects } from 'features/controlLayers/konva/util'; +import { mapId } from 'features/controlLayers/konva/util'; import type { CanvasEntity, CanvasEntityIdentifier, @@ -47,17 +45,22 @@ const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => { /** * Creates a regional guidance layer. * @param stage The konva stage - * @param rg The regional guidance layer state + * @param region The regional guidance layer state * @param onLayerPosChanged Callback for when the layer's position changes */ -const createRGLayer = ( +const getRegion = ( stage: Konva.Stage, - rg: RegionEntity, + regionMap: EntityToKonvaMap, + region: RegionEntity, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void -): Konva.Layer => { +): EntityToKonvaMapping => { + let mapping = regionMap.getMapping(region.id); + if (mapping) { + return mapping; + } // This layer hasn't been added to the konva state yet const konvaLayer = new Konva.Layer({ - id: rg.id, + id: region.id, name: RG_LAYER_NAME, draggable: true, dragDistance: 0, @@ -67,117 +70,114 @@ const createRGLayer = ( // the position - we do not need to call this on the `dragmove` event. if (onPosChanged) { konvaLayer.on('dragend', function (e) { - onPosChanged({ id: rg.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'regional_guidance'); + onPosChanged({ id: region.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'regional_guidance'); }); } - stage.add(konvaLayer); + const konvaObjectGroup = createObjectGroup(konvaLayer, RG_LAYER_OBJECT_GROUP_NAME); - return konvaLayer; + konvaLayer.add(konvaObjectGroup); + stage.add(konvaLayer); + mapping = regionMap.addMapping(region.id, konvaLayer, konvaObjectGroup); + return mapping; }; /** * Renders a raster layer. * @param stage The konva stage - * @param rg The regional guidance layer state + * @param region The regional guidance layer state * @param globalMaskLayerOpacity The global mask layer opacity * @param tool The current tool * @param onPosChanged Callback for when the layer's position changes */ export const renderRGLayer = ( stage: Konva.Stage, - rg: RegionEntity, + regionMap: EntityToKonvaMap, + region: RegionEntity, globalMaskLayerOpacity: number, tool: Tool, selectedEntityIdentifier: CanvasEntityIdentifier | null, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): void => { - const konvaLayer = stage.findOne(`#${rg.id}`) ?? createRGLayer(stage, rg, onPosChanged); + const mapping = getRegion(stage, regionMap, region, onPosChanged); // Update the layer's position and listening state - konvaLayer.setAttrs({ + mapping.konvaLayer.setAttrs({ listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events - x: Math.floor(rg.x), - y: Math.floor(rg.y), + x: Math.floor(region.x), + y: Math.floor(region.y), }); // Convert the color to a string, stripping the alpha - the object group will handle opacity. - const rgbColor = rgbColorToString(rg.fill); - - const konvaObjectGroup = - konvaLayer.findOne(`.${RG_LAYER_OBJECT_GROUP_NAME}`) ?? - createObjectGroup(konvaLayer, RG_LAYER_OBJECT_GROUP_NAME); + const rgbColor = rgbColorToString(region.fill); // We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required. let groupNeedsCache = false; - const objectIds = rg.objects.map(mapId); - // Destroy any objects that are no longer in the redux state - for (const objectNode of konvaObjectGroup.find(selectVectorMaskObjects)) { - if (!objectIds.includes(objectNode.id())) { - objectNode.destroy(); + const objectIds = region.objects.map(mapId); + // Destroy any objects that are no longer in state + for (const entry of mapping.getEntries()) { + if (!objectIds.includes(entry.id)) { + mapping.destroyEntry(entry.id); groupNeedsCache = true; } } - for (const obj of rg.objects) { + for (const obj of region.objects) { if (obj.type === 'brush_line') { - const konvaBrushLine = - stage.findOne(`#${obj.id}`) ?? getBrushLine(obj, konvaObjectGroup, RG_LAYER_BRUSH_LINE_NAME); + const entry = getBrushLine(mapping, obj, RG_LAYER_BRUSH_LINE_NAME); // Only update the points if they have changed. The point values are never mutated, they are only added to the // array, so checking the length is sufficient to determine if we need to re-cache. - if (konvaBrushLine.points().length !== obj.points.length) { - konvaBrushLine.points(obj.points); + if (entry.konvaLine.points().length !== obj.points.length) { + entry.konvaLine.points(obj.points); groupNeedsCache = true; } // Only update the color if it has changed. - if (konvaBrushLine.stroke() !== rgbColor) { - konvaBrushLine.stroke(rgbColor); + if (entry.konvaLine.stroke() !== rgbColor) { + entry.konvaLine.stroke(rgbColor); groupNeedsCache = true; } } else if (obj.type === 'eraser_line') { - const konvaEraserLine = - stage.findOne(`#${obj.id}`) ?? getEraserLine(obj, konvaObjectGroup, RG_LAYER_ERASER_LINE_NAME); + const entry = getEraserLine(mapping, obj, RG_LAYER_ERASER_LINE_NAME); // Only update the points if they have changed. The point values are never mutated, they are only added to the // array, so checking the length is sufficient to determine if we need to re-cache. - if (konvaEraserLine.points().length !== obj.points.length) { - konvaEraserLine.points(obj.points); + if (entry.konvaLine.points().length !== obj.points.length) { + entry.konvaLine.points(obj.points); groupNeedsCache = true; } // Only update the color if it has changed. - if (konvaEraserLine.stroke() !== rgbColor) { - konvaEraserLine.stroke(rgbColor); + if (entry.konvaLine.stroke() !== rgbColor) { + entry.konvaLine.stroke(rgbColor); groupNeedsCache = true; } } else if (obj.type === 'rect_shape') { - const konvaRectShape = - stage.findOne(`#${obj.id}`) ?? createRectShape(obj, konvaObjectGroup, RG_LAYER_RECT_SHAPE_NAME); + const entry = getRectShape(mapping, obj, RG_LAYER_RECT_SHAPE_NAME); // Only update the color if it has changed. - if (konvaRectShape.fill() !== rgbColor) { - konvaRectShape.fill(rgbColor); + if (entry.konvaRect.fill() !== rgbColor) { + entry.konvaRect.fill(rgbColor); groupNeedsCache = true; } } } // Only update layer visibility if it has changed. - if (konvaLayer.visible() !== rg.isEnabled) { - konvaLayer.visible(rg.isEnabled); + if (mapping.konvaLayer.visible() !== region.isEnabled) { + mapping.konvaLayer.visible(region.isEnabled); groupNeedsCache = true; } - if (konvaObjectGroup.getChildren().length === 0) { + if (mapping.konvaObjectGroup.getChildren().length === 0) { // No objects - clear the cache to reset the previous pixel data - konvaObjectGroup.clearCache(); + mapping.konvaObjectGroup.clearCache(); return; } const compositingRect = - konvaLayer.findOne(`.${COMPOSITING_RECT_NAME}`) ?? createCompositingRect(konvaLayer); - const isSelected = selectedEntityIdentifier?.id === rg.id; + mapping.konvaLayer.findOne(`.${COMPOSITING_RECT_NAME}`) ?? createCompositingRect(mapping.konvaLayer); + const isSelected = selectedEntityIdentifier?.id === region.id; /** * When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows @@ -192,54 +192,56 @@ export const renderRGLayer = ( */ if (isSelected && tool !== 'move') { // We must clear the cache first so Konva will re-draw the group with the new compositing rect - if (konvaObjectGroup.isCached()) { - konvaObjectGroup.clearCache(); + if (mapping.konvaObjectGroup.isCached()) { + mapping.konvaObjectGroup.clearCache(); } // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work - konvaObjectGroup.opacity(1); + mapping.konvaObjectGroup.opacity(1); compositingRect.setAttrs({ // The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already - ...(!rg.bboxNeedsUpdate && rg.bbox ? rg.bbox : getLayerBboxFast(konvaLayer)), + ...(!region.bboxNeedsUpdate && region.bbox ? region.bbox : getLayerBboxFast(mapping.konvaLayer)), fill: rgbColor, opacity: globalMaskLayerOpacity, // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) globalCompositeOperation: 'source-in', visible: true, // This rect must always be on top of all other shapes - zIndex: konvaObjectGroup.getChildren().length, + zIndex: mapping.konvaObjectGroup.getChildren().length, }); } else { // The compositing rect should only be shown when the layer is selected. compositingRect.visible(false); // Cache only if needed - or if we are on this code path and _don't_ have a cache - if (groupNeedsCache || !konvaObjectGroup.isCached()) { - konvaObjectGroup.cache(); + if (groupNeedsCache || !mapping.konvaObjectGroup.isCached()) { + mapping.konvaObjectGroup.cache(); } // Updating group opacity does not require re-caching - konvaObjectGroup.opacity(globalMaskLayerOpacity); + mapping.konvaObjectGroup.opacity(globalMaskLayerOpacity); } - const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rg, konvaLayer); + // const bboxRect = + // regionMap.konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rg, regionMap.konvaLayer); - if (rg.bbox) { - const active = !rg.bboxNeedsUpdate && isSelected && tool === 'move'; - bboxRect.setAttrs({ - visible: active, - listening: active, - x: rg.bbox.x, - y: rg.bbox.y, - width: rg.bbox.width, - height: rg.bbox.height, - stroke: isSelected ? BBOX_SELECTED_STROKE : '', - }); - } else { - bboxRect.visible(false); - } + // if (rg.bbox) { + // const active = !rg.bboxNeedsUpdate && isSelected && tool === 'move'; + // bboxRect.setAttrs({ + // visible: active, + // listening: active, + // x: rg.bbox.x, + // y: rg.bbox.y, + // width: rg.bbox.width, + // height: rg.bbox.height, + // stroke: isSelected ? BBOX_SELECTED_STROKE : '', + // }); + // } else { + // bboxRect.visible(false); + // } }; export const renderRegions = ( stage: Konva.Stage, + regionMap: EntityToKonvaMap, regions: RegionEntity[], maskOpacity: number, tool: Tool, @@ -247,12 +249,12 @@ export const renderRegions = ( onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): void => { // Destroy nonexistent layers - for (const konvaLayer of stage.find(`.${RG_LAYER_NAME}`)) { - if (!regions.find((rg) => rg.id === konvaLayer.id())) { - konvaLayer.destroy(); + for (const mapping of regionMap.getMappings()) { + if (!regions.find((rg) => rg.id === mapping.id)) { + regionMap.destroyMapping(mapping.id); } } for (const rg of regions) { - renderRGLayer(stage, rg, maskOpacity, tool, selectedEntityIdentifier, onPosChanged); + renderRGLayer(stage, regionMap, rg, maskOpacity, tool, selectedEntityIdentifier, onPosChanged); } }; From 0d3721324d439871078733efeafea8de7302583c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 19 Jun 2024 00:18:44 +1000 Subject: [PATCH 093/678] tidy(ui): organise renderers --- .../{konvaMap.ts => entityToKonvaMap.ts} | 0 .../features/controlLayers/konva/events.ts | 2 +- .../controlLayers/konva/renderers/arrange.ts | 23 +++ .../{caLayer.ts => controlAdapters.ts} | 16 +- .../controlLayers/konva/renderers/layers.ts | 166 ++++++++++++++++-- .../controlLayers/konva/renderers/objects.ts | 2 +- .../renderers/{previewLayer.ts => preview.ts} | 0 .../konva/renderers/rasterLayer.ts | 157 ----------------- .../renderers/{rgLayer.ts => regions.ts} | 6 +- .../controlLayers/konva/renderers/renderer.ts | 13 +- .../nodes/util/graph/generation/addRegions.ts | 2 +- 11 files changed, 194 insertions(+), 193 deletions(-) rename invokeai/frontend/web/src/features/controlLayers/konva/{konvaMap.ts => entityToKonvaMap.ts} (100%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts rename invokeai/frontend/web/src/features/controlLayers/konva/renderers/{caLayer.ts => controlAdapters.ts} (91%) rename invokeai/frontend/web/src/features/controlLayers/konva/renderers/{previewLayer.ts => preview.ts} (100%) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts rename invokeai/frontend/web/src/features/controlLayers/konva/renderers/{rgLayer.ts => regions.ts} (98%) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/konvaMap.ts b/invokeai/frontend/web/src/features/controlLayers/konva/entityToKonvaMap.ts similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/konva/konvaMap.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/entityToKonvaMap.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 19178a66518..0892d3da095 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -3,7 +3,7 @@ import { renderDocumentBoundsOverlay, renderToolPreview, scaleToolPreview, -} from 'features/controlLayers/konva/renderers/previewLayer'; +} from 'features/controlLayers/konva/renderers/preview'; import { fitDocumentToStage } from 'features/controlLayers/konva/renderers/stage'; import { getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util'; import type { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts new file mode 100644 index 00000000000..d0b148da4dc --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts @@ -0,0 +1,23 @@ +import { BACKGROUND_LAYER_ID, PREVIEW_LAYER_ID } from 'features/controlLayers/konva/naming'; +import type { ControlAdapterEntity, LayerEntity, RegionEntity } from 'features/controlLayers/store/types'; +import type Konva from 'konva'; + +export const arrangeEntities = ( + stage: Konva.Stage, + layers: LayerEntity[], + controlAdapters: ControlAdapterEntity[], + regions: RegionEntity[] +): void => { + let zIndex = 0; + stage.findOne(`#${BACKGROUND_LAYER_ID}`)?.zIndex(++zIndex); + for (const layer of layers) { + stage.findOne(`#${layer.id}`)?.zIndex(++zIndex); + } + for (const ca of controlAdapters) { + stage.findOne(`#${ca.id}`)?.zIndex(++zIndex); + } + for (const rg of regions) { + stage.findOne(`#${rg.id}`)?.zIndex(++zIndex); + } + stage.findOne(`#${PREVIEW_LAYER_ID}`)?.zIndex(++zIndex); +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts similarity index 91% rename from invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts index 71ed5623f4d..c69957e2c24 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts @@ -1,5 +1,5 @@ +import type { EntityToKonvaMap } from 'features/controlLayers/konva/entityToKonvaMap'; import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; -import type { EntityToKonvaMap } from 'features/controlLayers/konva/konvaMap'; import { CA_LAYER_IMAGE_NAME, CA_LAYER_NAME, getCAImageId } from 'features/controlLayers/konva/naming'; import type { ControlAdapterEntity } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -49,7 +49,7 @@ const createCALayerImage = (konvaLayer: Konva.Layer, imageEl: HTMLImageElement): * @param ca 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 ( +const updateControlAdapterImageSource = async ( stage: Konva.Stage, konvaLayer: Konva.Layer, ca: ControlAdapterEntity, @@ -74,7 +74,7 @@ const updateCALayerImageSource = async ( id: imageId, image: imageEl, }); - updateCALayerImageAttrs(stage, konvaImage, ca); + updateControlAdapterImageAttrs(stage, konvaImage, ca); // Must cache after this to apply the filters konvaImage.cache(); imageEl.id = imageId; @@ -92,7 +92,7 @@ const updateCALayerImageSource = async ( * @param ca The control adapter layer state */ -const updateCALayerImageAttrs = (stage: Konva.Stage, konvaImage: Konva.Image, ca: ControlAdapterEntity): void => { +const updateControlAdapterImageAttrs = (stage: Konva.Stage, konvaImage: Konva.Image, ca: ControlAdapterEntity): void => { let needsCache = false; // TODO(psyche): `node.filters()` returns null if no filters; report upstream const filters = konvaImage.filters() ?? []; @@ -128,7 +128,7 @@ const updateCALayerImageAttrs = (stage: Konva.Stage, konvaImage: Konva.Image, ca * @param ca The control adapter layer state * @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source */ -export const renderCALayer = ( +export const renderControlAdapter = ( stage: Konva.Stage, controlAdapterMap: EntityToKonvaMap, ca: ControlAdapterEntity, @@ -152,9 +152,9 @@ export const renderCALayer = ( } if (imageSourceNeedsUpdate) { - updateCALayerImageSource(stage, konvaLayer, ca, getImageDTO); + updateControlAdapterImageSource(stage, konvaLayer, ca, getImageDTO); } else if (konvaImage) { - updateCALayerImageAttrs(stage, konvaImage, ca); + updateControlAdapterImageAttrs(stage, konvaImage, ca); } }; @@ -171,6 +171,6 @@ export const renderControlAdapters = ( } } for (const ca of controlAdapters) { - renderCALayer(stage, controlAdapterMap, ca, getImageDTO); + renderControlAdapter(stage, controlAdapterMap, ca, getImageDTO); } }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts index d0b148da4dc..29b2e96842c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -1,23 +1,157 @@ -import { BACKGROUND_LAYER_ID, PREVIEW_LAYER_ID } from 'features/controlLayers/konva/naming'; -import type { ControlAdapterEntity, LayerEntity, RegionEntity } from 'features/controlLayers/store/types'; -import type Konva from 'konva'; +import type { EntityToKonvaMap, EntityToKonvaMapping } from 'features/controlLayers/konva/entityToKonvaMap'; +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, +} from 'features/controlLayers/konva/naming'; +import { + createImageObjectGroup, + createObjectGroup, + getBrushLine, + getEraserLine, + getRectShape, +} from 'features/controlLayers/konva/renderers/objects'; +import { mapId } from 'features/controlLayers/konva/util'; +import type { CanvasEntity, LayerEntity, PosChangedArg, Tool } from 'features/controlLayers/store/types'; +import Konva from 'konva'; -export const arrangeEntities = ( +/** + * Logic for creating and rendering raster layers. + */ + +/** + * Creates a raster layer. + * @param stage The konva stage + * @param layerState The raster layer state + * @param onPosChanged Callback for when the layer's position changes + */ +const getLayer = ( + stage: Konva.Stage, + layerMap: EntityToKonvaMap, + layerState: LayerEntity, + onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void +): EntityToKonvaMapping => { + let mapping = layerMap.getMapping(layerState.id); + if (mapping) { + return mapping; + } + // This layer hasn't been added to the konva state yet + const konvaLayer = new Konva.Layer({ + id: layerState.id, + name: RASTER_LAYER_NAME, + draggable: true, + dragDistance: 0, + }); + + // 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 (onPosChanged) { + konvaLayer.on('dragend', function (e) { + onPosChanged({ id: layerState.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'layer'); + }); + } + + const konvaObjectGroup = createObjectGroup(konvaLayer, RASTER_LAYER_OBJECT_GROUP_NAME); + konvaLayer.add(konvaObjectGroup); + stage.add(konvaLayer); + mapping = layerMap.addMapping(layerState.id, konvaLayer, konvaObjectGroup); + return mapping; +}; + +/** + * Renders a regional guidance layer. + * @param stage The konva stage + * @param layerState The regional guidance layer state + * @param tool The current tool + * @param onPosChanged Callback for when the layer's position changes + */ +export const renderLayer = async ( + stage: Konva.Stage, + layerMap: EntityToKonvaMap, + layerState: LayerEntity, + tool: Tool, + onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void +) => { + const mapping = getLayer(stage, layerMap, layerState, onPosChanged); + + // Update the layer's position and listening state + mapping.konvaLayer.setAttrs({ + listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events + x: Math.floor(layerState.x), + y: Math.floor(layerState.y), + }); + + const objectIds = layerState.objects.map(mapId); + // Destroy any objects that are no longer in state + for (const entry of mapping.getEntries()) { + if (!objectIds.includes(entry.id)) { + mapping.destroyEntry(entry.id); + } + } + + for (const obj of layerState.objects) { + if (obj.type === 'brush_line') { + const entry = getBrushLine(mapping, obj, RASTER_LAYER_BRUSH_LINE_NAME); + // Only update the points if they have changed. + if (entry.konvaLine.points().length !== obj.points.length) { + entry.konvaLine.points(obj.points); + } + } else if (obj.type === 'eraser_line') { + const entry = getEraserLine(mapping, obj, RASTER_LAYER_ERASER_LINE_NAME); + // Only update the points if they have changed. + if (entry.konvaLine.points().length !== obj.points.length) { + entry.konvaLine.points(obj.points); + } + } else if (obj.type === 'rect_shape') { + getRectShape(mapping, obj, RASTER_LAYER_RECT_SHAPE_NAME); + } else if (obj.type === 'image') { + createImageObjectGroup(mapping, obj, RASTER_LAYER_IMAGE_NAME); + } + } + + // Only update layer visibility if it has changed. + if (mapping.konvaLayer.visible() !== layerState.isEnabled) { + mapping.konvaLayer.visible(layerState.isEnabled); + } + + // const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer); + + // if (layerState.bbox) { + // const active = !layerState.bboxNeedsUpdate && layerState.isSelected && tool === 'move'; + // bboxRect.setAttrs({ + // visible: active, + // listening: active, + // x: layerState.bbox.x, + // y: layerState.bbox.y, + // width: layerState.bbox.width, + // height: layerState.bbox.height, + // stroke: layerState.isSelected ? BBOX_SELECTED_STROKE : '', + // strokeWidth: 1 / stage.scaleX(), + // }); + // } else { + // bboxRect.visible(false); + // } + + mapping.konvaObjectGroup.opacity(layerState.opacity); +}; + +export const renderLayers = ( stage: Konva.Stage, + layerMap: EntityToKonvaMap, layers: LayerEntity[], - controlAdapters: ControlAdapterEntity[], - regions: RegionEntity[] + tool: Tool, + onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): void => { - let zIndex = 0; - stage.findOne(`#${BACKGROUND_LAYER_ID}`)?.zIndex(++zIndex); - for (const layer of layers) { - stage.findOne(`#${layer.id}`)?.zIndex(++zIndex); - } - for (const ca of controlAdapters) { - stage.findOne(`#${ca.id}`)?.zIndex(++zIndex); + // Destroy nonexistent layers + for (const mapping of layerMap.getMappings()) { + if (!layers.find((l) => l.id === mapping.id)) { + layerMap.destroyMapping(mapping.id); + } } - for (const rg of regions) { - stage.findOne(`#${rg.id}`)?.zIndex(++zIndex); + for (const layer of layers) { + renderLayer(stage, layerMap, layer, tool, onPosChanged); } - stage.findOne(`#${PREVIEW_LAYER_ID}`)?.zIndex(++zIndex); }; 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 2c21aeb120b..c96fbda9af9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -5,7 +5,7 @@ import type { EraserLineEntry, ImageEntry, RectShapeEntry, -} from 'features/controlLayers/konva/konvaMap'; +} from 'features/controlLayers/konva/entityToKonvaMap'; import { getLayerBboxId, getObjectGroupId, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts deleted file mode 100644 index 13829d4dae0..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rasterLayer.ts +++ /dev/null @@ -1,157 +0,0 @@ -import type { EntityToKonvaMap, EntityToKonvaMapping } from 'features/controlLayers/konva/konvaMap'; -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, -} from 'features/controlLayers/konva/naming'; -import { - createImageObjectGroup, - createObjectGroup, - getBrushLine, - getEraserLine, - getRectShape, -} from 'features/controlLayers/konva/renderers/objects'; -import { mapId } from 'features/controlLayers/konva/util'; -import type { CanvasEntity, LayerEntity, PosChangedArg, Tool } from 'features/controlLayers/store/types'; -import Konva from 'konva'; - -/** - * Logic for creating and rendering raster layers. - */ - -/** - * Creates a raster layer. - * @param stage The konva stage - * @param layerState The raster layer state - * @param onPosChanged Callback for when the layer's position changes - */ -const getLayer = ( - stage: Konva.Stage, - layerMap: EntityToKonvaMap, - layerState: LayerEntity, - onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void -): EntityToKonvaMapping => { - let mapping = layerMap.getMapping(layerState.id); - if (mapping) { - return mapping; - } - // This layer hasn't been added to the konva state yet - const konvaLayer = new Konva.Layer({ - id: layerState.id, - name: RASTER_LAYER_NAME, - draggable: true, - dragDistance: 0, - }); - - // 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 (onPosChanged) { - konvaLayer.on('dragend', function (e) { - onPosChanged({ id: layerState.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'layer'); - }); - } - - const konvaObjectGroup = createObjectGroup(konvaLayer, RASTER_LAYER_OBJECT_GROUP_NAME); - konvaLayer.add(konvaObjectGroup); - stage.add(konvaLayer); - mapping = layerMap.addMapping(layerState.id, konvaLayer, konvaObjectGroup); - return mapping; -}; - -/** - * Renders a regional guidance layer. - * @param stage The konva stage - * @param layerState The regional guidance layer state - * @param tool The current tool - * @param onPosChanged Callback for when the layer's position changes - */ -export const renderRasterLayer = async ( - stage: Konva.Stage, - layerMap: EntityToKonvaMap, - layerState: LayerEntity, - tool: Tool, - onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void -) => { - const mapping = getLayer(stage, layerMap, layerState, onPosChanged); - - // Update the layer's position and listening state - mapping.konvaLayer.setAttrs({ - listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events - x: Math.floor(layerState.x), - y: Math.floor(layerState.y), - }); - - const objectIds = layerState.objects.map(mapId); - // Destroy any objects that are no longer in state - for (const entry of mapping.getEntries()) { - if (!objectIds.includes(entry.id)) { - mapping.destroyEntry(entry.id); - } - } - - for (const obj of layerState.objects) { - if (obj.type === 'brush_line') { - const entry = getBrushLine(mapping, obj, RASTER_LAYER_BRUSH_LINE_NAME); - // Only update the points if they have changed. - if (entry.konvaLine.points().length !== obj.points.length) { - entry.konvaLine.points(obj.points); - } - } else if (obj.type === 'eraser_line') { - const entry = getEraserLine(mapping, obj, RASTER_LAYER_ERASER_LINE_NAME); - // Only update the points if they have changed. - if (entry.konvaLine.points().length !== obj.points.length) { - entry.konvaLine.points(obj.points); - } - } else if (obj.type === 'rect_shape') { - getRectShape(mapping, obj, RASTER_LAYER_RECT_SHAPE_NAME); - } else if (obj.type === 'image') { - createImageObjectGroup(mapping, obj, RASTER_LAYER_IMAGE_NAME); - } - } - - // Only update layer visibility if it has changed. - if (mapping.konvaLayer.visible() !== layerState.isEnabled) { - mapping.konvaLayer.visible(layerState.isEnabled); - } - - // const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer); - - // if (layerState.bbox) { - // const active = !layerState.bboxNeedsUpdate && layerState.isSelected && tool === 'move'; - // bboxRect.setAttrs({ - // visible: active, - // listening: active, - // x: layerState.bbox.x, - // y: layerState.bbox.y, - // width: layerState.bbox.width, - // height: layerState.bbox.height, - // stroke: layerState.isSelected ? BBOX_SELECTED_STROKE : '', - // strokeWidth: 1 / stage.scaleX(), - // }); - // } else { - // bboxRect.visible(false); - // } - - mapping.konvaObjectGroup.opacity(layerState.opacity); -}; - -export const renderLayers = ( - stage: Konva.Stage, - layerMap: EntityToKonvaMap, - layers: LayerEntity[], - tool: Tool, - onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void -): void => { - // Destroy nonexistent layers - for (const mapping of layerMap.getMappings()) { - if (!layers.find((l) => l.id === mapping.id)) { - layerMap.destroyMapping(mapping.id); - } - } - for (const layer of layers) { - renderRasterLayer(stage, layerMap, layer, tool, onPosChanged); - } -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts similarity index 98% rename from invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts index c25e0e64868..eaa46f42c9a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/rgLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts @@ -1,5 +1,5 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; -import type { EntityToKonvaMap, EntityToKonvaMapping } from 'features/controlLayers/konva/konvaMap'; +import type { EntityToKonvaMap, EntityToKonvaMapping } from 'features/controlLayers/konva/entityToKonvaMap'; import { COMPOSITING_RECT_NAME, RG_LAYER_BRUSH_LINE_NAME, @@ -90,7 +90,7 @@ const getRegion = ( * @param tool The current tool * @param onPosChanged Callback for when the layer's position changes */ -export const renderRGLayer = ( +export const renderRegion = ( stage: Konva.Stage, regionMap: EntityToKonvaMap, region: RegionEntity, @@ -255,6 +255,6 @@ export const renderRegions = ( } } for (const rg of regions) { - renderRGLayer(stage, regionMap, rg, maskOpacity, tool, selectedEntityIdentifier, onPosChanged); + renderRegion(stage, regionMap, rg, maskOpacity, tool, selectedEntityIdentifier, onPosChanged); } }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts index 1fb844dcc6e..7912f4cc516 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -3,19 +3,19 @@ import type { Store } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import { $isDebugging } from 'app/store/nanostores/isDebugging'; import type { RootState } from 'app/store/store'; +import { EntityToKonvaMap } from 'features/controlLayers/konva/entityToKonvaMap'; import { setStageEventHandlers } from 'features/controlLayers/konva/events'; -import { EntityToKonvaMap } from 'features/controlLayers/konva/konvaMap'; +import { arrangeEntities } from 'features/controlLayers/konva/renderers/arrange'; import { renderBackgroundLayer } from 'features/controlLayers/konva/renderers/background'; import { updateBboxes } from 'features/controlLayers/konva/renderers/bbox'; -import { renderControlAdapters } from 'features/controlLayers/konva/renderers/caLayer'; -import { arrangeEntities } from 'features/controlLayers/konva/renderers/layers'; +import { renderControlAdapters } from 'features/controlLayers/konva/renderers/controlAdapters'; +import { renderLayers } from 'features/controlLayers/konva/renderers/layers'; import { renderBboxPreview, renderDocumentBoundsOverlay, scaleToolPreview, -} from 'features/controlLayers/konva/renderers/previewLayer'; -import { renderLayers } from 'features/controlLayers/konva/renderers/rasterLayer'; -import { renderRegions } from 'features/controlLayers/konva/renderers/rgLayer'; +} from 'features/controlLayers/konva/renderers/preview'; +import { renderRegions } from 'features/controlLayers/konva/renderers/regions'; import { fitDocumentToStage } from 'features/controlLayers/konva/renderers/stage'; import { $stageAttrs, @@ -55,6 +55,7 @@ import type { IRect, Vector2d } from 'konva/lib/types'; import { debounce } from 'lodash-es'; import type { RgbaColor } from 'react-colorful'; import { getImageDTO } from 'services/api/endpoints/images'; + /** * Initializes the canvas renderer. It subscribes to the redux store and listens for changes directly, bypassing the * react rendering cycle entirely, improving canvas performance. diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts index 168c941033c..9792ac1fa88 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts @@ -2,7 +2,7 @@ import { getStore } from 'app/store/nanostores/store'; import { deepClone } from 'common/util/deepClone'; import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; import { RG_LAYER_NAME } from 'features/controlLayers/konva/naming'; -import { renderRegions } from 'features/controlLayers/konva/renderers/rgLayer'; +import { renderRegions } from 'features/controlLayers/konva/renderers/regions'; import { blobToDataURL } from 'features/controlLayers/konva/util'; import { rgMaskImageUploaded } from 'features/controlLayers/store/canvasV2Slice'; import type { Dimensions, IPAdapterEntity, RegionEntity } from 'features/controlLayers/store/types'; From 9cc41331844dd90e5a8453376f9577583d263156 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 19 Jun 2024 20:30:49 +1000 Subject: [PATCH 094/678] refactor(ui): create classes to abstract mgmt of konva nodes --- .../listeners/controlAdapterPreprocessor.ts | 8 +- .../listeners/imageDeletionListeners.ts | 2 +- .../src/common/hooks/useIsReadyToEnqueue.ts | 4 +- .../ControlAdapter/CAImagePreview.tsx | 4 +- .../components/IPAdapter/IPASettings.tsx | 2 +- .../RegionalGuidance/RGIPAdapterSettings.tsx | 2 +- .../controlLayers/konva/entityToKonvaMap.ts | 22 +- .../features/controlLayers/konva/naming.ts | 61 +++-- .../controlLayers/konva/renderers/arrange.ts | 10 +- .../konva/renderers/controlAdapters.ts | 216 +++++++----------- .../controlLayers/konva/renderers/layers.ts | 58 +++-- .../controlLayers/konva/renderers/objects.ts | 172 ++++++++------ .../controlLayers/konva/renderers/regions.ts | 61 +++-- .../controlLayers/konva/renderers/renderer.ts | 22 +- .../src/features/controlLayers/konva/util.ts | 2 - .../controlLayers/store/canvasV2Slice.ts | 15 ++ .../store/controlAdaptersReducers.ts | 68 +++--- .../store/inpaintMaskReducers.ts | 1 + .../controlLayers/store/inpaintMaskSlice.ts | 0 .../controlLayers/store/ipAdaptersReducers.ts | 27 ++- .../controlLayers/store/layersReducers.ts | 25 +- .../controlLayers/store/regionsReducers.ts | 41 ++-- .../src/features/controlLayers/store/types.ts | 68 ++++-- .../deleteImageModal/store/selectors.ts | 2 +- .../web/src/features/metadata/util/parsers.ts | 2 +- .../src/features/metadata/util/recallers.ts | 12 +- .../util/graph/generation/addIPAdapters.ts | 4 +- .../nodes/util/graph/generation/addRegions.ts | 2 +- 28 files changed, 481 insertions(+), 432 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts delete mode 100644 invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskSlice.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts index 6d77a533425..d4611d23eb7 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts @@ -67,11 +67,11 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni // We should only process if the processor settings or image have changed const originalCA = selectCA(originalState.canvasV2, id); - const originalImage = originalCA?.image; + const originalImage = originalCA?.imageObject; const originalConfig = originalCA?.processorConfig; - const image = ca.image; - const processedImage = ca.processedImage; + const image = ca.imageObject; + const processedImage = ca.processedImageObject; const config = ca.processorConfig; if (isEqual(config, originalConfig) && isEqual(image, originalImage) && processedImage) { @@ -95,7 +95,7 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni } // TODO(psyche): I can't get TS to be happy, it thinkgs `config` is `never` but it should be inferred from the generic... I'll just cast it for now - const processorNode = CA_PROCESSOR_DATA[config.type].buildNode(image, config as never); + const processorNode = CA_PROCESSOR_DATA[config.type].buildNode(image.image, config as never); const enqueueBatchArg: BatchConfig = { prepend: true, batch: { diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts index 6f918f99595..9278cf94b6c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts @@ -49,7 +49,7 @@ const deleteControlAdapterImages = (state: RootState, dispatch: AppDispatch, ima }; const deleteIPAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { - state.canvasV2.ipAdapters.forEach(({ id, image }) => { + state.canvasV2.ipAdapters.forEach(({ id, imageObject: image }) => { if (image?.name === imageDTO.image_name) { dispatch(ipaImageChanged({ id, imageDTO: null })); } diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index 52edd663daf..56b94451755 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -178,7 +178,7 @@ const createSelector = (templates: Templates) => problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel')); } // Must have an image - if (!ipa.image) { + if (!ipa.imageObject) { problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected')); } @@ -214,7 +214,7 @@ const createSelector = (templates: Templates) => problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel')); } // Must have an image - if (!ipAdapter.image) { + if (!ipAdapter.imageObject) { problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected')); } }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx index f9d4a26fe98..b87362e573d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx @@ -47,10 +47,10 @@ export const CAImagePreview = memo( const [isMouseOverImage, setIsMouseOverImage] = useState(false); const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery( - controlAdapter.image?.name ?? skipToken + controlAdapter.imageObject?.image.name ?? skipToken ); const { currentData: processedControlImage, isError: isErrorProcessedControlImage } = useGetImageDTOQuery( - controlAdapter.processedImage?.name ?? skipToken + controlAdapter.processedImageObject?.image.name ?? skipToken ); const [changeIsIntermediate] = useChangeImageIsIntermediateMutation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx index f8911818eee..b051c7b9b53 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx @@ -95,7 +95,7 @@ export const IPASettings = memo(({ id }: Props) => { ; - constructor() { + constructor(stage: Konva.Stage) { + this.stage = stage; this.mappings = {}; } addMapping(id: string, konvaLayer: Konva.Layer, konvaObjectGroup: Konva.Group): EntityToKonvaMapping { - const mapping = new EntityToKonvaMapping(id, konvaLayer, konvaObjectGroup); + const mapping = new EntityToKonvaMapping(id, konvaLayer, konvaObjectGroup, this); this.mappings[id] = mapping; return mapping; } @@ -66,12 +72,14 @@ export class EntityToKonvaMapping { konvaLayer: Konva.Layer; konvaObjectGroup: Konva.Group; konvaNodeEntries: Record; + map: EntityToKonvaMap; - constructor(id: string, konvaLayer: Konva.Layer, konvaObjectGroup: Konva.Group) { + constructor(id: string, konvaLayer: Konva.Layer, konvaObjectGroup: Konva.Group, map: EntityToKonvaMap) { this.id = id; this.konvaLayer = konvaLayer; this.konvaObjectGroup = konvaObjectGroup; this.konvaNodeEntries = {}; + this.map = map; } addEntry(entry: T): T { @@ -83,8 +91,8 @@ export class EntityToKonvaMapping { return this.konvaNodeEntries[id] as T | undefined; } - getEntries(): Entry[] { - return Object.values(this.konvaNodeEntries); + getEntries(): T[] { + return Object.values(this.konvaNodeEntries) as T[]; } destroyEntry(id: string): void { @@ -97,7 +105,7 @@ export class EntityToKonvaMapping { } else if (entry.type === 'rect_shape') { entry.konvaRect.destroy(); } else if (entry.type === 'image') { - entry.konvaGroup.destroy(); + entry.konvaImageGroup.destroy(); } delete this.konvaNodeEntries[id]; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts index 63abe4d799b..eedaa94f6e6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts @@ -4,42 +4,40 @@ // IDs for singleton Konva layers and objects export const PREVIEW_LAYER_ID = 'preview_layer'; -export const PREVIEW_TOOL_GROUP_ID = 'preview_layer.tool_group'; -export const PREVIEW_BRUSH_GROUP_ID = 'preview_layer.brush_group'; -export const PREVIEW_BRUSH_FILL_ID = 'preview_layer.brush_fill'; -export const PREVIEW_BRUSH_BORDER_INNER_ID = 'preview_layer.brush_border_inner'; -export const PREVIEW_BRUSH_BORDER_OUTER_ID = 'preview_layer.brush_border_outer'; -export const PREVIEW_RECT_ID = 'preview_layer.rect'; -export const PREVIEW_GENERATION_BBOX_GROUP = 'preview_layer.gen_bbox_group'; -export const PREVIEW_GENERATION_BBOX_TRANSFORMER = 'preview_layer.gen_bbox_transformer'; -export const PREVIEW_GENERATION_BBOX_DUMMY_RECT = 'preview_layer.gen_bbox_dummy_rect'; -export const PREVIEW_DOCUMENT_SIZE_GROUP = 'preview_layer.doc_size_group'; -export const PREVIEW_DOCUMENT_SIZE_STAGE_RECT = 'preview_layer.doc_size_stage_rect'; -export const PREVIEW_DOCUMENT_SIZE_DOCUMENT_RECT = 'preview_layer.doc_size_doc_rect'; +export const PREVIEW_TOOL_GROUP_ID = `${PREVIEW_LAYER_ID}.tool_group`; +export const PREVIEW_BRUSH_GROUP_ID = `${PREVIEW_LAYER_ID}.brush_group`; +export const PREVIEW_BRUSH_FILL_ID = `${PREVIEW_LAYER_ID}.brush_fill`; +export const PREVIEW_BRUSH_BORDER_INNER_ID = `${PREVIEW_LAYER_ID}.brush_border_inner`; +export const PREVIEW_BRUSH_BORDER_OUTER_ID = `${PREVIEW_LAYER_ID}.brush_border_outer`; +export const PREVIEW_RECT_ID = `${PREVIEW_LAYER_ID}.rect`; +export const PREVIEW_GENERATION_BBOX_GROUP = `${PREVIEW_LAYER_ID}.gen_bbox_group`; +export const PREVIEW_GENERATION_BBOX_TRANSFORMER = `${PREVIEW_LAYER_ID}.gen_bbox_transformer`; +export const PREVIEW_GENERATION_BBOX_DUMMY_RECT = `${PREVIEW_LAYER_ID}.gen_bbox_dummy_rect`; +export const PREVIEW_DOCUMENT_SIZE_GROUP = `${PREVIEW_LAYER_ID}.doc_size_group`; +export const PREVIEW_DOCUMENT_SIZE_STAGE_RECT = `${PREVIEW_LAYER_ID}.doc_size_stage_rect`; +export const PREVIEW_DOCUMENT_SIZE_DOCUMENT_RECT = `${PREVIEW_LAYER_ID}.doc_size_doc_rect`; // 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 LAYER_BBOX_NAME = 'layer_bbox'; +export const COMPOSITING_RECT_NAME = 'compositing_rect'; +export const IMAGE_PLACEHOLDER_NAME = 'image_placeholder'; -export const CA_LAYER_NAME = 'control_adapter_layer'; -export const CA_LAYER_IMAGE_NAME = 'control_adapter_layer.image'; - -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 CA_LAYER_NAME = 'control_adapter'; +export const CA_LAYER_OBJECT_GROUP_NAME = `${CA_LAYER_NAME}.object_group`; +export const CA_LAYER_IMAGE_NAME = `${CA_LAYER_NAME}.image`; 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 RG_LAYER_OBJECT_GROUP_NAME = `${RG_LAYER_NAME}.object_group`; +export const RG_LAYER_BRUSH_LINE_NAME = `${RG_LAYER_NAME}.brush_line`; +export const RG_LAYER_ERASER_LINE_NAME = `${RG_LAYER_NAME}.eraser_line`; +export const RG_LAYER_RECT_SHAPE_NAME = `${RG_LAYER_NAME}.rect_shape`; export const RASTER_LAYER_NAME = 'raster_layer'; -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'; +export const RASTER_LAYER_OBJECT_GROUP_NAME = `${RASTER_LAYER_NAME}.object_group`; +export const RASTER_LAYER_BRUSH_LINE_NAME = `${RASTER_LAYER_NAME}.brush_line`; +export const RASTER_LAYER_ERASER_LINE_NAME = `${RASTER_LAYER_NAME}.eraser_line`; +export const RASTER_LAYER_RECT_SHAPE_NAME = `${RASTER_LAYER_NAME}.rect_shape`; +export const RASTER_LAYER_IMAGE_NAME = `${RASTER_LAYER_NAME}.image`; export const INPAINT_MASK_LAYER_NAME = 'inpaint_mask_layer'; @@ -51,9 +49,8 @@ export const getLayerId = (entityId: string) => `${RASTER_LAYER_NAME}_${entityId export const getBrushLineId = (entityId: string, lineId: string) => `${entityId}.brush_line_${lineId}`; export const getEraserLineId = (entityId: string, lineId: string) => `${entityId}.eraser_line_${lineId}`; export const getRectShapeId = (entityId: string, rectId: string) => `${entityId}.rect_${rectId}`; -export const getImageObjectId = (entityId: string, imageName: string) => `${entityId}.image_${imageName}`; +export const getImageObjectId = (entityId: string, imageId: string) => `${entityId}.image_${imageId}`; export const getObjectGroupId = (entityId: string, groupId: string) => `${entityId}.objectGroup_${groupId}`; export const getLayerBboxId = (entityId: string) => `${entityId}.bbox`; -export const getCAId = (entityId: string) => `control_adapter_${entityId}`; -export const getCAImageId = (entityId: string, imageName: string) => `${entityId}.image_${imageName}`; +export const getCAId = (entityId: string) => `${CA_LAYER_NAME}_${entityId}`; export const getIPAId = (entityId: string) => `ip_adapter_${entityId}`; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts index d0b148da4dc..02644c1cc41 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts @@ -1,23 +1,27 @@ +import type { EntityToKonvaMap } from 'features/controlLayers/konva/entityToKonvaMap'; import { BACKGROUND_LAYER_ID, PREVIEW_LAYER_ID } from 'features/controlLayers/konva/naming'; import type { ControlAdapterEntity, LayerEntity, RegionEntity } from 'features/controlLayers/store/types'; import type Konva from 'konva'; export const arrangeEntities = ( stage: Konva.Stage, + layerMap: EntityToKonvaMap, layers: LayerEntity[], + controlAdapterMap: EntityToKonvaMap, controlAdapters: ControlAdapterEntity[], + regionMap: EntityToKonvaMap, regions: RegionEntity[] ): void => { let zIndex = 0; stage.findOne(`#${BACKGROUND_LAYER_ID}`)?.zIndex(++zIndex); for (const layer of layers) { - stage.findOne(`#${layer.id}`)?.zIndex(++zIndex); + layerMap.getMapping(layer.id)?.konvaLayer.zIndex(++zIndex); } for (const ca of controlAdapters) { - stage.findOne(`#${ca.id}`)?.zIndex(++zIndex); + controlAdapterMap.getMapping(ca.id)?.konvaLayer.zIndex(++zIndex); } for (const rg of regions) { - stage.findOne(`#${rg.id}`)?.zIndex(++zIndex); + regionMap.getMapping(rg.id)?.konvaLayer.zIndex(++zIndex); } stage.findOne(`#${PREVIEW_LAYER_ID}`)?.zIndex(++zIndex); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts index c69957e2c24..c466db06da8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts @@ -1,9 +1,15 @@ -import type { EntityToKonvaMap } from 'features/controlLayers/konva/entityToKonvaMap'; +import type { EntityToKonvaMap, EntityToKonvaMapping, ImageEntry } from 'features/controlLayers/konva/entityToKonvaMap'; import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; -import { CA_LAYER_IMAGE_NAME, CA_LAYER_NAME, getCAImageId } from 'features/controlLayers/konva/naming'; +import { CA_LAYER_IMAGE_NAME, CA_LAYER_NAME, CA_LAYER_OBJECT_GROUP_NAME } from 'features/controlLayers/konva/naming'; +import { + createImageObjectGroup, + createObjectGroup, + updateImageSource, +} from 'features/controlLayers/konva/renderers/objects'; import type { ControlAdapterEntity } from 'features/controlLayers/store/types'; import Konva from 'konva'; -import type { ImageDTO } from 'services/api/types'; +import { isEqual } from 'lodash-es'; +import { assert } from 'tsafe'; /** * Logic for creating and rendering control adapter (control net & t2i adapter) layers. These layers have image objects @@ -13,164 +19,98 @@ import type { ImageDTO } from 'services/api/types'; /** * Creates a control adapter layer. * @param stage The konva stage - * @param ca The control adapter layer state + * @param entity The control adapter layer state */ -const createCALayer = (stage: Konva.Stage, ca: ControlAdapterEntity): Konva.Layer => { +const getControlAdapter = (map: EntityToKonvaMap, entity: ControlAdapterEntity): EntityToKonvaMapping => { + let mapping = map.getMapping(entity.id); + if (mapping) { + return mapping; + } const konvaLayer = new Konva.Layer({ - id: ca.id, + id: entity.id, name: CA_LAYER_NAME, imageSmoothingEnabled: false, listening: false, }); - stage.add(konvaLayer); - return konvaLayer; -}; - -/** - * 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({ - name: CA_LAYER_IMAGE_NAME, - image: imageEl, - listening: false, - }); - konvaLayer.add(konvaImage); - return konvaImage; + const konvaObjectGroup = createObjectGroup(konvaLayer, CA_LAYER_OBJECT_GROUP_NAME); + map.stage.add(konvaLayer); + mapping = map.addMapping(entity.id, konvaLayer, konvaObjectGroup); + return mapping; }; /** - * Updates the image source for a control adapter layer. This includes loading the image from the server and updating - * the konva image. + * 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 konvaLayer The konva layer - * @param ca The control adapter layer state + * @param entity The control adapter layer state * @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source */ -const updateControlAdapterImageSource = async ( - stage: Konva.Stage, - konvaLayer: Konva.Layer, - ca: ControlAdapterEntity, - getImageDTO: (imageName: string) => Promise -): Promise => { - const image = ca.processedImage ?? ca.image; - if (image) { - const imageName = image.name; - const imageDTO = await getImageDTO(imageName); - if (!imageDTO) { - return; - } - const imageEl = new Image(); - const imageId = getCAImageId(ca.id, imageName); - imageEl.onload = () => { - // Find the existing image or create a new one - must find using the name, bc the id may have just changed - const konvaImage = - konvaLayer.findOne(`.${CA_LAYER_IMAGE_NAME}`) ?? createCALayerImage(konvaLayer, imageEl); +export const renderControlAdapter = async (map: EntityToKonvaMap, entity: ControlAdapterEntity): Promise => { + const mapping = getControlAdapter(map, entity); + const imageObject = entity.processedImageObject ?? entity.imageObject; - // Update the image's attributes - konvaImage.setAttrs({ - id: imageId, - image: imageEl, - }); - updateControlAdapterImageAttrs(stage, konvaImage, ca); - // Must cache after this to apply the filters - konvaImage.cache(); - imageEl.id = imageId; - }; - imageEl.src = imageDTO.image_url; - } else { - konvaLayer.findOne(`.${CA_LAYER_IMAGE_NAME}`)?.destroy(); - } -}; - -/** - * 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 ca The control adapter layer state - */ - -const updateControlAdapterImageAttrs = (stage: Konva.Stage, konvaImage: Konva.Image, ca: ControlAdapterEntity): void => { - let needsCache = false; - // TODO(psyche): `node.filters()` returns null if no filters; report upstream - const filters = konvaImage.filters() ?? []; - const filter = filters[0] ?? null; - const filterNeedsUpdate = (filter === null && ca.filter !== 'none') || (filter && filter.name !== ca.filter); - if ( - konvaImage.x() !== ca.x || - konvaImage.y() !== ca.y || - konvaImage.visible() !== ca.isEnabled || - filterNeedsUpdate - ) { - konvaImage.setAttrs({ - opacity: ca.opacity, - scaleX: 1, - scaleY: 1, - visible: ca.isEnabled, - filters: ca.filter === 'LightnessToAlphaFilter' ? [LightnessToAlphaFilter] : [], + if (!imageObject) { + // The user has deleted/reset the image + mapping.getEntries().forEach((entry) => { + mapping.destroyEntry(entry.id); }); - needsCache = true; - } - if (konvaImage.opacity() !== ca.opacity) { - konvaImage.opacity(ca.opacity); - } - if (needsCache) { - konvaImage.cache(); + return; } -}; - -/** - * 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 ca The control adapter layer state - * @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source - */ -export const renderControlAdapter = ( - stage: Konva.Stage, - controlAdapterMap: EntityToKonvaMap, - ca: ControlAdapterEntity, - getImageDTO: (imageName: string) => Promise -): void => { - const konvaLayer = stage.findOne(`#${ca.id}`) ?? createCALayer(stage, ca); - const konvaImage = konvaLayer.findOne(`.${CA_LAYER_IMAGE_NAME}`); - const canvasImageSource = konvaImage?.image(); - let imageSourceNeedsUpdate = false; + let entry = mapping.getEntries()[0]; + const opacity = entity.opacity; + const visible = entity.isEnabled; + const filters = entity.filter === 'LightnessToAlphaFilter' ? [LightnessToAlphaFilter] : []; - if (canvasImageSource instanceof HTMLImageElement) { - const image = ca.processedImage ?? ca.image; - if (image && canvasImageSource.id !== getCAImageId(ca.id, image.name)) { - imageSourceNeedsUpdate = true; - } else if (!image) { - imageSourceNeedsUpdate = true; + if (!entry) { + entry = await createImageObjectGroup({ + mapping, + obj: imageObject, + name: CA_LAYER_IMAGE_NAME, + onLoad: (konvaImage) => { + konvaImage.filters(filters); + konvaImage.cache(); + konvaImage.opacity(opacity); + konvaImage.visible(visible); + }, + }); + } else { + if (entry.isLoading || entry.isError) { + return; + } + assert(entry.konvaImage, `Image entry ${entry.id} must have a konva image if it is not loading or in error state`); + const imageSource = entry.konvaImage.image(); + assert(imageSource instanceof HTMLImageElement, `Image source must be an HTMLImageElement`); + if (imageSource.id !== imageObject.image.name) { + updateImageSource({ + entry, + image: imageObject.image, + onLoad: (konvaImage) => { + konvaImage.filters(filters); + konvaImage.cache(); + konvaImage.opacity(opacity); + konvaImage.visible(visible); + }, + }); + } else { + if (!isEqual(entry.konvaImage.filters(), filters)) { + entry.konvaImage.filters(filters); + entry.konvaImage.cache(); + } + entry.konvaImage.opacity(opacity); + entry.konvaImage.visible(visible); } - } else if (!canvasImageSource) { - imageSourceNeedsUpdate = true; - } - - if (imageSourceNeedsUpdate) { - updateControlAdapterImageSource(stage, konvaLayer, ca, getImageDTO); - } else if (konvaImage) { - updateControlAdapterImageAttrs(stage, konvaImage, ca); } }; -export const renderControlAdapters = ( - stage: Konva.Stage, - controlAdapterMap: EntityToKonvaMap, - controlAdapters: ControlAdapterEntity[], - getImageDTO: (imageName: string) => Promise -): void => { +export const renderControlAdapters = (map: EntityToKonvaMap, entities: ControlAdapterEntity[]): void => { // Destroy nonexistent layers - for (const mapping of controlAdapterMap.getMappings()) { - if (!controlAdapters.find((ca) => ca.id === mapping.id)) { - controlAdapterMap.destroyMapping(mapping.id); + for (const mapping of map.getMappings()) { + if (!entities.find((ca) => ca.id === mapping.id)) { + map.destroyMapping(mapping.id); } } - for (const ca of controlAdapters) { - renderControlAdapter(stage, controlAdapterMap, ca, getImageDTO); + for (const ca of entities) { + renderControlAdapter(map, ca); } }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts index 29b2e96842c..6c128953fd5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -25,22 +25,21 @@ import Konva from 'konva'; /** * Creates a raster layer. * @param stage The konva stage - * @param layerState The raster layer state + * @param entity The raster layer state * @param onPosChanged Callback for when the layer's position changes */ const getLayer = ( - stage: Konva.Stage, - layerMap: EntityToKonvaMap, - layerState: LayerEntity, + map: EntityToKonvaMap, + entity: LayerEntity, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): EntityToKonvaMapping => { - let mapping = layerMap.getMapping(layerState.id); + let mapping = map.getMapping(entity.id); if (mapping) { return mapping; } // This layer hasn't been added to the konva state yet const konvaLayer = new Konva.Layer({ - id: layerState.id, + id: entity.id, name: RASTER_LAYER_NAME, draggable: true, dragDistance: 0, @@ -50,41 +49,39 @@ const getLayer = ( // the position - we do not need to call this on the `dragmove` event. if (onPosChanged) { konvaLayer.on('dragend', function (e) { - onPosChanged({ id: layerState.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'layer'); + onPosChanged({ id: entity.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'layer'); }); } const konvaObjectGroup = createObjectGroup(konvaLayer, RASTER_LAYER_OBJECT_GROUP_NAME); - konvaLayer.add(konvaObjectGroup); - stage.add(konvaLayer); - mapping = layerMap.addMapping(layerState.id, konvaLayer, konvaObjectGroup); + map.stage.add(konvaLayer); + mapping = map.addMapping(entity.id, konvaLayer, konvaObjectGroup); return mapping; }; /** * Renders a regional guidance layer. * @param stage The konva stage - * @param layerState The regional guidance layer state + * @param entity The regional guidance layer state * @param tool The current tool * @param onPosChanged Callback for when the layer's position changes */ export const renderLayer = async ( - stage: Konva.Stage, - layerMap: EntityToKonvaMap, - layerState: LayerEntity, + map: EntityToKonvaMap, + entity: LayerEntity, tool: Tool, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ) => { - const mapping = getLayer(stage, layerMap, layerState, onPosChanged); + const mapping = getLayer(map, entity, onPosChanged); // Update the layer's position and listening state mapping.konvaLayer.setAttrs({ listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events - x: Math.floor(layerState.x), - y: Math.floor(layerState.y), + x: Math.floor(entity.x), + y: Math.floor(entity.y), }); - const objectIds = layerState.objects.map(mapId); + const objectIds = entity.objects.map(mapId); // Destroy any objects that are no longer in state for (const entry of mapping.getEntries()) { if (!objectIds.includes(entry.id)) { @@ -92,7 +89,7 @@ export const renderLayer = async ( } } - for (const obj of layerState.objects) { + for (const obj of entity.objects) { if (obj.type === 'brush_line') { const entry = getBrushLine(mapping, obj, RASTER_LAYER_BRUSH_LINE_NAME); // Only update the points if they have changed. @@ -108,13 +105,13 @@ export const renderLayer = async ( } else if (obj.type === 'rect_shape') { getRectShape(mapping, obj, RASTER_LAYER_RECT_SHAPE_NAME); } else if (obj.type === 'image') { - createImageObjectGroup(mapping, obj, RASTER_LAYER_IMAGE_NAME); + createImageObjectGroup({ mapping, obj, name: RASTER_LAYER_IMAGE_NAME }); } } // Only update layer visibility if it has changed. - if (mapping.konvaLayer.visible() !== layerState.isEnabled) { - mapping.konvaLayer.visible(layerState.isEnabled); + if (mapping.konvaLayer.visible() !== entity.isEnabled) { + mapping.konvaLayer.visible(entity.isEnabled); } // const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer); @@ -135,23 +132,22 @@ export const renderLayer = async ( // bboxRect.visible(false); // } - mapping.konvaObjectGroup.opacity(layerState.opacity); + mapping.konvaObjectGroup.opacity(entity.opacity); }; export const renderLayers = ( - stage: Konva.Stage, - layerMap: EntityToKonvaMap, - layers: LayerEntity[], + map: EntityToKonvaMap, + entities: LayerEntity[], tool: Tool, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): void => { // Destroy nonexistent layers - for (const mapping of layerMap.getMappings()) { - if (!layers.find((l) => l.id === mapping.id)) { - layerMap.destroyMapping(mapping.id); + for (const mapping of map.getMappings()) { + if (!entities.find((l) => l.id === mapping.id)) { + map.destroyMapping(mapping.id); } } - for (const layer of layers) { - renderLayer(stage, layerMap, layer, tool, onPosChanged); + for (const layer of entities) { + renderLayer(map, layer, tool, onPosChanged); } }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts index c96fbda9af9..4054bc8ec74 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -9,14 +9,23 @@ import type { import { getLayerBboxId, getObjectGroupId, + IMAGE_PLACEHOLDER_NAME, LAYER_BBOX_NAME, PREVIEW_GENERATION_BBOX_DUMMY_RECT, } from 'features/controlLayers/konva/naming'; -import type { BrushLine, CanvasEntity, EraserLine, ImageObject, RectShape } from 'features/controlLayers/store/types'; +import type { + BrushLine, + CanvasEntity, + EraserLine, + ImageObject, + ImageWithDims, + RectShape, +} from 'features/controlLayers/store/types'; import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types'; import { t } from 'i18next'; import Konva from 'konva'; -import { getImageDTO } from 'services/api/endpoints/images'; +import { getImageDTO as defaultGetImageDTO } from 'services/api/endpoints/images'; +import type { ImageDTO } from 'services/api/types'; import { v4 as uuidv4 } from 'uuid'; /** @@ -120,16 +129,93 @@ export const getRectShape = (mapping: EntityToKonvaMapping, rectShape: RectShape return entry; }; +export const updateImageSource = async (arg: { + entry: ImageEntry; + image: ImageWithDims; + getImageDTO?: (imageName: string) => Promise; + onLoading?: () => void; + onLoad?: (konvaImage: Konva.Image) => void; + onError?: () => void; +}) => { + const { entry, image, getImageDTO = defaultGetImageDTO, onLoading, onLoad, onError } = arg; + + try { + entry.isLoading = true; + if (!entry.konvaImage) { + entry.konvaPlaceholderGroup.visible(true); + entry.konvaPlaceholderText.text(t('common.loadingImage', 'Loading Image')); + } + onLoading?.(); + + const imageDTO = await getImageDTO(image.name); + if (!imageDTO) { + entry.isLoading = false; + entry.isError = true; + entry.konvaPlaceholderGroup.visible(true); + entry.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load')); + onError?.(); + return; + } + const imageEl = new Image(); + imageEl.onload = () => { + if (entry.konvaImage) { + entry.konvaImage.setAttrs({ + image: imageEl, + }); + } else { + entry.konvaImage = new Konva.Image({ + id: entry.id, + listening: false, + image: imageEl, + }); + entry.konvaImageGroup.add(entry.konvaImage); + } + entry.isLoading = false; + entry.isError = false; + entry.konvaPlaceholderGroup.visible(false); + onLoad?.(entry.konvaImage); + }; + imageEl.onerror = () => { + entry.isLoading = false; + entry.isError = true; + entry.konvaPlaceholderGroup.visible(true); + entry.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load')); + onError?.(); + }; + imageEl.id = image.name; + imageEl.src = imageDTO.image_url; + } catch { + entry.isLoading = false; + entry.isError = true; + entry.konvaPlaceholderGroup.visible(true); + entry.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load')); + onError?.(); + } +}; + /** * Creates an image placeholder group for an image object. - * @param imageObject The image object state + * @param image The image object state * @returns The konva group for the image placeholder, and callbacks to handle loading and error states */ -const createImagePlaceholderGroup = ( - imageObject: ImageObject -): { konvaPlaceholderGroup: Konva.Group; onError: () => void; onLoading: () => void; onLoaded: () => void } => { - const { width, height } = imageObject.image; - const konvaPlaceholderGroup = new Konva.Group({ name: 'image-placeholder', listening: false }); +export const createImageObjectGroup = (arg: { + mapping: EntityToKonvaMapping; + obj: ImageObject; + name: string; + getImageDTO?: (imageName: string) => Promise; + onLoad?: (konvaImage: Konva.Image) => void; + onLoading?: () => void; + onError?: () => void; +}): ImageEntry => { + const { mapping, obj, name, getImageDTO = defaultGetImageDTO, onLoad, onLoading, onError } = arg; + let entry = mapping.getEntry(obj.id); + if (entry) { + return entry; + } + const { id, image } = obj; + const { width, height } = obj; + const konvaImageGroup = new Konva.Group({ id, name, listening: false }); + const konvaPlaceholderGroup = new Konva.Group({ name: IMAGE_PLACEHOLDER_NAME, listening: false }); const konvaPlaceholderRect = new Konva.Rect({ fill: 'hsl(220 12% 45% / 1)', // 'base.500' width, @@ -137,7 +223,6 @@ const createImagePlaceholderGroup = ( listening: false, }); const konvaPlaceholderText = new Konva.Text({ - name: 'image-placeholder-text', fill: 'hsl(220 12% 10% / 1)', // 'base.900' width, height, @@ -146,70 +231,25 @@ const createImagePlaceholderGroup = ( fontFamily: '"Inter Variable", sans-serif', fontSize: width / 16, fontStyle: '600', - text: 'Loading Image', + text: t('common.loadingImage', 'Loading Image'), listening: false, }); konvaPlaceholderGroup.add(konvaPlaceholderRect); konvaPlaceholderGroup.add(konvaPlaceholderText); - - const onError = () => { - konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load')); - }; - const onLoading = () => { - konvaPlaceholderText.text(t('common.loadingImage', 'Loading Image')); - }; - const onLoaded = () => { - konvaPlaceholderGroup.destroy(); - }; - return { konvaPlaceholderGroup, onError, onLoading, onLoaded }; -}; - -/** - * Creates an image object group. Because images are loaded asynchronously, and we need to handle loading an error state, - * the image is rendered in a group, which includes a placeholder. - * @param imageObject The image object state - * @param layerObjectGroup The konva layer's object group to add the image to - * @param name The konva name for the image - * @returns A promise that resolves to the konva group for the image object - */ -export const createImageObjectGroup = async ( - mapping: EntityToKonvaMapping, - imageObject: ImageObject, - name: string -): Promise => { - let entry = mapping.getEntry(imageObject.id); - if (entry) { - return entry; - } - const konvaImageGroup = new Konva.Group({ id: imageObject.id, name, listening: false }); - const placeholder = createImagePlaceholderGroup(imageObject); - konvaImageGroup.add(placeholder.konvaPlaceholderGroup); + konvaImageGroup.add(konvaPlaceholderGroup); mapping.konvaObjectGroup.add(konvaImageGroup); - entry = mapping.addEntry({ id: imageObject.id, type: 'image', konvaGroup: konvaImageGroup, konvaImage: null }); - getImageDTO(imageObject.image.name).then((imageDTO) => { - if (!imageDTO) { - placeholder.onError(); - return; - } - const imageEl = new Image(); - imageEl.onload = () => { - const konvaImage = new Konva.Image({ - id: imageObject.id, - name, - listening: false, - image: imageEl, - }); - placeholder.onLoaded(); - konvaImageGroup.add(konvaImage); - entry.konvaImage = konvaImage; - }; - imageEl.onerror = () => { - placeholder.onError(); - }; - imageEl.id = imageObject.id; - imageEl.src = imageDTO.image_url; + entry = mapping.addEntry({ + id, + type: 'image', + konvaImageGroup, + konvaPlaceholderGroup, + konvaPlaceholderText, + konvaImage: null, + isLoading: false, + isError: false, }); + updateImageSource({ entry, image, getImageDTO, onLoad, onLoading, onError }); return entry; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts index eaa46f42c9a..5119b2356da 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts @@ -45,22 +45,21 @@ const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => { /** * Creates a regional guidance layer. * @param stage The konva stage - * @param region The regional guidance layer state + * @param entity The regional guidance layer state * @param onLayerPosChanged Callback for when the layer's position changes */ const getRegion = ( - stage: Konva.Stage, - regionMap: EntityToKonvaMap, - region: RegionEntity, + map: EntityToKonvaMap, + entity: RegionEntity, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): EntityToKonvaMapping => { - let mapping = regionMap.getMapping(region.id); + let mapping = map.getMapping(entity.id); if (mapping) { return mapping; } // This layer hasn't been added to the konva state yet const konvaLayer = new Konva.Layer({ - id: region.id, + id: entity.id, name: RG_LAYER_NAME, draggable: true, dragDistance: 0, @@ -70,51 +69,48 @@ const getRegion = ( // the position - we do not need to call this on the `dragmove` event. if (onPosChanged) { konvaLayer.on('dragend', function (e) { - onPosChanged({ id: region.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'regional_guidance'); + onPosChanged({ id: entity.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'regional_guidance'); }); } const konvaObjectGroup = createObjectGroup(konvaLayer, RG_LAYER_OBJECT_GROUP_NAME); - - konvaLayer.add(konvaObjectGroup); - stage.add(konvaLayer); - mapping = regionMap.addMapping(region.id, konvaLayer, konvaObjectGroup); + map.stage.add(konvaLayer); + mapping = map.addMapping(entity.id, konvaLayer, konvaObjectGroup); return mapping; }; /** * Renders a raster layer. * @param stage The konva stage - * @param region The regional guidance layer state + * @param entity The regional guidance layer state * @param globalMaskLayerOpacity The global mask layer opacity * @param tool The current tool * @param onPosChanged Callback for when the layer's position changes */ export const renderRegion = ( - stage: Konva.Stage, - regionMap: EntityToKonvaMap, - region: RegionEntity, + map: EntityToKonvaMap, + entity: RegionEntity, globalMaskLayerOpacity: number, tool: Tool, selectedEntityIdentifier: CanvasEntityIdentifier | null, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): void => { - const mapping = getRegion(stage, regionMap, region, onPosChanged); + const mapping = getRegion(map, entity, onPosChanged); // Update the layer's position and listening state mapping.konvaLayer.setAttrs({ listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events - x: Math.floor(region.x), - y: Math.floor(region.y), + x: Math.floor(entity.x), + y: Math.floor(entity.y), }); // Convert the color to a string, stripping the alpha - the object group will handle opacity. - const rgbColor = rgbColorToString(region.fill); + const rgbColor = rgbColorToString(entity.fill); // We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required. let groupNeedsCache = false; - const objectIds = region.objects.map(mapId); + const objectIds = entity.objects.map(mapId); // Destroy any objects that are no longer in state for (const entry of mapping.getEntries()) { if (!objectIds.includes(entry.id)) { @@ -123,7 +119,7 @@ export const renderRegion = ( } } - for (const obj of region.objects) { + for (const obj of entity.objects) { if (obj.type === 'brush_line') { const entry = getBrushLine(mapping, obj, RG_LAYER_BRUSH_LINE_NAME); @@ -164,8 +160,8 @@ export const renderRegion = ( } // Only update layer visibility if it has changed. - if (mapping.konvaLayer.visible() !== region.isEnabled) { - mapping.konvaLayer.visible(region.isEnabled); + if (mapping.konvaLayer.visible() !== entity.isEnabled) { + mapping.konvaLayer.visible(entity.isEnabled); groupNeedsCache = true; } @@ -177,7 +173,7 @@ export const renderRegion = ( const compositingRect = mapping.konvaLayer.findOne(`.${COMPOSITING_RECT_NAME}`) ?? createCompositingRect(mapping.konvaLayer); - const isSelected = selectedEntityIdentifier?.id === region.id; + const isSelected = selectedEntityIdentifier?.id === entity.id; /** * When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows @@ -200,7 +196,7 @@ export const renderRegion = ( 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 - ...(!region.bboxNeedsUpdate && region.bbox ? region.bbox : getLayerBboxFast(mapping.konvaLayer)), + ...(!entity.bboxNeedsUpdate && entity.bbox ? entity.bbox : getLayerBboxFast(mapping.konvaLayer)), fill: rgbColor, opacity: globalMaskLayerOpacity, // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) @@ -240,21 +236,20 @@ export const renderRegion = ( }; export const renderRegions = ( - stage: Konva.Stage, - regionMap: EntityToKonvaMap, - regions: RegionEntity[], + map: EntityToKonvaMap, + entities: RegionEntity[], maskOpacity: number, tool: Tool, selectedEntityIdentifier: CanvasEntityIdentifier | null, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): void => { // Destroy nonexistent layers - for (const mapping of regionMap.getMappings()) { - if (!regions.find((rg) => rg.id === mapping.id)) { - regionMap.destroyMapping(mapping.id); + for (const mapping of map.getMappings()) { + if (!entities.find((rg) => rg.id === mapping.id)) { + map.destroyMapping(mapping.id); } } - for (const rg of regions) { - renderRegion(stage, regionMap, rg, maskOpacity, tool, selectedEntityIdentifier, onPosChanged); + for (const rg of entities) { + renderRegion(map, rg, maskOpacity, tool, selectedEntityIdentifier, onPosChanged); } }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts index 7912f4cc516..a458dbec54c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -54,7 +54,6 @@ import type Konva from 'konva'; import type { IRect, Vector2d } from 'konva/lib/types'; import { debounce } from 'lodash-es'; import type { RgbaColor } from 'react-colorful'; -import { getImageDTO } from 'services/api/endpoints/images'; /** * Initializes the canvas renderer. It subscribes to the redux store and listens for changes directly, bypassing the @@ -283,9 +282,9 @@ export const initializeRenderer = ( // the entire state over when needed. const debouncedUpdateBboxes = debounce(updateBboxes, 300); - const regionMap = new EntityToKonvaMap(); - const layerMap = new EntityToKonvaMap(); - const controlAdapterMap = new EntityToKonvaMap(); + const regionMap = new EntityToKonvaMap(stage); + const layerMap = new EntityToKonvaMap(stage); + const controlAdapterMap = new EntityToKonvaMap(stage); const renderCanvas = () => { const { canvasV2 } = store.getState(); @@ -304,7 +303,7 @@ export const initializeRenderer = ( canvasV2.tool.selected !== prevCanvasV2.tool.selected ) { logIfDebugging('Rendering layers'); - renderLayers(stage, layerMap, canvasV2.layers, canvasV2.tool.selected, onPosChanged); + renderLayers(layerMap, canvasV2.layers, canvasV2.tool.selected, onPosChanged); } if ( @@ -315,7 +314,6 @@ export const initializeRenderer = ( ) { logIfDebugging('Rendering regions'); renderRegions( - stage, regionMap, canvasV2.regions, canvasV2.settings.maskOpacity, @@ -327,7 +325,7 @@ export const initializeRenderer = ( if (isFirstRender || canvasV2.controlAdapters !== prevCanvasV2.controlAdapters) { logIfDebugging('Rendering control adapters'); - renderControlAdapters(stage, controlAdapterMap, canvasV2.controlAdapters, getImageDTO); + renderControlAdapters(controlAdapterMap, canvasV2.controlAdapters); } if (isFirstRender || canvasV2.document !== prevCanvasV2.document) { @@ -367,7 +365,15 @@ export const initializeRenderer = ( canvasV2.regions !== prevCanvasV2.regions ) { logIfDebugging('Arranging entities'); - arrangeEntities(stage, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions); + arrangeEntities( + stage, + layerMap, + canvasV2.layers, + controlAdapterMap, + canvasV2.controlAdapters, + regionMap, + canvasV2.regions + ); } prevCanvasV2 = canvasV2; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index ba0f3a33433..a4dd38627ea 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -1,6 +1,5 @@ import { CA_LAYER_NAME, - INITIAL_IMAGE_LAYER_NAME, INPAINT_MASK_LAYER_NAME, RASTER_LAYER_BRUSH_LINE_NAME, RASTER_LAYER_ERASER_LINE_NAME, @@ -88,7 +87,6 @@ export const mapId = (object: { id: string }): string => object.id; 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 || node.name() === INPAINT_MASK_LAYER_NAME; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 70efc6186c6..ec813fa2751 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -28,6 +28,21 @@ const initialState: CanvasV2State = { ipAdapters: [], regions: [], loras: [], + inpaintMask: { + bbox: null, + bboxNeedsUpdate: false, + fill: { + type: 'color_fill', + color: DEFAULT_RGBA_COLOR, + }, + id: 'inpaint_mask', + imageCache: null, + isEnabled: false, + maskObjects: [], + type: 'inpaint_mask', + x: 0, + y: 0, + }, tool: { selected: 'bbox', selectedBuffer: null, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts index 4d4787c9cfc..c12c93a0d09 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts @@ -18,7 +18,7 @@ import type { T2IAdapterConfig, T2IAdapterData, } from './types'; -import { buildControlAdapterProcessorV2, imageDTOToImageWithDims } from './types'; +import { buildControlAdapterProcessorV2, imageDTOToImageObject } from './types'; export const selectCA = (state: CanvasV2State, id: string) => state.controlAdapters.find((ca) => ca.id === id); export const selectCAOrThrow = (state: CanvasV2State, id: string) => { @@ -128,37 +128,43 @@ export const controlAdaptersReducers = { } moveToStart(state.controlAdapters, ca); }, - caImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => { - const { id, imageDTO } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - ca.bbox = null; - ca.bboxNeedsUpdate = true; - ca.isEnabled = true; - if (imageDTO) { - const newImage = imageDTOToImageWithDims(imageDTO); - if (isEqual(newImage, ca.image)) { + caImageChanged: { + reducer: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null; objectId: string }>) => { + const { id, imageDTO, objectId } = action.payload; + const ca = selectCA(state, id); + if (!ca) { return; } - ca.image = newImage; - ca.processedImage = null; - } else { - ca.image = null; - ca.processedImage = null; - } + ca.bbox = null; + ca.bboxNeedsUpdate = true; + ca.isEnabled = true; + if (imageDTO) { + const newImageObject = imageDTOToImageObject(id, objectId, imageDTO); + if (isEqual(newImageObject, ca.imageObject)) { + return; + } + ca.imageObject = newImageObject; + ca.processedImageObject = null; + } else { + ca.imageObject = null; + ca.processedImageObject = null; + } + }, + prepare: (payload: { id: string; imageDTO: ImageDTO | null }) => ({ payload: { ...payload, objectId: uuidv4() } }), }, - caProcessedImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => { - const { id, imageDTO } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - ca.bbox = null; - ca.bboxNeedsUpdate = true; - ca.isEnabled = true; - ca.processedImage = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; + caProcessedImageChanged: { + reducer: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null; objectId: string }>) => { + const { id, imageDTO, objectId } = action.payload; + const ca = selectCA(state, id); + if (!ca) { + return; + } + ca.bbox = null; + ca.bboxNeedsUpdate = true; + ca.isEnabled = true; + ca.processedImageObject = imageDTO ? imageDTOToImageObject(id, objectId, imageDTO) : null; + }, + prepare: (payload: { id: string; imageDTO: ImageDTO | null }) => ({ payload: { ...payload, objectId: uuidv4() } }), }, caModelChanged: ( state, @@ -182,7 +188,7 @@ export const controlAdaptersReducers = { if (candidateProcessorConfig?.type !== ca.processorConfig?.type) { // The processor has changed. For example, the previous model was a Canny model and the new model is a Depth // model. We need to use the new processor. - ca.processedImage = null; + ca.processedImageObject = null; ca.processorConfig = candidateProcessorConfig; } @@ -212,7 +218,7 @@ export const controlAdaptersReducers = { } ca.processorConfig = processorConfig; if (!processorConfig) { - ca.processedImage = null; + ca.processedImageObject = null; } }, caFilterChanged: (state, action: PayloadAction<{ id: string; filter: Filter }>) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts @@ -0,0 +1 @@ + diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskSlice.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts index 3bd5a9c47b3..bf9894d6c75 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts @@ -4,8 +4,14 @@ import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; -import type { CanvasV2State, CLIPVisionModelV2, IPAdapterConfig, IPAdapterEntity, IPMethodV2 } from './types'; -import { imageDTOToImageWithDims } from './types'; +import type { + CanvasV2State, + CLIPVisionModelV2, + IPAdapterConfig, + IPAdapterEntity, + IPMethodV2, +} from './types'; +import { imageDTOToImageObject } from './types'; export const selectIPA = (state: CanvasV2State, id: string) => state.ipAdapters.find((ipa) => ipa.id === id); export const selectIPAOrThrow = (state: CanvasV2State, id: string) => { @@ -48,13 +54,16 @@ export const ipAdaptersReducers = { ipaAllDeleted: (state) => { state.ipAdapters = []; }, - ipaImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => { - const { id, imageDTO } = action.payload; - const ipa = selectIPA(state, id); - if (!ipa) { - return; - } - ipa.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; + ipaImageChanged: { + reducer: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null; objectId: string }>) => { + const { id, imageDTO, objectId } = action.payload; + const ipa = selectIPA(state, id); + if (!ipa) { + return; + } + ipa.imageObject = imageDTO ? imageDTOToImageObject(id, objectId, imageDTO) : null; + }, + prepare: (payload: { id: string; imageDTO: ImageDTO | null }) => ({ payload: { ...payload, objectId: uuidv4() } }), }, ipaMethodChanged: (state, action: PayloadAction<{ id: string; method: IPMethodV2 }>) => { const { id, method } = action.payload; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index 2fef5abc4a6..980b6552322 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -1,6 +1,6 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; -import { getBrushLineId, getEraserLineId, getImageObjectId, getRectShapeId } from 'features/controlLayers/konva/naming'; +import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; import type { IRect } from 'konva/lib/types'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; @@ -14,7 +14,7 @@ import type { PointAddedToLineArg, RectShapeAddedArg, } from './types'; -import { isLine } from './types'; +import { imageDTOToImageObject, isLine } from './types'; export const selectLayer = (state: CanvasV2State, id: string) => state.layers.find((layer) => layer.id === id); export const selectLayerOrThrow = (state: CanvasV2State, id: string) => { @@ -73,7 +73,9 @@ export const layersReducers = { layer.bbox = bbox; layer.bboxNeedsUpdate = false; if (bbox === null) { - layer.objects = []; + // TODO(psyche): Clear objects when bbox is cleared - right now this doesn't work bc bbox calculation for layers + // doesn't work - always returns null + // layer.objects = []; } }, layerReset: (state, action: PayloadAction<{ id: string }>) => { @@ -212,24 +214,15 @@ export const layersReducers = { prepare: (payload: RectShapeAddedArg) => ({ payload: { ...payload, rectId: uuidv4() } }), }, layerImageAdded: { - reducer: (state, action: PayloadAction) => { - const { id, imageId, imageDTO } = action.payload; + reducer: (state, action: PayloadAction) => { + const { id, objectId, imageDTO } = action.payload; const layer = selectLayer(state, id); if (!layer) { return; } - const { width, height, image_name: name } = imageDTO; - layer.objects.push({ - type: 'image', - id: getImageObjectId(id, imageId), - x: 0, - y: 0, - width, - height, - image: { width, height, name }, - }); + layer.objects.push(imageDTOToImageObject(id, objectId, imageDTO)); layer.bboxNeedsUpdate = true; }, - prepare: (payload: ImageObjectAddedArg) => ({ payload: { ...payload, imageId: uuidv4() } }), + prepare: (payload: ImageObjectAddedArg) => ({ payload: { ...payload, objectId: uuidv4() } }), }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index f2009ab15ca..ff64111d8b2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -1,8 +1,12 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; -import type { CanvasV2State, CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types'; -import { imageDTOToImageWithDims } from 'features/controlLayers/store/types'; +import type { + CanvasV2State, + CLIPVisionModelV2, + IPMethodV2, +} from 'features/controlLayers/store/types'; +import { imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas'; import type { IRect } from 'konva/lib/types'; @@ -210,20 +214,25 @@ export const regionsReducers = { } rg.ipAdapters = rg.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId); }, - rgIPAdapterImageChanged: ( - state, - action: PayloadAction<{ id: string; ipAdapterId: string; imageDTO: ImageDTO | null }> - ) => { - const { id, ipAdapterId, imageDTO } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); - if (!ipa) { - return; - } - ipa.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; + rgIPAdapterImageChanged: { + reducer: ( + state, + action: PayloadAction<{ id: string; ipAdapterId: string; imageDTO: ImageDTO | null; objectId: string }> + ) => { + const { id, ipAdapterId, imageDTO, objectId } = action.payload; + const rg = selectRG(state, id); + if (!rg) { + return; + } + const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); + if (!ipa) { + return; + } + ipa.imageObject = imageDTO ? imageDTOToImageObject(id, objectId, imageDTO) : null; + }, + prepare: (payload: { id: string; ipAdapterId: string; imageDTO: ImageDTO | null }) => ({ + payload: { ...payload, objectId: uuidv4() }, + }), }, rgIPAdapterWeightChanged: (state, action: PayloadAction<{ id: string; ipAdapterId: string; weight: number }>) => { const { id, ipAdapterId, weight } = action.payload; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 0bf9f3bdfe6..5debf0da7a3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,3 +1,4 @@ +import { getImageObjectId } from 'features/controlLayers/konva/naming'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; import type { @@ -536,6 +537,9 @@ const zRectShape = z.object({ }); export type RectShape = z.infer; +const zFilter = z.enum(['LightnessToAlphaFilter']); +export type Filter = z.infer; + const zImageObject = z.object({ id: zId, type: z.literal('image'), @@ -544,6 +548,7 @@ const zImageObject = z.object({ y: z.number(), width: z.number().min(1), height: z.number().min(1), + filters: z.array(zFilter), }); export type ImageObject = z.infer; @@ -569,7 +574,7 @@ export const zIPAdapterEntity = z.object({ isEnabled: z.boolean(), weight: z.number().gte(-1).lte(2), method: zIPMethodV2, - image: zImageWithDims.nullable(), + imageObject: zImageObject.nullable(), model: zModelIdentifierField.nullable(), clipVisionModel: zCLIPVisionModelV2, beginEndStepPct: zBeginEndStepPct, @@ -577,7 +582,7 @@ export const zIPAdapterEntity = z.object({ export type IPAdapterEntity = z.infer; export type IPAdapterConfig = Pick< IPAdapterEntity, - 'weight' | 'image' | 'beginEndStepPct' | 'model' | 'clipVisionModel' | 'method' + 'weight' | 'imageObject' | 'beginEndStepPct' | 'model' | 'clipVisionModel' | 'method' >; const zMaskObject = z @@ -642,7 +647,7 @@ const zImageFill = z.object({ src: z.string(), }); const zFill = z.discriminatedUnion('type', [zColorFill, zImageFill]); -const zInpaintMaskData = z.object({ +const zInpaintMaskEntity = z.object({ id: zId, type: z.literal('inpaint_mask'), isEnabled: z.boolean(), @@ -654,10 +659,7 @@ const zInpaintMaskData = z.object({ fill: zFill, imageCache: zImageWithDims.nullable(), }); -export type InpaintMaskData = z.infer; - -const zFilter = z.enum(['none', 'LightnessToAlphaFilter']); -export type Filter = z.infer; +export type InpaintMaskEntity = z.infer; const zControlAdapterEntityBase = z.object({ id: zId, @@ -670,8 +672,8 @@ const zControlAdapterEntityBase = z.object({ opacity: zOpacity, filter: zFilter, weight: z.number().gte(-1).lte(2), - image: zImageWithDims.nullable(), - processedImage: zImageWithDims.nullable(), + imageObject: zImageObject.nullable(), + processedImageObject: zImageObject.nullable(), processorConfig: zProcessorConfig.nullable(), processorPendingBatchId: z.string().nullable().default(null), beginEndStepPct: zBeginEndStepPct, @@ -693,8 +695,8 @@ export type ControlNetConfig = Pick< ControlNetData, | 'adapterType' | 'weight' - | 'image' - | 'processedImage' + | 'imageObject' + | 'processedImageObject' | 'processorConfig' | 'beginEndStepPct' | 'model' @@ -702,7 +704,7 @@ export type ControlNetConfig = Pick< >; export type T2IAdapterConfig = Pick< T2IAdapterData, - 'adapterType' | 'weight' | 'image' | 'processedImage' | 'processorConfig' | 'beginEndStepPct' | 'model' + 'adapterType' | 'weight' | 'imageObject' | 'processedImageObject' | 'processorConfig' | 'beginEndStepPct' | 'model' >; export const initialControlNetV2: ControlNetConfig = { @@ -711,8 +713,8 @@ export const initialControlNetV2: ControlNetConfig = { weight: 1, beginEndStepPct: [0, 1], controlMode: 'balanced', - image: null, - processedImage: null, + imageObject: null, + processedImageObject: null, processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(), }; @@ -721,13 +723,13 @@ export const initialT2IAdapterV2: T2IAdapterConfig = { model: null, weight: 1, beginEndStepPct: [0, 1], - image: null, - processedImage: null, + imageObject: null, + processedImageObject: null, processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(), }; export const initialIPAdapterV2: IPAdapterConfig = { - image: null, + imageObject: null, model: null, beginEndStepPct: [0, 1], method: 'full', @@ -752,12 +754,30 @@ export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO) height, }); +export const imageDTOToImageObject = (entityId: string, objectId: string, imageDTO: ImageDTO): ImageObject => { + const { width, height, image_name } = imageDTO; + return { + id: getImageObjectId(entityId, objectId), + type: 'image', + x: 0, + y: 0, + width, + height, + filters: [], + image: { + name: image_name, + width, + height, + }, + }; +}; + const zBoundingBoxScaleMethod = z.enum(['none', 'auto', 'manual']); export type BoundingBoxScaleMethod = z.infer; export const isBoundingBoxScaleMethod = (v: unknown): v is BoundingBoxScaleMethod => zBoundingBoxScaleMethod.safeParse(v).success; -export type CanvasEntity = LayerEntity | IPAdapterEntity | ControlAdapterEntity | RegionEntity | InpaintMaskData; +export type CanvasEntity = LayerEntity | ControlAdapterEntity | RegionEntity | InpaintMaskEntity | IPAdapterEntity; export type CanvasEntityIdentifier = Pick; export type Dimensions = { @@ -775,6 +795,7 @@ export type LoRA = { export type CanvasV2State = { _version: 3; selectedEntityIdentifier: CanvasEntityIdentifier | null; + inpaintMask: InpaintMaskEntity; layers: LayerEntity[]; controlAdapters: ControlAdapterEntity[]; ipAdapters: IPAdapterEntity[]; @@ -871,3 +892,14 @@ export type ImageObjectAddedArg = { id: string; imageDTO: ImageDTO }; export const isLine = (obj: RenderableObject): obj is BrushLine | EraserLine => { return obj.type === 'brush_line' || obj.type === 'eraser_line'; }; + +/** + * A helper type to remove `[index: string]: any;` from a type. + * This is useful for some Konva types that include `[index: string]: any;` in addition to statically named + * properties, effectively widening the type signature to `Record`. For example, `LineConfig`, + * `RectConfig`, `ImageConfig`, etc all include `[index: string]: any;` in their type signature. + * TODO(psyche): Fix this upstream. + */ +export type RemoveIndexString = { + [K in keyof T as string extends K ? never : K]: T[K]; +}; diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts index 5874fe9c710..21b5177e211 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts @@ -25,7 +25,7 @@ export const getImageUsage = (nodes: NodesState, canvasV2: CanvasV2State, image_ (ca) => ca.image?.name === image_name || ca.processedImage?.name === image_name ); - const isIPAdapterImage = canvasV2.ipAdapters.some((ipa) => ipa.image?.name === image_name); + const isIPAdapterImage = canvasV2.ipAdapters.some((ipa) => ipa.imageObject?.name === image_name); const imageUsage: ImageUsage = { isLayerImage, diff --git a/invokeai/frontend/web/src/features/metadata/util/parsers.ts b/invokeai/frontend/web/src/features/metadata/util/parsers.ts index f864accd45a..5363133ff01 100644 --- a/invokeai/frontend/web/src/features/metadata/util/parsers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/parsers.ts @@ -692,7 +692,7 @@ const parseIPAdapterToIPAdapterLayer: MetadataParseFunc = async model: zModelIdentifierField.parse(ipAdapterModel), weight: typeof weight === 'number' ? weight : initialIPAdapterV2.weight, beginEndStepPct, - image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null, + imageObject: imageDTO ? imageDTOToImageWithDims(imageDTO) : null, clipVisionModel: initialIPAdapterV2.clipVisionModel, // TODO: This needs to be added to the zIPAdapterField... method: method ?? initialIPAdapterV2.method, }; diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index 00662e3b7f5..e2f1c21e8a0 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -278,10 +278,10 @@ const recallCA: MetadataRecallFunc = async (ca) => { const recallIPA: MetadataRecallFunc = async (ipa) => { const { dispatch } = getStore(); const clone = deepClone(ipa); - if (clone.image) { - const imageDTO = await getImageDTO(clone.image.name); + if (clone.imageObject) { + const imageDTO = await getImageDTO(clone.imageObject.name); if (!imageDTO) { - clone.image = null; + clone.imageObject = null; } } if (clone.model) { @@ -305,10 +305,10 @@ const recallRG: MetadataRecallFunc = async (rg) => { clone.imageCache = null; for (const ipAdapter of clone.ipAdapters) { - if (ipAdapter.image) { - const imageDTO = await getImageDTO(ipAdapter.image.name); + if (ipAdapter.imageObject) { + const imageDTO = await getImageDTO(ipAdapter.imageObject.name); if (!imageDTO) { - ipAdapter.image = null; + ipAdapter.imageObject = null; } } if (ipAdapter.model) { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts index 07f3c019ac7..c6357b6f5c6 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts @@ -34,7 +34,7 @@ export const addIPAdapterCollectorSafe = (g: Graph, denoise: Invocation<'denoise }; const addIPAdapter = (ipa: IPAdapterEntity, g: Graph, denoise: Invocation<'denoise_latents'>) => { - const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipa; + const { id, weight, model, clipVisionModel, method, beginEndStepPct, imageObject: image } = ipa; assert(image, 'IP Adapter image is required'); assert(model, 'IP Adapter model is required'); const ipAdapterCollect = addIPAdapterCollectorSafe(g, denoise); @@ -59,6 +59,6 @@ export const isValidIPAdapter = (ipa: IPAdapterEntity, base: BaseModelType): boo // Must be have a model that matches the current base and must have a control image const hasModel = Boolean(ipa.model); const modelMatchesBase = ipa.model?.base === base; - const hasImage = Boolean(ipa.image); + const hasImage = Boolean(ipa.imageObject); return hasModel && modelMatchesBase && hasImage; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts index 9792ac1fa88..958b0efca8e 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts @@ -190,7 +190,7 @@ export const addRegions = async ( for (const ipa of validRGIPAdapters) { const ipAdapterCollect = addIPAdapterCollectorSafe(g, denoise); - const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipa; + const { id, weight, model, clipVisionModel, method, beginEndStepPct, imageObject: image } = ipa; assert(model, 'IP Adapter model is required'); assert(image, 'IP Adapter image is required'); From 7aa5f5cb80beb7e56f4131c50c409aabbba9bf41 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 19 Jun 2024 21:19:20 +1000 Subject: [PATCH 095/678] feat(ui): rename konva node manager --- .../controlLayers/konva/entityToKonvaMap.ts | 112 ------------ .../controlLayers/konva/nodeManager.ts | 126 +++++++++++++ .../controlLayers/konva/renderers/arrange.ts | 14 +- .../konva/renderers/controlAdapters.ts | 42 ++--- .../controlLayers/konva/renderers/layers.ts | 62 +++---- .../controlLayers/konva/renderers/objects.ts | 172 +++++++++--------- .../controlLayers/konva/renderers/regions.ts | 92 +++++----- .../controlLayers/konva/renderers/renderer.ts | 8 +- 8 files changed, 319 insertions(+), 309 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/entityToKonvaMap.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/entityToKonvaMap.ts b/invokeai/frontend/web/src/features/controlLayers/konva/entityToKonvaMap.ts deleted file mode 100644 index 0bbe51575d9..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/entityToKonvaMap.ts +++ /dev/null @@ -1,112 +0,0 @@ -import type { BrushLine, EraserLine, ImageObject, RectShape } from 'features/controlLayers/store/types'; -import type Konva from 'konva'; - -export type BrushLineEntry = { - id: string; - type: BrushLine['type']; - konvaLine: Konva.Line; - konvaLineGroup: Konva.Group; -}; - -export type EraserLineEntry = { - id: string; - type: EraserLine['type']; - konvaLine: Konva.Line; - konvaLineGroup: Konva.Group; -}; - -export type RectShapeEntry = { - id: string; - type: RectShape['type']; - konvaRect: Konva.Rect; -}; - -export type ImageEntry = { - id: string; - type: ImageObject['type']; - konvaImageGroup: Konva.Group; - konvaPlaceholderGroup: Konva.Group; - konvaPlaceholderText: Konva.Text; - konvaImage: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately - isLoading: boolean; - isError: boolean; -}; - -type Entry = BrushLineEntry | EraserLineEntry | RectShapeEntry | ImageEntry; - -export class EntityToKonvaMap { - stage: Konva.Stage; - mappings: Record; - - constructor(stage: Konva.Stage) { - this.stage = stage; - this.mappings = {}; - } - - addMapping(id: string, konvaLayer: Konva.Layer, konvaObjectGroup: Konva.Group): EntityToKonvaMapping { - const mapping = new EntityToKonvaMapping(id, konvaLayer, konvaObjectGroup, this); - this.mappings[id] = mapping; - return mapping; - } - - getMapping(id: string): EntityToKonvaMapping | undefined { - return this.mappings[id]; - } - - getMappings(): EntityToKonvaMapping[] { - return Object.values(this.mappings); - } - - destroyMapping(id: string): void { - const mapping = this.getMapping(id); - if (!mapping) { - return; - } - mapping.konvaObjectGroup.destroy(); - delete this.mappings[id]; - } -} - -export class EntityToKonvaMapping { - id: string; - konvaLayer: Konva.Layer; - konvaObjectGroup: Konva.Group; - konvaNodeEntries: Record; - map: EntityToKonvaMap; - - constructor(id: string, konvaLayer: Konva.Layer, konvaObjectGroup: Konva.Group, map: EntityToKonvaMap) { - this.id = id; - this.konvaLayer = konvaLayer; - this.konvaObjectGroup = konvaObjectGroup; - this.konvaNodeEntries = {}; - this.map = map; - } - - addEntry(entry: T): T { - this.konvaNodeEntries[entry.id] = entry; - return entry; - } - - getEntry(id: string): T | undefined { - return this.konvaNodeEntries[id] as T | undefined; - } - - getEntries(): T[] { - return Object.values(this.konvaNodeEntries) as T[]; - } - - destroyEntry(id: string): void { - const entry = this.getEntry(id); - if (!entry) { - return; - } - if (entry.type === 'brush_line' || entry.type === 'eraser_line') { - entry.konvaLineGroup.destroy(); - } else if (entry.type === 'rect_shape') { - entry.konvaRect.destroy(); - } else if (entry.type === 'image') { - entry.konvaImageGroup.destroy(); - } - delete this.konvaNodeEntries[id]; - } -} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts new file mode 100644 index 00000000000..f96f1f98938 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts @@ -0,0 +1,126 @@ +import type { BrushLine, EraserLine, ImageObject, RectShape } from 'features/controlLayers/store/types'; +import type Konva from 'konva'; + +export type BrushLineObjectRecord = { + id: string; + type: BrushLine['type']; + konvaLine: Konva.Line; + konvaLineGroup: Konva.Group; +}; + +export type EraserLineObjectRecord = { + id: string; + type: EraserLine['type']; + konvaLine: Konva.Line; + konvaLineGroup: Konva.Group; +}; + +export type RectShapeObjectRecord = { + id: string; + type: RectShape['type']; + konvaRect: Konva.Rect; +}; + +export type ImageObjectRecord = { + id: string; + type: ImageObject['type']; + konvaImageGroup: Konva.Group; + konvaPlaceholderGroup: Konva.Group; + konvaPlaceholderRect: Konva.Rect; + konvaPlaceholderText: Konva.Text; + konvaImage: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately + isLoading: boolean; + isError: boolean; +}; + +type ObjectRecord = BrushLineObjectRecord | EraserLineObjectRecord | RectShapeObjectRecord | ImageObjectRecord; + +export class KonvaNodeManager { + stage: Konva.Stage; + adapters: Map; + + constructor(stage: Konva.Stage) { + this.stage = stage; + this.adapters = new Map(); + } + + add(id: string, konvaLayer: Konva.Layer, konvaObjectGroup: Konva.Group): EntityKonvaAdapter { + const adapter = new EntityKonvaAdapter(id, konvaLayer, konvaObjectGroup, this); + this.adapters.set(id, adapter); + return adapter; + } + + get(id: string): EntityKonvaAdapter | undefined { + return this.adapters.get(id); + } + + getAll(): EntityKonvaAdapter[] { + return Array.from(this.adapters.values()); + } + + destroy(id: string): boolean { + const adapter = this.get(id); + if (!adapter) { + return false; + } + adapter.konvaLayer.destroy(); + return this.adapters.delete(id); + } +} + +export class EntityKonvaAdapter { + id: string; + konvaLayer: Konva.Layer; // Every entity is associated with a konva layer + konvaObjectGroup: Konva.Group; // Every entity's nodes are part of an object group + objectRecords: Map; + manager: KonvaNodeManager; + + constructor(id: string, konvaLayer: Konva.Layer, konvaObjectGroup: Konva.Group, manager: KonvaNodeManager) { + this.id = id; + this.konvaLayer = konvaLayer; + this.konvaObjectGroup = konvaObjectGroup; + this.objectRecords = new Map(); + this.manager = manager; + this.konvaLayer.add(this.konvaObjectGroup); + this.manager.stage.add(this.konvaLayer); + } + + add(objectRecord: T): T { + this.objectRecords.set(objectRecord.id, objectRecord); + if (objectRecord.type === 'brush_line' || objectRecord.type === 'eraser_line') { + objectRecord.konvaLineGroup.add(objectRecord.konvaLine); + this.konvaObjectGroup.add(objectRecord.konvaLineGroup); + } else if (objectRecord.type === 'rect_shape') { + this.konvaObjectGroup.add(objectRecord.konvaRect); + } else if (objectRecord.type === 'image') { + objectRecord.konvaPlaceholderGroup.add(objectRecord.konvaPlaceholderRect); + objectRecord.konvaPlaceholderGroup.add(objectRecord.konvaPlaceholderText); + objectRecord.konvaImageGroup.add(objectRecord.konvaPlaceholderGroup); + this.konvaObjectGroup.add(objectRecord.konvaImageGroup); + } + return objectRecord; + } + + get(id: string): T | undefined { + return this.objectRecords.get(id) as T | undefined; + } + + getAll(): T[] { + return Array.from(this.objectRecords.values()) as T[]; + } + + destroy(id: string): boolean { + const record = this.get(id); + if (!record) { + return false; + } + if (record.type === 'brush_line' || record.type === 'eraser_line') { + record.konvaLineGroup.destroy(); + } else if (record.type === 'rect_shape') { + record.konvaRect.destroy(); + } else if (record.type === 'image') { + record.konvaImageGroup.destroy(); + } + return this.objectRecords.delete(id); + } +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts index 02644c1cc41..95bc6d005c7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts @@ -1,27 +1,27 @@ -import type { EntityToKonvaMap } from 'features/controlLayers/konva/entityToKonvaMap'; import { BACKGROUND_LAYER_ID, PREVIEW_LAYER_ID } from 'features/controlLayers/konva/naming'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import type { ControlAdapterEntity, LayerEntity, RegionEntity } from 'features/controlLayers/store/types'; import type Konva from 'konva'; export const arrangeEntities = ( stage: Konva.Stage, - layerMap: EntityToKonvaMap, + layerManager: KonvaNodeManager, layers: LayerEntity[], - controlAdapterMap: EntityToKonvaMap, + controlAdapterManager: KonvaNodeManager, controlAdapters: ControlAdapterEntity[], - regionMap: EntityToKonvaMap, + regionManager: KonvaNodeManager, regions: RegionEntity[] ): void => { let zIndex = 0; stage.findOne(`#${BACKGROUND_LAYER_ID}`)?.zIndex(++zIndex); for (const layer of layers) { - layerMap.getMapping(layer.id)?.konvaLayer.zIndex(++zIndex); + layerManager.get(layer.id)?.konvaLayer.zIndex(++zIndex); } for (const ca of controlAdapters) { - controlAdapterMap.getMapping(ca.id)?.konvaLayer.zIndex(++zIndex); + controlAdapterManager.get(ca.id)?.konvaLayer.zIndex(++zIndex); } for (const rg of regions) { - regionMap.getMapping(rg.id)?.konvaLayer.zIndex(++zIndex); + regionManager.get(rg.id)?.konvaLayer.zIndex(++zIndex); } stage.findOne(`#${PREVIEW_LAYER_ID}`)?.zIndex(++zIndex); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts index c466db06da8..9b9d624a2b6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts @@ -1,6 +1,6 @@ -import type { EntityToKonvaMap, EntityToKonvaMapping, ImageEntry } from 'features/controlLayers/konva/entityToKonvaMap'; import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; import { CA_LAYER_IMAGE_NAME, CA_LAYER_NAME, CA_LAYER_OBJECT_GROUP_NAME } from 'features/controlLayers/konva/naming'; +import type { EntityKonvaAdapter, ImageObjectRecord, KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import { createImageObjectGroup, createObjectGroup, @@ -21,10 +21,10 @@ import { assert } from 'tsafe'; * @param stage The konva stage * @param entity The control adapter layer state */ -const getControlAdapter = (map: EntityToKonvaMap, entity: ControlAdapterEntity): EntityToKonvaMapping => { - let mapping = map.getMapping(entity.id); - if (mapping) { - return mapping; +const getControlAdapter = (manager: KonvaNodeManager, entity: ControlAdapterEntity): EntityKonvaAdapter => { + const adapter = manager.get(entity.id); + if (adapter) { + return adapter; } const konvaLayer = new Konva.Layer({ id: entity.id, @@ -33,9 +33,9 @@ const getControlAdapter = (map: EntityToKonvaMap, entity: ControlAdapterEntity): listening: false, }); const konvaObjectGroup = createObjectGroup(konvaLayer, CA_LAYER_OBJECT_GROUP_NAME); - map.stage.add(konvaLayer); - mapping = map.addMapping(entity.id, konvaLayer, konvaObjectGroup); - return mapping; + konvaLayer.add(konvaObjectGroup); + manager.stage.add(konvaLayer); + return manager.add(entity.id, konvaLayer, konvaObjectGroup); }; /** @@ -45,26 +45,26 @@ const getControlAdapter = (map: EntityToKonvaMap, entity: ControlAdapterEntity): * @param entity The control adapter layer state * @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source */ -export const renderControlAdapter = async (map: EntityToKonvaMap, entity: ControlAdapterEntity): Promise => { - const mapping = getControlAdapter(map, entity); +export const renderControlAdapter = async (manager: KonvaNodeManager, entity: ControlAdapterEntity): Promise => { + const adapter = getControlAdapter(manager, entity); const imageObject = entity.processedImageObject ?? entity.imageObject; if (!imageObject) { // The user has deleted/reset the image - mapping.getEntries().forEach((entry) => { - mapping.destroyEntry(entry.id); + adapter.getAll().forEach((entry) => { + adapter.destroy(entry.id); }); return; } - let entry = mapping.getEntries()[0]; + let entry = adapter.getAll()[0]; const opacity = entity.opacity; const visible = entity.isEnabled; const filters = entity.filter === 'LightnessToAlphaFilter' ? [LightnessToAlphaFilter] : []; if (!entry) { entry = await createImageObjectGroup({ - mapping, + adapter: adapter, obj: imageObject, name: CA_LAYER_IMAGE_NAME, onLoad: (konvaImage) => { @@ -83,7 +83,7 @@ export const renderControlAdapter = async (map: EntityToKonvaMap, entity: Contro assert(imageSource instanceof HTMLImageElement, `Image source must be an HTMLImageElement`); if (imageSource.id !== imageObject.image.name) { updateImageSource({ - entry, + objectRecord: entry, image: imageObject.image, onLoad: (konvaImage) => { konvaImage.filters(filters); @@ -103,14 +103,14 @@ export const renderControlAdapter = async (map: EntityToKonvaMap, entity: Contro } }; -export const renderControlAdapters = (map: EntityToKonvaMap, entities: ControlAdapterEntity[]): void => { +export const renderControlAdapters = (manager: KonvaNodeManager, entities: ControlAdapterEntity[]): void => { // Destroy nonexistent layers - for (const mapping of map.getMappings()) { - if (!entities.find((ca) => ca.id === mapping.id)) { - map.destroyMapping(mapping.id); + for (const adapters of manager.getAll()) { + if (!entities.find((ca) => ca.id === adapters.id)) { + manager.destroy(adapters.id); } } - for (const ca of entities) { - renderControlAdapter(map, ca); + for (const entity of entities) { + renderControlAdapter(manager, entity); } }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts index 6c128953fd5..a3c144f744a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -1,4 +1,3 @@ -import type { EntityToKonvaMap, EntityToKonvaMapping } from 'features/controlLayers/konva/entityToKonvaMap'; import { RASTER_LAYER_BRUSH_LINE_NAME, RASTER_LAYER_ERASER_LINE_NAME, @@ -7,6 +6,7 @@ import { RASTER_LAYER_OBJECT_GROUP_NAME, RASTER_LAYER_RECT_SHAPE_NAME, } from 'features/controlLayers/konva/naming'; +import type { EntityKonvaAdapter, KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import { createImageObjectGroup, createObjectGroup, @@ -29,13 +29,13 @@ import Konva from 'konva'; * @param onPosChanged Callback for when the layer's position changes */ const getLayer = ( - map: EntityToKonvaMap, + map: KonvaNodeManager, entity: LayerEntity, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void -): EntityToKonvaMapping => { - let mapping = map.getMapping(entity.id); - if (mapping) { - return mapping; +): EntityKonvaAdapter => { + const adapter = map.get(entity.id); + if (adapter) { + return adapter; } // This layer hasn't been added to the konva state yet const konvaLayer = new Konva.Layer({ @@ -54,9 +54,7 @@ const getLayer = ( } const konvaObjectGroup = createObjectGroup(konvaLayer, RASTER_LAYER_OBJECT_GROUP_NAME); - map.stage.add(konvaLayer); - mapping = map.addMapping(entity.id, konvaLayer, konvaObjectGroup); - return mapping; + return map.add(entity.id, konvaLayer, konvaObjectGroup); }; /** @@ -67,15 +65,15 @@ const getLayer = ( * @param onPosChanged Callback for when the layer's position changes */ export const renderLayer = async ( - map: EntityToKonvaMap, + manager: KonvaNodeManager, entity: LayerEntity, tool: Tool, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ) => { - const mapping = getLayer(map, entity, onPosChanged); + const adapter = getLayer(manager, entity, onPosChanged); // Update the layer's position and listening state - mapping.konvaLayer.setAttrs({ + adapter.konvaLayer.setAttrs({ listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events x: Math.floor(entity.x), y: Math.floor(entity.y), @@ -83,35 +81,35 @@ export const renderLayer = async ( const objectIds = entity.objects.map(mapId); // Destroy any objects that are no longer in state - for (const entry of mapping.getEntries()) { - if (!objectIds.includes(entry.id)) { - mapping.destroyEntry(entry.id); + for (const objectRecord of adapter.getAll()) { + if (!objectIds.includes(objectRecord.id)) { + adapter.destroy(objectRecord.id); } } for (const obj of entity.objects) { if (obj.type === 'brush_line') { - const entry = getBrushLine(mapping, obj, RASTER_LAYER_BRUSH_LINE_NAME); + const objectRecord = getBrushLine(adapter, obj, RASTER_LAYER_BRUSH_LINE_NAME); // Only update the points if they have changed. - if (entry.konvaLine.points().length !== obj.points.length) { - entry.konvaLine.points(obj.points); + if (objectRecord.konvaLine.points().length !== obj.points.length) { + objectRecord.konvaLine.points(obj.points); } } else if (obj.type === 'eraser_line') { - const entry = getEraserLine(mapping, obj, RASTER_LAYER_ERASER_LINE_NAME); + const objectRecord = getEraserLine(adapter, obj, RASTER_LAYER_ERASER_LINE_NAME); // Only update the points if they have changed. - if (entry.konvaLine.points().length !== obj.points.length) { - entry.konvaLine.points(obj.points); + if (objectRecord.konvaLine.points().length !== obj.points.length) { + objectRecord.konvaLine.points(obj.points); } } else if (obj.type === 'rect_shape') { - getRectShape(mapping, obj, RASTER_LAYER_RECT_SHAPE_NAME); + getRectShape(adapter, obj, RASTER_LAYER_RECT_SHAPE_NAME); } else if (obj.type === 'image') { - createImageObjectGroup({ mapping, obj, name: RASTER_LAYER_IMAGE_NAME }); + createImageObjectGroup({ adapter, obj, name: RASTER_LAYER_IMAGE_NAME }); } } // Only update layer visibility if it has changed. - if (mapping.konvaLayer.visible() !== entity.isEnabled) { - mapping.konvaLayer.visible(entity.isEnabled); + if (adapter.konvaLayer.visible() !== entity.isEnabled) { + adapter.konvaLayer.visible(entity.isEnabled); } // const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer); @@ -132,22 +130,22 @@ export const renderLayer = async ( // bboxRect.visible(false); // } - mapping.konvaObjectGroup.opacity(entity.opacity); + adapter.konvaObjectGroup.opacity(entity.opacity); }; export const renderLayers = ( - map: EntityToKonvaMap, + manager: KonvaNodeManager, entities: LayerEntity[], tool: Tool, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): void => { // Destroy nonexistent layers - for (const mapping of map.getMappings()) { - if (!entities.find((l) => l.id === mapping.id)) { - map.destroyMapping(mapping.id); + for (const adapter of manager.getAll()) { + if (!entities.find((l) => l.id === adapter.id)) { + manager.destroy(adapter.id); } } - for (const layer of entities) { - renderLayer(map, layer, tool, onPosChanged); + for (const entity of entities) { + renderLayer(manager, entity, tool, onPosChanged); } }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts index 4054bc8ec74..64c39b21d59 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -1,11 +1,4 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; -import type { - BrushLineEntry, - EntityToKonvaMapping, - EraserLineEntry, - ImageEntry, - RectShapeEntry, -} from 'features/controlLayers/konva/entityToKonvaMap'; import { getLayerBboxId, getObjectGroupId, @@ -13,6 +6,13 @@ import { LAYER_BBOX_NAME, PREVIEW_GENERATION_BBOX_DUMMY_RECT, } from 'features/controlLayers/konva/naming'; +import type { + BrushLineObjectRecord, + EntityKonvaAdapter, + EraserLineObjectRecord, + ImageObjectRecord, + RectShapeObjectRecord, +} from 'features/controlLayers/konva/nodeManager'; import type { BrushLine, CanvasEntity, @@ -39,32 +39,33 @@ import { v4 as uuidv4 } from 'uuid'; * @param layerObjectGroup The konva layer's object group to add the line to * @param name The konva name for the line */ -export const getBrushLine = (mapping: EntityToKonvaMapping, brushLine: BrushLine, name: string): BrushLineEntry => { - let entry = mapping.getEntry(brushLine.id); - if (entry) { - return entry; +export const getBrushLine = ( + adapter: EntityKonvaAdapter, + brushLine: BrushLine, + name: string +): BrushLineObjectRecord => { + const objectRecord = adapter.get(brushLine.id); + if (objectRecord) { + return objectRecord; } - + const { id, strokeWidth, clip, color } = brushLine; const konvaLineGroup = new Konva.Group({ - clip: brushLine.clip, + clip, listening: false, }); const konvaLine = new Konva.Line({ - id: brushLine.id, + id, name, - strokeWidth: brushLine.strokeWidth, + strokeWidth, tension: 0, lineCap: 'round', lineJoin: 'round', shadowForStrokeEnabled: false, globalCompositeOperation: 'source-over', listening: false, - stroke: rgbaColorToString(brushLine.color), + stroke: rgbaColorToString(color), }); - konvaLineGroup.add(konvaLine); - mapping.konvaObjectGroup.add(konvaLineGroup); - entry = mapping.addEntry({ id: brushLine.id, type: 'brush_line', konvaLine, konvaLineGroup }); - return entry; + return adapter.add({ id, type: 'brush_line', konvaLine, konvaLineGroup }); }; /** @@ -73,20 +74,25 @@ export const getBrushLine = (mapping: EntityToKonvaMapping, brushLine: BrushLine * @param layerObjectGroup The konva layer's object group to add the line to * @param name The konva name for the line */ -export const getEraserLine = (mapping: EntityToKonvaMapping, eraserLine: EraserLine, name: string): EraserLineEntry => { - let entry = mapping.getEntry(eraserLine.id); - if (entry) { - return entry; +export const getEraserLine = ( + adapter: EntityKonvaAdapter, + eraserLine: EraserLine, + name: string +): EraserLineObjectRecord => { + const objectRecord = adapter.get(eraserLine.id); + if (objectRecord) { + return objectRecord; } + const { id, strokeWidth, clip } = eraserLine; const konvaLineGroup = new Konva.Group({ - clip: eraserLine.clip, + clip, listening: false, }); const konvaLine = new Konva.Line({ - id: eraserLine.id, + id, name, - strokeWidth: eraserLine.strokeWidth, + strokeWidth, tension: 0, lineCap: 'round', lineJoin: 'round', @@ -94,12 +100,8 @@ export const getEraserLine = (mapping: EntityToKonvaMapping, eraserLine: EraserL globalCompositeOperation: 'destination-out', listening: false, stroke: rgbaColorToString(DEFAULT_RGBA_COLOR), - clip: eraserLine.clip, }); - konvaLineGroup.add(konvaLine); - mapping.konvaObjectGroup.add(konvaLineGroup); - entry = mapping.addEntry({ id: eraserLine.id, type: 'eraser_line', konvaLine, konvaLineGroup }); - return entry; + return adapter.add({ id, type: 'eraser_line', konvaLine, konvaLineGroup }); }; /** @@ -108,87 +110,89 @@ export const getEraserLine = (mapping: EntityToKonvaMapping, eraserLine: EraserL * @param layerObjectGroup The konva layer's object group to add the rect to * @param name The konva name for the rect */ -export const getRectShape = (mapping: EntityToKonvaMapping, rectShape: RectShape, name: string): RectShapeEntry => { - let entry = mapping.getEntry(rectShape.id); - if (entry) { - return entry; +export const getRectShape = ( + adapter: EntityKonvaAdapter, + rectShape: RectShape, + name: string +): RectShapeObjectRecord => { + const objectRecord = adapter.get(rectShape.id); + if (objectRecord) { + return objectRecord; } + const { id, x, y, width, height } = rectShape; const konvaRect = new Konva.Rect({ - id: rectShape.id, - key: rectShape.id, + id, name, - x: rectShape.x, - y: rectShape.y, - width: rectShape.width, - height: rectShape.height, + x, + y, + width, + height, listening: false, fill: rgbaColorToString(rectShape.color), }); - mapping.konvaObjectGroup.add(konvaRect); - entry = mapping.addEntry({ id: rectShape.id, type: 'rect_shape', konvaRect }); - return entry; + return adapter.add({ id: rectShape.id, type: 'rect_shape', konvaRect }); }; export const updateImageSource = async (arg: { - entry: ImageEntry; + objectRecord: ImageObjectRecord; image: ImageWithDims; getImageDTO?: (imageName: string) => Promise; onLoading?: () => void; onLoad?: (konvaImage: Konva.Image) => void; onError?: () => void; }) => { - const { entry, image, getImageDTO = defaultGetImageDTO, onLoading, onLoad, onError } = arg; + const { objectRecord, image, getImageDTO = defaultGetImageDTO, onLoading, onLoad, onError } = arg; try { - entry.isLoading = true; - if (!entry.konvaImage) { - entry.konvaPlaceholderGroup.visible(true); - entry.konvaPlaceholderText.text(t('common.loadingImage', 'Loading Image')); + objectRecord.isLoading = true; + if (!objectRecord.konvaImage) { + objectRecord.konvaPlaceholderGroup.visible(true); + objectRecord.konvaPlaceholderText.text(t('common.loadingImage', 'Loading Image')); } onLoading?.(); const imageDTO = await getImageDTO(image.name); if (!imageDTO) { - entry.isLoading = false; - entry.isError = true; - entry.konvaPlaceholderGroup.visible(true); - entry.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load')); + objectRecord.isLoading = false; + objectRecord.isError = true; + objectRecord.konvaPlaceholderGroup.visible(true); + objectRecord.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load')); onError?.(); return; } const imageEl = new Image(); imageEl.onload = () => { - if (entry.konvaImage) { - entry.konvaImage.setAttrs({ + if (objectRecord.konvaImage) { + objectRecord.konvaImage.setAttrs({ image: imageEl, }); } else { - entry.konvaImage = new Konva.Image({ - id: entry.id, + objectRecord.konvaImage = new Konva.Image({ + id: objectRecord.id, listening: false, image: imageEl, }); - entry.konvaImageGroup.add(entry.konvaImage); + objectRecord.konvaImageGroup.add(objectRecord.konvaImage); } - entry.isLoading = false; - entry.isError = false; - entry.konvaPlaceholderGroup.visible(false); - onLoad?.(entry.konvaImage); + objectRecord.isLoading = false; + objectRecord.isError = false; + objectRecord.konvaPlaceholderGroup.visible(false); + onLoad?.(objectRecord.konvaImage); }; imageEl.onerror = () => { - entry.isLoading = false; - entry.isError = true; - entry.konvaPlaceholderGroup.visible(true); - entry.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load')); + objectRecord.isLoading = false; + objectRecord.isError = true; + objectRecord.konvaPlaceholderGroup.visible(true); + objectRecord.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load')); onError?.(); }; imageEl.id = image.name; imageEl.src = imageDTO.image_url; } catch { - entry.isLoading = false; - entry.isError = true; - entry.konvaPlaceholderGroup.visible(true); - entry.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load')); + objectRecord.isLoading = false; + objectRecord.isError = true; + objectRecord.konvaPlaceholderGroup.visible(true); + objectRecord.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load')); onError?.(); } }; @@ -199,18 +203,18 @@ export const updateImageSource = async (arg: { * @returns The konva group for the image placeholder, and callbacks to handle loading and error states */ export const createImageObjectGroup = (arg: { - mapping: EntityToKonvaMapping; + adapter: EntityKonvaAdapter; obj: ImageObject; name: string; getImageDTO?: (imageName: string) => Promise; onLoad?: (konvaImage: Konva.Image) => void; onLoading?: () => void; onError?: () => void; -}): ImageEntry => { - const { mapping, obj, name, getImageDTO = defaultGetImageDTO, onLoad, onLoading, onError } = arg; - let entry = mapping.getEntry(obj.id); - if (entry) { - return entry; +}): ImageObjectRecord => { + const { adapter, obj, name, getImageDTO = defaultGetImageDTO, onLoad, onLoading, onError } = arg; + let objectRecord = adapter.get(obj.id); + if (objectRecord) { + return objectRecord; } const { id, image } = obj; const { width, height } = obj; @@ -234,23 +238,19 @@ export const createImageObjectGroup = (arg: { text: t('common.loadingImage', 'Loading Image'), listening: false, }); - konvaPlaceholderGroup.add(konvaPlaceholderRect); - konvaPlaceholderGroup.add(konvaPlaceholderText); - konvaImageGroup.add(konvaPlaceholderGroup); - mapping.konvaObjectGroup.add(konvaImageGroup); - - entry = mapping.addEntry({ + objectRecord = adapter.add({ id, type: 'image', konvaImageGroup, konvaPlaceholderGroup, + konvaPlaceholderRect, konvaPlaceholderText, konvaImage: null, isLoading: false, isError: false, }); - updateImageSource({ entry, image, getImageDTO, onLoad, onLoading, onError }); - return entry; + updateImageSource({ objectRecord, image, getImageDTO, onLoad, onLoading, onError }); + return objectRecord; }; /** diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts index 5119b2356da..7e5f4b748a8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts @@ -1,5 +1,4 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; -import type { EntityToKonvaMap, EntityToKonvaMapping } from 'features/controlLayers/konva/entityToKonvaMap'; import { COMPOSITING_RECT_NAME, RG_LAYER_BRUSH_LINE_NAME, @@ -8,6 +7,7 @@ import { RG_LAYER_OBJECT_GROUP_NAME, RG_LAYER_RECT_SHAPE_NAME, } from 'features/controlLayers/konva/naming'; +import type { EntityKonvaAdapter, KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import { getLayerBboxFast } from 'features/controlLayers/konva/renderers/bbox'; import { createObjectGroup, @@ -49,13 +49,13 @@ const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => { * @param onLayerPosChanged Callback for when the layer's position changes */ const getRegion = ( - map: EntityToKonvaMap, + manager: KonvaNodeManager, entity: RegionEntity, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void -): EntityToKonvaMapping => { - let mapping = map.getMapping(entity.id); - if (mapping) { - return mapping; +): EntityKonvaAdapter => { + const adapter = manager.get(entity.id); + if (adapter) { + return adapter; } // This layer hasn't been added to the konva state yet const konvaLayer = new Konva.Layer({ @@ -74,9 +74,7 @@ const getRegion = ( } const konvaObjectGroup = createObjectGroup(konvaLayer, RG_LAYER_OBJECT_GROUP_NAME); - map.stage.add(konvaLayer); - mapping = map.addMapping(entity.id, konvaLayer, konvaObjectGroup); - return mapping; + return manager.add(entity.id, konvaLayer, konvaObjectGroup); }; /** @@ -88,17 +86,17 @@ const getRegion = ( * @param onPosChanged Callback for when the layer's position changes */ export const renderRegion = ( - map: EntityToKonvaMap, + manager: KonvaNodeManager, entity: RegionEntity, globalMaskLayerOpacity: number, tool: Tool, selectedEntityIdentifier: CanvasEntityIdentifier | null, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): void => { - const mapping = getRegion(map, entity, onPosChanged); + const adapter = getRegion(manager, entity, onPosChanged); // Update the layer's position and listening state - mapping.konvaLayer.setAttrs({ + adapter.konvaLayer.setAttrs({ listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events x: Math.floor(entity.x), y: Math.floor(entity.y), @@ -112,67 +110,67 @@ export const renderRegion = ( const objectIds = entity.objects.map(mapId); // Destroy any objects that are no longer in state - for (const entry of mapping.getEntries()) { - if (!objectIds.includes(entry.id)) { - mapping.destroyEntry(entry.id); + for (const objectRecord of adapter.getAll()) { + if (!objectIds.includes(objectRecord.id)) { + adapter.destroy(objectRecord.id); groupNeedsCache = true; } } for (const obj of entity.objects) { if (obj.type === 'brush_line') { - const entry = getBrushLine(mapping, obj, RG_LAYER_BRUSH_LINE_NAME); + const objectRecord = getBrushLine(adapter, obj, RG_LAYER_BRUSH_LINE_NAME); // Only update the points if they have changed. The point values are never mutated, they are only added to the // array, so checking the length is sufficient to determine if we need to re-cache. - if (entry.konvaLine.points().length !== obj.points.length) { - entry.konvaLine.points(obj.points); + if (objectRecord.konvaLine.points().length !== obj.points.length) { + objectRecord.konvaLine.points(obj.points); groupNeedsCache = true; } // Only update the color if it has changed. - if (entry.konvaLine.stroke() !== rgbColor) { - entry.konvaLine.stroke(rgbColor); + if (objectRecord.konvaLine.stroke() !== rgbColor) { + objectRecord.konvaLine.stroke(rgbColor); groupNeedsCache = true; } } else if (obj.type === 'eraser_line') { - const entry = getEraserLine(mapping, obj, RG_LAYER_ERASER_LINE_NAME); + const objectRecord = getEraserLine(adapter, obj, RG_LAYER_ERASER_LINE_NAME); // Only update the points if they have changed. The point values are never mutated, they are only added to the // array, so checking the length is sufficient to determine if we need to re-cache. - if (entry.konvaLine.points().length !== obj.points.length) { - entry.konvaLine.points(obj.points); + if (objectRecord.konvaLine.points().length !== obj.points.length) { + objectRecord.konvaLine.points(obj.points); groupNeedsCache = true; } // Only update the color if it has changed. - if (entry.konvaLine.stroke() !== rgbColor) { - entry.konvaLine.stroke(rgbColor); + if (objectRecord.konvaLine.stroke() !== rgbColor) { + objectRecord.konvaLine.stroke(rgbColor); groupNeedsCache = true; } } else if (obj.type === 'rect_shape') { - const entry = getRectShape(mapping, obj, RG_LAYER_RECT_SHAPE_NAME); + const objectRecord = getRectShape(adapter, obj, RG_LAYER_RECT_SHAPE_NAME); // Only update the color if it has changed. - if (entry.konvaRect.fill() !== rgbColor) { - entry.konvaRect.fill(rgbColor); + if (objectRecord.konvaRect.fill() !== rgbColor) { + objectRecord.konvaRect.fill(rgbColor); groupNeedsCache = true; } } } // Only update layer visibility if it has changed. - if (mapping.konvaLayer.visible() !== entity.isEnabled) { - mapping.konvaLayer.visible(entity.isEnabled); + if (adapter.konvaLayer.visible() !== entity.isEnabled) { + adapter.konvaLayer.visible(entity.isEnabled); groupNeedsCache = true; } - if (mapping.konvaObjectGroup.getChildren().length === 0) { + if (adapter.konvaObjectGroup.getChildren().length === 0) { // No objects - clear the cache to reset the previous pixel data - mapping.konvaObjectGroup.clearCache(); + adapter.konvaObjectGroup.clearCache(); return; } const compositingRect = - mapping.konvaLayer.findOne(`.${COMPOSITING_RECT_NAME}`) ?? createCompositingRect(mapping.konvaLayer); + adapter.konvaLayer.findOne(`.${COMPOSITING_RECT_NAME}`) ?? createCompositingRect(adapter.konvaLayer); const isSelected = selectedEntityIdentifier?.id === entity.id; /** @@ -188,32 +186,32 @@ export const renderRegion = ( */ if (isSelected && tool !== 'move') { // We must clear the cache first so Konva will re-draw the group with the new compositing rect - if (mapping.konvaObjectGroup.isCached()) { - mapping.konvaObjectGroup.clearCache(); + if (adapter.konvaObjectGroup.isCached()) { + adapter.konvaObjectGroup.clearCache(); } // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work - mapping.konvaObjectGroup.opacity(1); + adapter.konvaObjectGroup.opacity(1); compositingRect.setAttrs({ // The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already - ...(!entity.bboxNeedsUpdate && entity.bbox ? entity.bbox : getLayerBboxFast(mapping.konvaLayer)), + ...(!entity.bboxNeedsUpdate && entity.bbox ? entity.bbox : getLayerBboxFast(adapter.konvaLayer)), fill: rgbColor, opacity: globalMaskLayerOpacity, // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) globalCompositeOperation: 'source-in', visible: true, // This rect must always be on top of all other shapes - zIndex: mapping.konvaObjectGroup.getChildren().length, + zIndex: adapter.konvaObjectGroup.getChildren().length, }); } else { // The compositing rect should only be shown when the layer is selected. compositingRect.visible(false); // Cache only if needed - or if we are on this code path and _don't_ have a cache - if (groupNeedsCache || !mapping.konvaObjectGroup.isCached()) { - mapping.konvaObjectGroup.cache(); + if (groupNeedsCache || !adapter.konvaObjectGroup.isCached()) { + adapter.konvaObjectGroup.cache(); } // Updating group opacity does not require re-caching - mapping.konvaObjectGroup.opacity(globalMaskLayerOpacity); + adapter.konvaObjectGroup.opacity(globalMaskLayerOpacity); } // const bboxRect = @@ -236,7 +234,7 @@ export const renderRegion = ( }; export const renderRegions = ( - map: EntityToKonvaMap, + manager: KonvaNodeManager, entities: RegionEntity[], maskOpacity: number, tool: Tool, @@ -244,12 +242,12 @@ export const renderRegions = ( onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): void => { // Destroy nonexistent layers - for (const mapping of map.getMappings()) { - if (!entities.find((rg) => rg.id === mapping.id)) { - map.destroyMapping(mapping.id); + for (const adapter of manager.getAll()) { + if (!entities.find((rg) => rg.id === adapter.id)) { + manager.destroy(adapter.id); } } - for (const rg of entities) { - renderRegion(map, rg, maskOpacity, tool, selectedEntityIdentifier, onPosChanged); + for (const entity of entities) { + renderRegion(manager, entity, maskOpacity, tool, selectedEntityIdentifier, onPosChanged); } }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts index a458dbec54c..4411a373949 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -3,7 +3,7 @@ import type { Store } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import { $isDebugging } from 'app/store/nanostores/isDebugging'; import type { RootState } from 'app/store/store'; -import { EntityToKonvaMap } from 'features/controlLayers/konva/entityToKonvaMap'; +import { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import { setStageEventHandlers } from 'features/controlLayers/konva/events'; import { arrangeEntities } from 'features/controlLayers/konva/renderers/arrange'; import { renderBackgroundLayer } from 'features/controlLayers/konva/renderers/background'; @@ -282,9 +282,9 @@ export const initializeRenderer = ( // the entire state over when needed. const debouncedUpdateBboxes = debounce(updateBboxes, 300); - const regionMap = new EntityToKonvaMap(stage); - const layerMap = new EntityToKonvaMap(stage); - const controlAdapterMap = new EntityToKonvaMap(stage); + const regionMap = new KonvaNodeManager(stage); + const layerMap = new KonvaNodeManager(stage); + const controlAdapterMap = new KonvaNodeManager(stage); const renderCanvas = () => { const { canvasV2 } = store.getState(); From 460ea1aa076d247f3d7fcd54d9df68b1e05fb49e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 20 Jun 2024 10:41:11 +1000 Subject: [PATCH 096/678] feat(ui): add toggle for clipToBbox --- .../ControlLayersSettingsPopover.tsx | 11 ++- .../features/controlLayers/konva/events.ts | 85 +++++++++++-------- .../konva/renderers/controlAdapters.ts | 2 - .../controlLayers/konva/renderers/renderer.ts | 4 +- .../controlLayers/store/canvasV2Slice.ts | 1 + .../controlLayers/store/settingsReducers.ts | 3 + 6 files changed, 66 insertions(+), 40 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx index 4b4a9842a28..afaafe69f29 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx @@ -11,7 +11,7 @@ import { } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { MaskOpacity } from 'features/controlLayers/components/MaskOpacity'; -import { invertScrollChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { clipToBboxChanged, invertScrollChanged } from 'features/controlLayers/store/canvasV2Slice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -20,11 +20,16 @@ import { RiSettings4Fill } from 'react-icons/ri'; const ControlLayersSettingsPopover = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); + const clipToBbox = useAppSelector((s) => s.canvasV2.settings.clipToBbox); const invertScroll = useAppSelector((s) => s.canvasV2.tool.invertScroll); const onChangeInvertScroll = useCallback( (e: ChangeEvent) => dispatch(invertScrollChanged(e.target.checked)), [dispatch] ); + const onChangeClipToBbox = useCallback( + (e: ChangeEvent) => dispatch(clipToBboxChanged(e.target.checked)), + [dispatch] + ); return ( @@ -38,6 +43,10 @@ const ControlLayersSettingsPopover = () => { {t('unifiedCanvas.invertBrushSizeScrollDirection')} + + {t('unifiedCanvas.clipToBbox')} + + diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 0892d3da095..d3e7d54a92d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -53,6 +53,7 @@ type Arg = { setSpaceKey: (val: boolean) => void; getDocument: () => CanvasV2State['document']; getBbox: () => CanvasV2State['bbox']; + getSettings: () => CanvasV2State['settings']; onBrushLineAdded: (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => void; onEraserLineAdded: (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => void; onPointAddedToLine: (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => void; @@ -155,6 +156,7 @@ export const setStageEventHandlers = ({ setSpaceKey, getDocument, getBbox, + getSettings, onBrushLineAdded, onEraserLineAdded, onPointAddedToLine, @@ -203,6 +205,17 @@ export const setStageEventHandlers = ({ if (toolState.selected === 'brush') { const bbox = getBbox(); + const settings = getSettings(); + + const clip = settings.clipToBbox + ? { + x: bbox.x, + y: bbox.y, + width: bbox.width, + height: bbox.height, + } + : null; + if (e.evt.shiftKey) { const lastAddedPoint = getLastAddedPoint(); // Create a straight line if holding shift @@ -218,12 +231,7 @@ export const setStageEventHandlers = ({ ], color: getCurrentFill(), width: toolState.brush.width, - clip: { - x: bbox.x, - y: bbox.y, - width: bbox.width, - height: bbox.height, - }, + clip, }, selectedEntity.type ); @@ -240,12 +248,7 @@ export const setStageEventHandlers = ({ ], color: getCurrentFill(), width: toolState.brush.width, - clip: { - x: bbox.x, - y: bbox.y, - width: bbox.width, - height: bbox.height, - }, + clip, }, selectedEntity.type ); @@ -255,6 +258,16 @@ export const setStageEventHandlers = ({ if (toolState.selected === 'eraser') { const bbox = getBbox(); + const settings = getSettings(); + + const clip = settings.clipToBbox + ? { + x: bbox.x, + y: bbox.y, + width: bbox.width, + height: bbox.height, + } + : null; if (e.evt.shiftKey) { // Create a straight line if holding shift const lastAddedPoint = getLastAddedPoint(); @@ -269,12 +282,7 @@ export const setStageEventHandlers = ({ pos.y - selectedEntity.y, ], width: toolState.eraser.width, - clip: { - x: bbox.x, - y: bbox.y, - width: bbox.width, - height: bbox.height, - }, + clip, }, selectedEntity.type ); @@ -290,12 +298,7 @@ export const setStageEventHandlers = ({ pos.y - selectedEntity.y, ], width: toolState.eraser.width, - clip: { - x: bbox.x, - y: bbox.y, - width: bbox.width, - height: bbox.height, - }, + clip, }, selectedEntity.type ); @@ -402,6 +405,16 @@ export const setStageEventHandlers = ({ ); } else { const bbox = getBbox(); + const settings = getSettings(); + + const clip = settings.clipToBbox + ? { + x: bbox.x, + y: bbox.y, + width: bbox.width, + height: bbox.height, + } + : null; // Start a new line onBrushLineAdded( { @@ -414,12 +427,7 @@ export const setStageEventHandlers = ({ ], width: toolState.brush.width, color: getCurrentFill(), - clip: { - x: bbox.x, - y: bbox.y, - width: bbox.width, - height: bbox.height, - }, + clip, }, selectedEntity.type ); @@ -441,6 +449,16 @@ export const setStageEventHandlers = ({ ); } else { const bbox = getBbox(); + const settings = getSettings(); + + const clip = settings.clipToBbox + ? { + x: bbox.x, + y: bbox.y, + width: bbox.width, + height: bbox.height, + } + : null; // Start a new line onEraserLineAdded( { @@ -452,12 +470,7 @@ export const setStageEventHandlers = ({ pos.y - selectedEntity.y, ], width: toolState.eraser.width, - clip: { - x: bbox.x, - y: bbox.y, - width: bbox.width, - height: bbox.height, - }, + clip, }, selectedEntity.type ); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts index 9b9d624a2b6..6c8d908be6c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts @@ -33,8 +33,6 @@ const getControlAdapter = (manager: KonvaNodeManager, entity: ControlAdapterEnti listening: false, }); const konvaObjectGroup = createObjectGroup(konvaLayer, CA_LAYER_OBJECT_GROUP_NAME); - konvaLayer.add(konvaObjectGroup); - manager.stage.add(konvaLayer); return manager.add(entity.id, konvaLayer, konvaObjectGroup); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts index 4411a373949..7c04a0a89bb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -3,8 +3,8 @@ import type { Store } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import { $isDebugging } from 'app/store/nanostores/isDebugging'; import type { RootState } from 'app/store/store'; -import { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import { setStageEventHandlers } from 'features/controlLayers/konva/events'; +import { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import { arrangeEntities } from 'features/controlLayers/konva/renderers/arrange'; import { renderBackgroundLayer } from 'features/controlLayers/konva/renderers/background'; import { updateBboxes } from 'features/controlLayers/konva/renderers/bbox'; @@ -209,6 +209,7 @@ export const initializeRenderer = ( const getBbox = () => getState().canvasV2.bbox; const getDocument = () => getState().canvasV2.document; const getToolState = () => getState().canvasV2.tool; + const getSettings = () => getState().canvasV2.settings; // Read-write state, ephemeral interaction state let isDrawing = false; @@ -268,6 +269,7 @@ export const initializeRenderer = ( setStageAttrs: $stageAttrs.set, getDocument, getBbox, + getSettings, onBrushLineAdded, onEraserLineAdded, onPointAddedToLine, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index ec813fa2751..fd48383b86c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -184,6 +184,7 @@ export const { allEntitiesDeleted, scaledBboxChanged, bboxScaleMethodChanged, + clipToBboxChanged, // layers layerAdded, layerRecalled, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/settingsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/settingsReducers.ts index d3b7dd40d9e..d9f9a8d3e7c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/settingsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/settingsReducers.ts @@ -5,4 +5,7 @@ export const settingsReducers = { maskOpacityChanged: (state, action: PayloadAction) => { state.settings.maskOpacity = action.payload; }, + clipToBboxChanged: (state, action: PayloadAction) => { + state.settings.clipToBbox = action.payload; + }, } satisfies SliceCaseReducers; From 4f7bf5ad583c3b700db4d1f8e80ffa19108ba380 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 20 Jun 2024 11:12:28 +1000 Subject: [PATCH 097/678] fix(ui): fix generation graphs --- .../src/common/hooks/useIsReadyToEnqueue.ts | 4 ++-- .../graph/generation/addControlAdapters.ts | 18 +++++++++++++----- .../util/graph/generation/addIPAdapters.ts | 6 +++--- .../nodes/util/graph/generation/addRegions.ts | 9 +++++---- .../generation/buildGenerationTabSDXLGraph.ts | 2 +- 5 files changed, 24 insertions(+), 15 deletions(-) diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index 56b94451755..c10a9af0067 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -141,9 +141,9 @@ const createSelector = (templates: Templates) => problems.push(i18n.t('parameters.invoke.layer.controlAdapterIncompatibleBaseModel')); } // Must have a control image OR, if it has a processor, it must have a processed image - if (!ca.image) { + if (!ca.imageObject) { problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoImageSelected')); - } else if (ca.processorConfig && !ca.processedImage) { + } else if (ca.processorConfig && !ca.processedImageObject) { problems.push(i18n.t('parameters.invoke.layer.controlAdapterImageNotProcessed')); } // T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts index 3cd1a727f87..3759a0822b4 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts @@ -46,9 +46,13 @@ const addControlNetCollectorSafe = (g: Graph, denoise: Invocation<'denoise_laten }; const addControlNetToGraph = (ca: ControlNetData, g: Graph, denoise: Invocation<'denoise_latents'>) => { - const { id, beginEndStepPct, controlMode, image, model, processedImage, processorConfig, weight } = ca; + const { id, beginEndStepPct, controlMode, imageObject, model, processedImageObject, processorConfig, weight } = ca; assert(model, 'ControlNet model is required'); - const controlImage = buildControlImage(image, processedImage, processorConfig); + const controlImage = buildControlImage( + imageObject?.image ?? null, + processedImageObject?.image ?? null, + processorConfig + ); const controlNetCollect = addControlNetCollectorSafe(g, denoise); const controlNet = g.addNode({ @@ -84,9 +88,13 @@ const addT2IAdapterCollectorSafe = (g: Graph, denoise: Invocation<'denoise_laten }; const addT2IAdapterToGraph = (ca: T2IAdapterData, g: Graph, denoise: Invocation<'denoise_latents'>) => { - const { id, beginEndStepPct, image, model, processedImage, processorConfig, weight } = ca; + const { id, beginEndStepPct, imageObject, model, processedImageObject, processorConfig, weight } = ca; assert(model, 'T2I Adapter model is required'); - const controlImage = buildControlImage(image, processedImage, processorConfig); + const controlImage = buildControlImage( + imageObject?.image ?? null, + processedImageObject?.image ?? null, + processorConfig + ); const t2iAdapterCollect = addT2IAdapterCollectorSafe(g, denoise); const t2iAdapter = g.addNode({ @@ -126,6 +134,6 @@ const isValidControlAdapter = (ca: ControlAdapterEntity, base: BaseModelType): b // Must be have a model that matches the current base and must have a control image const hasModel = Boolean(ca.model); const modelMatchesBase = ca.model?.base === base; - const hasControlImage = Boolean(ca.image || (ca.processedImage && ca.processorConfig)); + const hasControlImage = Boolean(ca.imageObject || (ca.processedImageObject && ca.processorConfig)); return hasModel && modelMatchesBase && hasControlImage; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts index c6357b6f5c6..2eb501fad92 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts @@ -34,8 +34,8 @@ export const addIPAdapterCollectorSafe = (g: Graph, denoise: Invocation<'denoise }; const addIPAdapter = (ipa: IPAdapterEntity, g: Graph, denoise: Invocation<'denoise_latents'>) => { - const { id, weight, model, clipVisionModel, method, beginEndStepPct, imageObject: image } = ipa; - assert(image, 'IP Adapter image is required'); + const { id, weight, model, clipVisionModel, method, beginEndStepPct, imageObject } = ipa; + assert(imageObject, 'IP Adapter image is required'); assert(model, 'IP Adapter model is required'); const ipAdapterCollect = addIPAdapterCollectorSafe(g, denoise); @@ -49,7 +49,7 @@ const addIPAdapter = (ipa: IPAdapterEntity, g: Graph, denoise: Invocation<'denoi begin_step_percent: beginEndStepPct[0], end_step_percent: beginEndStepPct[1], image: { - image_name: image.name, + image_name: imageObject.image.name, }, }); g.addEdge(ipAdapter, 'ip_adapter', ipAdapterCollect, 'item'); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts index 958b0efca8e..c20fcd56d6d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts @@ -2,6 +2,7 @@ import { getStore } from 'app/store/nanostores/store'; import { deepClone } from 'common/util/deepClone'; import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; import { RG_LAYER_NAME } from 'features/controlLayers/konva/naming'; +import { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import { renderRegions } from 'features/controlLayers/konva/renderers/regions'; import { blobToDataURL } from 'features/controlLayers/konva/util'; import { rgMaskImageUploaded } from 'features/controlLayers/store/canvasV2Slice'; @@ -190,9 +191,9 @@ export const addRegions = async ( for (const ipa of validRGIPAdapters) { const ipAdapterCollect = addIPAdapterCollectorSafe(g, denoise); - const { id, weight, model, clipVisionModel, method, beginEndStepPct, imageObject: image } = ipa; + const { id, weight, model, clipVisionModel, method, beginEndStepPct, imageObject } = ipa; assert(model, 'IP Adapter model is required'); - assert(image, 'IP Adapter image is required'); + assert(imageObject, 'IP Adapter image is required'); const ipAdapter = g.addNode({ id: `ip_adapter_${id}`, @@ -204,7 +205,7 @@ export const addRegions = async ( begin_step_percent: beginEndStepPct[0], end_step_percent: beginEndStepPct[1], image: { - image_name: image.name, + image_name: imageObject.image.name, }, }); @@ -260,7 +261,7 @@ export const getRGMaskBlobs = async ( ): Promise> => { const container = document.createElement('div'); const stage = new Konva.Stage({ container, ...documentSize }); - renderRegions(stage, regions, 1, 'brush', null); + renderRegions(new KonvaNodeManager(stage), regions, 1, 'brush', null); const konvaLayers = stage.find(`.${RG_LAYER_NAME}`); const blobs: Record = {}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabSDXLGraph.ts index f68eb920519..250cd333634 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabSDXLGraph.ts @@ -43,7 +43,7 @@ export const buildGenerationTabSDXLGraph = async (state: RootState): Promise Date: Thu, 20 Jun 2024 11:23:03 +1000 Subject: [PATCH 098/678] feat(ui): persist bbox --- .../web/src/features/controlLayers/store/canvasV2Slice.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index fd48383b86c..89f26e224f8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -335,5 +335,5 @@ export const canvasV2PersistConfig: PersistConfig = { name: canvasV2Slice.name, initialState, migrate, - persistDenylist: ['bbox'], + persistDenylist: [], }; From 55260a886de5201c216dc72173d3bb77066f0d3d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 20 Jun 2024 11:24:04 +1000 Subject: [PATCH 099/678] feat(ui): use node manager for addRegions --- .../nodes/util/graph/generation/addRegions.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts index c20fcd56d6d..7d47ac33311 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts @@ -1,7 +1,6 @@ import { getStore } from 'app/store/nanostores/store'; import { deepClone } from 'common/util/deepClone'; import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; -import { RG_LAYER_NAME } from 'features/controlLayers/konva/naming'; import { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import { renderRegions } from 'features/controlLayers/konva/renderers/regions'; import { blobToDataURL } from 'features/controlLayers/konva/util'; @@ -261,22 +260,23 @@ export const getRGMaskBlobs = async ( ): Promise> => { const container = document.createElement('div'); const stage = new Konva.Stage({ container, ...documentSize }); - renderRegions(new KonvaNodeManager(stage), regions, 1, 'brush', null); - const konvaLayers = stage.find(`.${RG_LAYER_NAME}`); + const manager = new KonvaNodeManager(stage); + renderRegions(manager, regions, 1, 'brush', null); + const adapters = manager.getAll(); const blobs: Record = {}; // First remove all layers - for (const layer of konvaLayers) { - layer.remove(); + for (const adapter of adapters) { + adapter.konvaLayer.remove(); } // Next render each layer to a blob - for (const layer of konvaLayers) { - const rg = regions.find((l) => l.id === layer.id()); - if (!rg) { + for (const adapter of adapters) { + const region = regions.find((l) => l.id === adapter.id); + if (!region) { continue; } - stage.add(layer); + stage.add(adapter.konvaLayer); const blob = await new Promise((resolve) => { stage.toBlob({ callback: (blob) => { @@ -292,12 +292,12 @@ export const getRGMaskBlobs = async ( openBase64ImageInTab([ { base64, - caption: `${rg.id}: ${rg.positivePrompt} / ${rg.negativePrompt}`, + caption: `${region.id}: ${region.positivePrompt} / ${region.negativePrompt}`, }, ]); } - layer.remove(); - blobs[layer.id()] = blob; + adapter.konvaLayer.remove(); + blobs[adapter.id] = blob; } return blobs; From 475b1cb1b8b0b8925349dfe18dcf342f98cc305f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:09:13 +1000 Subject: [PATCH 100/678] refactor(ui): node manager handles more tedious annoying stuff --- .../features/controlLayers/konva/events.ts | 71 ++---- .../controlLayers/konva/nodeManager.ts | 95 ++++++- .../controlLayers/konva/renderers/arrange.ts | 17 +- .../konva/renderers/background.ts | 26 +- .../konva/renderers/controlAdapters.ts | 8 +- .../controlLayers/konva/renderers/layers.ts | 12 +- .../controlLayers/konva/renderers/objects.ts | 10 +- .../controlLayers/konva/renderers/preview.ts | 234 +++++++----------- .../controlLayers/konva/renderers/regions.ts | 8 +- .../controlLayers/konva/renderers/renderer.ts | 46 +--- 10 files changed, 247 insertions(+), 280 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index d3e7d54a92d..3745046f201 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -1,3 +1,4 @@ +import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import { renderBackgroundLayer } from 'features/controlLayers/konva/renderers/background'; import { renderDocumentBoundsOverlay, @@ -32,7 +33,7 @@ import { import { PREVIEW_TOOL_GROUP_ID } from './naming'; type Arg = { - stage: Konva.Stage; + manager: KonvaNodeManager; getToolState: () => CanvasV2State['tool']; getCurrentFill: () => RgbaColor; setTool: (tool: Tool) => void; @@ -135,7 +136,7 @@ const maybeAddNextPoint = ( }; export const setStageEventHandlers = ({ - stage, + manager, getToolState, getCurrentFill, setTool, @@ -164,16 +165,14 @@ export const setStageEventHandlers = ({ onBrushWidthChanged: onBrushSizeChanged, onEraserWidthChanged: onEraserSizeChanged, }: Arg): (() => void) => { + const stage = manager.stage; + //#region mouseenter - stage.on('mouseenter', (e) => { - const stage = e.target.getStage(); - if (!stage) { - return; - } + stage.on('mouseenter', () => { const tool = getToolState().selected; stage.findOne(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); renderToolPreview( - stage, + manager, getToolState(), getCurrentFill(), getSelectedEntity(), @@ -186,10 +185,6 @@ export const setStageEventHandlers = ({ //#region mousedown stage.on('mousedown', (e) => { - const stage = e.target.getStage(); - if (!stage) { - return; - } setIsMouseDown(true); const toolState = getToolState(); const pos = updateLastCursorPos(stage, setLastCursorPos); @@ -307,7 +302,7 @@ export const setStageEventHandlers = ({ } } renderToolPreview( - stage, + manager, getToolState(), getCurrentFill(), getSelectedEntity(), @@ -319,11 +314,7 @@ export const setStageEventHandlers = ({ }); //#region mouseup - stage.on('mouseup', (e) => { - const stage = e.target.getStage(); - if (!stage) { - return; - } + stage.on('mouseup', () => { setIsMouseDown(false); const pos = getLastCursorPos(); const selectedEntity = getSelectedEntity(); @@ -360,7 +351,7 @@ export const setStageEventHandlers = ({ } renderToolPreview( - stage, + manager, getToolState(), getCurrentFill(), getSelectedEntity(), @@ -372,11 +363,7 @@ export const setStageEventHandlers = ({ }); //#region mousemove - stage.on('mousemove', (e) => { - const stage = e.target.getStage(); - if (!stage) { - return; - } + stage.on('mousemove', () => { const toolState = getToolState(); const pos = updateLastCursorPos(stage, setLastCursorPos); const selectedEntity = getSelectedEntity(); @@ -481,7 +468,7 @@ export const setStageEventHandlers = ({ } renderToolPreview( - stage, + manager, getToolState(), getCurrentFill(), getSelectedEntity(), @@ -493,11 +480,7 @@ export const setStageEventHandlers = ({ }); //#region mouseleave - stage.on('mouseleave', (e) => { - const stage = e.target.getStage(); - if (!stage) { - return; - } + stage.on('mouseleave', () => { const pos = updateLastCursorPos(stage, setLastCursorPos); setIsDrawing(false); setLastCursorPos(null); @@ -525,7 +508,7 @@ export const setStageEventHandlers = ({ } renderToolPreview( - stage, + manager, getToolState(), getCurrentFill(), getSelectedEntity(), @@ -574,13 +557,13 @@ export const setStageEventHandlers = ({ stage.scaleY(newScale); stage.position(newPos); setStageAttrs({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale }); - renderBackgroundLayer(stage); - scaleToolPreview(stage, getToolState()); - renderDocumentBoundsOverlay(stage, getDocument); + renderBackgroundLayer(manager); + scaleToolPreview(manager, getToolState()); + renderDocumentBoundsOverlay(manager, getDocument); } } renderToolPreview( - stage, + manager, getToolState(), getCurrentFill(), getSelectedEntity(), @@ -600,10 +583,10 @@ export const setStageEventHandlers = ({ height: stage.height(), scale: stage.scaleX(), }); - renderBackgroundLayer(stage); - renderDocumentBoundsOverlay(stage, getDocument); + renderBackgroundLayer(manager); + renderDocumentBoundsOverlay(manager, getDocument); renderToolPreview( - stage, + manager, getToolState(), getCurrentFill(), getSelectedEntity(), @@ -625,7 +608,7 @@ export const setStageEventHandlers = ({ scale: stage.scaleX(), }); renderToolPreview( - stage, + manager, getToolState(), getCurrentFill(), getSelectedEntity(), @@ -656,12 +639,12 @@ export const setStageEventHandlers = ({ } else if (e.key === 'r') { const stageAttrs = fitDocumentToStage(stage, getDocument()); setStageAttrs(stageAttrs); - scaleToolPreview(stage, getToolState()); - renderBackgroundLayer(stage); - renderDocumentBoundsOverlay(stage, getDocument); + scaleToolPreview(manager, getToolState()); + renderBackgroundLayer(manager); + renderDocumentBoundsOverlay(manager, getDocument); } renderToolPreview( - stage, + manager, getToolState(), getCurrentFill(), getSelectedEntity(), @@ -688,7 +671,7 @@ export const setStageEventHandlers = ({ setSpaceKey(false); } renderToolPreview( - stage, + manager, getToolState(), getCurrentFill(), getSelectedEntity(), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts index f96f1f98938..d1cba7bd710 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts @@ -1,4 +1,19 @@ -import type { BrushLine, EraserLine, ImageObject, RectShape } from 'features/controlLayers/store/types'; +import { createBackgroundLayer } from 'features/controlLayers/konva/renderers/background'; +import { + createBboxPreview, + createDocumentOverlay, + createPreviewLayer, + createToolPreview, +} from 'features/controlLayers/konva/renderers/preview'; +import type { + BrushLine, + CanvasEntity, + CanvasV2State, + EraserLine, + ImageObject, + Rect, + RectShape, +} from 'features/controlLayers/store/types'; import type Konva from 'konva'; export type BrushLineObjectRecord = { @@ -37,25 +52,77 @@ type ObjectRecord = BrushLineObjectRecord | EraserLineObjectRecord | RectShapeOb export class KonvaNodeManager { stage: Konva.Stage; - adapters: Map; - - constructor(stage: Konva.Stage) { + adapters: Map; + background: { layer: Konva.Layer }; + preview: { + layer: Konva.Layer; + bbox: { + group: Konva.Group; + rect: Konva.Rect; + transformer: Konva.Transformer; + }; + tool: { + group: Konva.Group; + brush: { + group: Konva.Group; + fill: Konva.Circle; + innerBorder: Konva.Circle; + outerBorder: Konva.Circle; + }; + rect: { + rect: Konva.Rect; + }; + }; + documentOverlay: { + group: Konva.Group; + innerRect: Konva.Rect; + outerRect: Konva.Rect; + }; + }; + + constructor( + stage: Konva.Stage, + getBbox: () => CanvasV2State['bbox'], + onBboxTransformed: (bbox: Rect) => void, + getShiftKey: () => boolean, + getCtrlKey: () => boolean, + getMetaKey: () => boolean, + getAltKey: () => boolean + ) { this.stage = stage; this.adapters = new Map(); + + this.background = { layer: createBackgroundLayer() }; + this.stage.add(this.background.layer); + + this.preview = { + layer: createPreviewLayer(), + bbox: createBboxPreview(stage, getBbox, onBboxTransformed, getShiftKey, getCtrlKey, getMetaKey, getAltKey), + tool: createToolPreview(stage), + documentOverlay: createDocumentOverlay(), + }; + this.preview.layer.add(this.preview.bbox.group); + this.preview.layer.add(this.preview.tool.group); + this.preview.layer.add(this.preview.documentOverlay.group); + this.stage.add(this.preview.layer); } - add(id: string, konvaLayer: Konva.Layer, konvaObjectGroup: Konva.Group): EntityKonvaAdapter { - const adapter = new EntityKonvaAdapter(id, konvaLayer, konvaObjectGroup, this); - this.adapters.set(id, adapter); + add(entity: CanvasEntity, konvaLayer: Konva.Layer, konvaObjectGroup: Konva.Group): KonvaEntityAdapter { + const adapter = new KonvaEntityAdapter(entity, konvaLayer, konvaObjectGroup, this); + this.adapters.set(adapter.id, adapter); return adapter; } - get(id: string): EntityKonvaAdapter | undefined { + get(id: string): KonvaEntityAdapter | undefined { return this.adapters.get(id); } - getAll(): EntityKonvaAdapter[] { - return Array.from(this.adapters.values()); + getAll(type?: CanvasEntity['type']): KonvaEntityAdapter[] { + if (type) { + return Array.from(this.adapters.values()).filter((adapter) => adapter.entityType === type); + } else { + return Array.from(this.adapters.values()); + } } destroy(id: string): boolean { @@ -68,15 +135,17 @@ export class KonvaNodeManager { } } -export class EntityKonvaAdapter { +export class KonvaEntityAdapter { id: string; + entityType: CanvasEntity['type']; konvaLayer: Konva.Layer; // Every entity is associated with a konva layer konvaObjectGroup: Konva.Group; // Every entity's nodes are part of an object group objectRecords: Map; manager: KonvaNodeManager; - constructor(id: string, konvaLayer: Konva.Layer, konvaObjectGroup: Konva.Group, manager: KonvaNodeManager) { - this.id = id; + constructor(entity: CanvasEntity, konvaLayer: Konva.Layer, konvaObjectGroup: Konva.Group, manager: KonvaNodeManager) { + this.id = entity.id; + this.entityType = entity.type; this.konvaLayer = konvaLayer; this.konvaObjectGroup = konvaObjectGroup; this.objectRecords = new Map(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts index 95bc6d005c7..324d6ab5a53 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts @@ -1,27 +1,22 @@ -import { BACKGROUND_LAYER_ID, PREVIEW_LAYER_ID } from 'features/controlLayers/konva/naming'; import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import type { ControlAdapterEntity, LayerEntity, RegionEntity } from 'features/controlLayers/store/types'; -import type Konva from 'konva'; export const arrangeEntities = ( - stage: Konva.Stage, - layerManager: KonvaNodeManager, + manager: KonvaNodeManager, layers: LayerEntity[], - controlAdapterManager: KonvaNodeManager, controlAdapters: ControlAdapterEntity[], - regionManager: KonvaNodeManager, regions: RegionEntity[] ): void => { let zIndex = 0; - stage.findOne(`#${BACKGROUND_LAYER_ID}`)?.zIndex(++zIndex); + manager.background.layer.zIndex(++zIndex); for (const layer of layers) { - layerManager.get(layer.id)?.konvaLayer.zIndex(++zIndex); + manager.get(layer.id)?.konvaLayer.zIndex(++zIndex); } for (const ca of controlAdapters) { - controlAdapterManager.get(ca.id)?.konvaLayer.zIndex(++zIndex); + manager.get(ca.id)?.konvaLayer.zIndex(++zIndex); } for (const rg of regions) { - regionManager.get(rg.id)?.konvaLayer.zIndex(++zIndex); + manager.get(rg.id)?.konvaLayer.zIndex(++zIndex); } - stage.findOne(`#${PREVIEW_LAYER_ID}`)?.zIndex(++zIndex); + manager.preview.layer.zIndex(++zIndex); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts index 0f5c4ceaa54..9fff013070e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts @@ -1,5 +1,6 @@ import { getArbitraryBaseColor } from '@invoke-ai/ui-library'; import { BACKGROUND_LAYER_ID } from 'features/controlLayers/konva/naming'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import Konva from 'konva'; const baseGridLineColor = getArbitraryBaseColor(27); @@ -24,26 +25,17 @@ const getGridSpacing = (scale: number): number => { return 256; }; -export const getBackgroundLayer = (stage: Konva.Stage): Konva.Layer => { - let background = stage.findOne(`#${BACKGROUND_LAYER_ID}`); - if (background) { - return background; - } - - background = new Konva.Layer({ id: BACKGROUND_LAYER_ID }); - stage.add(background); - return background; -}; +export const createBackgroundLayer = (): Konva.Layer => new Konva.Layer({ id: BACKGROUND_LAYER_ID, listening: false }); -export const renderBackgroundLayer = (stage: Konva.Stage): void => { - const background = getBackgroundLayer(stage); +export const renderBackgroundLayer = (manager: KonvaNodeManager): void => { + const background = manager.background.layer; background.zIndex(0); - const scale = stage.scaleX(); + const scale = manager.stage.scaleX(); const gridSpacing = getGridSpacing(scale); - const x = stage.x(); - const y = stage.y(); - const width = stage.width(); - const height = stage.height(); + const x = manager.stage.x(); + const y = manager.stage.y(); + const width = manager.stage.width(); + const height = manager.stage.height(); const stageRect = { x1: 0, y1: 0, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts index 6c8d908be6c..952567a74d3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts @@ -1,6 +1,6 @@ import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; import { CA_LAYER_IMAGE_NAME, CA_LAYER_NAME, CA_LAYER_OBJECT_GROUP_NAME } from 'features/controlLayers/konva/naming'; -import type { EntityKonvaAdapter, ImageObjectRecord, KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import type { ImageObjectRecord, KonvaEntityAdapter, KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import { createImageObjectGroup, createObjectGroup, @@ -21,7 +21,7 @@ import { assert } from 'tsafe'; * @param stage The konva stage * @param entity The control adapter layer state */ -const getControlAdapter = (manager: KonvaNodeManager, entity: ControlAdapterEntity): EntityKonvaAdapter => { +const getControlAdapter = (manager: KonvaNodeManager, entity: ControlAdapterEntity): KonvaEntityAdapter => { const adapter = manager.get(entity.id); if (adapter) { return adapter; @@ -33,7 +33,7 @@ const getControlAdapter = (manager: KonvaNodeManager, entity: ControlAdapterEnti listening: false, }); const konvaObjectGroup = createObjectGroup(konvaLayer, CA_LAYER_OBJECT_GROUP_NAME); - return manager.add(entity.id, konvaLayer, konvaObjectGroup); + return manager.add(entity, konvaLayer, konvaObjectGroup); }; /** @@ -103,7 +103,7 @@ export const renderControlAdapter = async (manager: KonvaNodeManager, entity: Co export const renderControlAdapters = (manager: KonvaNodeManager, entities: ControlAdapterEntity[]): void => { // Destroy nonexistent layers - for (const adapters of manager.getAll()) { + for (const adapters of manager.getAll('control_adapter')) { if (!entities.find((ca) => ca.id === adapters.id)) { manager.destroy(adapters.id); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts index a3c144f744a..475692a5999 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -6,7 +6,7 @@ import { RASTER_LAYER_OBJECT_GROUP_NAME, RASTER_LAYER_RECT_SHAPE_NAME, } from 'features/controlLayers/konva/naming'; -import type { EntityKonvaAdapter, KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import type { KonvaEntityAdapter, KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import { createImageObjectGroup, createObjectGroup, @@ -29,11 +29,11 @@ import Konva from 'konva'; * @param onPosChanged Callback for when the layer's position changes */ const getLayer = ( - map: KonvaNodeManager, + manager: KonvaNodeManager, entity: LayerEntity, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void -): EntityKonvaAdapter => { - const adapter = map.get(entity.id); +): KonvaEntityAdapter => { + const adapter = manager.get(entity.id); if (adapter) { return adapter; } @@ -54,7 +54,7 @@ const getLayer = ( } const konvaObjectGroup = createObjectGroup(konvaLayer, RASTER_LAYER_OBJECT_GROUP_NAME); - return map.add(entity.id, konvaLayer, konvaObjectGroup); + return manager.add(entity, konvaLayer, konvaObjectGroup); }; /** @@ -140,7 +140,7 @@ export const renderLayers = ( onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): void => { // Destroy nonexistent layers - for (const adapter of manager.getAll()) { + for (const adapter of manager.getAll('layer')) { if (!entities.find((l) => l.id === adapter.id)) { manager.destroy(adapter.id); } 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 64c39b21d59..d1f46db6ce1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -8,7 +8,7 @@ import { } from 'features/controlLayers/konva/naming'; import type { BrushLineObjectRecord, - EntityKonvaAdapter, + KonvaEntityAdapter, EraserLineObjectRecord, ImageObjectRecord, RectShapeObjectRecord, @@ -40,7 +40,7 @@ import { v4 as uuidv4 } from 'uuid'; * @param name The konva name for the line */ export const getBrushLine = ( - adapter: EntityKonvaAdapter, + adapter: KonvaEntityAdapter, brushLine: BrushLine, name: string ): BrushLineObjectRecord => { @@ -75,7 +75,7 @@ export const getBrushLine = ( * @param name The konva name for the line */ export const getEraserLine = ( - adapter: EntityKonvaAdapter, + adapter: KonvaEntityAdapter, eraserLine: EraserLine, name: string ): EraserLineObjectRecord => { @@ -111,7 +111,7 @@ export const getEraserLine = ( * @param name The konva name for the rect */ export const getRectShape = ( - adapter: EntityKonvaAdapter, + adapter: KonvaEntityAdapter, rectShape: RectShape, name: string ): RectShapeObjectRecord => { @@ -203,7 +203,7 @@ export const updateImageSource = async (arg: { * @returns The konva group for the image placeholder, and callbacks to handle loading and error states */ export const createImageObjectGroup = (arg: { - adapter: EntityKonvaAdapter; + adapter: KonvaEntityAdapter; obj: ImageObject; name: string; getImageDTO?: (imageName: string) => Promise; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts index 7416c3db201..c0b49ddbe07 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts @@ -18,29 +18,15 @@ import { PREVIEW_RECT_ID, PREVIEW_TOOL_GROUP_ID, } from 'features/controlLayers/konva/naming'; -import { selectRenderableLayers } from 'features/controlLayers/konva/util'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import type { CanvasEntity, CanvasV2State, RgbaColor, Tool } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect, Vector2d } from 'konva/lib/types'; import { atom } from 'nanostores'; -import { assert } from 'tsafe'; -/** - * Creates the singleton preview layer and all its objects. - * @param stage The konva stage - */ -const getPreviewLayer = (stage: Konva.Stage): Konva.Layer => { - let previewLayer = stage.findOne(`#${PREVIEW_LAYER_ID}`); - if (previewLayer) { - return previewLayer; - } - // Initialize the preview layer & add to the stage - previewLayer = new Konva.Layer({ id: PREVIEW_LAYER_ID, listening: true }); - stage.add(previewLayer); - return previewLayer; -}; +export const createPreviewLayer = (): Konva.Layer => new Konva.Layer({ id: PREVIEW_LAYER_ID, listening: true }); -export const getBboxPreviewGroup = ( +export const createBboxPreview = ( stage: Konva.Stage, getBbox: () => IRect, onBboxTransformed: (bbox: IRect) => void, @@ -48,14 +34,7 @@ export const getBboxPreviewGroup = ( getCtrlKey: () => boolean, getMetaKey: () => boolean, getAltKey: () => boolean -): Konva.Group => { - const previewLayer = getPreviewLayer(stage); - let bboxPreviewGroup = previewLayer.findOne(`#${PREVIEW_GENERATION_BBOX_GROUP}`); - - if (bboxPreviewGroup) { - return bboxPreviewGroup; - } - +): { group: Konva.Group; rect: Konva.Rect; transformer: Konva.Transformer } => { // Create a stash to hold onto the last aspect ratio of the bbox - this allows for locking the aspect ratio when // transforming the bbox. const bbox = getBbox(); @@ -63,28 +42,28 @@ export const getBboxPreviewGroup = ( // Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully // transparent rect for this purpose. - bboxPreviewGroup = new Konva.Group({ id: PREVIEW_GENERATION_BBOX_GROUP, listening: false }); - const bboxRect = new Konva.Rect({ + const group = new Konva.Group({ id: PREVIEW_GENERATION_BBOX_GROUP, listening: false }); + const rect = new Konva.Rect({ id: PREVIEW_GENERATION_BBOX_DUMMY_RECT, listening: false, strokeEnabled: false, draggable: true, ...getBbox(), }); - bboxRect.on('dragmove', () => { + rect.on('dragmove', () => { const gridSize = getCtrlKey() || getMetaKey() ? 8 : 64; const oldBbox = getBbox(); const newBbox: IRect = { ...oldBbox, - x: roundToMultiple(bboxRect.x(), gridSize), - y: roundToMultiple(bboxRect.y(), gridSize), + x: roundToMultiple(rect.x(), gridSize), + y: roundToMultiple(rect.y(), gridSize), }; - bboxRect.setAttrs(newBbox); + rect.setAttrs(newBbox); if (oldBbox.x !== newBbox.x || oldBbox.y !== newBbox.y) { onBboxTransformed(newBbox); } }); - const bboxTransformer = new Konva.Transformer({ + const transformer = new Konva.Transformer({ id: PREVIEW_GENERATION_BBOX_TRANSFORMER, borderDash: [5, 5], borderStroke: 'rgba(212,216,234,1)', @@ -136,11 +115,11 @@ export const getBboxPreviewGroup = ( }, }); - bboxTransformer.on('transform', () => { + transformer.on('transform', () => { // In the transform callback, we calculate the bbox's new dims and pos and update the konva object. // Some special handling is needed depending on the anchor being dragged. - const anchor = bboxTransformer.getActiveAnchor(); + const anchor = transformer.getActiveAnchor(); if (!anchor) { // Pretty sure we should always have an anchor here? return; @@ -163,14 +142,14 @@ export const getBboxPreviewGroup = ( } // The coords should be correct per the anchorDragBoundFunc. - let x = bboxRect.x(); - let y = bboxRect.y(); + let x = rect.x(); + let y = rect.y(); // Konva transforms by scaling the dims, not directly changing width and height. At this point, the width and height // *have not changed*, only the scale has changed. To get the final height, we need to scale the dims and then snap // them to the grid. - let width = roundToMultipleMin(bboxRect.width() * bboxRect.scaleX(), gridSize); - let height = roundToMultipleMin(bboxRect.height() * bboxRect.scaleY(), gridSize); + let width = roundToMultipleMin(rect.width() * rect.scaleX(), gridSize); + let height = roundToMultipleMin(rect.height() * rect.scaleY(), gridSize); // If shift is held and we are resizing from a corner, retain aspect ratio - needs special handling. We skip this // if alt/opt is held - this requires math too big for my brain. @@ -210,7 +189,7 @@ export const getBboxPreviewGroup = ( // Update the bboxRect's attrs directly with the new transform, and reset its scale to 1. // TODO(psyche): In `renderBboxPreview()` we also call setAttrs, need to do it twice to ensure it renders correctly. // Gotta be a way to avoid setting it twice... - bboxRect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1 }); + rect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1 }); // Update the bbox in internal state. onBboxTransformed(bbox); @@ -222,18 +201,17 @@ export const getBboxPreviewGroup = ( } }); - bboxTransformer.on('transformend', () => { + transformer.on('transformend', () => { // Always update the aspect ratio buffer when the transform ends, so if the next transform starts with shift held, // we have the correct aspect ratio to start from. - $aspectRatioBuffer.set(bboxRect.width() / bboxRect.height()); + $aspectRatioBuffer.set(rect.width() / rect.height()); }); // The transformer will always be transforming the dummy rect - bboxTransformer.nodes([bboxRect]); - bboxPreviewGroup.add(bboxRect); - bboxPreviewGroup.add(bboxTransformer); - previewLayer.add(bboxPreviewGroup); - return bboxPreviewGroup; + transformer.nodes([rect]); + group.add(rect); + group.add(transformer); + return { group, rect, transformer }; }; const ALL_ANCHORS: string[] = [ @@ -249,79 +227,66 @@ const ALL_ANCHORS: string[] = [ const CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right']; const NO_ANCHORS: string[] = []; -export const renderBboxPreview = ( - stage: Konva.Stage, - bbox: IRect, - tool: Tool, - getBbox: () => CanvasV2State['bbox'], - onBboxTransformed: (bbox: IRect) => void, - getShiftKey: () => boolean, - getCtrlKey: () => boolean, - getMetaKey: () => boolean, - getAltKey: () => boolean -): void => { - const bboxGroup = getBboxPreviewGroup( - stage, - getBbox, - onBboxTransformed, - getShiftKey, - getCtrlKey, - getMetaKey, - getAltKey - ); - const bboxRect = bboxGroup.findOne(`#${PREVIEW_GENERATION_BBOX_DUMMY_RECT}`); - const bboxTransformer = bboxGroup.findOne(`#${PREVIEW_GENERATION_BBOX_TRANSFORMER}`); - bboxGroup.listening(tool === 'bbox'); +export const renderBboxPreview = (manager: KonvaNodeManager, bbox: IRect, tool: Tool): void => { + manager.preview.bbox.group.listening(tool === 'bbox'); // This updates the bbox during transformation - bboxRect?.setAttrs({ ...bbox, scaleX: 1, scaleY: 1, listening: tool === 'bbox' }); - bboxTransformer?.setAttrs({ listening: tool === 'bbox', enabledAnchors: tool === 'bbox' ? ALL_ANCHORS : NO_ANCHORS }); + manager.preview.bbox.rect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1, listening: tool === 'bbox' }); + manager.preview.bbox.transformer.setAttrs({ + listening: tool === 'bbox', + enabledAnchors: tool === 'bbox' ? ALL_ANCHORS : NO_ANCHORS, + }); }; -export const getToolPreviewGroup = (stage: Konva.Stage): Konva.Group => { - const previewLayer = getPreviewLayer(stage); - let toolPreviewGroup = previewLayer.findOne(`#${PREVIEW_TOOL_GROUP_ID}`); - if (toolPreviewGroup) { - return toolPreviewGroup; - } +export const createToolPreview = (stage: Konva.Stage): KonvaNodeManager['preview']['tool'] => { const scale = stage.scaleX(); - toolPreviewGroup = new Konva.Group({ id: PREVIEW_TOOL_GROUP_ID }); + const group = new Konva.Group({ id: PREVIEW_TOOL_GROUP_ID }); // Create the brush preview group & circles - const brushPreviewGroup = new Konva.Group({ id: PREVIEW_BRUSH_GROUP_ID }); - const brushPreviewFill = new Konva.Circle({ + const brushGroup = new Konva.Group({ id: PREVIEW_BRUSH_GROUP_ID }); + const brushFill = new Konva.Circle({ id: PREVIEW_BRUSH_FILL_ID, listening: false, strokeEnabled: false, }); - brushPreviewGroup.add(brushPreviewFill); - const brushPreviewBorderInner = new Konva.Circle({ + brushGroup.add(brushFill); + const brushBorderInner = new Konva.Circle({ id: PREVIEW_BRUSH_BORDER_INNER_ID, listening: false, stroke: BRUSH_BORDER_INNER_COLOR, strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, strokeEnabled: true, }); - brushPreviewGroup.add(brushPreviewBorderInner); - const brushPreviewBorderOuter = new Konva.Circle({ + brushGroup.add(brushBorderInner); + const brushBorderOuter = new Konva.Circle({ id: PREVIEW_BRUSH_BORDER_OUTER_ID, listening: false, stroke: BRUSH_BORDER_OUTER_COLOR, strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, strokeEnabled: true, }); - brushPreviewGroup.add(brushPreviewBorderOuter); + brushGroup.add(brushBorderOuter); // 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({ + const rect = new Konva.Rect({ id: PREVIEW_RECT_ID, listening: false, strokeEnabled: false, }); - toolPreviewGroup.add(rectPreview); - toolPreviewGroup.add(brushPreviewGroup); - previewLayer.add(toolPreviewGroup); - return toolPreviewGroup; + group.add(rect); + group.add(brushGroup); + return { + group, + brush: { + group: brushGroup, + fill: brushFill, + innerBorder: brushBorderInner, + outerBorder: brushBorderOuter, + }, + rect: { + rect, + }, + }; }; /** @@ -336,7 +301,7 @@ export const getToolPreviewGroup = (stage: Konva.Stage): Konva.Group => { * @param brushSize The brush size */ export const renderToolPreview = ( - stage: Konva.Stage, + manager: KonvaNodeManager, toolState: CanvasV2State['tool'], currentFill: RgbaColor, selectedEntity: CanvasEntity | null, @@ -345,7 +310,8 @@ export const renderToolPreview = ( isDrawing: boolean, isMouseDown: boolean ): void => { - const layerCount = stage.find(selectRenderableLayers).length; + const stage = manager.stage; + const layerCount = manager.adapters.size; const tool = toolState.selected; // Update the stage's pointer style if (tool === 'view') { @@ -372,31 +338,22 @@ export const renderToolPreview = ( stage.draggable(tool === 'view'); - const toolPreviewGroup = getToolPreviewGroup(stage); - if ( !cursorPos || layerCount === 0 || (selectedEntity?.type !== 'regional_guidance' && selectedEntity?.type !== 'layer') ) { // We can bail early if the mouse isn't over the stage or there are no layers - toolPreviewGroup.visible(false); + manager.preview.tool.group.visible(false); } else { - toolPreviewGroup.visible(true); - - const brushPreviewGroup = stage.findOne(`#${PREVIEW_BRUSH_GROUP_ID}`); - assert(brushPreviewGroup, 'Brush preview group not found'); - - const rectPreview = stage.findOne(`#${PREVIEW_RECT_ID}`); - assert(rectPreview, 'Rect preview not found'); + manager.preview.tool.group.visible(true); // No need to render the brush preview if the cursor position or color is missing if (cursorPos && (tool === 'brush' || tool === 'eraser')) { const scale = stage.scaleX(); // Update the fill circle - const brushPreviewFill = brushPreviewGroup.findOne(`#${PREVIEW_BRUSH_FILL_ID}`); const radius = (tool === 'brush' ? toolState.brush.width : toolState.eraser.width) / 2; - brushPreviewFill?.setAttrs({ + manager.preview.tool.brush.fill.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius, @@ -405,83 +362,74 @@ export const renderToolPreview = ( }); // Update the inner border of the brush preview - const brushPreviewInner = brushPreviewGroup.findOne(`#${PREVIEW_BRUSH_BORDER_INNER_ID}`); - brushPreviewInner?.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius }); + manager.preview.tool.brush.innerBorder.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius }); // Update the outer border of the brush preview - const brushPreviewOuter = brushPreviewGroup.findOne(`#${PREVIEW_BRUSH_BORDER_OUTER_ID}`); - brushPreviewOuter?.setAttrs({ + manager.preview.tool.brush.outerBorder.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, }); - scaleToolPreview(stage, toolState); + scaleToolPreview(manager, toolState); - brushPreviewGroup.visible(true); + manager.preview.tool.brush.group.visible(true); } else { - brushPreviewGroup.visible(false); + manager.preview.tool.brush.group.visible(false); } if (cursorPos && lastMouseDownPos && tool === 'rect') { - const rectPreview = toolPreviewGroup.findOne(`#${PREVIEW_RECT_ID}`); - rectPreview?.setAttrs({ + manager.preview.tool.rect.rect.setAttrs({ x: Math.min(cursorPos.x, lastMouseDownPos.x), y: Math.min(cursorPos.y, lastMouseDownPos.y), width: Math.abs(cursorPos.x - lastMouseDownPos.x), height: Math.abs(cursorPos.y - lastMouseDownPos.y), fill: rgbaColorToString(currentFill), + visible: true, }); - rectPreview?.visible(true); } else { - rectPreview?.visible(false); + manager.preview.tool.rect.rect.visible(false); } } }; -export const scaleToolPreview = (stage: Konva.Stage, toolState: CanvasV2State['tool']): void => { - const scale = stage.scaleX(); +export const scaleToolPreview = (manager: KonvaNodeManager, toolState: CanvasV2State['tool']): void => { + const scale = manager.stage.scaleX(); const radius = (toolState.selected === 'brush' ? toolState.brush.width : toolState.eraser.width) / 2; - const brushPreviewGroup = stage.findOne(`#${PREVIEW_BRUSH_GROUP_ID}`); - brushPreviewGroup - ?.findOne(`#${PREVIEW_BRUSH_BORDER_INNER_ID}`) - ?.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale); - brushPreviewGroup - ?.findOne(`#${PREVIEW_BRUSH_BORDER_OUTER_ID}`) - ?.setAttrs({ strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale }); + manager.preview.tool.brush.innerBorder.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale); + manager.preview.tool.brush.outerBorder.setAttrs({ + strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, + radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, + }); }; -const getDocumentOverlayGroup = (stage: Konva.Stage): Konva.Group => { - const previewLayer = getPreviewLayer(stage); - let documentOverlayGroup = previewLayer.findOne('#document_overlay_group'); - if (documentOverlayGroup) { - return documentOverlayGroup; - } - - documentOverlayGroup = new Konva.Group({ id: 'document_overlay_group', listening: false }); - const documentOverlayOuterRect = new Konva.Rect({ +export const createDocumentOverlay = (): KonvaNodeManager['preview']['documentOverlay'] => { + const group = new Konva.Group({ id: 'document_overlay_group', listening: false }); + const outerRect = new Konva.Rect({ id: 'document_overlay_outer_rect', listening: false, fill: getArbitraryBaseColor(10), opacity: 0.7, }); - const documentOverlayInnerRect = new Konva.Rect({ + const innerRect = new Konva.Rect({ id: 'document_overlay_inner_rect', listening: false, fill: 'white', globalCompositeOperation: 'destination-out', }); - documentOverlayGroup.add(documentOverlayOuterRect); - documentOverlayGroup.add(documentOverlayInnerRect); - previewLayer.add(documentOverlayGroup); - return documentOverlayGroup; + group.add(outerRect); + group.add(innerRect); + return { group, innerRect, outerRect }; }; -export const renderDocumentBoundsOverlay = (stage: Konva.Stage, getDocument: () => CanvasV2State['document']): void => { +export const renderDocumentBoundsOverlay = ( + manager: KonvaNodeManager, + getDocument: () => CanvasV2State['document'] +): void => { const document = getDocument(); - const documentOverlayGroup = getDocumentOverlayGroup(stage); + const stage = manager.stage; - documentOverlayGroup.zIndex(0); + manager.preview.documentOverlay.group.zIndex(0); const x = stage.x(); const y = stage.y(); @@ -489,14 +437,14 @@ export const renderDocumentBoundsOverlay = (stage: Konva.Stage, getDocument: () const height = stage.height(); const scale = stage.scaleX(); - documentOverlayGroup.findOne('#document_overlay_outer_rect')?.setAttrs({ + manager.preview.documentOverlay.outerRect.setAttrs({ offsetX: x / scale, offsetY: y / scale, width: width / scale, height: height / scale, }); - documentOverlayGroup.findOne('#document_overlay_inner_rect')?.setAttrs({ + manager.preview.documentOverlay.innerRect.setAttrs({ x: 0, y: 0, width: document.width, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts index 7e5f4b748a8..ae830055e25 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts @@ -7,7 +7,7 @@ import { RG_LAYER_OBJECT_GROUP_NAME, RG_LAYER_RECT_SHAPE_NAME, } from 'features/controlLayers/konva/naming'; -import type { EntityKonvaAdapter, KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import type { KonvaEntityAdapter, KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import { getLayerBboxFast } from 'features/controlLayers/konva/renderers/bbox'; import { createObjectGroup, @@ -52,7 +52,7 @@ const getRegion = ( manager: KonvaNodeManager, entity: RegionEntity, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void -): EntityKonvaAdapter => { +): KonvaEntityAdapter => { const adapter = manager.get(entity.id); if (adapter) { return adapter; @@ -74,7 +74,7 @@ const getRegion = ( } const konvaObjectGroup = createObjectGroup(konvaLayer, RG_LAYER_OBJECT_GROUP_NAME); - return manager.add(entity.id, konvaLayer, konvaObjectGroup); + return manager.add(entity, konvaLayer, konvaObjectGroup); }; /** @@ -242,7 +242,7 @@ export const renderRegions = ( onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): void => { // Destroy nonexistent layers - for (const adapter of manager.getAll()) { + for (const adapter of manager.getAll('regional_guidance')) { if (!entities.find((rg) => rg.id === adapter.id)) { manager.destroy(adapter.id); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts index 7c04a0a89bb..a2533f78a96 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -248,8 +248,10 @@ export const initializeRenderer = ( spaceKey = val; }; + const manager = new KonvaNodeManager(stage, getBbox, onBboxTransformed, $shift.get, $ctrl.get, $meta.get, $alt.get); + const cleanupListeners = setStageEventHandlers({ - stage, + manager, getToolState, setTool, setToolBuffer, @@ -284,10 +286,6 @@ export const initializeRenderer = ( // the entire state over when needed. const debouncedUpdateBboxes = debounce(updateBboxes, 300); - const regionMap = new KonvaNodeManager(stage); - const layerMap = new KonvaNodeManager(stage); - const controlAdapterMap = new KonvaNodeManager(stage); - const renderCanvas = () => { const { canvasV2 } = store.getState(); @@ -305,7 +303,7 @@ export const initializeRenderer = ( canvasV2.tool.selected !== prevCanvasV2.tool.selected ) { logIfDebugging('Rendering layers'); - renderLayers(layerMap, canvasV2.layers, canvasV2.tool.selected, onPosChanged); + renderLayers(manager, canvasV2.layers, canvasV2.tool.selected, onPosChanged); } if ( @@ -316,7 +314,7 @@ export const initializeRenderer = ( ) { logIfDebugging('Rendering regions'); renderRegions( - regionMap, + manager, canvasV2.regions, canvasV2.settings.maskOpacity, canvasV2.tool.selected, @@ -327,27 +325,17 @@ export const initializeRenderer = ( if (isFirstRender || canvasV2.controlAdapters !== prevCanvasV2.controlAdapters) { logIfDebugging('Rendering control adapters'); - renderControlAdapters(controlAdapterMap, canvasV2.controlAdapters); + renderControlAdapters(manager, canvasV2.controlAdapters); } if (isFirstRender || canvasV2.document !== prevCanvasV2.document) { logIfDebugging('Rendering document bounds overlay'); - renderDocumentBoundsOverlay(stage, getDocument); + renderDocumentBoundsOverlay(manager, getDocument); } if (isFirstRender || canvasV2.bbox !== prevCanvasV2.bbox || canvasV2.tool.selected !== prevCanvasV2.tool.selected) { logIfDebugging('Rendering generation bbox'); - renderBboxPreview( - stage, - canvasV2.bbox, - canvasV2.tool.selected, - getBbox, - onBboxTransformed, - $shift.get, - $ctrl.get, - $meta.get, - $alt.get - ); + renderBboxPreview(manager, canvasV2.bbox, canvasV2.tool.selected); } if ( @@ -357,7 +345,7 @@ export const initializeRenderer = ( canvasV2.regions !== prevCanvasV2.regions ) { logIfDebugging('Updating entity bboxes'); - debouncedUpdateBboxes(stage, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions, onBboxChanged); + // debouncedUpdateBboxes(stage, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions, onBboxChanged); } if ( @@ -367,15 +355,7 @@ export const initializeRenderer = ( canvasV2.regions !== prevCanvasV2.regions ) { logIfDebugging('Arranging entities'); - arrangeEntities( - stage, - layerMap, - canvasV2.layers, - controlAdapterMap, - canvasV2.controlAdapters, - regionMap, - canvasV2.regions - ); + arrangeEntities(manager, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions); } prevCanvasV2 = canvasV2; @@ -399,8 +379,8 @@ export const initializeRenderer = ( height: stage.height(), scale: stage.scaleX(), }); - renderBackgroundLayer(stage); - renderDocumentBoundsOverlay(stage, getDocument); + renderBackgroundLayer(manager); + renderDocumentBoundsOverlay(manager, getDocument); }; const resizeObserver = new ResizeObserver(fitStageToContainer); @@ -414,7 +394,7 @@ export const initializeRenderer = ( const stageAttrs = fitDocumentToStage(stage, prevCanvasV2.document); // The HUD displays some of the stage attributes, so we need to update it here. $stageAttrs.set(stageAttrs); - scaleToolPreview(stage, getToolState()); + scaleToolPreview(manager, getToolState()); renderCanvas(); return () => { From 3a5295574f548f9fee2ab8741e42b4a6c8d1eee9 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 20 Jun 2024 21:03:33 +1000 Subject: [PATCH 101/678] feat(ui): get region and base layer canvas to blob logic working --- .../controlLayers/konva/renderers/bbox.ts | 2 +- .../controlLayers/konva/renderers/objects.ts | 2 +- .../controlLayers/konva/renderers/renderer.ts | 7 +- .../controlLayers/store/canvasV2Slice.ts | 9 +- .../controlLayers/store/layersReducers.ts | 17 +++ .../src/features/controlLayers/store/types.ts | 1 + .../nodes/util/graph/generation/addLayers.ts | 96 ++++++++++++ .../nodes/util/graph/generation/addRegions.ts | 144 +++++++++--------- 8 files changed, 203 insertions(+), 75 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts index c14d643657d..5c226d017a1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts @@ -216,7 +216,7 @@ export const updateBboxes = ( onBboxChanged({ id: entityState.id, bbox: getLayerBboxPixels(konvaLayer, filterLayerChildren) }, 'layer'); } } else if (entityState.type === 'control_adapter') { - if (!entityState.image && !entityState.processedImage) { + if (!entityState.imageObject && !entityState.processedImageObject) { // No objects - no bbox to calculate onBboxChanged({ id: entityState.id, bbox: null }, 'control_adapter'); } else { 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 d1f46db6ce1..5e5df4e96a3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -8,9 +8,9 @@ import { } from 'features/controlLayers/konva/naming'; import type { BrushLineObjectRecord, - KonvaEntityAdapter, EraserLineObjectRecord, ImageObjectRecord, + KonvaEntityAdapter, RectShapeObjectRecord, } from 'features/controlLayers/konva/nodeManager'; import type { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts index a2533f78a96..dcf283984d9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -53,8 +53,11 @@ import type { import type Konva from 'konva'; import type { IRect, Vector2d } from 'konva/lib/types'; import { debounce } from 'lodash-es'; +import { atom } from 'nanostores'; import type { RgbaColor } from 'react-colorful'; +export const $nodeManager = atom(null); + /** * Initializes the canvas renderer. It subscribes to the redux store and listens for changes directly, bypassing the * react rendering cycle entirely, improving canvas performance. @@ -249,6 +252,8 @@ export const initializeRenderer = ( }; const manager = new KonvaNodeManager(stage, getBbox, onBboxTransformed, $shift.get, $ctrl.get, $meta.get, $alt.get); + console.log(manager); + $nodeManager.set(manager); const cleanupListeners = setStageEventHandlers({ manager, @@ -344,7 +349,7 @@ export const initializeRenderer = ( canvasV2.controlAdapters !== prevCanvasV2.controlAdapters || canvasV2.regions !== prevCanvasV2.regions ) { - logIfDebugging('Updating entity bboxes'); + // logIfDebugging('Updating entity bboxes'); // debouncedUpdateBboxes(stage, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions, onBboxChanged); } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 89f26e224f8..3de6e777de0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -16,9 +16,10 @@ import { toolReducers } from 'features/controlLayers/store/toolReducers'; import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; import { atom } from 'nanostores'; +import type { ImageDTO } from 'services/api/types'; import type { CanvasEntityIdentifier, CanvasV2State, StageAttrs } from './types'; -import { DEFAULT_RGBA_COLOR } from './types'; +import { DEFAULT_RGBA_COLOR, imageDTOToImageWithDims } from './types'; const initialState: CanvasV2State = { _version: 3, @@ -119,6 +120,7 @@ const initialState: CanvasV2State = { refinerNegativeAestheticScore: 2.5, refinerStart: 0.8, }, + baseLayerImageCache: null, }; export const canvasV2Slice = createSlice({ @@ -164,6 +166,10 @@ export const canvasV2Slice = createSlice({ state.layers = []; state.ipAdapters = []; state.controlAdapters = []; + state.baseLayerImageCache = null; + }, + baseLayerImageCacheChanged: (state, action: PayloadAction) => { + state.baseLayerImageCache = action.payload ? imageDTOToImageWithDims(action.payload) : null; }, }, }); @@ -185,6 +191,7 @@ export const { scaledBboxChanged, bboxScaleMethodChanged, clipToBboxChanged, + baseLayerImageCacheChanged, // layers layerAdded, layerRecalled, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index 980b6552322..1f4dbeedc19 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -39,6 +39,7 @@ export const layersReducers = { y: 0, }); state.selectedEntityIdentifier = { type: 'layer', id }; + state.baseLayerImageCache = null; }, prepare: () => ({ payload: { id: uuidv4() } }), }, @@ -46,6 +47,7 @@ export const layersReducers = { const { data } = action.payload; state.layers.push(data); state.selectedEntityIdentifier = { type: 'layer', id: data.id }; + state.baseLayerImageCache = null; }, layerIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -54,6 +56,7 @@ export const layersReducers = { return; } layer.isEnabled = !layer.isEnabled; + state.baseLayerImageCache = null; }, layerTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => { const { id, x, y } = action.payload; @@ -63,6 +66,7 @@ export const layersReducers = { } layer.x = x; layer.y = y; + state.baseLayerImageCache = null; }, layerBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { const { id, bbox } = action.payload; @@ -88,13 +92,16 @@ export const layersReducers = { layer.objects = []; layer.bbox = null; layer.bboxNeedsUpdate = false; + state.baseLayerImageCache = null; }, layerDeleted: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; state.layers = state.layers.filter((l) => l.id !== id); + state.baseLayerImageCache = null; }, layerAllDeleted: (state) => { state.layers = []; + state.baseLayerImageCache = null; }, layerOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => { const { id, opacity } = action.payload; @@ -103,6 +110,7 @@ export const layersReducers = { return; } layer.opacity = opacity; + state.baseLayerImageCache = null; }, layerMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -111,6 +119,7 @@ export const layersReducers = { return; } moveOneToEnd(state.layers, layer); + state.baseLayerImageCache = null; }, layerMovedToFront: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -119,6 +128,7 @@ export const layersReducers = { return; } moveToEnd(state.layers, layer); + state.baseLayerImageCache = null; }, layerMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -127,6 +137,7 @@ export const layersReducers = { return; } moveOneToStart(state.layers, layer); + state.baseLayerImageCache = null; }, layerMovedToBack: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -135,6 +146,7 @@ export const layersReducers = { return; } moveToStart(state.layers, layer); + state.baseLayerImageCache = null; }, layerBrushLineAdded: { reducer: (state, action: PayloadAction) => { @@ -153,6 +165,7 @@ export const layersReducers = { clip, }); layer.bboxNeedsUpdate = true; + state.baseLayerImageCache = null; }, prepare: (payload: BrushLineAddedArg) => ({ payload: { ...payload, lineId: uuidv4() }, @@ -174,6 +187,7 @@ export const layersReducers = { clip, }); layer.bboxNeedsUpdate = true; + state.baseLayerImageCache = null; }, prepare: (payload: EraserLineAddedArg) => ({ payload: { ...payload, lineId: uuidv4() }, @@ -191,6 +205,7 @@ export const layersReducers = { } lastObject.points.push(...point); layer.bboxNeedsUpdate = true; + state.baseLayerImageCache = null; }, layerRectAdded: { reducer: (state, action: PayloadAction) => { @@ -210,6 +225,7 @@ export const layersReducers = { color, }); layer.bboxNeedsUpdate = true; + state.baseLayerImageCache = null; }, prepare: (payload: RectShapeAddedArg) => ({ payload: { ...payload, rectId: uuidv4() } }), }, @@ -222,6 +238,7 @@ export const layersReducers = { } layer.objects.push(imageDTOToImageObject(id, objectId, imageDTO)); layer.bboxNeedsUpdate = true; + state.baseLayerImageCache = null; }, prepare: (payload: ImageObjectAddedArg) => ({ payload: { ...payload, objectId: uuidv4() } }), }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 5debf0da7a3..f39f097920f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -872,6 +872,7 @@ export type CanvasV2State = { refinerNegativeAestheticScore: number; refinerStart: number; }; + baseLayerImageCache: ImageWithDims | null; }; export type StageAttrs = { x: number; y: number; width: number; height: number; scale: number }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts new file mode 100644 index 00000000000..8d4b74e76c0 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts @@ -0,0 +1,96 @@ +import { getStore } from 'app/store/nanostores/store'; +import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; +import { $nodeManager } from 'features/controlLayers/konva/renderers/renderer'; +import { blobToDataURL } from 'features/controlLayers/konva/util'; +import { baseLayerImageCacheChanged } from 'features/controlLayers/store/canvasV2Slice'; +import type { LayerEntity } from 'features/controlLayers/store/types'; +import type Konva from 'konva'; +import type { IRect } from 'konva/lib/types'; +import { getImageDTO, imagesApi } from 'services/api/endpoints/images'; +import type { ImageDTO } from 'services/api/types'; +import { assert } from 'tsafe'; + +const isValidLayer = (entity: LayerEntity) => { + return ( + entity.isEnabled && + // Boolean(entity.bbox) && TODO(psyche): Re-enable this check when we have a way to calculate bbox for all layers + entity.objects.length > 0 + ); +}; + +/** + * Get the blobs of all regional prompt layers. Only visible layers are returned. + * @param layerIds The IDs of the layers to get blobs for. If not provided, all regional prompt layers are used. + * @param preview Whether to open a new tab displaying each layer. + * @returns A map of layer IDs to blobs. + */ + +const getBaseLayer = async (layers: LayerEntity[], bbox: IRect, preview: boolean = false): Promise => { + const manager = $nodeManager.get(); + assert(manager, 'Node manager is null'); + + const stage = manager.stage.clone(); + + stage.scaleX(1); + stage.scaleY(1); + stage.x(0); + stage.y(0); + + const validLayers = layers.filter(isValidLayer); + + // Konva bug (?) - when iterating over the array returned from `stage.getLayers()`, if you destroy a layer, the array + // is mutated in-place and the next iteration will skip the next layer. To avoid this, we first collect the layers + // to delete in a separate array and then destroy them. + // TODO(psyche): Maybe report this? + const toDelete: Konva.Layer[] = []; + + for (const konvaLayer of stage.getLayers()) { + const layer = validLayers.find((l) => l.id === konvaLayer.id()); + if (!layer) { + toDelete.push(konvaLayer); + } + } + + for (const konvaLayer of toDelete) { + konvaLayer.destroy(); + } + + const blob = await new Promise((resolve) => { + stage.toBlob({ + callback: (blob) => { + assert(blob, 'Blob is null'); + resolve(blob); + }, + ...bbox, + }); + }); + + if (preview) { + const base64 = await blobToDataURL(blob); + openBase64ImageInTab([{ base64, caption: 'base layer' }]); + } + + stage.destroy(); + + return blob; +}; + +export const getBaseLayerImage = async (): Promise => { + const { dispatch, getState } = getStore(); + const state = getState(); + if (state.canvasV2.baseLayerImageCache) { + const imageDTO = await getImageDTO(state.canvasV2.baseLayerImageCache.name); + if (imageDTO) { + return imageDTO; + } + } + const blob = await getBaseLayer(state.canvasV2.layers, state.canvasV2.bbox, true); + const file = new File([blob], 'image.png', { type: 'image/png' }); + const req = dispatch( + imagesApi.endpoints.uploadImage.initiate({ file, image_category: 'general', is_intermediate: true }) + ); + req.reset(); + const imageDTO = await req.unwrap(); + dispatch(baseLayerImageCacheChanged(imageDTO)); + return imageDTO; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts index 7d47ac33311..ae99dd6e320 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts @@ -1,8 +1,8 @@ import { getStore } from 'app/store/nanostores/store'; import { deepClone } from 'common/util/deepClone'; import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; -import { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; -import { renderRegions } from 'features/controlLayers/konva/renderers/regions'; +import type { KonvaEntityAdapter } from 'features/controlLayers/konva/nodeManager'; +import { $nodeManager } from 'features/controlLayers/konva/renderers/renderer'; import { blobToDataURL } from 'features/controlLayers/konva/util'; import { rgMaskImageUploaded } from 'features/controlLayers/store/canvasV2Slice'; import type { Dimensions, IPAdapterEntity, RegionEntity } from 'features/controlLayers/store/types'; @@ -15,9 +15,7 @@ import { } from 'features/nodes/util/graph/constants'; import { addIPAdapterCollectorSafe, isValidIPAdapter } from 'features/nodes/util/graph/generation/addIPAdapters'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; -import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; -import { size } from 'lodash-es'; import { getImageDTO, imagesApi } from 'services/api/endpoints/images'; import type { BaseModelType, ImageDTO, Invocation } from 'services/api/types'; import { assert } from 'tsafe'; @@ -50,38 +48,34 @@ export const addRegions = async ( const isSDXL = base === 'sdxl'; const validRegions = regions.filter((rg) => isValidRegion(rg, base)); - const blobs = await getRGMaskBlobs(validRegions, documentSize, bbox); - assert(size(blobs) === size(validRegions), 'Mismatch between layer IDs and blobs'); - for (const rg of validRegions) { - const blob = blobs[rg.id]; - assert(blob, `Blob for layer ${rg.id} not found`); + for (const region of validRegions) { // Upload the mask image, or get the cached image if it exists - const { image_name } = await getMaskImage(rg, blob); + const { image_name } = await getRegionMaskImage(region, bbox, true); // The main mask-to-tensor node const maskToTensor = g.addNode({ - id: `${PROMPT_REGION_MASK_TO_TENSOR_PREFIX}_${rg.id}`, + id: `${PROMPT_REGION_MASK_TO_TENSOR_PREFIX}_${region.id}`, type: 'alpha_mask_to_tensor', image: { image_name, }, }); - if (rg.positivePrompt) { + if (region.positivePrompt) { // The main positive conditioning node const regionalPosCond = g.addNode( isSDXL ? { type: 'sdxl_compel_prompt', - id: `${PROMPT_REGION_POSITIVE_COND_PREFIX}_${rg.id}`, - prompt: rg.positivePrompt, - style: rg.positivePrompt, // TODO: Should we put the positive prompt in both fields? + id: `${PROMPT_REGION_POSITIVE_COND_PREFIX}_${region.id}`, + prompt: region.positivePrompt, + style: region.positivePrompt, // TODO: Should we put the positive prompt in both fields? } : { type: 'compel', - id: `${PROMPT_REGION_POSITIVE_COND_PREFIX}_${rg.id}`, - prompt: rg.positivePrompt, + id: `${PROMPT_REGION_POSITIVE_COND_PREFIX}_${region.id}`, + prompt: region.positivePrompt, } ); // Connect the mask to the conditioning @@ -106,20 +100,20 @@ export const addRegions = async ( } } - if (rg.negativePrompt) { + if (region.negativePrompt) { // The main negative conditioning node const regionalNegCond = g.addNode( isSDXL ? { type: 'sdxl_compel_prompt', - id: `${PROMPT_REGION_NEGATIVE_COND_PREFIX}_${rg.id}`, - prompt: rg.negativePrompt, - style: rg.negativePrompt, + id: `${PROMPT_REGION_NEGATIVE_COND_PREFIX}_${region.id}`, + prompt: region.negativePrompt, + style: region.negativePrompt, } : { type: 'compel', - id: `${PROMPT_REGION_NEGATIVE_COND_PREFIX}_${rg.id}`, - prompt: rg.negativePrompt, + id: `${PROMPT_REGION_NEGATIVE_COND_PREFIX}_${region.id}`, + prompt: region.negativePrompt, } ); // Connect the mask to the conditioning @@ -143,10 +137,10 @@ export const addRegions = async ( } // If we are using the "invert" auto-negative setting, we need to add an additional negative conditioning node - if (rg.autoNegative === 'invert' && rg.positivePrompt) { + if (region.autoNegative === 'invert' && region.positivePrompt) { // We re-use the mask image, but invert it when converting to tensor const invertTensorMask = g.addNode({ - id: `${PROMPT_REGION_INVERT_TENSOR_MASK_PREFIX}_${rg.id}`, + id: `${PROMPT_REGION_INVERT_TENSOR_MASK_PREFIX}_${region.id}`, type: 'invert_tensor_mask', }); // Connect the OG mask image to the inverted mask-to-tensor node @@ -156,14 +150,14 @@ export const addRegions = async ( isSDXL ? { type: 'sdxl_compel_prompt', - id: `${PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX}_${rg.id}`, - prompt: rg.positivePrompt, - style: rg.positivePrompt, + id: `${PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX}_${region.id}`, + prompt: region.positivePrompt, + style: region.positivePrompt, } : { type: 'compel', - id: `${PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX}_${rg.id}`, - prompt: rg.positivePrompt, + id: `${PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX}_${region.id}`, + prompt: region.positivePrompt, } ); // Connect the inverted mask to the conditioning @@ -186,7 +180,7 @@ export const addRegions = async ( } } - const validRGIPAdapters: IPAdapterEntity[] = rg.ipAdapters.filter((ipa) => isValidIPAdapter(ipa, base)); + const validRGIPAdapters: IPAdapterEntity[] = region.ipAdapters.filter((ipa) => isValidIPAdapter(ipa, base)); for (const ipa of validRGIPAdapters) { const ipAdapterCollect = addIPAdapterCollectorSafe(g, denoise); @@ -245,6 +239,20 @@ export const getMaskImage = async (rg: RegionEntity, blob: Blob): Promise => { + const { dispatch } = getStore(); + // No cached mask, or the cached image no longer exists - we need to upload the mask image + const file = new File([blob], `${id}_mask.png`, { type: 'image/png' }); + const req = dispatch( + imagesApi.endpoints.uploadImage.initiate({ file, image_category: 'mask', is_intermediate: true }) + ); + req.reset(); + + const imageDTO = await req.unwrap(); + dispatch(rgMaskImageUploaded({ id, imageDTO })); + return imageDTO; +}; + /** * Get the blobs of all regional prompt layers. Only visible layers are returned. * @param layerIds The IDs of the layers to get blobs for. If not provided, all regional prompt layers are used. @@ -252,53 +260,47 @@ export const getMaskImage = async (rg: RegionEntity, blob: Blob): Promise> => { - const container = document.createElement('div'); - const stage = new Konva.Stage({ container, ...documentSize }); - const manager = new KonvaNodeManager(stage); - renderRegions(manager, regions, 1, 'brush', null); - const adapters = manager.getAll(); - const blobs: Record = {}; +): Promise => { + const manager = $nodeManager.get(); + assert(manager, 'Node manager is null'); - // First remove all layers - for (const adapter of adapters) { - adapter.konvaLayer.remove(); + // TODO(psyche): Why do I need to annotate this? TS must have some kind of circular ref w/ this type but I can't figure it out... + const adapter: KonvaEntityAdapter | undefined = manager.get(region.id); + assert(adapter, `Adapter for region ${region.id} not found`); + if (region.imageCache) { + const imageDTO = await getImageDTO(region.imageCache.name); + if (imageDTO) { + return imageDTO; + } } + const layer = adapter.konvaLayer.clone(); + const objectGroup = adapter.konvaObjectGroup.clone(); + layer.destroyChildren(); + layer.add(objectGroup); + objectGroup.opacity(1); + objectGroup.cache(); - // Next render each layer to a blob - for (const adapter of adapters) { - const region = regions.find((l) => l.id === adapter.id); - if (!region) { - continue; - } - stage.add(adapter.konvaLayer); - const blob = await new Promise((resolve) => { - stage.toBlob({ - callback: (blob) => { - assert(blob, 'Blob is null'); - resolve(blob); - }, - ...bbox, - }); + const blob = await new Promise((resolve) => { + layer.toBlob({ + callback: (blob) => { + assert(blob, 'Blob is null'); + resolve(blob); + }, + ...bbox, }); + }); - if (preview) { - const base64 = await blobToDataURL(blob); - openBase64ImageInTab([ - { - base64, - caption: `${region.id}: ${region.positivePrompt} / ${region.negativePrompt}`, - }, - ]); - } - adapter.konvaLayer.remove(); - blobs[adapter.id] = blob; + if (preview) { + const base64 = await blobToDataURL(blob); + const caption = `${region.id}: ${region.positivePrompt} / ${region.negativePrompt}`; + openBase64ImageInTab([{ base64, caption }]); } - return blobs; + layer.destroy(); + + return await uploadMaskImage(region, blob); }; From 1ed43614c254e42b490b8f67dab5e1161eed8740 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 20 Jun 2024 21:16:36 +1000 Subject: [PATCH 102/678] refactor(ui): divvy up canvas state a bit --- .../src/common/hooks/useIsReadyToEnqueue.ts | 15 ++--- .../components/AddPromptButtons.tsx | 4 +- .../ControlAdapter/CAActionsMenu.tsx | 4 +- .../components/ControlLayersPanelContent.tsx | 10 ++-- .../components/DeleteAllLayersButton.tsx | 8 +-- .../components/Layer/LayerActionsMenu.tsx | 4 +- .../RegionalGuidance/RGActionsMenu.tsx | 4 +- .../controlLayers/konva/renderers/renderer.ts | 28 ++++----- .../controlLayers/store/canvasV2Slice.ts | 25 ++++---- .../store/controlAdaptersReducers.ts | 22 +++---- .../controlLayers/store/ipAdaptersReducers.ts | 10 ++-- .../controlLayers/store/layersReducers.ts | 58 ++++++++++--------- .../controlLayers/store/regionsReducers.ts | 20 +++---- .../features/controlLayers/store/selectors.ts | 5 +- .../src/features/controlLayers/store/types.ts | 12 ++-- .../deleteImageModal/store/selectors.ts | 8 +-- .../nodes/util/graph/generation/addLayers.ts | 6 +- .../generation/buildGenerationTabGraph.ts | 6 +- .../generation/buildGenerationTabSDXLGraph.ts | 6 +- .../ParametersPanelTextToImage.tsx | 3 +- 20 files changed, 128 insertions(+), 130 deletions(-) diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index c10a9af0067..eb9fc760145 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -124,7 +124,7 @@ const createSelector = (templates: Templates) => reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') }); } - canvasV2.controlAdapters + canvasV2.controlAdapters.entities .filter((ca) => ca.isEnabled) .forEach((ca, i) => { const layerLiteral = i18n.t('controlLayers.layers_one'); @@ -160,7 +160,7 @@ const createSelector = (templates: Templates) => } }); - canvasV2.ipAdapters + canvasV2.ipAdapters.entities .filter((ipa) => ipa.isEnabled) .forEach((ipa, i) => { const layerLiteral = i18n.t('controlLayers.layers_one'); @@ -188,7 +188,7 @@ const createSelector = (templates: Templates) => } }); - canvasV2.regions + canvasV2.regions.entities .filter((rg) => rg.isEnabled) .forEach((rg, i) => { const layerLiteral = i18n.t('controlLayers.layers_one'); @@ -225,7 +225,7 @@ const createSelector = (templates: Templates) => } }); - canvasV2.layers + canvasV2.layers.entities .filter((l) => l.isEnabled) .forEach((l, i) => { const layerLiteral = i18n.t('controlLayers.layers_one'); @@ -234,13 +234,6 @@ const createSelector = (templates: Templates) => const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; const problems: string[] = []; - // if (l.type === 'initial_image_layer') { - // // Must have an image - // if (!l.image) { - // problems.push(i18n.t('parameters.invoke.layer.initialImageNoImageSelected')); - // } - // } - if (problems.length) { const content = upperFirst(problems.join(', ')); reasons.push({ prefix, content }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx index 3f4222b2028..7256d6d9e02 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx @@ -21,8 +21,8 @@ export const AddPromptButtons = ({ id }: AddPromptButtonProps) => { const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToRGLayer(id); const selectValidActions = useMemo( () => - createMemoizedSelector(selectCanvasV2Slice, (caState) => { - const rg = caState.regions.find((rg) => rg.id === id); + createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { + const rg = canvasV2.regions.entities.find((rg) => rg.id === id); return { canAddPositivePrompt: rg?.positivePrompt === null, canAddNegativePrompt: rg?.negativePrompt === null, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx index 2bf75373778..1c532dda0f4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx @@ -32,8 +32,8 @@ export const CAActionsMenu = memo(({ id }: Props) => { () => createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { const ca = selectCAOrThrow(canvasV2, id); - const caIndex = canvasV2.controlAdapters.indexOf(ca); - const caCount = canvasV2.controlAdapters.length; + const caIndex = canvasV2.controlAdapters.entities.indexOf(ca); + const caCount = canvasV2.controlAdapters.entities.length; return { canMoveForward: caIndex < caCount - 1, canMoveBackward: caIndex > 0, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index c4620483353..405bf43c211 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -15,11 +15,11 @@ import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice' import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2State) => { - const rgIds = canvasV2State.regions.map(mapId).reverse(); - const caIds = canvasV2State.controlAdapters.map(mapId).reverse(); - const ipaIds = canvasV2State.ipAdapters.map(mapId).reverse(); - const layerIds = canvasV2State.layers.map(mapId).reverse(); +const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { + const rgIds = canvasV2.regions.entities.map(mapId).reverse(); + const caIds = canvasV2.controlAdapters.entities.map(mapId).reverse(); + const ipaIds = canvasV2.ipAdapters.entities.map(mapId).reverse(); + const layerIds = canvasV2.layers.entities.map(mapId).reverse(); const entityCount = rgIds.length + caIds.length + ipaIds.length + layerIds.length; return { rgIds, caIds, ipaIds, layerIds, entityCount }; }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx index fbe9c22f680..16a3c88d83f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx @@ -10,10 +10,10 @@ export const DeleteAllLayersButton = memo(() => { const dispatch = useAppDispatch(); const entityCount = useAppSelector((s) => { return ( - s.canvasV2.regions.length + - s.canvasV2.controlAdapters.length + - s.canvasV2.ipAdapters.length + - s.canvasV2.layers.length + s.canvasV2.regions.entities.length + + s.canvasV2.controlAdapters.entities.length + + s.canvasV2.ipAdapters.entities.length + + s.canvasV2.layers.entities.length ); }); const onClick = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx index 6c161531dd2..7ab753012fc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx @@ -32,8 +32,8 @@ export const LayerActionsMenu = memo(({ id }: Props) => { () => createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { const layer = selectLayerOrThrow(canvasV2, id); - const layerIndex = canvasV2.layers.indexOf(layer); - const layerCount = canvasV2.layers.length; + const layerIndex = canvasV2.layers.entities.indexOf(layer); + const layerCount = canvasV2.layers.entities.length; return { canMoveForward: layerIndex < layerCount - 1, canMoveBackward: layerIndex > 0, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx index 1df34679596..784253b5d2a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx @@ -39,8 +39,8 @@ export const RGActionsMenu = memo(({ id }: Props) => { () => createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { const rg = selectRGOrThrow(canvasV2, id); - const rgIndex = canvasV2.regions.indexOf(rg); - const rgCount = canvasV2.regions.length; + const rgIndex = canvasV2.regions.entities.indexOf(rg); + const rgCount = canvasV2.regions.entities.length; return { isMoveForwardOneDisabled: rgIndex < rgCount - 1, isMoveBackardOneDisabled: rgIndex > 0, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts index dcf283984d9..3c888053a2b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -170,13 +170,13 @@ export const initializeRenderer = ( if (!identifier) { selectedEntity = null; } else if (identifier.type === 'layer') { - selectedEntity = canvasV2.layers.find((i) => i.id === identifier.id) ?? null; + selectedEntity = canvasV2.layers.entities.find((i) => i.id === identifier.id) ?? null; } else if (identifier.type === 'control_adapter') { - selectedEntity = canvasV2.controlAdapters.find((i) => i.id === identifier.id) ?? null; + selectedEntity = canvasV2.controlAdapters.entities.find((i) => i.id === identifier.id) ?? null; } else if (identifier.type === 'ip_adapter') { - selectedEntity = canvasV2.ipAdapters.find((i) => i.id === identifier.id) ?? null; + selectedEntity = canvasV2.ipAdapters.entities.find((i) => i.id === identifier.id) ?? null; } else if (identifier.type === 'regional_guidance') { - selectedEntity = canvasV2.regions.find((i) => i.id === identifier.id) ?? null; + selectedEntity = canvasV2.regions.entities.find((i) => i.id === identifier.id) ?? null; } else { selectedEntity = null; } @@ -304,23 +304,23 @@ export const initializeRenderer = ( if ( isFirstRender || - canvasV2.layers !== prevCanvasV2.layers || + canvasV2.layers.entities !== prevCanvasV2.layers.entities || canvasV2.tool.selected !== prevCanvasV2.tool.selected ) { logIfDebugging('Rendering layers'); - renderLayers(manager, canvasV2.layers, canvasV2.tool.selected, onPosChanged); + renderLayers(manager, canvasV2.layers.entities, canvasV2.tool.selected, onPosChanged); } if ( isFirstRender || - canvasV2.regions !== prevCanvasV2.regions || + canvasV2.regions.entities !== prevCanvasV2.regions.entities || canvasV2.settings.maskOpacity !== prevCanvasV2.settings.maskOpacity || canvasV2.tool.selected !== prevCanvasV2.tool.selected ) { logIfDebugging('Rendering regions'); renderRegions( manager, - canvasV2.regions, + canvasV2.regions.entities, canvasV2.settings.maskOpacity, canvasV2.tool.selected, canvasV2.selectedEntityIdentifier, @@ -328,9 +328,9 @@ export const initializeRenderer = ( ); } - if (isFirstRender || canvasV2.controlAdapters !== prevCanvasV2.controlAdapters) { + if (isFirstRender || canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities) { logIfDebugging('Rendering control adapters'); - renderControlAdapters(manager, canvasV2.controlAdapters); + renderControlAdapters(manager, canvasV2.controlAdapters.entities); } if (isFirstRender || canvasV2.document !== prevCanvasV2.document) { @@ -355,12 +355,12 @@ export const initializeRenderer = ( if ( isFirstRender || - canvasV2.layers !== prevCanvasV2.layers || - canvasV2.controlAdapters !== prevCanvasV2.controlAdapters || - canvasV2.regions !== prevCanvasV2.regions + canvasV2.layers.entities !== prevCanvasV2.layers.entities || + canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities || + canvasV2.regions.entities !== prevCanvasV2.regions.entities ) { logIfDebugging('Arranging entities'); - arrangeEntities(manager, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions); + arrangeEntities(manager, canvasV2.layers.entities, canvasV2.controlAdapters.entities, canvasV2.regions.entities); } prevCanvasV2 = canvasV2; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 3de6e777de0..9d9088f75c8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -16,18 +16,17 @@ import { toolReducers } from 'features/controlLayers/store/toolReducers'; import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; import { atom } from 'nanostores'; -import type { ImageDTO } from 'services/api/types'; import type { CanvasEntityIdentifier, CanvasV2State, StageAttrs } from './types'; -import { DEFAULT_RGBA_COLOR, imageDTOToImageWithDims } from './types'; +import { DEFAULT_RGBA_COLOR } from './types'; const initialState: CanvasV2State = { _version: 3, selectedEntityIdentifier: null, - layers: [], - controlAdapters: [], - ipAdapters: [], - regions: [], + layers: { entities: [], baseLayerImageCache: null }, + controlAdapters: { entities: [] }, + ipAdapters: { entities: [] }, + regions: { entities: [] }, loras: [], inpaintMask: { bbox: null, @@ -120,7 +119,6 @@ const initialState: CanvasV2State = { refinerNegativeAestheticScore: 2.5, refinerStart: 0.8, }, - baseLayerImageCache: null, }; export const canvasV2Slice = createSlice({ @@ -162,14 +160,11 @@ export const canvasV2Slice = createSlice({ state.selectedEntityIdentifier = action.payload; }, allEntitiesDeleted: (state) => { - state.regions = []; - state.layers = []; - state.ipAdapters = []; - state.controlAdapters = []; - state.baseLayerImageCache = null; - }, - baseLayerImageCacheChanged: (state, action: PayloadAction) => { - state.baseLayerImageCache = action.payload ? imageDTOToImageWithDims(action.payload) : null; + state.regions.entities = []; + state.layers.entities = []; + state.layers.baseLayerImageCache = null; + state.ipAdapters.entities = []; + state.controlAdapters.entities = []; }, }, }); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts index c12c93a0d09..0601061f73d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts @@ -20,7 +20,7 @@ import type { } from './types'; import { buildControlAdapterProcessorV2, imageDTOToImageObject } from './types'; -export const selectCA = (state: CanvasV2State, id: string) => state.controlAdapters.find((ca) => ca.id === id); +export const selectCA = (state: CanvasV2State, id: string) => state.controlAdapters.entities.find((ca) => ca.id === id); export const selectCAOrThrow = (state: CanvasV2State, id: string) => { const ca = selectCA(state, id); assert(ca, `Control Adapter with id ${id} not found`); @@ -31,7 +31,7 @@ export const controlAdaptersReducers = { caAdded: { reducer: (state, action: PayloadAction<{ id: string; config: ControlNetConfig | T2IAdapterConfig }>) => { const { id, config } = action.payload; - state.controlAdapters.push({ + state.controlAdapters.entities.push({ id, type: 'control_adapter', x: 0, @@ -52,7 +52,7 @@ export const controlAdaptersReducers = { }, caRecalled: (state, action: PayloadAction<{ data: ControlAdapterEntity }>) => { const { data } = action.payload; - state.controlAdapters.push(data); + state.controlAdapters.entities.push(data); state.selectedEntityIdentifier = { type: 'control_adapter', id: data.id }; }, caIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { @@ -83,10 +83,10 @@ export const controlAdaptersReducers = { }, caDeleted: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; - state.controlAdapters = state.controlAdapters.filter((ca) => ca.id !== id); + state.controlAdapters.entities = state.controlAdapters.entities.filter((ca) => ca.id !== id); }, caAllDeleted: (state) => { - state.controlAdapters = []; + state.controlAdapters.entities = []; }, caOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => { const { id, opacity } = action.payload; @@ -102,7 +102,7 @@ export const controlAdaptersReducers = { if (!ca) { return; } - moveOneToEnd(state.controlAdapters, ca); + moveOneToEnd(state.controlAdapters.entities, ca); }, caMovedToFront: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -110,7 +110,7 @@ export const controlAdaptersReducers = { if (!ca) { return; } - moveToEnd(state.controlAdapters, ca); + moveToEnd(state.controlAdapters.entities, ca); }, caMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -118,7 +118,7 @@ export const controlAdaptersReducers = { if (!ca) { return; } - moveOneToStart(state.controlAdapters, ca); + moveOneToStart(state.controlAdapters.entities, ca); }, caMovedToBack: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -126,7 +126,7 @@ export const controlAdaptersReducers = { if (!ca) { return; } - moveToStart(state.controlAdapters, ca); + moveToStart(state.controlAdapters.entities, ca); }, caImageChanged: { reducer: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null; objectId: string }>) => { @@ -195,11 +195,11 @@ export const controlAdaptersReducers = { // We may need to convert the CA to match the model if (ca.adapterType === 't2i_adapter' && ca.model.type === 'controlnet') { const convertedCA: ControlNetData = { ...ca, adapterType: 'controlnet', controlMode: 'balanced' }; - state.controlAdapters.splice(state.controlAdapters.indexOf(ca), 1, convertedCA); + state.controlAdapters.entities.splice(state.controlAdapters.entities.indexOf(ca), 1, convertedCA); } else if (ca.adapterType === 'controlnet' && ca.model.type === 't2i_adapter') { const { controlMode: _, ...rest } = ca; const convertedCA: T2IAdapterData = { ...rest, adapterType: 't2i_adapter' }; - state.controlAdapters.splice(state.controlAdapters.indexOf(ca), 1, convertedCA); + state.controlAdapters.entities.splice(state.controlAdapters.entities.indexOf(ca), 1, convertedCA); } }, caControlModeChanged: (state, action: PayloadAction<{ id: string; controlMode: ControlModeV2 }>) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts index bf9894d6c75..ce29909b7d0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts @@ -13,7 +13,7 @@ import type { } from './types'; import { imageDTOToImageObject } from './types'; -export const selectIPA = (state: CanvasV2State, id: string) => state.ipAdapters.find((ipa) => ipa.id === id); +export const selectIPA = (state: CanvasV2State, id: string) => state.ipAdapters.entities.find((ipa) => ipa.id === id); export const selectIPAOrThrow = (state: CanvasV2State, id: string) => { const ipa = selectIPA(state, id); assert(ipa, `IP Adapter with id ${id} not found`); @@ -30,14 +30,14 @@ export const ipAdaptersReducers = { isEnabled: true, ...config, }; - state.ipAdapters.push(layer); + state.ipAdapters.entities.push(layer); state.selectedEntityIdentifier = { type: 'ip_adapter', id }; }, prepare: (payload: { config: IPAdapterConfig }) => ({ payload: { id: uuidv4(), ...payload } }), }, ipaRecalled: (state, action: PayloadAction<{ data: IPAdapterEntity }>) => { const { data } = action.payload; - state.ipAdapters.push(data); + state.ipAdapters.entities.push(data); state.selectedEntityIdentifier = { type: 'ip_adapter', id: data.id }; }, ipaIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { @@ -49,10 +49,10 @@ export const ipAdaptersReducers = { }, ipaDeleted: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; - state.ipAdapters = state.ipAdapters.filter((ipa) => ipa.id !== id); + state.ipAdapters.entities = state.ipAdapters.entities.filter((ipa) => ipa.id !== id); }, ipaAllDeleted: (state) => { - state.ipAdapters = []; + state.ipAdapters.entities = []; }, ipaImageChanged: { reducer: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null; objectId: string }>) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index 1f4dbeedc19..c7303f00cc4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -2,6 +2,7 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; import type { IRect } from 'konva/lib/types'; +import type { ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; @@ -14,9 +15,9 @@ import type { PointAddedToLineArg, RectShapeAddedArg, } from './types'; -import { imageDTOToImageObject, isLine } from './types'; +import { imageDTOToImageObject, imageDTOToImageWithDims, isLine } from './types'; -export const selectLayer = (state: CanvasV2State, id: string) => state.layers.find((layer) => layer.id === id); +export const selectLayer = (state: CanvasV2State, id: string) => state.layers.entities.find((layer) => layer.id === id); export const selectLayerOrThrow = (state: CanvasV2State, id: string) => { const layer = selectLayer(state, id); assert(layer, `Layer with id ${id} not found`); @@ -27,7 +28,7 @@ export const layersReducers = { layerAdded: { reducer: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; - state.layers.push({ + state.layers.entities.push({ id, type: 'layer', isEnabled: true, @@ -39,15 +40,15 @@ export const layersReducers = { y: 0, }); state.selectedEntityIdentifier = { type: 'layer', id }; - state.baseLayerImageCache = null; + state.layers.baseLayerImageCache = null; }, prepare: () => ({ payload: { id: uuidv4() } }), }, layerRecalled: (state, action: PayloadAction<{ data: LayerEntity }>) => { const { data } = action.payload; - state.layers.push(data); + state.layers.entities.push(data); state.selectedEntityIdentifier = { type: 'layer', id: data.id }; - state.baseLayerImageCache = null; + state.layers.baseLayerImageCache = null; }, layerIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -56,7 +57,7 @@ export const layersReducers = { return; } layer.isEnabled = !layer.isEnabled; - state.baseLayerImageCache = null; + state.layers.baseLayerImageCache = null; }, layerTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => { const { id, x, y } = action.payload; @@ -66,7 +67,7 @@ export const layersReducers = { } layer.x = x; layer.y = y; - state.baseLayerImageCache = null; + state.layers.baseLayerImageCache = null; }, layerBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { const { id, bbox } = action.payload; @@ -92,16 +93,16 @@ export const layersReducers = { layer.objects = []; layer.bbox = null; layer.bboxNeedsUpdate = false; - state.baseLayerImageCache = null; + state.layers.baseLayerImageCache = null; }, layerDeleted: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; - state.layers = state.layers.filter((l) => l.id !== id); - state.baseLayerImageCache = null; + state.layers.entities = state.layers.entities.filter((l) => l.id !== id); + state.layers.baseLayerImageCache = null; }, layerAllDeleted: (state) => { - state.layers = []; - state.baseLayerImageCache = null; + state.layers.entities = []; + state.layers.baseLayerImageCache = null; }, layerOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => { const { id, opacity } = action.payload; @@ -110,7 +111,7 @@ export const layersReducers = { return; } layer.opacity = opacity; - state.baseLayerImageCache = null; + state.layers.baseLayerImageCache = null; }, layerMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -118,8 +119,8 @@ export const layersReducers = { if (!layer) { return; } - moveOneToEnd(state.layers, layer); - state.baseLayerImageCache = null; + moveOneToEnd(state.layers.entities, layer); + state.layers.baseLayerImageCache = null; }, layerMovedToFront: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -127,8 +128,8 @@ export const layersReducers = { if (!layer) { return; } - moveToEnd(state.layers, layer); - state.baseLayerImageCache = null; + moveToEnd(state.layers.entities, layer); + state.layers.baseLayerImageCache = null; }, layerMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -136,8 +137,8 @@ export const layersReducers = { if (!layer) { return; } - moveOneToStart(state.layers, layer); - state.baseLayerImageCache = null; + moveOneToStart(state.layers.entities, layer); + state.layers.baseLayerImageCache = null; }, layerMovedToBack: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -145,8 +146,8 @@ export const layersReducers = { if (!layer) { return; } - moveToStart(state.layers, layer); - state.baseLayerImageCache = null; + moveToStart(state.layers.entities, layer); + state.layers.baseLayerImageCache = null; }, layerBrushLineAdded: { reducer: (state, action: PayloadAction) => { @@ -165,7 +166,7 @@ export const layersReducers = { clip, }); layer.bboxNeedsUpdate = true; - state.baseLayerImageCache = null; + state.layers.baseLayerImageCache = null; }, prepare: (payload: BrushLineAddedArg) => ({ payload: { ...payload, lineId: uuidv4() }, @@ -187,7 +188,7 @@ export const layersReducers = { clip, }); layer.bboxNeedsUpdate = true; - state.baseLayerImageCache = null; + state.layers.baseLayerImageCache = null; }, prepare: (payload: EraserLineAddedArg) => ({ payload: { ...payload, lineId: uuidv4() }, @@ -205,7 +206,7 @@ export const layersReducers = { } lastObject.points.push(...point); layer.bboxNeedsUpdate = true; - state.baseLayerImageCache = null; + state.layers.baseLayerImageCache = null; }, layerRectAdded: { reducer: (state, action: PayloadAction) => { @@ -225,7 +226,7 @@ export const layersReducers = { color, }); layer.bboxNeedsUpdate = true; - state.baseLayerImageCache = null; + state.layers.baseLayerImageCache = null; }, prepare: (payload: RectShapeAddedArg) => ({ payload: { ...payload, rectId: uuidv4() } }), }, @@ -238,8 +239,11 @@ export const layersReducers = { } layer.objects.push(imageDTOToImageObject(id, objectId, imageDTO)); layer.bboxNeedsUpdate = true; - state.baseLayerImageCache = null; + state.layers.baseLayerImageCache = null; }, prepare: (payload: ImageObjectAddedArg) => ({ payload: { ...payload, objectId: uuidv4() } }), }, + baseLayerImageCacheChanged: (state, action: PayloadAction) => { + state.layers.baseLayerImageCache = action.payload ? imageDTOToImageWithDims(action.payload) : null; + }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index ff64111d8b2..752e6a0af14 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -26,7 +26,7 @@ import type { } from './types'; import { isLine } from './types'; -export const selectRG = (state: CanvasV2State, id: string) => state.regions.find((rg) => rg.id === id); +export const selectRG = (state: CanvasV2State, id: string) => state.regions.entities.find((rg) => rg.id === id); export const selectRGOrThrow = (state: CanvasV2State, id: string) => { const rg = selectRG(state, id); assert(rg, `Region with id ${id} not found`); @@ -44,7 +44,7 @@ const DEFAULT_MASK_COLORS: RgbColor[] = [ ]; const getRGMaskFill = (state: CanvasV2State): RgbColor => { - const lastFill = state.regions.slice(-1)[0]?.fill; + const lastFill = state.regions.entities.slice(-1)[0]?.fill; let i = DEFAULT_MASK_COLORS.findIndex((c) => isEqual(c, lastFill)); if (i === -1) { i = 0; @@ -75,7 +75,7 @@ export const regionsReducers = { ipAdapters: [], imageCache: null, }; - state.regions.push(rg); + state.regions.entities.push(rg); state.selectedEntityIdentifier = { type: 'regional_guidance', id }; }, prepare: () => ({ payload: { id: uuidv4() } }), @@ -93,7 +93,7 @@ export const regionsReducers = { }, rgRecalled: (state, action: PayloadAction<{ data: RegionEntity }>) => { const { data } = action.payload; - state.regions.push(data); + state.regions.entities.push(data); state.selectedEntityIdentifier = { type: 'regional_guidance', id: data.id }; }, rgIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { @@ -121,10 +121,10 @@ export const regionsReducers = { }, rgDeleted: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; - state.regions = state.regions.filter((ca) => ca.id !== id); + state.regions.entities = state.regions.entities.filter((ca) => ca.id !== id); }, rgAllDeleted: (state) => { - state.regions = []; + state.regions.entities = []; }, rgMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -132,7 +132,7 @@ export const regionsReducers = { if (!rg) { return; } - moveOneToEnd(state.regions, rg); + moveOneToEnd(state.regions.entities, rg); }, rgMovedToFront: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -140,7 +140,7 @@ export const regionsReducers = { if (!rg) { return; } - moveToEnd(state.regions, rg); + moveToEnd(state.regions.entities, rg); }, rgMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -148,7 +148,7 @@ export const regionsReducers = { if (!rg) { return; } - moveOneToStart(state.regions, rg); + moveOneToStart(state.regions.entities, rg); }, rgMovedToBack: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -156,7 +156,7 @@ export const regionsReducers = { if (!rg) { return; } - moveToStart(state.regions, rg); + moveToStart(state.regions.entities, rg); }, rgPositivePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => { const { id, prompt } = action.payload; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts index ebd59de4801..4a3fd7812d7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts @@ -4,7 +4,10 @@ import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; export const selectEntityCount = createSelector(selectCanvasV2Slice, (canvasV2) => { return ( - canvasV2.regions.length + canvasV2.controlAdapters.length + canvasV2.ipAdapters.length + canvasV2.layers.length + canvasV2.regions.entities.length + + canvasV2.controlAdapters.entities.length + + canvasV2.ipAdapters.entities.length + + canvasV2.layers.entities.length ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index f39f097920f..44ea8cb5000 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -796,10 +796,13 @@ export type CanvasV2State = { _version: 3; selectedEntityIdentifier: CanvasEntityIdentifier | null; inpaintMask: InpaintMaskEntity; - layers: LayerEntity[]; - controlAdapters: ControlAdapterEntity[]; - ipAdapters: IPAdapterEntity[]; - regions: RegionEntity[]; + layers: { + baseLayerImageCache: ImageWithDims | null; + entities: LayerEntity[]; + }; + controlAdapters: { entities: ControlAdapterEntity[] }; + ipAdapters: { entities: IPAdapterEntity[] }; + regions: { entities: RegionEntity[] }; loras: LoRA[]; tool: { selected: Tool; @@ -872,7 +875,6 @@ export type CanvasV2State = { refinerNegativeAestheticScore: number; refinerStart: number; }; - baseLayerImageCache: ImageWithDims | null; }; export type StageAttrs = { x: number; y: number; width: number; height: number; scale: number }; diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts index 21b5177e211..7207ea80d46 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts @@ -11,7 +11,7 @@ import { some } from 'lodash-es'; import type { ImageUsage } from './types'; export const getImageUsage = (nodes: NodesState, canvasV2: CanvasV2State, image_name: string) => { - const isLayerImage = canvasV2.layers.some((layer) => + const isLayerImage = canvasV2.layers.entities.some((layer) => layer.objects.some((obj) => obj.type === 'image' && obj.image.name === image_name) ); @@ -21,11 +21,11 @@ export const getImageUsage = (nodes: NodesState, canvasV2: CanvasV2State, image_ some(node.data.inputs, (input) => isImageFieldInputInstance(input) && input.value?.image_name === image_name) ); - const isControlAdapterImage = canvasV2.controlAdapters.some( - (ca) => ca.image?.name === image_name || ca.processedImage?.name === image_name + const isControlAdapterImage = canvasV2.controlAdapters.entities.some( + (ca) => ca.imageObject?.image.name === image_name || ca.processedImageObject?.image.name === image_name ); - const isIPAdapterImage = canvasV2.ipAdapters.some((ipa) => ipa.imageObject?.name === image_name); + const isIPAdapterImage = canvasV2.ipAdapters.entities.some((ipa) => ipa.imageObject?.image.name === image_name); const imageUsage: ImageUsage = { isLayerImage, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts index 8d4b74e76c0..d665d53d254 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts @@ -78,13 +78,13 @@ const getBaseLayer = async (layers: LayerEntity[], bbox: IRect, preview: boolean export const getBaseLayerImage = async (): Promise => { const { dispatch, getState } = getStore(); const state = getState(); - if (state.canvasV2.baseLayerImageCache) { - const imageDTO = await getImageDTO(state.canvasV2.baseLayerImageCache.name); + if (state.canvasV2.layers.baseLayerImageCache) { + const imageDTO = await getImageDTO(state.canvasV2.layers.baseLayerImageCache.name); if (imageDTO) { return imageDTO; } } - const blob = await getBaseLayer(state.canvasV2.layers, state.canvasV2.bbox, true); + const blob = await getBaseLayer(state.canvasV2.layers.entities, state.canvasV2.bbox, true); const file = new File([blob], 'image.png', { type: 'image/png' }); const req = dispatch( imagesApi.endpoints.uploadImage.initiate({ file, image_category: 'general', is_intermediate: true }) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts index acd502c4091..d9adb21cd58 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts @@ -156,10 +156,10 @@ export const buildGenerationTabGraph = async (state: RootState): Promise { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const controlLayersCount = useAppSelector((s) => s.canvasV2.layers.length); + const controlLayersCount = useAppSelector(selectEntityCount); const controlLayersTitle = useMemo(() => { if (controlLayersCount === 0) { return t('controlLayers.controlLayers'); From 2a92a223f6ad02f8894600464792a483946387ac Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 20 Jun 2024 21:29:17 +1000 Subject: [PATCH 103/678] feat(ui): internal state for inpaint mask --- .../controlLayers/store/canvasV2Slice.ts | 24 ++-- .../store/inpaintMaskReducers.ts | 107 ++++++++++++++++++ .../src/features/controlLayers/store/types.ts | 4 +- 3 files changed, 126 insertions(+), 9 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 9d9088f75c8..4f139dda314 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -6,6 +6,7 @@ import { roundDownToMultiple } from 'common/util/roundDownToMultiple'; import { bboxReducers } from 'features/controlLayers/store/bboxReducers'; import { compositingReducers } from 'features/controlLayers/store/compositingReducers'; import { controlAdaptersReducers } from 'features/controlLayers/store/controlAdaptersReducers'; +import { inpaintMaskReducers } from 'features/controlLayers/store/inpaintMaskReducers'; import { ipAdaptersReducers } from 'features/controlLayers/store/ipAdaptersReducers'; import { layersReducers } from 'features/controlLayers/store/layersReducers'; import { lorasReducers } from 'features/controlLayers/store/lorasReducers'; @@ -29,17 +30,14 @@ const initialState: CanvasV2State = { regions: { entities: [] }, loras: [], inpaintMask: { + id: 'inpaint_mask', + type: 'inpaint_mask', bbox: null, bboxNeedsUpdate: false, - fill: { - type: 'color_fill', - color: DEFAULT_RGBA_COLOR, - }, - id: 'inpaint_mask', + fill: DEFAULT_RGBA_COLOR, imageCache: null, isEnabled: false, - maskObjects: [], - type: 'inpaint_mask', + objects: [], x: 0, y: 0, }, @@ -135,6 +133,7 @@ export const canvasV2Slice = createSlice({ ...settingsReducers, ...toolReducers, ...bboxReducers, + ...inpaintMaskReducers, widthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }>) => { const { width, updateAspectRatio, clamp } = action.payload; state.document.width = clamp ? Math.max(roundDownToMultiple(width, 8), 64) : width; @@ -314,6 +313,17 @@ export const { loraWeightChanged, loraIsEnabledChanged, loraAllDeleted, + // Inpaint mask + imReset, + imRecalled, + imIsEnabledToggled, + imTranslated, + imBboxChanged, + imImageCacheChanged, + imBrushLineAdded, + imEraserLineAdded, + imLinePointAdded, + imRectAdded, } = canvasV2Slice.actions; export const selectCanvasV2Slice = (state: RootState) => state.canvasV2; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts index 8b137891791..bff4a8a9bb7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts @@ -1 +1,108 @@ +import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; +import type { CanvasV2State, InpaintMaskEntity } from 'features/controlLayers/store/types'; +import { imageDTOToImageWithDims } from 'features/controlLayers/store/types'; +import type { IRect } from 'konva/lib/types'; +import type { ImageDTO } from 'services/api/types'; +import { v4 as uuidv4 } from 'uuid'; +import type { BrushLineAddedArg, EraserLineAddedArg, PointAddedToLineArg, RectShapeAddedArg, RgbColor } from './types'; +import { isLine } from './types'; + +export const inpaintMaskReducers = { + imReset: (state) => { + state.inpaintMask.objects = []; + state.inpaintMask.bbox = null; + state.inpaintMask.bboxNeedsUpdate = false; + state.inpaintMask.imageCache = null; + }, + imRecalled: (state, action: PayloadAction<{ data: InpaintMaskEntity }>) => { + const { data } = action.payload; + state.inpaintMask = data; + state.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id }; + }, + imIsEnabledToggled: (state) => { + state.inpaintMask.isEnabled = !state.inpaintMask.isEnabled; + }, + imTranslated: (state, action: PayloadAction<{ x: number; y: number }>) => { + const { x, y } = action.payload; + state.inpaintMask.x = x; + state.inpaintMask.y = y; + }, + imBboxChanged: (state, action: PayloadAction<{ bbox: IRect | null }>) => { + const { bbox } = action.payload; + state.inpaintMask.bbox = bbox; + state.inpaintMask.bboxNeedsUpdate = false; + }, + inpaintMaskFillChanged: (state, action: PayloadAction<{ fill: RgbColor }>) => { + const { fill } = action.payload; + state.inpaintMask.fill = fill; + }, + imImageCacheChanged: (state, action: PayloadAction<{ imageDTO: ImageDTO | null }>) => { + const { imageDTO } = action.payload; + state.inpaintMask.imageCache = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; + }, + imBrushLineAdded: { + reducer: (state, action: PayloadAction & { lineId: string }>) => { + const { points, lineId, color, width, clip } = action.payload; + state.inpaintMask.objects.push({ + id: getBrushLineId(state.inpaintMask.id, lineId), + type: 'brush_line', + points, + strokeWidth: width, + color, + clip, + }); + state.inpaintMask.bboxNeedsUpdate = true; + state.inpaintMask.imageCache = null; + }, + prepare: (payload: Omit) => ({ + payload: { ...payload, lineId: uuidv4() }, + }), + }, + imEraserLineAdded: { + reducer: (state, action: PayloadAction & { lineId: string }>) => { + const { points, lineId, width, clip } = action.payload; + state.inpaintMask.objects.push({ + id: getEraserLineId(state.inpaintMask.id, lineId), + type: 'eraser_line', + points, + strokeWidth: width, + clip, + }); + state.inpaintMask.bboxNeedsUpdate = true; + state.inpaintMask.imageCache = null; + }, + prepare: (payload: Omit) => ({ + payload: { ...payload, lineId: uuidv4() }, + }), + }, + imLinePointAdded: (state, action: PayloadAction>) => { + const { point } = action.payload; + const lastObject = state.inpaintMask.objects[state.inpaintMask.objects.length - 1]; + if (!lastObject || !isLine(lastObject)) { + return; + } + lastObject.points.push(...point); + state.inpaintMask.bboxNeedsUpdate = true; + state.inpaintMask.imageCache = null; + }, + imRectAdded: { + reducer: (state, action: PayloadAction & { rectId: string }>) => { + const { rect, rectId, color } = action.payload; + if (rect.height === 0 || rect.width === 0) { + // Ignore zero-area rectangles + return; + } + state.inpaintMask.objects.push({ + type: 'rect_shape', + id: getRectShapeId(state.inpaintMask.id, rectId), + ...rect, + color, + }); + state.inpaintMask.bboxNeedsUpdate = true; + state.inpaintMask.imageCache = null; + }, + prepare: (payload: Omit) => ({ payload: { ...payload, rectId: uuidv4() } }), + }, +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 44ea8cb5000..f8410bb4b97 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -655,8 +655,8 @@ const zInpaintMaskEntity = z.object({ y: z.number(), bbox: zRect.nullable(), bboxNeedsUpdate: z.boolean(), - maskObjects: z.array(zMaskObject), - fill: zFill, + objects: z.array(zMaskObject), + fill: zRgbColor, imageCache: zImageWithDims.nullable(), }); export type InpaintMaskEntity = z.infer; From 70f430f635e6a777ca247ab12a126ede14b7d456 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 20 Jun 2024 21:35:48 +1000 Subject: [PATCH 104/678] fix(ui): models loaded handler --- .../listenerMiddleware/listeners/modelSelected.ts | 2 +- .../middleware/listenerMiddleware/listeners/modelsLoaded.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts index 38260b23a20..aa0e9a3d61e 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts @@ -45,7 +45,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = } // handle incompatible controlnets - state.canvasV2.controlAdapters.forEach((ca) => { + state.canvasV2.controlAdapters.entities.forEach((ca) => { if (ca.model?.base !== newBaseModel) { modelsCleared += 1; if (ca.isEnabled) { diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts index 47555a0ef13..a94b0b45e36 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts @@ -172,7 +172,7 @@ const handleLoRAModels: ModelHandler = (models, state, dispatch, _log) => { const handleControlAdapterModels: ModelHandler = (models, state, dispatch, _log) => { const caModels = models.filter(isControlNetOrT2IAdapterModelConfig); - state.canvasV2.controlAdapters.forEach((ca) => { + state.canvasV2.controlAdapters.entities.forEach((ca) => { const isModelAvailable = caModels.some((m) => m.key === ca.model?.key); if (isModelAvailable) { return; @@ -183,7 +183,7 @@ const handleControlAdapterModels: ModelHandler = (models, state, dispatch, _log) const handleIPAdapterModels: ModelHandler = (models, state, dispatch, _log) => { const ipaModels = models.filter(isIPAdapterModelConfig); - state.canvasV2.controlAdapters.forEach(({ id, model }) => { + state.canvasV2.ipAdapters.entities.forEach(({ id, model }) => { const isModelAvailable = ipaModels.some((m) => m.key === model?.key); if (isModelAvailable) { return; @@ -191,7 +191,7 @@ const handleIPAdapterModels: ModelHandler = (models, state, dispatch, _log) => { dispatch(ipaModelChanged({ id, modelConfig: null })); }); - state.canvasV2.regions.forEach(({ id, ipAdapters }) => { + state.canvasV2.regions.entities.forEach(({ id, ipAdapters }) => { ipAdapters.forEach(({ id: ipAdapterId, model }) => { const isModelAvailable = ipaModels.some((m) => m.key === model?.key); if (isModelAvailable) { From af7d222a1e8d5b8123931d642f975786d79eff0f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 20 Jun 2024 22:50:36 +1000 Subject: [PATCH 105/678] feat(ui): inpaint mask rendering (wip) --- .../features/controlLayers/konva/events.ts | 55 ++-- .../features/controlLayers/konva/naming.ts | 4 + .../controlLayers/konva/renderers/arrange.ts | 1 + .../konva/renderers/inpaintMask.ts | 234 ++++++++++++++++++ .../controlLayers/konva/renderers/preview.ts | 15 +- .../controlLayers/konva/renderers/renderer.ts | 35 +++ .../controlLayers/store/canvasV2Slice.ts | 4 +- 7 files changed, 312 insertions(+), 36 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 3745046f201..0f4e4c05b21 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -106,7 +106,12 @@ const maybeAddNextPoint = ( setLastAddedPoint: Arg['setLastAddedPoint'], onPointAddedToLine: Arg['onPointAddedToLine'] ) => { - if (selectedEntity.type !== 'layer' && selectedEntity.type !== 'regional_guidance') { + const isDrawableEntity = + selectedEntity?.type === 'regional_guidance' || + selectedEntity?.type === 'layer' || + selectedEntity?.type === 'inpaint_mask'; + + if (!isDrawableEntity) { return; } // Continue the last line @@ -189,12 +194,12 @@ export const setStageEventHandlers = ({ const toolState = getToolState(); const pos = updateLastCursorPos(stage, setLastCursorPos); const selectedEntity = getSelectedEntity(); - if ( - pos && - selectedEntity && - (selectedEntity?.type === 'regional_guidance' || selectedEntity?.type === 'layer') && - !getSpaceKey() - ) { + const isDrawableEntity = + selectedEntity?.type === 'regional_guidance' || + selectedEntity?.type === 'layer' || + selectedEntity?.type === 'inpaint_mask'; + + if (pos && selectedEntity && isDrawableEntity && !getSpaceKey()) { setIsDrawing(true); setLastMouseDownPos(pos); @@ -318,13 +323,12 @@ export const setStageEventHandlers = ({ setIsMouseDown(false); const pos = getLastCursorPos(); const selectedEntity = getSelectedEntity(); + const isDrawableEntity = + selectedEntity?.type === 'regional_guidance' || + selectedEntity?.type === 'layer' || + selectedEntity?.type === 'inpaint_mask'; - if ( - pos && - selectedEntity && - (selectedEntity?.type === 'regional_guidance' || selectedEntity?.type === 'layer') && - !getSpaceKey() - ) { + if (pos && selectedEntity && isDrawableEntity && !getSpaceKey()) { const toolState = getToolState(); if (toolState.selected === 'rect') { @@ -372,13 +376,12 @@ export const setStageEventHandlers = ({ .findOne(`#${PREVIEW_TOOL_GROUP_ID}`) ?.visible(toolState.selected === 'brush' || toolState.selected === 'eraser'); - if ( - pos && - selectedEntity && - (selectedEntity.type === 'regional_guidance' || selectedEntity.type === 'layer') && - !getSpaceKey() && - getIsMouseDown() - ) { + const isDrawableEntity = + selectedEntity?.type === 'regional_guidance' || + selectedEntity?.type === 'layer' || + selectedEntity?.type === 'inpaint_mask'; + + if (pos && selectedEntity && isDrawableEntity && !getSpaceKey() && getIsMouseDown()) { if (toolState.selected === 'brush') { if (getIsDrawing()) { // Continue the last line @@ -489,14 +492,12 @@ export const setStageEventHandlers = ({ const toolState = getToolState(); stage.findOne(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(false); + const isDrawableEntity = + selectedEntity?.type === 'regional_guidance' || + selectedEntity?.type === 'layer' || + selectedEntity?.type === 'inpaint_mask'; - if ( - pos && - selectedEntity && - (selectedEntity.type === 'regional_guidance' || selectedEntity.type === 'layer') && - !getSpaceKey() && - getIsMouseDown() - ) { + if (pos && selectedEntity && isDrawableEntity && !getSpaceKey() && getIsMouseDown()) { if (getIsMouseDown()) { if (toolState.selected === 'brush') { onPointAddedToLine({ id: selectedEntity.id, point: [pos.x, pos.y] }, selectedEntity.type); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts index eedaa94f6e6..0d35ed86317 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts @@ -40,6 +40,10 @@ export const RASTER_LAYER_RECT_SHAPE_NAME = `${RASTER_LAYER_NAME}.rect_shape`; export const RASTER_LAYER_IMAGE_NAME = `${RASTER_LAYER_NAME}.image`; export const INPAINT_MASK_LAYER_NAME = 'inpaint_mask_layer'; +export const INPAINT_MASK_LAYER_OBJECT_GROUP_NAME = `${INPAINT_MASK_LAYER_NAME}.object_group`; +export const INPAINT_MASK_LAYER_BRUSH_LINE_NAME = `${INPAINT_MASK_LAYER_NAME}.brush_line`; +export const INPAINT_MASK_LAYER_ERASER_LINE_NAME = `${INPAINT_MASK_LAYER_NAME}.eraser_line`; +export const INPAINT_MASK_LAYER_RECT_SHAPE_NAME = `${INPAINT_MASK_LAYER_NAME}.rect_shape`; export const BACKGROUND_LAYER_ID = 'background_layer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts index 324d6ab5a53..8e5650a3b20 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts @@ -18,5 +18,6 @@ export const arrangeEntities = ( for (const rg of regions) { manager.get(rg.id)?.konvaLayer.zIndex(++zIndex); } + manager.get('inpaint_mask')?.konvaLayer.zIndex(++zIndex); manager.preview.layer.zIndex(++zIndex); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts new file mode 100644 index 00000000000..afa09e0e176 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts @@ -0,0 +1,234 @@ +import { rgbColorToString } from 'common/util/colorCodeTransformers'; +import { + COMPOSITING_RECT_NAME, + INPAINT_MASK_LAYER_BRUSH_LINE_NAME, + INPAINT_MASK_LAYER_ERASER_LINE_NAME, + INPAINT_MASK_LAYER_NAME, + INPAINT_MASK_LAYER_OBJECT_GROUP_NAME, + INPAINT_MASK_LAYER_RECT_SHAPE_NAME, +} from 'features/controlLayers/konva/naming'; +import type { KonvaEntityAdapter, KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import { getLayerBboxFast } from 'features/controlLayers/konva/renderers/bbox'; +import { + createObjectGroup, + getBrushLine, + getEraserLine, + getRectShape, +} from 'features/controlLayers/konva/renderers/objects'; +import { mapId } from 'features/controlLayers/konva/util'; +import type { + CanvasEntity, + CanvasEntityIdentifier, + InpaintMaskEntity, + PosChangedArg, + Tool, +} from 'features/controlLayers/store/types'; +import Konva from 'konva'; + +/** + * Logic for creating and rendering regional guidance layers. + * + * Some special handling is needed to render layer opacity correctly using a "compositing rect". See the comments + * in `renderRGLayer`. + */ + +/** + * Creates the "compositing rect" for a regional guidance layer. + * @param konvaLayer The konva layer + */ +const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => { + const compositingRect = new Konva.Rect({ name: COMPOSITING_RECT_NAME, listening: false }); + konvaLayer.add(compositingRect); + return compositingRect; +}; + +/** + * Creates a regional guidance layer. + * @param stage The konva stage + * @param entity The regional guidance layer state + * @param onLayerPosChanged Callback for when the layer's position changes + */ +const getInpaintMask = ( + manager: KonvaNodeManager, + entity: InpaintMaskEntity, + onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void +): KonvaEntityAdapter => { + const adapter = manager.get(entity.id); + if (adapter) { + return adapter; + } + // This layer hasn't been added to the konva state yet + const konvaLayer = new Konva.Layer({ + id: entity.id, + name: INPAINT_MASK_LAYER_NAME, + draggable: true, + dragDistance: 0, + }); + + // 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 (onPosChanged) { + konvaLayer.on('dragend', function (e) { + onPosChanged({ id: entity.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'inpaint_mask'); + }); + } + + const konvaObjectGroup = createObjectGroup(konvaLayer, INPAINT_MASK_LAYER_OBJECT_GROUP_NAME); + return manager.add(entity, konvaLayer, konvaObjectGroup); +}; + +/** + * Renders a raster layer. + * @param stage The konva stage + * @param entity The regional guidance layer state + * @param globalMaskLayerOpacity The global mask layer opacity + * @param tool The current tool + * @param onPosChanged Callback for when the layer's position changes + */ +export const renderInpaintMask = ( + manager: KonvaNodeManager, + entity: InpaintMaskEntity, + globalMaskLayerOpacity: number, + tool: Tool, + selectedEntityIdentifier: CanvasEntityIdentifier | null, + onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void +): void => { + const adapter = getInpaintMask(manager, entity, onPosChanged); + + // Update the layer's position and listening state + adapter.konvaLayer.setAttrs({ + listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events + x: Math.floor(entity.x), + y: Math.floor(entity.y), + }); + + // Convert the color to a string, stripping the alpha - the object group will handle opacity. + const rgbColor = rgbColorToString(entity.fill); + + // We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required. + let groupNeedsCache = false; + + const objectIds = entity.objects.map(mapId); + // Destroy any objects that are no longer in state + for (const objectRecord of adapter.getAll()) { + if (!objectIds.includes(objectRecord.id)) { + adapter.destroy(objectRecord.id); + groupNeedsCache = true; + } + } + + for (const obj of entity.objects) { + if (obj.type === 'brush_line') { + const objectRecord = getBrushLine(adapter, obj, INPAINT_MASK_LAYER_BRUSH_LINE_NAME); + + // Only update the points if they have changed. The point values are never mutated, they are only added to the + // array, so checking the length is sufficient to determine if we need to re-cache. + if (objectRecord.konvaLine.points().length !== obj.points.length) { + objectRecord.konvaLine.points(obj.points); + groupNeedsCache = true; + } + // Only update the color if it has changed. + if (objectRecord.konvaLine.stroke() !== rgbColor) { + objectRecord.konvaLine.stroke(rgbColor); + groupNeedsCache = true; + } + } else if (obj.type === 'eraser_line') { + const objectRecord = getEraserLine(adapter, obj, INPAINT_MASK_LAYER_ERASER_LINE_NAME); + + // Only update the points if they have changed. The point values are never mutated, they are only added to the + // array, so checking the length is sufficient to determine if we need to re-cache. + if (objectRecord.konvaLine.points().length !== obj.points.length) { + objectRecord.konvaLine.points(obj.points); + groupNeedsCache = true; + } + // Only update the color if it has changed. + if (objectRecord.konvaLine.stroke() !== rgbColor) { + objectRecord.konvaLine.stroke(rgbColor); + groupNeedsCache = true; + } + } else if (obj.type === 'rect_shape') { + const objectRecord = getRectShape(adapter, obj, INPAINT_MASK_LAYER_RECT_SHAPE_NAME); + + // Only update the color if it has changed. + if (objectRecord.konvaRect.fill() !== rgbColor) { + objectRecord.konvaRect.fill(rgbColor); + groupNeedsCache = true; + } + } + } + + // Only update layer visibility if it has changed. + if (adapter.konvaLayer.visible() !== entity.isEnabled) { + adapter.konvaLayer.visible(entity.isEnabled); + groupNeedsCache = true; + } + + if (adapter.konvaObjectGroup.getChildren().length === 0) { + // No objects - clear the cache to reset the previous pixel data + adapter.konvaObjectGroup.clearCache(); + return; + } + + const compositingRect = + adapter.konvaLayer.findOne(`.${COMPOSITING_RECT_NAME}`) ?? createCompositingRect(adapter.konvaLayer); + const isSelected = selectedEntityIdentifier?.id === entity.id; + + /** + * When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows + * shapes to render as a "raster" layer with all pixels drawn at the same color and opacity. + * + * Without this special handling, each shape is drawn individually with the given opacity, atop the other shapes. The + * effect is like if you have a Photoshop Group consisting of many shapes, each of which has the given opacity. + * Overlapping shapes will have their colors blended together, and the final color is the result of all the shapes. + * + * 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. + */ + if (isSelected && tool !== 'move') { + // We must clear the cache first so Konva will re-draw the group with the new compositing rect + if (adapter.konvaObjectGroup.isCached()) { + adapter.konvaObjectGroup.clearCache(); + } + // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work + adapter.konvaObjectGroup.opacity(1); + + compositingRect.setAttrs({ + // The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already + ...(!entity.bboxNeedsUpdate && entity.bbox ? entity.bbox : getLayerBboxFast(adapter.konvaLayer)), + fill: rgbColor, + opacity: globalMaskLayerOpacity, + // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) + globalCompositeOperation: 'source-in', + visible: true, + // This rect must always be on top of all other shapes + zIndex: adapter.konvaObjectGroup.getChildren().length, + }); + } else { + // The compositing rect should only be shown when the layer is selected. + compositingRect.visible(false); + // Cache only if needed - or if we are on this code path and _don't_ have a cache + if (groupNeedsCache || !adapter.konvaObjectGroup.isCached()) { + adapter.konvaObjectGroup.cache(); + } + // Updating group opacity does not require re-caching + adapter.konvaObjectGroup.opacity(globalMaskLayerOpacity); + } + + // const bboxRect = + // regionMap.konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rg, regionMap.konvaLayer); + + // if (rg.bbox) { + // const active = !rg.bboxNeedsUpdate && isSelected && tool === 'move'; + // bboxRect.setAttrs({ + // visible: active, + // listening: active, + // x: rg.bbox.x, + // y: rg.bbox.y, + // width: rg.bbox.width, + // height: rg.bbox.height, + // stroke: isSelected ? BBOX_SELECTED_STROKE : '', + // }); + // } else { + // bboxRect.visible(false); + // } +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts index c0b49ddbe07..3926dfadc02 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts @@ -313,6 +313,11 @@ export const renderToolPreview = ( const stage = manager.stage; const layerCount = manager.adapters.size; const tool = toolState.selected; + const isDrawableEntity = + selectedEntity?.type === 'regional_guidance' || + selectedEntity?.type === 'layer' || + selectedEntity?.type === 'inpaint_mask'; + // Update the stage's pointer style if (tool === 'view') { // View gets a hand @@ -320,8 +325,8 @@ export const renderToolPreview = ( } else if (layerCount === 0) { // We have no layers, so we should not render any tool stage.container().style.cursor = 'default'; - } else if (selectedEntity?.type !== 'regional_guidance' && selectedEntity?.type !== 'layer') { - // Non-mask-guidance layers don't have tools + } else if (!isDrawableEntity) { + // Non-drawable layers don't have tools stage.container().style.cursor = 'not-allowed'; } else if (tool === 'move') { // Move tool gets a pointer @@ -338,11 +343,7 @@ export const renderToolPreview = ( stage.draggable(tool === 'view'); - if ( - !cursorPos || - layerCount === 0 || - (selectedEntity?.type !== 'regional_guidance' && selectedEntity?.type !== 'layer') - ) { + if (!cursorPos || layerCount === 0 || !isDrawableEntity) { // We can bail early if the mouse isn't over the stage or there are no layers manager.preview.tool.group.visible(false); } else { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts index 3c888053a2b..08a996581d1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -9,6 +9,7 @@ import { arrangeEntities } from 'features/controlLayers/konva/renderers/arrange' import { renderBackgroundLayer } from 'features/controlLayers/konva/renderers/background'; import { updateBboxes } from 'features/controlLayers/konva/renderers/bbox'; import { renderControlAdapters } from 'features/controlLayers/konva/renderers/controlAdapters'; +import { renderInpaintMask } from 'features/controlLayers/konva/renderers/inpaintMask'; import { renderLayers } from 'features/controlLayers/konva/renderers/layers'; import { renderBboxPreview, @@ -24,6 +25,11 @@ import { caBboxChanged, caTranslated, eraserWidthChanged, + imBboxChanged, + imBrushLineAdded, + imEraserLineAdded, + imLinePointAdded, + imTranslated, layerBboxChanged, layerBrushLineAdded, layerEraserLineAdded, @@ -99,6 +105,8 @@ export const initializeRenderer = ( dispatch(caTranslated(arg)); } else if (entityType === 'regional_guidance') { dispatch(rgTranslated(arg)); + } else if (entityType === 'inpaint_mask') { + dispatch(imTranslated(arg)); } }; const onBboxChanged = (arg: BboxChangedArg, entityType: CanvasEntity['type']) => { @@ -109,6 +117,8 @@ export const initializeRenderer = ( dispatch(caBboxChanged(arg)); } else if (entityType === 'regional_guidance') { dispatch(rgBboxChanged(arg)); + } else if (entityType === 'inpaint_mask') { + dispatch(imBboxChanged(arg)); } }; const onBrushLineAdded = (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => { @@ -117,6 +127,8 @@ export const initializeRenderer = ( dispatch(layerBrushLineAdded(arg)); } else if (entityType === 'regional_guidance') { dispatch(rgBrushLineAdded(arg)); + } else if (entityType === 'inpaint_mask') { + dispatch(imBrushLineAdded(arg)); } }; const onEraserLineAdded = (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => { @@ -125,6 +137,8 @@ export const initializeRenderer = ( dispatch(layerEraserLineAdded(arg)); } else if (entityType === 'regional_guidance') { dispatch(rgEraserLineAdded(arg)); + } else if (entityType === 'inpaint_mask') { + dispatch(imEraserLineAdded(arg)); } }; const onPointAddedToLine = (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => { @@ -133,6 +147,8 @@ export const initializeRenderer = ( dispatch(layerLinePointAdded(arg)); } else if (entityType === 'regional_guidance') { dispatch(rgLinePointAdded(arg)); + } else if (entityType === 'inpaint_mask') { + dispatch(imLinePointAdded(arg)); } }; const onRectShapeAdded = (arg: RectShapeAddedArg, entityType: CanvasEntity['type']) => { @@ -177,6 +193,8 @@ export const initializeRenderer = ( selectedEntity = canvasV2.ipAdapters.entities.find((i) => i.id === identifier.id) ?? null; } else if (identifier.type === 'regional_guidance') { selectedEntity = canvasV2.regions.entities.find((i) => i.id === identifier.id) ?? null; + } else if (identifier.type === 'inpaint_mask') { + selectedEntity = canvasV2.inpaintMask; } else { selectedEntity = null; } @@ -328,6 +346,23 @@ export const initializeRenderer = ( ); } + if ( + isFirstRender || + canvasV2.inpaintMask !== prevCanvasV2.inpaintMask || + canvasV2.settings.maskOpacity !== prevCanvasV2.settings.maskOpacity || + canvasV2.tool.selected !== prevCanvasV2.tool.selected + ) { + logIfDebugging('Rendering inpaint mask'); + renderInpaintMask( + manager, + canvasV2.inpaintMask, + canvasV2.settings.maskOpacity, + canvasV2.tool.selected, + canvasV2.selectedEntityIdentifier, + onPosChanged + ); + } + if (isFirstRender || canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities) { logIfDebugging('Rendering control adapters'); renderControlAdapters(manager, canvasV2.controlAdapters.entities); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 4f139dda314..9f282f9734d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -23,7 +23,7 @@ import { DEFAULT_RGBA_COLOR } from './types'; const initialState: CanvasV2State = { _version: 3, - selectedEntityIdentifier: null, + selectedEntityIdentifier: { type: 'inpaint_mask', id: 'inpaint_mask' }, layers: { entities: [], baseLayerImageCache: null }, controlAdapters: { entities: [] }, ipAdapters: { entities: [] }, @@ -36,7 +36,7 @@ const initialState: CanvasV2State = { bboxNeedsUpdate: false, fill: DEFAULT_RGBA_COLOR, imageCache: null, - isEnabled: false, + isEnabled: true, objects: [], x: 0, y: 0, From 205a71964964a51107638d5074b2bf3d9d3fb9b9 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 21 Jun 2024 12:52:39 +1000 Subject: [PATCH 106/678] feat(ui): inpaint mask UI components --- .../components/ControlLayersPanelContent.tsx | 2 + .../components/InpaintMask/IM.tsx | 26 +++++++++++ .../components/InpaintMask/IMActionsMenu.tsx | 28 +++++++++++ .../components/InpaintMask/IMHeader.tsx | 36 +++++++++++++++ .../InpaintMask/IMMaskFillColorPicker.tsx | 46 +++++++++++++++++++ .../components/InpaintMask/IMSettings.tsx | 8 ++++ .../controlLayers/components/ToolChooser.tsx | 9 +++- .../controlLayers/store/canvasV2Slice.ts | 1 + .../store/inpaintMaskReducers.ts | 2 +- 9 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMActionsMenu.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMHeader.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMMaskFillColorPicker.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMSettings.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index 405bf43c211..9e771eb48eb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -7,6 +7,7 @@ import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableCon import { AddLayerButton } from 'features/controlLayers/components/AddLayerButton'; import { CA } from 'features/controlLayers/components/ControlAdapter/CA'; import { DeleteAllLayersButton } from 'features/controlLayers/components/DeleteAllLayersButton'; +import { IM } from 'features/controlLayers/components/InpaintMask/IM'; import { IPA } from 'features/controlLayers/components/IPAdapter/IPA'; import { Layer } from 'features/controlLayers/components/Layer/Layer'; import { RG } from 'features/controlLayers/components/RegionalGuidance/RG'; @@ -34,6 +35,7 @@ export const ControlLayersPanelContent = memo(() => {
+ {entityCount > 0 && ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx new file mode 100644 index 00000000000..3c0fb3b75b6 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx @@ -0,0 +1,26 @@ +import { useDisclosure } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { rgbColorToString } from 'common/util/colorCodeTransformers'; +import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; +import { IMHeader } from 'features/controlLayers/components/InpaintMask/IMHeader'; +import { IMSettings } from 'features/controlLayers/components/InpaintMask/IMSettings'; +import { entitySelected } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; + +export const IM = memo(() => { + const dispatch = useAppDispatch(); + const selectedBorderColor = useAppSelector((s) => rgbColorToString(s.canvasV2.inpaintMask.fill)); + const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === 'inpaint_mask'); + const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); + const onSelect = useCallback(() => { + dispatch(entitySelected({ id: 'inpaint_mask', type: 'inpaint_mask' })); + }, [dispatch]); + return ( + + + {isOpen && } + + ); +}); + +IM.displayName = 'IM'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMActionsMenu.tsx new file mode 100644 index 00000000000..14462abc738 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMActionsMenu.tsx @@ -0,0 +1,28 @@ +import { Menu, MenuItem, MenuList } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; +import { imReset } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiArrowCounterClockwiseBold } from 'react-icons/pi'; + +export const IMActionsMenu = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const onReset = useCallback(() => { + dispatch(imReset()); + }, [dispatch]); + + return ( + + + + }> + {t('accessibility.reset')} + + + + ); +}); + +IMActionsMenu.displayName = 'IMActionsMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMHeader.tsx new file mode 100644 index 00000000000..1102e056f3f --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMHeader.tsx @@ -0,0 +1,36 @@ +import { Spacer } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; +import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; +import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; +import { IMActionsMenu } from 'features/controlLayers/components/InpaintMask/IMActionsMenu'; +import { imIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { IMMaskFillColorPicker } from './IMMaskFillColorPicker'; + +type Props = { + onToggleVisibility: () => void; +}; + +export const IMHeader = memo(({ onToggleVisibility }: Props) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isEnabled = useAppSelector((s) => s.canvasV2.inpaintMask.isEnabled); + const onToggleIsEnabled = useCallback(() => { + dispatch(imIsEnabledToggled()); + }, [dispatch]); + + return ( + + + + + + + + ); +}); + +IMHeader.displayName = 'IMHeader'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMMaskFillColorPicker.tsx new file mode 100644 index 00000000000..144012143db --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMMaskFillColorPicker.tsx @@ -0,0 +1,46 @@ +import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import RgbColorPicker from 'common/components/RgbColorPicker'; +import { rgbColorToString } from 'common/util/colorCodeTransformers'; +import { stopPropagation } from 'common/util/stopPropagation'; +import { imFillChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; +import type { RgbColor } from 'react-colorful'; +import { useTranslation } from 'react-i18next'; + +export const IMMaskFillColorPicker = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const fill = useAppSelector((s) => s.canvasV2.inpaintMask.fill); + const onChange = useCallback( + (fill: RgbColor) => { + dispatch(imFillChanged({ fill })); + }, + [dispatch] + ); + return ( + + + + + + + + + + + ); +}); + +IMMaskFillColorPicker.displayName = 'IMMaskFillColorPicker'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMSettings.tsx new file mode 100644 index 00000000000..3c1b7296d9a --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMSettings.tsx @@ -0,0 +1,8 @@ +import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; +import { memo } from 'react'; + +export const IMSettings = memo(() => { + return PLACEHOLDER; +}); + +IMSettings.displayName = 'IMSettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx index 863a56c1bd7..688be5f0430 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx @@ -3,6 +3,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { caDeleted, + imReset, ipaDeleted, layerDeleted, layerReset, @@ -85,9 +86,15 @@ export const ToolChooser: React.FC = () => { if (type === 'regional_guidance') { dispatch(rgReset({ id })); } + if (type === 'inpaint_mask') { + dispatch(imReset()); + } }, [dispatch, selectedEntityIdentifier]); const isResetEnabled = useMemo( - () => selectedEntityIdentifier?.type === 'layer' || selectedEntityIdentifier?.type === 'regional_guidance', + () => + selectedEntityIdentifier?.type === 'layer' || + selectedEntityIdentifier?.type === 'regional_guidance' || + selectedEntityIdentifier?.type === 'inpaint_mask', [selectedEntityIdentifier] ); useHotkeys('shift+c', resetSelectedLayer, { enabled: isResetEnabled }, [isResetEnabled, resetSelectedLayer]); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 9f282f9734d..20bbeade0ae 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -319,6 +319,7 @@ export const { imIsEnabledToggled, imTranslated, imBboxChanged, + imFillChanged, imImageCacheChanged, imBrushLineAdded, imEraserLineAdded, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts index bff4a8a9bb7..881028b0d62 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts @@ -34,7 +34,7 @@ export const inpaintMaskReducers = { state.inpaintMask.bbox = bbox; state.inpaintMask.bboxNeedsUpdate = false; }, - inpaintMaskFillChanged: (state, action: PayloadAction<{ fill: RgbColor }>) => { + imFillChanged: (state, action: PayloadAction<{ fill: RgbColor }>) => { const { fill } = action.payload; state.inpaintMask.fill = fill; }, From 60cd505ee1511a06c5dfee6e27ca0832c9decfae Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 21 Jun 2024 14:14:09 +1000 Subject: [PATCH 107/678] feat(ui): revised docstrings for renderers & simplified api --- .../features/controlLayers/konva/events.ts | 138 +------ .../controlLayers/konva/nodeManager.ts | 142 +++---- .../controlLayers/konva/renderers/arrange.ts | 56 ++- .../konva/renderers/background.ts | 17 +- .../konva/renderers/controlAdapters.ts | 47 ++- .../konva/renderers/inpaintMask.ts | 323 ++++++++-------- .../controlLayers/konva/renderers/layers.ts | 58 +-- .../controlLayers/konva/renderers/preview.ts | 359 +++++++++++------- .../controlLayers/konva/renderers/regions.ts | 66 ++-- .../controlLayers/konva/renderers/renderer.ts | 160 +++++--- .../controlLayers/konva/renderers/stage.ts | 42 +- 11 files changed, 762 insertions(+), 646 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 0f4e4c05b21..955162cd4c7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -1,11 +1,4 @@ import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; -import { renderBackgroundLayer } from 'features/controlLayers/konva/renderers/background'; -import { - renderDocumentBoundsOverlay, - renderToolPreview, - scaleToolPreview, -} from 'features/controlLayers/konva/renderers/preview'; -import { fitDocumentToStage } from 'features/controlLayers/konva/renderers/stage'; import { getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util'; import type { BrushLineAddedArg, @@ -52,7 +45,6 @@ type Arg = { getSelectedEntity: () => CanvasEntity | null; getSpaceKey: () => boolean; setSpaceKey: (val: boolean) => void; - getDocument: () => CanvasV2State['document']; getBbox: () => CanvasV2State['bbox']; getSettings: () => CanvasV2State['settings']; onBrushLineAdded: (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => void; @@ -160,7 +152,6 @@ export const setStageEventHandlers = ({ getSelectedEntity, getSpaceKey, setSpaceKey, - getDocument, getBbox, getSettings, onBrushLineAdded, @@ -176,16 +167,7 @@ export const setStageEventHandlers = ({ stage.on('mouseenter', () => { const tool = getToolState().selected; stage.findOne(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); - renderToolPreview( - manager, - getToolState(), - getCurrentFill(), - getSelectedEntity(), - getLastCursorPos(), - getLastMouseDownPos(), - getIsDrawing(), - getIsMouseDown() - ); + manager.renderers.renderToolPreview(); }); //#region mousedown @@ -306,16 +288,7 @@ export const setStageEventHandlers = ({ setLastAddedPoint(pos); } } - renderToolPreview( - manager, - getToolState(), - getCurrentFill(), - getSelectedEntity(), - getLastCursorPos(), - getLastMouseDownPos(), - getIsDrawing(), - getIsMouseDown() - ); + manager.renderers.renderToolPreview(); }); //#region mouseup @@ -354,16 +327,7 @@ export const setStageEventHandlers = ({ setLastMouseDownPos(null); } - renderToolPreview( - manager, - getToolState(), - getCurrentFill(), - getSelectedEntity(), - getLastCursorPos(), - getLastMouseDownPos(), - getIsDrawing(), - getIsMouseDown() - ); + manager.renderers.renderToolPreview(); }); //#region mousemove @@ -469,17 +433,7 @@ export const setStageEventHandlers = ({ } } } - - renderToolPreview( - manager, - getToolState(), - getCurrentFill(), - getSelectedEntity(), - getLastCursorPos(), - getLastMouseDownPos(), - getIsDrawing(), - getIsMouseDown() - ); + manager.renderers.renderToolPreview(); }); //#region mouseleave @@ -508,16 +462,7 @@ export const setStageEventHandlers = ({ } } - renderToolPreview( - manager, - getToolState(), - getCurrentFill(), - getSelectedEntity(), - getLastCursorPos(), - getLastMouseDownPos(), - getIsDrawing(), - getIsMouseDown() - ); + manager.renderers.renderToolPreview(); }); //#region wheel @@ -558,21 +503,11 @@ export const setStageEventHandlers = ({ stage.scaleY(newScale); stage.position(newPos); setStageAttrs({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale }); - renderBackgroundLayer(manager); - scaleToolPreview(manager, getToolState()); - renderDocumentBoundsOverlay(manager, getDocument); + manager.renderers.renderBackground(); + manager.renderers.renderDocumentOverlay(); } } - renderToolPreview( - manager, - getToolState(), - getCurrentFill(), - getSelectedEntity(), - getLastCursorPos(), - getLastMouseDownPos(), - getIsDrawing(), - getIsMouseDown() - ); + manager.renderers.renderToolPreview(); }); //#region dragmove @@ -584,18 +519,9 @@ export const setStageEventHandlers = ({ height: stage.height(), scale: stage.scaleX(), }); - renderBackgroundLayer(manager); - renderDocumentBoundsOverlay(manager, getDocument); - renderToolPreview( - manager, - getToolState(), - getCurrentFill(), - getSelectedEntity(), - getLastCursorPos(), - getLastMouseDownPos(), - getIsDrawing(), - getIsMouseDown() - ); + manager.renderers.renderBackground(); + manager.renderers.renderDocumentOverlay(); + manager.renderers.renderToolPreview(); }); //#region dragend @@ -608,16 +534,7 @@ export const setStageEventHandlers = ({ height: stage.height(), scale: stage.scaleX(), }); - renderToolPreview( - manager, - getToolState(), - getCurrentFill(), - getSelectedEntity(), - getLastCursorPos(), - getLastMouseDownPos(), - getIsDrawing(), - getIsMouseDown() - ); + manager.renderers.renderToolPreview(); }); //#region key @@ -638,22 +555,12 @@ export const setStageEventHandlers = ({ setTool('view'); setSpaceKey(true); } else if (e.key === 'r') { - const stageAttrs = fitDocumentToStage(stage, getDocument()); - setStageAttrs(stageAttrs); - scaleToolPreview(manager, getToolState()); - renderBackgroundLayer(manager); - renderDocumentBoundsOverlay(manager, getDocument); + manager.renderers.fitDocumentToStage(); + manager.renderers.renderToolPreview(); + manager.renderers.renderBackground(); + manager.renderers.renderDocumentOverlay(); } - renderToolPreview( - manager, - getToolState(), - getCurrentFill(), - getSelectedEntity(), - getLastCursorPos(), - getLastMouseDownPos(), - getIsDrawing(), - getIsMouseDown() - ); + manager.renderers.renderToolPreview(); }; window.addEventListener('keydown', onKeyDown); @@ -671,16 +578,7 @@ export const setStageEventHandlers = ({ setToolBuffer(null); setSpaceKey(false); } - renderToolPreview( - manager, - getToolState(), - getCurrentFill(), - getSelectedEntity(), - getLastCursorPos(), - getLastMouseDownPos(), - getIsDrawing(), - getIsMouseDown() - ); + manager.renderers.renderToolPreview(); }; window.addEventListener('keyup', onKeyUp); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts index d1cba7bd710..1ac2e273c6b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts @@ -1,20 +1,6 @@ -import { createBackgroundLayer } from 'features/controlLayers/konva/renderers/background'; -import { - createBboxPreview, - createDocumentOverlay, - createPreviewLayer, - createToolPreview, -} from 'features/controlLayers/konva/renderers/preview'; -import type { - BrushLine, - CanvasEntity, - CanvasV2State, - EraserLine, - ImageObject, - Rect, - RectShape, -} from 'features/controlLayers/store/types'; +import type { BrushLine, CanvasEntity, EraserLine, ImageObject, RectShape } from 'features/controlLayers/store/types'; import type Konva from 'konva'; +import { assert } from 'tsafe'; export type BrushLineObjectRecord = { id: string; @@ -50,61 +36,62 @@ export type ImageObjectRecord = { type ObjectRecord = BrushLineObjectRecord | EraserLineObjectRecord | RectShapeObjectRecord | ImageObjectRecord; -export class KonvaNodeManager { - stage: Konva.Stage; - adapters: Map; - background: { layer: Konva.Layer }; - preview: { - layer: Konva.Layer; - bbox: { - group: Konva.Group; - rect: Konva.Rect; - transformer: Konva.Transformer; - }; - tool: { +type KonvaRenderers = { + renderRegions: () => void; + renderLayers: () => void; + renderControlAdapters: () => void; + renderInpaintMask: () => void; + renderBbox: () => void; + renderDocumentOverlay: () => void; + renderBackground: () => void; + renderToolPreview: () => void; + fitDocumentToStage: () => void; + arrangeEntities: () => void; +}; + +type BackgroundLayer = { + layer: Konva.Layer; +}; + +type PreviewLayer = { + layer: Konva.Layer; + bbox: { + group: Konva.Group; + rect: Konva.Rect; + transformer: Konva.Transformer; + }; + tool: { + group: Konva.Group; + brush: { group: Konva.Group; - brush: { - group: Konva.Group; - fill: Konva.Circle; - innerBorder: Konva.Circle; - outerBorder: Konva.Circle; - }; - rect: { - rect: Konva.Rect; - }; + fill: Konva.Circle; + innerBorder: Konva.Circle; + outerBorder: Konva.Circle; }; - documentOverlay: { - group: Konva.Group; - innerRect: Konva.Rect; - outerRect: Konva.Rect; + rect: { + rect: Konva.Rect; }; }; + documentOverlay: { + group: Konva.Group; + innerRect: Konva.Rect; + outerRect: Konva.Rect; + }; +}; + +export class KonvaNodeManager { + stage: Konva.Stage; + adapters: Map; + _background: BackgroundLayer | null; + _preview: PreviewLayer | null; + _renderers: KonvaRenderers | null; - constructor( - stage: Konva.Stage, - getBbox: () => CanvasV2State['bbox'], - onBboxTransformed: (bbox: Rect) => void, - getShiftKey: () => boolean, - getCtrlKey: () => boolean, - getMetaKey: () => boolean, - getAltKey: () => boolean - ) { + constructor(stage: Konva.Stage) { this.stage = stage; + this._renderers = null; + this._preview = null; + this._background = null; this.adapters = new Map(); - - this.background = { layer: createBackgroundLayer() }; - this.stage.add(this.background.layer); - - this.preview = { - layer: createPreviewLayer(), - bbox: createBboxPreview(stage, getBbox, onBboxTransformed, getShiftKey, getCtrlKey, getMetaKey, getAltKey), - tool: createToolPreview(stage), - documentOverlay: createDocumentOverlay(), - }; - this.preview.layer.add(this.preview.bbox.group); - this.preview.layer.add(this.preview.tool.group); - this.preview.layer.add(this.preview.documentOverlay.group); - this.stage.add(this.preview.layer); } add(entity: CanvasEntity, konvaLayer: Konva.Layer, konvaObjectGroup: Konva.Group): KonvaEntityAdapter { @@ -133,6 +120,33 @@ export class KonvaNodeManager { adapter.konvaLayer.destroy(); return this.adapters.delete(id); } + + set renderers(renderers: KonvaRenderers) { + this._renderers = renderers; + } + + get renderers(): KonvaRenderers { + assert(this._renderers !== null, 'Konva renderers have not been set'); + return this._renderers; + } + + set preview(preview: PreviewLayer) { + this._preview = preview; + } + + get preview(): PreviewLayer { + assert(this._preview !== null, 'Konva preview layer has not been set'); + return this._preview; + } + + set background(background: BackgroundLayer) { + this._background = background; + } + + get background(): BackgroundLayer { + assert(this._background !== null, 'Konva background layer has not been set'); + return this._background; + } } export class KonvaEntityAdapter { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts index 8e5650a3b20..6c040aaea50 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts @@ -1,23 +1,37 @@ import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; -import type { ControlAdapterEntity, LayerEntity, RegionEntity } from 'features/controlLayers/store/types'; +import type { CanvasV2State } from 'features/controlLayers/store/types'; -export const arrangeEntities = ( - manager: KonvaNodeManager, - layers: LayerEntity[], - controlAdapters: ControlAdapterEntity[], - regions: RegionEntity[] -): void => { - let zIndex = 0; - manager.background.layer.zIndex(++zIndex); - for (const layer of layers) { - manager.get(layer.id)?.konvaLayer.zIndex(++zIndex); - } - for (const ca of controlAdapters) { - manager.get(ca.id)?.konvaLayer.zIndex(++zIndex); - } - for (const rg of regions) { - manager.get(rg.id)?.konvaLayer.zIndex(++zIndex); - } - manager.get('inpaint_mask')?.konvaLayer.zIndex(++zIndex); - manager.preview.layer.zIndex(++zIndex); -}; +/** + * Gets a function to arrange the entities in the konva stage. + * @param manager The konva node manager + * @param getLayerEntityStates A function to get all layer entity states + * @param getControlAdapterEntityStates A function to get all control adapter entity states + * @param getRegionEntityStates A function to get all region entity states + * @returns An arrange entities function + */ +export const getArrangeEntities = + (arg: { + manager: KonvaNodeManager; + getLayerEntityStates: () => CanvasV2State['layers']['entities']; + getControlAdapterEntityStates: () => CanvasV2State['controlAdapters']['entities']; + getRegionEntityStates: () => CanvasV2State['regions']['entities']; + }) => + (): void => { + const { manager, getLayerEntityStates, getControlAdapterEntityStates, getRegionEntityStates } = arg; + const layers = getLayerEntityStates(); + const controlAdapters = getControlAdapterEntityStates(); + const regions = getRegionEntityStates(); + let zIndex = 0; + manager.background.layer.zIndex(++zIndex); + for (const layer of layers) { + manager.get(layer.id)?.konvaLayer.zIndex(++zIndex); + } + for (const ca of controlAdapters) { + manager.get(ca.id)?.konvaLayer.zIndex(++zIndex); + } + for (const rg of regions) { + manager.get(rg.id)?.konvaLayer.zIndex(++zIndex); + } + manager.get('inpaint_mask')?.konvaLayer.zIndex(++zIndex); + manager.preview.layer.zIndex(++zIndex); + }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts index 9fff013070e..fe7ccebe922 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts @@ -6,6 +6,11 @@ import Konva from 'konva'; const baseGridLineColor = getArbitraryBaseColor(27); const fineGridLineColor = getArbitraryBaseColor(18); +/** + * Gets the grid spacing. The value depends on the stage scale - at higher scales, the grid spacing is smaller. + * @param scale The stage scale + * @returns The grid spacing based on the stage scale + */ const getGridSpacing = (scale: number): number => { if (scale >= 2) { return 8; @@ -25,9 +30,19 @@ const getGridSpacing = (scale: number): number => { return 256; }; +/** + * Creates the background konva layer. + * @returns The background konva layer + */ export const createBackgroundLayer = (): Konva.Layer => new Konva.Layer({ id: BACKGROUND_LAYER_ID, listening: false }); -export const renderBackgroundLayer = (manager: KonvaNodeManager): void => { +/** + * Gets a render function for the background layer. + * @param arg.manager The konva node manager + * @returns A function to render the background grid + */ +export const getRenderBackground = (arg: { manager: KonvaNodeManager }) => (): void => { + const { manager } = arg; const background = manager.background.layer; background.zIndex(0); const scale = manager.stage.scaleX(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts index 952567a74d3..58b8f32922c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts @@ -6,7 +6,7 @@ import { createObjectGroup, updateImageSource, } from 'features/controlLayers/konva/renderers/objects'; -import type { ControlAdapterEntity } from 'features/controlLayers/store/types'; +import type { CanvasV2State, ControlAdapterEntity } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { isEqual } from 'lodash-es'; import { assert } from 'tsafe'; @@ -17,8 +17,8 @@ import { assert } from 'tsafe'; */ /** - * Creates a control adapter layer. - * @param stage The konva stage + * Gets a control adapter entity's konva nodes and entity adapter, creating them if they do not exist. + * @param manager The konva node manager * @param entity The control adapter layer state */ const getControlAdapter = (manager: KonvaNodeManager, entity: ControlAdapterEntity): KonvaEntityAdapter => { @@ -37,11 +37,9 @@ const getControlAdapter = (manager: KonvaNodeManager, entity: ControlAdapterEnti }; /** - * 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 entity The control adapter layer state - * @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source + * Renders a control adapter. + * @param manager The konva node manager + * @param entity The control adapter entity state */ export const renderControlAdapter = async (manager: KonvaNodeManager, entity: ControlAdapterEntity): Promise => { const adapter = getControlAdapter(manager, entity); @@ -101,14 +99,27 @@ export const renderControlAdapter = async (manager: KonvaNodeManager, entity: Co } }; -export const renderControlAdapters = (manager: KonvaNodeManager, entities: ControlAdapterEntity[]): void => { - // Destroy nonexistent layers - for (const adapters of manager.getAll('control_adapter')) { - if (!entities.find((ca) => ca.id === adapters.id)) { - manager.destroy(adapters.id); +/** + * Gets a function to render all control adapters. + * @param manager The konva node manager + * @param getControlAdapterEntityStates A function to get all control adapter entities + * @returns A function to render all control adapters + */ +export const getRenderControlAdapters = + (arg: { + manager: KonvaNodeManager; + getControlAdapterEntityStates: () => CanvasV2State['controlAdapters']['entities']; + }) => + (): void => { + const { manager, getControlAdapterEntityStates } = arg; + const entities = getControlAdapterEntityStates(); + // Destroy nonexistent layers + for (const adapters of manager.getAll('control_adapter')) { + if (!entities.find((ca) => ca.id === adapters.id)) { + manager.destroy(adapters.id); + } } - } - for (const entity of entities) { - renderControlAdapter(manager, entity); - } -}; + for (const entity of entities) { + renderControlAdapter(manager, entity); + } + }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts index afa09e0e176..ed1f155eaea 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts @@ -16,24 +16,11 @@ import { getRectShape, } from 'features/controlLayers/konva/renderers/objects'; import { mapId } from 'features/controlLayers/konva/util'; -import type { - CanvasEntity, - CanvasEntityIdentifier, - InpaintMaskEntity, - PosChangedArg, - Tool, -} from 'features/controlLayers/store/types'; +import type { CanvasEntity, CanvasV2State, InpaintMaskEntity, PosChangedArg } from 'features/controlLayers/store/types'; import Konva from 'konva'; /** - * Logic for creating and rendering regional guidance layers. - * - * Some special handling is needed to render layer opacity correctly using a "compositing rect". See the comments - * in `renderRGLayer`. - */ - -/** - * Creates the "compositing rect" for a regional guidance layer. + * Creates the "compositing rect" for the inpaint mask. * @param konvaLayer The konva layer */ const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => { @@ -43,23 +30,24 @@ const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => { }; /** - * Creates a regional guidance layer. - * @param stage The konva stage - * @param entity The regional guidance layer state - * @param onLayerPosChanged Callback for when the layer's position changes + * Gets the singleton inpaint mask entity's konva nodes and entity adapter, creating them if they do not exist. + * @param manager The konva node manager + * @param entityState The inpaint mask entity state + * @param onPosChanged Callback for when the position changes (e.g. the entity is dragged) + * @returns The konva entity adapter for the inpaint mask */ const getInpaintMask = ( manager: KonvaNodeManager, - entity: InpaintMaskEntity, + entityState: InpaintMaskEntity, onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void ): KonvaEntityAdapter => { - const adapter = manager.get(entity.id); + const adapter = manager.get(entityState.id); if (adapter) { return adapter; } // This layer hasn't been added to the konva state yet const konvaLayer = new Konva.Layer({ - id: entity.id, + id: entityState.id, name: INPAINT_MASK_LAYER_NAME, draggable: true, dragDistance: 0, @@ -69,166 +57,175 @@ const getInpaintMask = ( // the position - we do not need to call this on the `dragmove` event. if (onPosChanged) { konvaLayer.on('dragend', function (e) { - onPosChanged({ id: entity.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'inpaint_mask'); + onPosChanged({ id: entityState.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'inpaint_mask'); }); } const konvaObjectGroup = createObjectGroup(konvaLayer, INPAINT_MASK_LAYER_OBJECT_GROUP_NAME); - return manager.add(entity, konvaLayer, konvaObjectGroup); + return manager.add(entityState, konvaLayer, konvaObjectGroup); }; /** - * Renders a raster layer. - * @param stage The konva stage - * @param entity The regional guidance layer state - * @param globalMaskLayerOpacity The global mask layer opacity - * @param tool The current tool - * @param onPosChanged Callback for when the layer's position changes + * Gets the inpaint mask render function. + * @param manager The konva node manager + * @param getEntityState A function to get the inpaint mask entity state + * @param getMaskOpacity A function to get the mask opacity + * @param getToolState A function to get the tool state + * @param getSelectedEntity A function to get the selected entity + * @param onPosChanged Callback for when the position changes (e.g. the entity is dragged) + * @returns The inpaint mask render function */ -export const renderInpaintMask = ( - manager: KonvaNodeManager, - entity: InpaintMaskEntity, - globalMaskLayerOpacity: number, - tool: Tool, - selectedEntityIdentifier: CanvasEntityIdentifier | null, - onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void -): void => { - const adapter = getInpaintMask(manager, entity, onPosChanged); - - // Update the layer's position and listening state - adapter.konvaLayer.setAttrs({ - listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events - x: Math.floor(entity.x), - y: Math.floor(entity.y), - }); - - // Convert the color to a string, stripping the alpha - the object group will handle opacity. - const rgbColor = rgbColorToString(entity.fill); - - // We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required. - let groupNeedsCache = false; - - const objectIds = entity.objects.map(mapId); - // Destroy any objects that are no longer in state - for (const objectRecord of adapter.getAll()) { - if (!objectIds.includes(objectRecord.id)) { - adapter.destroy(objectRecord.id); - groupNeedsCache = true; - } - } +export const getRenderInpaintMask = + (arg: { + manager: KonvaNodeManager; + getInpaintMaskEntityState: () => CanvasV2State['inpaintMask']; + getMaskOpacity: () => number; + getToolState: () => CanvasV2State['tool']; + getSelectedEntity: () => CanvasEntity | null; + onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void; + }) => + (): void => { + const { manager, getInpaintMaskEntityState, getMaskOpacity, getToolState, getSelectedEntity, onPosChanged } = arg; + const entity = getInpaintMaskEntityState(); + const globalMaskLayerOpacity = getMaskOpacity(); + const toolState = getToolState(); + const selectedEntity = getSelectedEntity(); + const adapter = getInpaintMask(manager, entity, onPosChanged); + + // Update the layer's position and listening state + adapter.konvaLayer.setAttrs({ + listening: toolState.selected === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events + x: Math.floor(entity.x), + y: Math.floor(entity.y), + }); - for (const obj of entity.objects) { - if (obj.type === 'brush_line') { - const objectRecord = getBrushLine(adapter, obj, INPAINT_MASK_LAYER_BRUSH_LINE_NAME); + // Convert the color to a string, stripping the alpha - the object group will handle opacity. + const rgbColor = rgbColorToString(entity.fill); - // Only update the points if they have changed. The point values are never mutated, they are only added to the - // array, so checking the length is sufficient to determine if we need to re-cache. - if (objectRecord.konvaLine.points().length !== obj.points.length) { - objectRecord.konvaLine.points(obj.points); - groupNeedsCache = true; - } - // Only update the color if it has changed. - if (objectRecord.konvaLine.stroke() !== rgbColor) { - objectRecord.konvaLine.stroke(rgbColor); - groupNeedsCache = true; - } - } else if (obj.type === 'eraser_line') { - const objectRecord = getEraserLine(adapter, obj, INPAINT_MASK_LAYER_ERASER_LINE_NAME); + // We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required. + let groupNeedsCache = false; - // Only update the points if they have changed. The point values are never mutated, they are only added to the - // array, so checking the length is sufficient to determine if we need to re-cache. - if (objectRecord.konvaLine.points().length !== obj.points.length) { - objectRecord.konvaLine.points(obj.points); + const objectIds = entity.objects.map(mapId); + // Destroy any objects that are no longer in state + for (const objectRecord of adapter.getAll()) { + if (!objectIds.includes(objectRecord.id)) { + adapter.destroy(objectRecord.id); groupNeedsCache = true; } - // Only update the color if it has changed. - if (objectRecord.konvaLine.stroke() !== rgbColor) { - objectRecord.konvaLine.stroke(rgbColor); - groupNeedsCache = true; - } - } else if (obj.type === 'rect_shape') { - const objectRecord = getRectShape(adapter, obj, INPAINT_MASK_LAYER_RECT_SHAPE_NAME); + } - // Only update the color if it has changed. - if (objectRecord.konvaRect.fill() !== rgbColor) { - objectRecord.konvaRect.fill(rgbColor); - groupNeedsCache = true; + for (const obj of entity.objects) { + if (obj.type === 'brush_line') { + const objectRecord = getBrushLine(adapter, obj, INPAINT_MASK_LAYER_BRUSH_LINE_NAME); + + // Only update the points if they have changed. The point values are never mutated, they are only added to the + // array, so checking the length is sufficient to determine if we need to re-cache. + if (objectRecord.konvaLine.points().length !== obj.points.length) { + objectRecord.konvaLine.points(obj.points); + groupNeedsCache = true; + } + // Only update the color if it has changed. + if (objectRecord.konvaLine.stroke() !== rgbColor) { + objectRecord.konvaLine.stroke(rgbColor); + groupNeedsCache = true; + } + } else if (obj.type === 'eraser_line') { + const objectRecord = getEraserLine(adapter, obj, INPAINT_MASK_LAYER_ERASER_LINE_NAME); + + // Only update the points if they have changed. The point values are never mutated, they are only added to the + // array, so checking the length is sufficient to determine if we need to re-cache. + if (objectRecord.konvaLine.points().length !== obj.points.length) { + objectRecord.konvaLine.points(obj.points); + groupNeedsCache = true; + } + // Only update the color if it has changed. + if (objectRecord.konvaLine.stroke() !== rgbColor) { + objectRecord.konvaLine.stroke(rgbColor); + groupNeedsCache = true; + } + } else if (obj.type === 'rect_shape') { + const objectRecord = getRectShape(adapter, obj, INPAINT_MASK_LAYER_RECT_SHAPE_NAME); + + // Only update the color if it has changed. + if (objectRecord.konvaRect.fill() !== rgbColor) { + objectRecord.konvaRect.fill(rgbColor); + groupNeedsCache = true; + } } } - } - - // Only update layer visibility if it has changed. - if (adapter.konvaLayer.visible() !== entity.isEnabled) { - adapter.konvaLayer.visible(entity.isEnabled); - groupNeedsCache = true; - } - if (adapter.konvaObjectGroup.getChildren().length === 0) { - // No objects - clear the cache to reset the previous pixel data - adapter.konvaObjectGroup.clearCache(); - return; - } + // Only update layer visibility if it has changed. + if (adapter.konvaLayer.visible() !== entity.isEnabled) { + adapter.konvaLayer.visible(entity.isEnabled); + groupNeedsCache = true; + } - const compositingRect = - adapter.konvaLayer.findOne(`.${COMPOSITING_RECT_NAME}`) ?? createCompositingRect(adapter.konvaLayer); - const isSelected = selectedEntityIdentifier?.id === entity.id; - - /** - * When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows - * shapes to render as a "raster" layer with all pixels drawn at the same color and opacity. - * - * Without this special handling, each shape is drawn individually with the given opacity, atop the other shapes. The - * effect is like if you have a Photoshop Group consisting of many shapes, each of which has the given opacity. - * Overlapping shapes will have their colors blended together, and the final color is the result of all the shapes. - * - * 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. - */ - if (isSelected && tool !== 'move') { - // We must clear the cache first so Konva will re-draw the group with the new compositing rect - if (adapter.konvaObjectGroup.isCached()) { + if (adapter.konvaObjectGroup.getChildren().length === 0) { + // No objects - clear the cache to reset the previous pixel data adapter.konvaObjectGroup.clearCache(); + return; } - // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work - adapter.konvaObjectGroup.opacity(1); - - compositingRect.setAttrs({ - // The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already - ...(!entity.bboxNeedsUpdate && entity.bbox ? entity.bbox : getLayerBboxFast(adapter.konvaLayer)), - fill: rgbColor, - opacity: globalMaskLayerOpacity, - // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) - globalCompositeOperation: 'source-in', - visible: true, - // This rect must always be on top of all other shapes - zIndex: adapter.konvaObjectGroup.getChildren().length, - }); - } else { - // The compositing rect should only be shown when the layer is selected. - compositingRect.visible(false); - // Cache only if needed - or if we are on this code path and _don't_ have a cache - if (groupNeedsCache || !adapter.konvaObjectGroup.isCached()) { - adapter.konvaObjectGroup.cache(); + + const compositingRect = + adapter.konvaLayer.findOne(`.${COMPOSITING_RECT_NAME}`) ?? createCompositingRect(adapter.konvaLayer); + const isSelected = selectedEntity?.id === entity.id; + + /** + * When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows + * shapes to render as a "raster" layer with all pixels drawn at the same color and opacity. + * + * Without this special handling, each shape is drawn individually with the given opacity, atop the other shapes. The + * effect is like if you have a Photoshop Group consisting of many shapes, each of which has the given opacity. + * Overlapping shapes will have their colors blended together, and the final color is the result of all the shapes. + * + * 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. + */ + if (isSelected && toolState.selected !== 'move') { + // We must clear the cache first so Konva will re-draw the group with the new compositing rect + if (adapter.konvaObjectGroup.isCached()) { + adapter.konvaObjectGroup.clearCache(); + } + // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work + adapter.konvaObjectGroup.opacity(1); + + compositingRect.setAttrs({ + // The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already + ...(!entity.bboxNeedsUpdate && entity.bbox ? entity.bbox : getLayerBboxFast(adapter.konvaLayer)), + fill: rgbColor, + opacity: globalMaskLayerOpacity, + // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) + globalCompositeOperation: 'source-in', + visible: true, + // This rect must always be on top of all other shapes + zIndex: adapter.konvaObjectGroup.getChildren().length, + }); + } else { + // The compositing rect should only be shown when the layer is selected. + compositingRect.visible(false); + // Cache only if needed - or if we are on this code path and _don't_ have a cache + if (groupNeedsCache || !adapter.konvaObjectGroup.isCached()) { + adapter.konvaObjectGroup.cache(); + } + // Updating group opacity does not require re-caching + adapter.konvaObjectGroup.opacity(globalMaskLayerOpacity); } - // Updating group opacity does not require re-caching - adapter.konvaObjectGroup.opacity(globalMaskLayerOpacity); - } - // const bboxRect = - // regionMap.konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rg, regionMap.konvaLayer); - - // if (rg.bbox) { - // const active = !rg.bboxNeedsUpdate && isSelected && tool === 'move'; - // bboxRect.setAttrs({ - // visible: active, - // listening: active, - // x: rg.bbox.x, - // y: rg.bbox.y, - // width: rg.bbox.width, - // height: rg.bbox.height, - // stroke: isSelected ? BBOX_SELECTED_STROKE : '', - // }); - // } else { - // bboxRect.visible(false); - // } -}; + // const bboxRect = + // regionMap.konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rg, regionMap.konvaLayer); + + // if (rg.bbox) { + // const active = !rg.bboxNeedsUpdate && isSelected && tool === 'move'; + // bboxRect.setAttrs({ + // visible: active, + // listening: active, + // x: rg.bbox.x, + // y: rg.bbox.y, + // width: rg.bbox.width, + // height: rg.bbox.height, + // stroke: isSelected ? BBOX_SELECTED_STROKE : '', + // }); + // } else { + // bboxRect.visible(false); + // } + }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts index 475692a5999..a2ddceeef82 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -15,7 +15,7 @@ import { getRectShape, } from 'features/controlLayers/konva/renderers/objects'; import { mapId } from 'features/controlLayers/konva/util'; -import type { CanvasEntity, LayerEntity, PosChangedArg, Tool } from 'features/controlLayers/store/types'; +import type { CanvasEntity, CanvasV2State, LayerEntity, PosChangedArg, Tool } from 'features/controlLayers/store/types'; import Konva from 'konva'; /** @@ -23,10 +23,11 @@ import Konva from 'konva'; */ /** - * Creates a raster layer. - * @param stage The konva stage - * @param entity The raster layer state + * Gets layer entity's konva nodes and entity adapter, creating them if they do not exist. + * @param manager The konva node manager + * @param entity The layer entity state * @param onPosChanged Callback for when the layer's position changes + * @returns The konva entity adapter for the layer */ const getLayer = ( manager: KonvaNodeManager, @@ -58,9 +59,9 @@ const getLayer = ( }; /** - * Renders a regional guidance layer. - * @param stage The konva stage - * @param entity The regional guidance layer state + * Renders a layer. + * @param manager The konva node manager + * @param entity The layer entity state * @param tool The current tool * @param onPosChanged Callback for when the layer's position changes */ @@ -133,19 +134,32 @@ export const renderLayer = async ( adapter.konvaObjectGroup.opacity(entity.opacity); }; -export const renderLayers = ( - manager: KonvaNodeManager, - entities: LayerEntity[], - tool: Tool, - onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void -): void => { - // Destroy nonexistent layers - for (const adapter of manager.getAll('layer')) { - if (!entities.find((l) => l.id === adapter.id)) { - manager.destroy(adapter.id); +/** + * Gets a function to render all layers. + * @param manager The konva node manager + * @param getLayerEntityStates A function to get all layer entities + * @param getToolState A function to get the current tool state + * @param onPosChanged Callback for when the layer's position changes + * @returns A function to render all layers + */ +export const getRenderLayers = + (arg: { + manager: KonvaNodeManager; + getLayerEntityStates: () => CanvasV2State['layers']['entities']; + getToolState: () => CanvasV2State['tool']; + onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void; + }) => + (): void => { + const { manager, getLayerEntityStates, getToolState, onPosChanged } = arg; + const entities = getLayerEntityStates(); + const tool = getToolState(); + // Destroy nonexistent layers + for (const adapter of manager.getAll('layer')) { + if (!entities.find((l) => l.id === adapter.id)) { + manager.destroy(adapter.id); + } } - } - for (const entity of entities) { - renderLayer(manager, entity, tool, onPosChanged); - } -}; + for (const entity of entities) { + renderLayer(manager, entity, tool.selected, onPosChanged); + } + }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts index 3926dfadc02..18ff6bfb15f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts @@ -19,14 +19,29 @@ import { PREVIEW_TOOL_GROUP_ID, } from 'features/controlLayers/konva/naming'; import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; -import type { CanvasEntity, CanvasV2State, RgbaColor, Tool } from 'features/controlLayers/store/types'; +import type { CanvasEntity, CanvasV2State, RgbaColor } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect, Vector2d } from 'konva/lib/types'; import { atom } from 'nanostores'; +/** + * Creates the konva preview layer. + * @returns The konva preview layer + */ export const createPreviewLayer = (): Konva.Layer => new Konva.Layer({ id: PREVIEW_LAYER_ID, listening: true }); -export const createBboxPreview = ( +/** + * Creates the bbox konva nodes. + * @param stage The konva stage + * @param getBbox A function to get the bbox + * @param onBboxTransformed A callback for when the bbox is transformed + * @param getShiftKey A function to get the shift key state + * @param getCtrlKey A function to get the ctrl key state + * @param getMetaKey A function to get the meta key state + * @param getAltKey A function to get the alt key state + * @returns The bbox nodes + */ +export const createBboxNodes = ( stage: Konva.Stage, getBbox: () => IRect, onBboxTransformed: (bbox: IRect) => void, @@ -227,18 +242,40 @@ const ALL_ANCHORS: string[] = [ const CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right']; const NO_ANCHORS: string[] = []; -export const renderBboxPreview = (manager: KonvaNodeManager, bbox: IRect, tool: Tool): void => { - manager.preview.bbox.group.listening(tool === 'bbox'); - // This updates the bbox during transformation - manager.preview.bbox.rect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1, listening: tool === 'bbox' }); - manager.preview.bbox.transformer.setAttrs({ - listening: tool === 'bbox', - enabledAnchors: tool === 'bbox' ? ALL_ANCHORS : NO_ANCHORS, - }); -}; +/** + * Gets the bbox render function. + * @param manager The konva node manager + * @param getBbox A function to get the bbox + * @param getToolState A function to get the tool state + * @returns The bbox render function + */ +export const getRenderBbox = + (manager: KonvaNodeManager, getBbox: () => CanvasV2State['bbox'], getToolState: () => CanvasV2State['tool']) => + (): void => { + const bbox = getBbox(); + const toolState = getToolState(); + manager.preview.bbox.group.listening(toolState.selected === 'bbox'); + // This updates the bbox during transformation + manager.preview.bbox.rect.setAttrs({ + x: bbox.x, + y: bbox.y, + width: bbox.width, + height: bbox.height, + scaleX: 1, + scaleY: 1, + listening: toolState.selected === 'bbox', + }); + manager.preview.bbox.transformer.setAttrs({ + listening: toolState.selected === 'bbox', + enabledAnchors: toolState.selected === 'bbox' ? ALL_ANCHORS : NO_ANCHORS, + }); + }; -export const createToolPreview = (stage: Konva.Stage): KonvaNodeManager['preview']['tool'] => { - const scale = stage.scaleX(); +/** + * Gets the tool preview konva nodes. + * @returns The tool preview konva nodes + */ +export const createToolPreviewNodes = (): KonvaNodeManager['preview']['tool'] => { const group = new Konva.Group({ id: PREVIEW_TOOL_GROUP_ID }); // Create the brush preview group & circles @@ -253,7 +290,7 @@ export const createToolPreview = (stage: Konva.Stage): KonvaNodeManager['preview id: PREVIEW_BRUSH_BORDER_INNER_ID, listening: false, stroke: BRUSH_BORDER_INNER_COLOR, - strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, + strokeWidth: BRUSH_ERASER_BORDER_WIDTH, strokeEnabled: true, }); brushGroup.add(brushBorderInner); @@ -261,7 +298,7 @@ export const createToolPreview = (stage: Konva.Stage): KonvaNodeManager['preview id: PREVIEW_BRUSH_BORDER_OUTER_ID, listening: false, stroke: BRUSH_BORDER_OUTER_COLOR, - strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, + strokeWidth: BRUSH_ERASER_BORDER_WIDTH, strokeEnabled: true, }); brushGroup.add(brushBorderOuter); @@ -290,111 +327,138 @@ export const createToolPreview = (stage: Konva.Stage): KonvaNodeManager['preview }; /** - * Renders the preview layer. - * @param stage The konva stage - * @param tool The selected tool - * @param currentFill The selected layer's color - * @param selectedEntity The selected layer's type - * @param globalMaskLayerOpacity The global mask layer opacity - * @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 + * Gets the tool preview (brush, eraser, rect) render function. + * @param arg.manager The konva node manager + * @param arg.getToolState The selected tool + * @param arg.currentFill The selected layer's color + * @param arg.selectedEntity The selected layer's type + * @param arg.globalMaskLayerOpacity The global mask layer opacity + * @param arg.cursorPos The cursor position + * @param arg.lastMouseDownPos The position of the last mouse down event - used for the rect tool + * @param arg.brushSize The brush size + * @returns The tool preview render function */ -export const renderToolPreview = ( - manager: KonvaNodeManager, - toolState: CanvasV2State['tool'], - currentFill: RgbaColor, - selectedEntity: CanvasEntity | null, - cursorPos: Vector2d | null, - lastMouseDownPos: Vector2d | null, - isDrawing: boolean, - isMouseDown: boolean -): void => { - const stage = manager.stage; - const layerCount = manager.adapters.size; - const tool = toolState.selected; - const isDrawableEntity = - selectedEntity?.type === 'regional_guidance' || - selectedEntity?.type === 'layer' || - selectedEntity?.type === 'inpaint_mask'; - - // Update the stage's pointer style - if (tool === 'view') { - // View gets a hand - stage.container().style.cursor = isMouseDown ? 'grabbing' : 'grab'; - } else if (layerCount === 0) { - // We have no layers, so we should not render any tool - stage.container().style.cursor = 'default'; - } else if (!isDrawableEntity) { - // Non-drawable layers don't have tools - stage.container().style.cursor = 'not-allowed'; - } else if (tool === 'move') { - // Move tool gets a pointer - stage.container().style.cursor = 'default'; - } else if (tool === 'rect') { - // Rect gets a crosshair - stage.container().style.cursor = 'crosshair'; - } else if (tool === 'brush' || tool === 'eraser') { - // Hide the native cursor and use the konva-rendered brush preview - stage.container().style.cursor = 'none'; - } else if (tool === 'bbox') { - stage.container().style.cursor = 'default'; - } - - stage.draggable(tool === 'view'); - - if (!cursorPos || layerCount === 0 || !isDrawableEntity) { - // We can bail early if the mouse isn't over the stage or there are no layers - manager.preview.tool.group.visible(false); - } else { - manager.preview.tool.group.visible(true); - - // No need to render the brush preview if the cursor position or color is missing - if (cursorPos && (tool === 'brush' || tool === 'eraser')) { - const scale = stage.scaleX(); - // Update the fill circle - const radius = (tool === 'brush' ? toolState.brush.width : toolState.eraser.width) / 2; - manager.preview.tool.brush.fill.setAttrs({ - x: cursorPos.x, - y: cursorPos.y, - radius, - fill: isDrawing ? '' : rgbaColorToString(currentFill), - globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out', - }); - - // Update the inner border of the brush preview - manager.preview.tool.brush.innerBorder.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius }); - - // Update the outer border of the brush preview - manager.preview.tool.brush.outerBorder.setAttrs({ - x: cursorPos.x, - y: cursorPos.y, - radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, - }); - - scaleToolPreview(manager, toolState); - - manager.preview.tool.brush.group.visible(true); - } else { - manager.preview.tool.brush.group.visible(false); +export const getRenderToolPreview = + (arg: { + manager: KonvaNodeManager; + getToolState: () => CanvasV2State['tool']; + getCurrentFill: () => RgbaColor; + getSelectedEntity: () => CanvasEntity | null; + getLastCursorPos: () => Vector2d | null; + getLastMouseDownPos: () => Vector2d | null; + getIsDrawing: () => boolean; + getIsMouseDown: () => boolean; + }) => + (): void => { + const { + manager, + getToolState, + getCurrentFill, + getSelectedEntity, + getLastCursorPos, + getLastMouseDownPos, + getIsDrawing, + getIsMouseDown, + } = arg; + + const stage = manager.stage; + const layerCount = manager.adapters.size; + const toolState = getToolState(); + const currentFill = getCurrentFill(); + const selectedEntity = getSelectedEntity(); + const cursorPos = getLastCursorPos(); + const lastMouseDownPos = getLastMouseDownPos(); + const isDrawing = getIsDrawing(); + const isMouseDown = getIsMouseDown(); + const tool = toolState.selected; + const isDrawableEntity = + selectedEntity?.type === 'regional_guidance' || + selectedEntity?.type === 'layer' || + selectedEntity?.type === 'inpaint_mask'; + + // Update the stage's pointer style + if (tool === 'view') { + // View gets a hand + stage.container().style.cursor = isMouseDown ? 'grabbing' : 'grab'; + } else if (layerCount === 0) { + // We have no layers, so we should not render any tool + stage.container().style.cursor = 'default'; + } else if (!isDrawableEntity) { + // Non-drawable layers don't have tools + stage.container().style.cursor = 'not-allowed'; + } else if (tool === 'move') { + // Move tool gets a pointer + stage.container().style.cursor = 'default'; + } else if (tool === 'rect') { + // Rect gets a crosshair + stage.container().style.cursor = 'crosshair'; + } else if (tool === 'brush' || tool === 'eraser') { + // Hide the native cursor and use the konva-rendered brush preview + stage.container().style.cursor = 'none'; + } else if (tool === 'bbox') { + stage.container().style.cursor = 'default'; } - if (cursorPos && lastMouseDownPos && tool === 'rect') { - manager.preview.tool.rect.rect.setAttrs({ - x: Math.min(cursorPos.x, lastMouseDownPos.x), - y: Math.min(cursorPos.y, lastMouseDownPos.y), - width: Math.abs(cursorPos.x - lastMouseDownPos.x), - height: Math.abs(cursorPos.y - lastMouseDownPos.y), - fill: rgbaColorToString(currentFill), - visible: true, - }); + stage.draggable(tool === 'view'); + + if (!cursorPos || layerCount === 0 || !isDrawableEntity) { + // We can bail early if the mouse isn't over the stage or there are no layers + manager.preview.tool.group.visible(false); } else { - manager.preview.tool.rect.rect.visible(false); + manager.preview.tool.group.visible(true); + + // No need to render the brush preview if the cursor position or color is missing + if (cursorPos && (tool === 'brush' || tool === 'eraser')) { + const scale = stage.scaleX(); + // Update the fill circle + const radius = (tool === 'brush' ? toolState.brush.width : toolState.eraser.width) / 2; + manager.preview.tool.brush.fill.setAttrs({ + x: cursorPos.x, + y: cursorPos.y, + radius, + fill: isDrawing ? '' : rgbaColorToString(currentFill), + globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out', + }); + + // Update the inner border of the brush preview + manager.preview.tool.brush.innerBorder.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius }); + + // Update the outer border of the brush preview + manager.preview.tool.brush.outerBorder.setAttrs({ + x: cursorPos.x, + y: cursorPos.y, + radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, + }); + + scaleToolPreview(manager, toolState); + + manager.preview.tool.brush.group.visible(true); + } else { + manager.preview.tool.brush.group.visible(false); + } + + if (cursorPos && lastMouseDownPos && tool === 'rect') { + manager.preview.tool.rect.rect.setAttrs({ + x: Math.min(cursorPos.x, lastMouseDownPos.x), + y: Math.min(cursorPos.y, lastMouseDownPos.y), + width: Math.abs(cursorPos.x - lastMouseDownPos.x), + height: Math.abs(cursorPos.y - lastMouseDownPos.y), + fill: rgbaColorToString(currentFill), + visible: true, + }); + } else { + manager.preview.tool.rect.rect.visible(false); + } } - } -}; + }; -export const scaleToolPreview = (manager: KonvaNodeManager, toolState: CanvasV2State['tool']): void => { +/** + * Scales the tool preview nodes. Depending on the scale of the stage, the border width and radius of the brush preview + * need to be adjusted. + * @param manager The konva node manager + * @param toolState The tool state + */ +const scaleToolPreview = (manager: KonvaNodeManager, toolState: CanvasV2State['tool']): void => { const scale = manager.stage.scaleX(); const radius = (toolState.selected === 'brush' ? toolState.brush.width : toolState.eraser.width) / 2; manager.preview.tool.brush.innerBorder.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale); @@ -404,6 +468,10 @@ export const scaleToolPreview = (manager: KonvaNodeManager, toolState: CanvasV2S }); }; +/** + * Creates the document overlay konva nodes. + * @returns The document overlay konva nodes + */ export const createDocumentOverlay = (): KonvaNodeManager['preview']['documentOverlay'] => { const group = new Konva.Group({ id: 'document_overlay_group', listening: false }); const outerRect = new Konva.Rect({ @@ -423,32 +491,37 @@ export const createDocumentOverlay = (): KonvaNodeManager['preview']['documentOv return { group, innerRect, outerRect }; }; -export const renderDocumentBoundsOverlay = ( - manager: KonvaNodeManager, - getDocument: () => CanvasV2State['document'] -): void => { - const document = getDocument(); - const stage = manager.stage; - - manager.preview.documentOverlay.group.zIndex(0); - - const x = stage.x(); - const y = stage.y(); - const width = stage.width(); - const height = stage.height(); - const scale = stage.scaleX(); - - manager.preview.documentOverlay.outerRect.setAttrs({ - offsetX: x / scale, - offsetY: y / scale, - width: width / scale, - height: height / scale, - }); - - manager.preview.documentOverlay.innerRect.setAttrs({ - x: 0, - y: 0, - width: document.width, - height: document.height, - }); -}; +/** + * Gets the document overlay render function. + * @param arg.manager The konva node manager + * @param arg.getDocument A function to get the document state + * @returns The document overlay render function + */ +export const getRenderDocumentOverlay = + (arg: { manager: KonvaNodeManager; getDocument: () => CanvasV2State['document'] }) => (): void => { + const { manager, getDocument } = arg; + const document = getDocument(); + const stage = manager.stage; + + manager.preview.documentOverlay.group.zIndex(0); + + const x = stage.x(); + const y = stage.y(); + const width = stage.width(); + const height = stage.height(); + const scale = stage.scaleX(); + + manager.preview.documentOverlay.outerRect.setAttrs({ + offsetX: x / scale, + offsetY: y / scale, + width: width / scale, + height: height / scale, + }); + + manager.preview.documentOverlay.innerRect.setAttrs({ + x: 0, + y: 0, + width: document.width, + height: document.height, + }); + }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts index ae830055e25..79badb3e3ff 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts @@ -19,19 +19,13 @@ import { mapId } from 'features/controlLayers/konva/util'; import type { CanvasEntity, CanvasEntityIdentifier, + CanvasV2State, PosChangedArg, RegionEntity, Tool, } from 'features/controlLayers/store/types'; import Konva from 'konva'; -/** - * Logic for creating and rendering regional guidance layers. - * - * Some special handling is needed to render layer opacity correctly using a "compositing rect". See the comments - * in `renderRGLayer`. - */ - /** * Creates the "compositing rect" for a regional guidance layer. * @param konvaLayer The konva layer @@ -43,10 +37,11 @@ const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => { }; /** - * Creates a regional guidance layer. + * Gets a region's konva nodes and entity adapter, creating them if they do not exist. * @param stage The konva stage * @param entity The regional guidance layer state * @param onLayerPosChanged Callback for when the layer's position changes + * @returns The konva entity adapter for the region */ const getRegion = ( manager: KonvaNodeManager, @@ -78,7 +73,7 @@ const getRegion = ( }; /** - * Renders a raster layer. + * Renders a region. * @param stage The konva stage * @param entity The regional guidance layer state * @param globalMaskLayerOpacity The global mask layer opacity @@ -233,21 +228,40 @@ export const renderRegion = ( // } }; -export const renderRegions = ( - manager: KonvaNodeManager, - entities: RegionEntity[], - maskOpacity: number, - tool: Tool, - selectedEntityIdentifier: CanvasEntityIdentifier | null, - onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void -): void => { - // Destroy nonexistent layers - for (const adapter of manager.getAll('regional_guidance')) { - if (!entities.find((rg) => rg.id === adapter.id)) { - manager.destroy(adapter.id); +/** + * Gets a function to render all regions. + * @param arg.manager The konva node manager + * @param arg.getRegionEntityStates A function to get all region entities + * @param arg.getMaskOpacity A function to get the mask opacity + * @param arg.getToolState A function to get the tool state + * @param arg.getSelectedEntity A function to get the selectedEntity + * @param arg.onPosChanged A callback for when the position of an entity changes + * @returns A function to render all regions + */ +export const getRenderRegions = + (arg: { + manager: KonvaNodeManager; + getRegionEntityStates: () => CanvasV2State['regions']['entities']; + getMaskOpacity: () => number; + getToolState: () => CanvasV2State['tool']; + getSelectedEntity: () => CanvasEntity | null; + onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void; + }) => + () => { + const { manager, getRegionEntityStates, getMaskOpacity, getToolState, getSelectedEntity, onPosChanged } = arg; + const entities = getRegionEntityStates(); + const maskOpacity = getMaskOpacity(); + const toolState = getToolState(); + const selectedEntity = getSelectedEntity(); + + // Destroy the konva nodes for nonexistent entities + for (const adapter of manager.getAll('regional_guidance')) { + if (!entities.find((rg) => rg.id === adapter.id)) { + manager.destroy(adapter.id); + } } - } - for (const entity of entities) { - renderRegion(manager, entity, maskOpacity, tool, selectedEntityIdentifier, onPosChanged); - } -}; + + for (const entity of entities) { + renderRegion(manager, entity, maskOpacity, toolState.selected, selectedEntity, onPosChanged); + } + }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts index 08a996581d1..99115297f79 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -5,19 +5,23 @@ import { $isDebugging } from 'app/store/nanostores/isDebugging'; import type { RootState } from 'app/store/store'; import { setStageEventHandlers } from 'features/controlLayers/konva/events'; import { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; -import { arrangeEntities } from 'features/controlLayers/konva/renderers/arrange'; -import { renderBackgroundLayer } from 'features/controlLayers/konva/renderers/background'; +import { getArrangeEntities } from 'features/controlLayers/konva/renderers/arrange'; +import { createBackgroundLayer, getRenderBackground } from 'features/controlLayers/konva/renderers/background'; import { updateBboxes } from 'features/controlLayers/konva/renderers/bbox'; -import { renderControlAdapters } from 'features/controlLayers/konva/renderers/controlAdapters'; -import { renderInpaintMask } from 'features/controlLayers/konva/renderers/inpaintMask'; -import { renderLayers } from 'features/controlLayers/konva/renderers/layers'; +import { getRenderControlAdapters } from 'features/controlLayers/konva/renderers/controlAdapters'; +import { getRenderInpaintMask } from 'features/controlLayers/konva/renderers/inpaintMask'; +import { getRenderLayers } from 'features/controlLayers/konva/renderers/layers'; import { - renderBboxPreview, - renderDocumentBoundsOverlay, - scaleToolPreview, + createBboxNodes, + createDocumentOverlay, + createPreviewLayer, + createToolPreviewNodes, + getRenderBbox, + getRenderDocumentOverlay, + getRenderToolPreview, } from 'features/controlLayers/konva/renderers/preview'; -import { renderRegions } from 'features/controlLayers/konva/renderers/regions'; -import { fitDocumentToStage } from 'features/controlLayers/konva/renderers/stage'; +import { getRenderRegions } from 'features/controlLayers/konva/renderers/regions'; +import { getFitDocumentToStage } from 'features/controlLayers/konva/renderers/stage'; import { $stageAttrs, bboxChanged, @@ -67,8 +71,8 @@ export const $nodeManager = atom(null); /** * Initializes the canvas renderer. It subscribes to the redux store and listens for changes directly, bypassing the * react rendering cycle entirely, improving canvas performance. - * @param store The Redux store - * @param stage The Konva stage + * @param store The redux store + * @param stage The konva stage * @param container The stage's target container element * @returns A cleanup function */ @@ -180,7 +184,7 @@ export const initializeRenderer = ( dispatch(toolBufferChanged(toolBuffer)); }; - const _getSelectedEntity = (canvasV2: CanvasV2State): CanvasEntity | null => { + const selectSelectedEntity = (canvasV2: CanvasV2State): CanvasEntity | null => { const identifier = canvasV2.selectedEntityIdentifier; let selectedEntity: CanvasEntity | null = null; if (!identifier) { @@ -202,10 +206,14 @@ export const initializeRenderer = ( return selectedEntity; }; - const _getCurrentFill = (canvasV2: CanvasV2State, selectedEntity: CanvasEntity | null) => { + const selectCurrentFill = (canvasV2: CanvasV2State, selectedEntity: CanvasEntity | null) => { let currentFill: RgbaColor = canvasV2.tool.fill; - if (selectedEntity && selectedEntity.type === 'regional_guidance') { - currentFill = { ...selectedEntity.fill, a: canvasV2.settings.maskOpacity }; + if (selectedEntity) { + if (selectedEntity.type === 'regional_guidance') { + currentFill = { ...selectedEntity.fill, a: canvasV2.settings.maskOpacity }; + } else if (selectedEntity.type === 'inpaint_mask') { + currentFill = { ...canvasV2.inpaintMask.fill, a: canvasV2.settings.maskOpacity }; + } } else { currentFill = canvasV2.tool.fill; } @@ -223,14 +231,20 @@ export const initializeRenderer = ( // Read-only state, derived from redux let prevCanvasV2 = getState().canvasV2; - let prevSelectedEntity: CanvasEntity | null = _getSelectedEntity(prevCanvasV2); - let prevCurrentFill: RgbaColor = _getCurrentFill(prevCanvasV2, prevSelectedEntity); + let canvasV2 = getState().canvasV2; + let prevSelectedEntity: CanvasEntity | null = selectSelectedEntity(prevCanvasV2); + let prevCurrentFill: RgbaColor = selectCurrentFill(prevCanvasV2, prevSelectedEntity); const getSelectedEntity = () => prevSelectedEntity; const getCurrentFill = () => prevCurrentFill; - const getBbox = () => getState().canvasV2.bbox; - const getDocument = () => getState().canvasV2.document; - const getToolState = () => getState().canvasV2.tool; - const getSettings = () => getState().canvasV2.settings; + const getBbox = () => canvasV2.bbox; + const getDocument = () => canvasV2.document; + const getToolState = () => canvasV2.tool; + const getSettings = () => canvasV2.settings; + const getRegionEntityStates = () => canvasV2.regions.entities; + const getLayerEntityStates = () => canvasV2.layers.entities; + const getControlAdapterEntityStates = () => canvasV2.controlAdapters.entities; + const getMaskOpacity = () => canvasV2.settings.maskOpacity; + const getInpaintMaskEntityState = () => canvasV2.inpaintMask; // Read-write state, ephemeral interaction state let isDrawing = false; @@ -269,10 +283,22 @@ export const initializeRenderer = ( spaceKey = val; }; - const manager = new KonvaNodeManager(stage, getBbox, onBboxTransformed, $shift.get, $ctrl.get, $meta.get, $alt.get); - console.log(manager); + const manager = new KonvaNodeManager(stage); $nodeManager.set(manager); + manager.background = { layer: createBackgroundLayer() }; + manager.stage.add(manager.background.layer); + manager.preview = { + layer: createPreviewLayer(), + bbox: createBboxNodes(stage, getBbox, onBboxTransformed, $shift.get, $ctrl.get, $meta.get, $alt.get), + tool: createToolPreviewNodes(), + documentOverlay: createDocumentOverlay(), + }; + manager.preview.layer.add(manager.preview.bbox.group); + manager.preview.layer.add(manager.preview.tool.group); + manager.preview.layer.add(manager.preview.documentOverlay.group); + manager.stage.add(manager.preview.layer); + const cleanupListeners = setStageEventHandlers({ manager, getToolState, @@ -292,7 +318,6 @@ export const initializeRenderer = ( getSpaceKey, setSpaceKey, setStageAttrs: $stageAttrs.set, - getDocument, getBbox, getSettings, onBrushLineAdded, @@ -309,16 +334,57 @@ export const initializeRenderer = ( // the entire state over when needed. const debouncedUpdateBboxes = debounce(updateBboxes, 300); + manager.renderers = { + renderRegions: getRenderRegions({ + manager, + getRegionEntityStates, + getMaskOpacity, + getToolState, + getSelectedEntity, + onPosChanged, + }), + renderLayers: getRenderLayers({ manager, getLayerEntityStates, getToolState, onPosChanged }), + renderControlAdapters: getRenderControlAdapters({ manager, getControlAdapterEntityStates }), + renderInpaintMask: getRenderInpaintMask({ + manager, + getInpaintMaskEntityState, + getMaskOpacity, + getToolState, + getSelectedEntity, + onPosChanged, + }), + renderBbox: getRenderBbox(manager, getBbox, getToolState), + renderToolPreview: getRenderToolPreview({ + manager, + getToolState, + getCurrentFill, + getSelectedEntity, + getLastCursorPos, + getLastMouseDownPos, + getIsDrawing, + getIsMouseDown, + }), + renderDocumentOverlay: getRenderDocumentOverlay({ manager, getDocument }), + renderBackground: getRenderBackground({ manager }), + fitDocumentToStage: getFitDocumentToStage({ manager, getDocument, setStageAttrs: $stageAttrs.set }), + arrangeEntities: getArrangeEntities({ + manager, + getLayerEntityStates, + getControlAdapterEntityStates, + getRegionEntityStates, + }), + }; + const renderCanvas = () => { - const { canvasV2 } = store.getState(); + canvasV2 = store.getState().canvasV2; if (prevCanvasV2 === canvasV2 && !isFirstRender) { logIfDebugging('No changes detected, skipping render'); return; } - const selectedEntity = _getSelectedEntity(canvasV2); - const currentFill = _getCurrentFill(canvasV2, selectedEntity); + const selectedEntity = selectSelectedEntity(canvasV2); + const currentFill = selectCurrentFill(canvasV2, selectedEntity); if ( isFirstRender || @@ -326,7 +392,7 @@ export const initializeRenderer = ( canvasV2.tool.selected !== prevCanvasV2.tool.selected ) { logIfDebugging('Rendering layers'); - renderLayers(manager, canvasV2.layers.entities, canvasV2.tool.selected, onPosChanged); + manager.renderers.renderLayers(); } if ( @@ -336,14 +402,7 @@ export const initializeRenderer = ( canvasV2.tool.selected !== prevCanvasV2.tool.selected ) { logIfDebugging('Rendering regions'); - renderRegions( - manager, - canvasV2.regions.entities, - canvasV2.settings.maskOpacity, - canvasV2.tool.selected, - canvasV2.selectedEntityIdentifier, - onPosChanged - ); + manager.renderers.renderRegions(); } if ( @@ -353,29 +412,22 @@ export const initializeRenderer = ( canvasV2.tool.selected !== prevCanvasV2.tool.selected ) { logIfDebugging('Rendering inpaint mask'); - renderInpaintMask( - manager, - canvasV2.inpaintMask, - canvasV2.settings.maskOpacity, - canvasV2.tool.selected, - canvasV2.selectedEntityIdentifier, - onPosChanged - ); + manager.renderers.renderInpaintMask(); } if (isFirstRender || canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities) { logIfDebugging('Rendering control adapters'); - renderControlAdapters(manager, canvasV2.controlAdapters.entities); + manager.renderers.renderControlAdapters(); } if (isFirstRender || canvasV2.document !== prevCanvasV2.document) { logIfDebugging('Rendering document bounds overlay'); - renderDocumentBoundsOverlay(manager, getDocument); + manager.renderers.renderDocumentOverlay(); } if (isFirstRender || canvasV2.bbox !== prevCanvasV2.bbox || canvasV2.tool.selected !== prevCanvasV2.tool.selected) { logIfDebugging('Rendering generation bbox'); - renderBboxPreview(manager, canvasV2.bbox, canvasV2.tool.selected); + manager.renderers.renderBbox(); } if ( @@ -395,7 +447,7 @@ export const initializeRenderer = ( canvasV2.regions.entities !== prevCanvasV2.regions.entities ) { logIfDebugging('Arranging entities'); - arrangeEntities(manager, canvasV2.layers.entities, canvasV2.controlAdapters.entities, canvasV2.regions.entities); + manager.renderers.arrangeEntities(); } prevCanvasV2 = canvasV2; @@ -419,8 +471,8 @@ export const initializeRenderer = ( height: stage.height(), scale: stage.scaleX(), }); - renderBackgroundLayer(manager); - renderDocumentBoundsOverlay(manager, getDocument); + manager.renderers.renderBackground(); + manager.renderers.renderDocumentOverlay(); }; const resizeObserver = new ResizeObserver(fitStageToContainer); @@ -431,10 +483,8 @@ export const initializeRenderer = ( logIfDebugging('First render of konva stage'); // On first render, the document should be fit to the stage. - const stageAttrs = fitDocumentToStage(stage, prevCanvasV2.document); - // The HUD displays some of the stage attributes, so we need to update it here. - $stageAttrs.set(stageAttrs); - scaleToolPreview(manager, getToolState()); + manager.renderers.fitDocumentToStage(); + manager.renderers.renderToolPreview(); renderCanvas(); return () => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stage.ts index 3af86041ed5..b4741c88ac2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stage.ts @@ -1,16 +1,32 @@ import { DOCUMENT_FIT_PADDING_PX } from 'features/controlLayers/konva/constants'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import type { CanvasV2State, StageAttrs } from 'features/controlLayers/store/types'; -import type Konva from 'konva'; -export const fitDocumentToStage = (stage: Konva.Stage, document: CanvasV2State['document']): StageAttrs => { - // Fit & center the document on the stage - const width = stage.width(); - const height = stage.height(); - const docWidthWithBuffer = document.width + DOCUMENT_FIT_PADDING_PX * 2; - const docHeightWithBuffer = document.height + DOCUMENT_FIT_PADDING_PX * 2; - const scale = Math.min(Math.min(width / docWidthWithBuffer, height / docHeightWithBuffer), 1); - const x = (width - docWidthWithBuffer * scale) / 2 + DOCUMENT_FIT_PADDING_PX * scale; - const y = (height - docHeightWithBuffer * scale) / 2 + DOCUMENT_FIT_PADDING_PX * scale; - stage.setAttrs({ x, y, width, height, scaleX: scale, scaleY: scale }); - return { x, y, width, height, scale }; -}; +/** + * Gets a function to fit the document to the stage, resetting the stage scale to 100%. + * If the document is smaller than the stage, the stage scale is increased to fit the document. + * @param arg.manager The konva node manager + * @param arg.getDocument A function to get the current document state + * @param arg.setStageAttrs A function to set the stage attributes + * @returns A function to fit the document to the stage + */ +export const getFitDocumentToStage = + (arg: { + manager: KonvaNodeManager; + getDocument: () => CanvasV2State['document']; + setStageAttrs: (stageAttrs: StageAttrs) => void; + }) => + (): void => { + const { manager, getDocument, setStageAttrs } = arg; + const document = getDocument(); + // Fit & center the document on the stage + const width = manager.stage.width(); + const height = manager.stage.height(); + const docWidthWithBuffer = document.width + DOCUMENT_FIT_PADDING_PX * 2; + const docHeightWithBuffer = document.height + DOCUMENT_FIT_PADDING_PX * 2; + const scale = Math.min(Math.min(width / docWidthWithBuffer, height / docHeightWithBuffer), 1); + const x = (width - docWidthWithBuffer * scale) / 2 + DOCUMENT_FIT_PADDING_PX * scale; + const y = (height - docHeightWithBuffer * scale) / 2 + DOCUMENT_FIT_PADDING_PX * scale; + manager.stage.setAttrs({ x, y, width, height, scaleX: scale, scaleY: scale }); + setStageAttrs({ x, y, width, height, scale }); + }; From 0ec0feed2cb3c22823b1058fb28e665d4ede2a3e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 21 Jun 2024 14:47:57 +1000 Subject: [PATCH 108/678] feat(ui): even more simplified API - lean on the konva node manager to abstract imperative state API & rendering --- .../features/controlLayers/konva/events.ts | 155 +++++++----------- .../controlLayers/konva/nodeManager.ts | 94 +++++++++-- .../controlLayers/konva/renderers/arrange.ts | 22 +-- .../konva/renderers/background.ts | 139 ++++++++-------- .../konva/renderers/controlAdapters.ts | 24 +-- .../konva/renderers/inpaintMask.ts | 31 ++-- .../controlLayers/konva/renderers/layers.ts | 27 +-- .../controlLayers/konva/renderers/preview.ts | 74 ++++----- .../controlLayers/konva/renderers/regions.ts | 28 +--- .../controlLayers/konva/renderers/renderer.ts | 124 ++++++-------- .../controlLayers/konva/renderers/stage.ts | 46 ++++-- 11 files changed, 372 insertions(+), 392 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 955162cd4c7..6b8aa4bf7d9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -1,16 +1,6 @@ import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import { getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util'; -import type { - BrushLineAddedArg, - CanvasEntity, - CanvasV2State, - EraserLineAddedArg, - PointAddedToLineArg, - RectShapeAddedArg, - RgbaColor, - StageAttrs, - Tool, -} from 'features/controlLayers/store/types'; +import type { CanvasEntity } from 'features/controlLayers/store/types'; import type Konva from 'konva'; import type { Vector2d } from 'konva/lib/types'; import { clamp } from 'lodash-es'; @@ -25,43 +15,16 @@ import { } from './constants'; import { PREVIEW_TOOL_GROUP_ID } from './naming'; -type Arg = { - manager: KonvaNodeManager; - getToolState: () => CanvasV2State['tool']; - getCurrentFill: () => RgbaColor; - setTool: (tool: Tool) => void; - setToolBuffer: (tool: Tool | null) => void; - getIsDrawing: () => boolean; - setIsDrawing: (isDrawing: boolean) => void; - getIsMouseDown: () => boolean; - setIsMouseDown: (isMouseDown: boolean) => void; - getLastMouseDownPos: () => Vector2d | null; - setLastMouseDownPos: (pos: Vector2d | null) => void; - getLastCursorPos: () => Vector2d | null; - setLastCursorPos: (pos: Vector2d | null) => void; - getLastAddedPoint: () => Vector2d | null; - setLastAddedPoint: (pos: Vector2d | null) => void; - setStageAttrs: (attrs: StageAttrs) => void; - getSelectedEntity: () => CanvasEntity | null; - getSpaceKey: () => boolean; - setSpaceKey: (val: boolean) => void; - getBbox: () => CanvasV2State['bbox']; - getSettings: () => CanvasV2State['settings']; - onBrushLineAdded: (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => void; - onEraserLineAdded: (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => void; - onPointAddedToLine: (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => void; - onRectShapeAdded: (arg: RectShapeAddedArg, entityType: CanvasEntity['type']) => void; - onBrushWidthChanged: (size: number) => void; - onEraserWidthChanged: (size: number) => void; -}; - /** * Updates the last cursor position atom with the current cursor position, returning the new position or `null` if the * cursor is not over the stage. * @param stage The konva stage * @param setLastCursorPos The callback to store the cursor pos */ -const updateLastCursorPos = (stage: Konva.Stage, setLastCursorPos: Arg['setLastCursorPos']) => { +const updateLastCursorPos = ( + stage: Konva.Stage, + setLastCursorPos: KonvaNodeManager['stateApi']['setLastCursorPos'] +) => { const pos = getScaledFlooredCursorPosition(stage); if (!pos) { return null; @@ -93,10 +56,10 @@ const calculateNewBrushSize = (brushSize: number, delta: number) => { const maybeAddNextPoint = ( selectedEntity: CanvasEntity, currentPos: Vector2d, - getToolState: Arg['getToolState'], - getLastAddedPoint: Arg['getLastAddedPoint'], - setLastAddedPoint: Arg['setLastAddedPoint'], - onPointAddedToLine: Arg['onPointAddedToLine'] + getToolState: KonvaNodeManager['stateApi']['getToolState'], + getLastAddedPoint: KonvaNodeManager['stateApi']['getLastAddedPoint'], + setLastAddedPoint: KonvaNodeManager['stateApi']['setLastAddedPoint'], + onPointAddedToLine: KonvaNodeManager['stateApi']['onPointAddedToLine'] ) => { const isDrawableEntity = selectedEntity?.type === 'regional_guidance' || @@ -132,42 +95,42 @@ const maybeAddNextPoint = ( ); }; -export const setStageEventHandlers = ({ - manager, - getToolState, - getCurrentFill, - setTool, - setToolBuffer, - getIsDrawing, - setIsDrawing, - getIsMouseDown, - setIsMouseDown, - getLastMouseDownPos, - setLastMouseDownPos, - getLastCursorPos, - setLastCursorPos, - getLastAddedPoint, - setLastAddedPoint, - setStageAttrs, - getSelectedEntity, - getSpaceKey, - setSpaceKey, - getBbox, - getSettings, - onBrushLineAdded, - onEraserLineAdded, - onPointAddedToLine, - onRectShapeAdded, - onBrushWidthChanged: onBrushSizeChanged, - onEraserWidthChanged: onEraserSizeChanged, -}: Arg): (() => void) => { - const stage = manager.stage; +export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) => { + const { stage, stateApi } = manager; + const { + getToolState, + getCurrentFill, + setTool, + setToolBuffer, + getIsDrawing, + setIsDrawing, + getIsMouseDown, + setIsMouseDown, + getLastMouseDownPos, + setLastMouseDownPos, + getLastCursorPos, + setLastCursorPos, + getLastAddedPoint, + setLastAddedPoint, + setStageAttrs, + getSelectedEntity, + getSpaceKey, + setSpaceKey, + getBbox, + getSettings, + onBrushLineAdded, + onEraserLineAdded, + onPointAddedToLine, + onRectShapeAdded, + onBrushWidthChanged, + onEraserWidthChanged, + } = stateApi; //#region mouseenter stage.on('mouseenter', () => { const tool = getToolState().selected; stage.findOne(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); - manager.renderers.renderToolPreview(); + manager.konvaApi.renderToolPreview(); }); //#region mousedown @@ -288,7 +251,7 @@ export const setStageEventHandlers = ({ setLastAddedPoint(pos); } } - manager.renderers.renderToolPreview(); + manager.konvaApi.renderToolPreview(); }); //#region mouseup @@ -327,7 +290,7 @@ export const setStageEventHandlers = ({ setLastMouseDownPos(null); } - manager.renderers.renderToolPreview(); + manager.konvaApi.renderToolPreview(); }); //#region mousemove @@ -433,7 +396,7 @@ export const setStageEventHandlers = ({ } } } - manager.renderers.renderToolPreview(); + manager.konvaApi.renderToolPreview(); }); //#region mouseleave @@ -462,7 +425,7 @@ export const setStageEventHandlers = ({ } } - manager.renderers.renderToolPreview(); + manager.konvaApi.renderToolPreview(); }); //#region wheel @@ -477,9 +440,9 @@ export const setStageEventHandlers = ({ } // Holding ctrl or meta while scrolling changes the brush size if (toolState.selected === 'brush') { - onBrushSizeChanged(calculateNewBrushSize(toolState.brush.width, delta)); + onBrushWidthChanged(calculateNewBrushSize(toolState.brush.width, delta)); } else if (toolState.selected === 'eraser') { - onEraserSizeChanged(calculateNewBrushSize(toolState.eraser.width, delta)); + onEraserWidthChanged(calculateNewBrushSize(toolState.eraser.width, delta)); } } else { // We need the absolute cursor position - not the scaled position @@ -503,11 +466,11 @@ export const setStageEventHandlers = ({ stage.scaleY(newScale); stage.position(newPos); setStageAttrs({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale }); - manager.renderers.renderBackground(); - manager.renderers.renderDocumentOverlay(); + manager.konvaApi.renderBackground(); + manager.konvaApi.renderDocumentOverlay(); } } - manager.renderers.renderToolPreview(); + manager.konvaApi.renderToolPreview(); }); //#region dragmove @@ -519,9 +482,9 @@ export const setStageEventHandlers = ({ height: stage.height(), scale: stage.scaleX(), }); - manager.renderers.renderBackground(); - manager.renderers.renderDocumentOverlay(); - manager.renderers.renderToolPreview(); + manager.konvaApi.renderBackground(); + manager.konvaApi.renderDocumentOverlay(); + manager.konvaApi.renderToolPreview(); }); //#region dragend @@ -534,7 +497,7 @@ export const setStageEventHandlers = ({ height: stage.height(), scale: stage.scaleX(), }); - manager.renderers.renderToolPreview(); + manager.konvaApi.renderToolPreview(); }); //#region key @@ -555,12 +518,12 @@ export const setStageEventHandlers = ({ setTool('view'); setSpaceKey(true); } else if (e.key === 'r') { - manager.renderers.fitDocumentToStage(); - manager.renderers.renderToolPreview(); - manager.renderers.renderBackground(); - manager.renderers.renderDocumentOverlay(); + manager.konvaApi.fitDocumentToStage(); + manager.konvaApi.renderToolPreview(); + manager.konvaApi.renderBackground(); + manager.konvaApi.renderDocumentOverlay(); } - manager.renderers.renderToolPreview(); + manager.konvaApi.renderToolPreview(); }; window.addEventListener('keydown', onKeyDown); @@ -578,7 +541,7 @@ export const setStageEventHandlers = ({ setToolBuffer(null); setSpaceKey(false); } - manager.renderers.renderToolPreview(); + manager.konvaApi.renderToolPreview(); }; window.addEventListener('keyup', onKeyUp); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts index 1ac2e273c6b..c093f87ee03 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts @@ -1,5 +1,22 @@ -import type { BrushLine, CanvasEntity, EraserLine, ImageObject, RectShape } from 'features/controlLayers/store/types'; +import type { + BrushLine, + BrushLineAddedArg, + CanvasEntity, + CanvasV2State, + EraserLine, + EraserLineAddedArg, + ImageObject, + PointAddedToLineArg, + PosChangedArg, + Rect, + RectShape, + RectShapeAddedArg, + RgbaColor, + StageAttrs, + Tool, +} from 'features/controlLayers/store/types'; import type Konva from 'konva'; +import type { Vector2d } from 'konva/lib/types'; import { assert } from 'tsafe'; export type BrushLineObjectRecord = { @@ -36,7 +53,7 @@ export type ImageObjectRecord = { type ObjectRecord = BrushLineObjectRecord | EraserLineObjectRecord | RectShapeObjectRecord | ImageObjectRecord; -type KonvaRenderers = { +type KonvaApi = { renderRegions: () => void; renderLayers: () => void; renderControlAdapters: () => void; @@ -45,8 +62,9 @@ type KonvaRenderers = { renderDocumentOverlay: () => void; renderBackground: () => void; renderToolPreview: () => void; - fitDocumentToStage: () => void; arrangeEntities: () => void; + fitDocumentToStage: () => void; + fitStageToContainer: () => void; }; type BackgroundLayer = { @@ -79,18 +97,63 @@ type PreviewLayer = { }; }; +type StateApi = { + getToolState: () => CanvasV2State['tool']; + getCurrentFill: () => RgbaColor; + setTool: (tool: Tool) => void; + setToolBuffer: (tool: Tool | null) => void; + getIsDrawing: () => boolean; + setIsDrawing: (isDrawing: boolean) => void; + getIsMouseDown: () => boolean; + setIsMouseDown: (isMouseDown: boolean) => void; + getLastMouseDownPos: () => Vector2d | null; + setLastMouseDownPos: (pos: Vector2d | null) => void; + getLastCursorPos: () => Vector2d | null; + setLastCursorPos: (pos: Vector2d | null) => void; + getLastAddedPoint: () => Vector2d | null; + setLastAddedPoint: (pos: Vector2d | null) => void; + setStageAttrs: (attrs: StageAttrs) => void; + getSelectedEntity: () => CanvasEntity | null; + getSpaceKey: () => boolean; + setSpaceKey: (val: boolean) => void; + getBbox: () => CanvasV2State['bbox']; + getSettings: () => CanvasV2State['settings']; + onBrushLineAdded: (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => void; + onEraserLineAdded: (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => void; + onPointAddedToLine: (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => void; + onRectShapeAdded: (arg: RectShapeAddedArg, entityType: CanvasEntity['type']) => void; + onBrushWidthChanged: (size: number) => void; + onEraserWidthChanged: (size: number) => void; + getMaskOpacity: () => number; + onPosChanged: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void; + onBboxTransformed: (bbox: Rect) => void; + getShiftKey: () => boolean; + getCtrlKey: () => boolean; + getMetaKey: () => boolean; + getAltKey: () => boolean; + getDocument: () => CanvasV2State['document']; + getLayerEntityStates: () => CanvasV2State['layers']['entities']; + getControlAdapterEntityStates: () => CanvasV2State['controlAdapters']['entities']; + getRegionEntityStates: () => CanvasV2State['regions']['entities']; + getInpaintMaskEntityState: () => CanvasV2State['inpaintMask']; +}; + export class KonvaNodeManager { stage: Konva.Stage; + container: HTMLDivElement; adapters: Map; _background: BackgroundLayer | null; _preview: PreviewLayer | null; - _renderers: KonvaRenderers | null; + _konvaApi: KonvaApi | null; + _stateApi: StateApi | null; - constructor(stage: Konva.Stage) { + constructor(stage: Konva.Stage, container: HTMLDivElement) { this.stage = stage; - this._renderers = null; + this.container = container; + this._konvaApi = null; this._preview = null; this._background = null; + this._stateApi = null; this.adapters = new Map(); } @@ -121,13 +184,13 @@ export class KonvaNodeManager { return this.adapters.delete(id); } - set renderers(renderers: KonvaRenderers) { - this._renderers = renderers; + set konvaApi(konvaApi: KonvaApi) { + this._konvaApi = konvaApi; } - get renderers(): KonvaRenderers { - assert(this._renderers !== null, 'Konva renderers have not been set'); - return this._renderers; + get konvaApi(): KonvaApi { + assert(this._konvaApi !== null, 'Konva API has not been set'); + return this._konvaApi; } set preview(preview: PreviewLayer) { @@ -147,6 +210,15 @@ export class KonvaNodeManager { assert(this._background !== null, 'Konva background layer has not been set'); return this._background; } + + set stateApi(stateApi: StateApi) { + this._stateApi = stateApi; + } + + get stateApi(): StateApi { + assert(this._stateApi !== null, 'State API has not been set'); + return this._stateApi; + } } export class KonvaEntityAdapter { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts index 6c040aaea50..dc640b10bbe 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts @@ -1,23 +1,14 @@ import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; -import type { CanvasV2State } from 'features/controlLayers/store/types'; /** * Gets a function to arrange the entities in the konva stage. * @param manager The konva node manager - * @param getLayerEntityStates A function to get all layer entity states - * @param getControlAdapterEntityStates A function to get all control adapter entity states - * @param getRegionEntityStates A function to get all region entity states * @returns An arrange entities function */ -export const getArrangeEntities = - (arg: { - manager: KonvaNodeManager; - getLayerEntityStates: () => CanvasV2State['layers']['entities']; - getControlAdapterEntityStates: () => CanvasV2State['controlAdapters']['entities']; - getRegionEntityStates: () => CanvasV2State['regions']['entities']; - }) => - (): void => { - const { manager, getLayerEntityStates, getControlAdapterEntityStates, getRegionEntityStates } = arg; +export const getArrangeEntities = (manager: KonvaNodeManager) => { + const { getLayerEntityStates, getControlAdapterEntityStates, getRegionEntityStates } = manager.stateApi; + + function arrangeEntities(): void { const layers = getLayerEntityStates(); const controlAdapters = getControlAdapterEntityStates(); const regions = getRegionEntityStates(); @@ -34,4 +25,7 @@ export const getArrangeEntities = } manager.get('inpaint_mask')?.konvaLayer.zIndex(++zIndex); manager.preview.layer.zIndex(++zIndex); - }; + } + + return arrangeEntities; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts index fe7ccebe922..15aa97e096d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts @@ -38,82 +38,85 @@ export const createBackgroundLayer = (): Konva.Layer => new Konva.Layer({ id: BA /** * Gets a render function for the background layer. - * @param arg.manager The konva node manager + * @param manager The konva node manager * @returns A function to render the background grid */ -export const getRenderBackground = (arg: { manager: KonvaNodeManager }) => (): void => { - const { manager } = arg; - const background = manager.background.layer; - background.zIndex(0); - const scale = manager.stage.scaleX(); - const gridSpacing = getGridSpacing(scale); - const x = manager.stage.x(); - const y = manager.stage.y(); - const width = manager.stage.width(); - const height = manager.stage.height(); - const stageRect = { - x1: 0, - y1: 0, - x2: width, - y2: height, - }; +export const getRenderBackground = (manager: KonvaNodeManager) => { + function renderBackground(): void { + const background = manager.background.layer; + background.zIndex(0); + const scale = manager.stage.scaleX(); + const gridSpacing = getGridSpacing(scale); + const x = manager.stage.x(); + const y = manager.stage.y(); + const width = manager.stage.width(); + const height = manager.stage.height(); + const stageRect = { + x1: 0, + y1: 0, + x2: width, + y2: height, + }; - const gridOffset = { - x: Math.ceil(x / scale / gridSpacing) * gridSpacing, - y: Math.ceil(y / scale / gridSpacing) * gridSpacing, - }; + const gridOffset = { + x: Math.ceil(x / scale / gridSpacing) * gridSpacing, + y: Math.ceil(y / scale / gridSpacing) * gridSpacing, + }; - const gridRect = { - x1: -gridOffset.x, - y1: -gridOffset.y, - x2: width / scale - gridOffset.x + gridSpacing, - y2: height / scale - gridOffset.y + gridSpacing, - }; + const gridRect = { + x1: -gridOffset.x, + y1: -gridOffset.y, + x2: width / scale - gridOffset.x + gridSpacing, + y2: height / scale - gridOffset.y + gridSpacing, + }; - const gridFullRect = { - x1: Math.min(stageRect.x1, gridRect.x1), - y1: Math.min(stageRect.y1, gridRect.y1), - x2: Math.max(stageRect.x2, gridRect.x2), - y2: Math.max(stageRect.y2, gridRect.y2), - }; + const gridFullRect = { + x1: Math.min(stageRect.x1, gridRect.x1), + y1: Math.min(stageRect.y1, gridRect.y1), + x2: Math.max(stageRect.x2, gridRect.x2), + y2: Math.max(stageRect.y2, gridRect.y2), + }; - // find the x & y size of the grid - const xSize = gridFullRect.x2 - gridFullRect.x1; - const ySize = gridFullRect.y2 - gridFullRect.y1; - // compute the number of steps required on each axis. - const xSteps = Math.round(xSize / gridSpacing) + 1; - const ySteps = Math.round(ySize / gridSpacing) + 1; + // find the x & y size of the grid + const xSize = gridFullRect.x2 - gridFullRect.x1; + const ySize = gridFullRect.y2 - gridFullRect.y1; + // compute the number of steps required on each axis. + const xSteps = Math.round(xSize / gridSpacing) + 1; + const ySteps = Math.round(ySize / gridSpacing) + 1; - const strokeWidth = 1 / scale; - let _x = 0; - let _y = 0; + const strokeWidth = 1 / scale; + let _x = 0; + let _y = 0; - background.destroyChildren(); + background.destroyChildren(); - for (let i = 0; i < xSteps; i++) { - _x = gridFullRect.x1 + i * gridSpacing; - background.add( - new Konva.Line({ - x: _x, - y: gridFullRect.y1, - points: [0, 0, 0, ySize], - stroke: _x % 64 ? fineGridLineColor : baseGridLineColor, - strokeWidth, - listening: false, - }) - ); - } - for (let i = 0; i < ySteps; i++) { - _y = gridFullRect.y1 + i * gridSpacing; - background.add( - new Konva.Line({ - x: gridFullRect.x1, - y: _y, - points: [0, 0, xSize, 0], - stroke: _y % 64 ? fineGridLineColor : baseGridLineColor, - strokeWidth, - listening: false, - }) - ); + for (let i = 0; i < xSteps; i++) { + _x = gridFullRect.x1 + i * gridSpacing; + background.add( + new Konva.Line({ + x: _x, + y: gridFullRect.y1, + points: [0, 0, 0, ySize], + stroke: _x % 64 ? fineGridLineColor : baseGridLineColor, + strokeWidth, + listening: false, + }) + ); + } + for (let i = 0; i < ySteps; i++) { + _y = gridFullRect.y1 + i * gridSpacing; + background.add( + new Konva.Line({ + x: gridFullRect.x1, + y: _y, + points: [0, 0, xSize, 0], + stroke: _y % 64 ? fineGridLineColor : baseGridLineColor, + strokeWidth, + listening: false, + }) + ); + } } + + return renderBackground; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts index 58b8f32922c..d9db711147a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts @@ -6,16 +6,11 @@ import { createObjectGroup, updateImageSource, } from 'features/controlLayers/konva/renderers/objects'; -import type { CanvasV2State, ControlAdapterEntity } from 'features/controlLayers/store/types'; +import type { ControlAdapterEntity } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { isEqual } from 'lodash-es'; import { assert } from 'tsafe'; -/** - * Logic for creating and rendering control adapter (control net & t2i adapter) layers. These layers have image objects - * and require some special handling to update the source and attributes as control images are swapped or processed. - */ - /** * Gets a control adapter entity's konva nodes and entity adapter, creating them if they do not exist. * @param manager The konva node manager @@ -102,16 +97,12 @@ export const renderControlAdapter = async (manager: KonvaNodeManager, entity: Co /** * Gets a function to render all control adapters. * @param manager The konva node manager - * @param getControlAdapterEntityStates A function to get all control adapter entities * @returns A function to render all control adapters */ -export const getRenderControlAdapters = - (arg: { - manager: KonvaNodeManager; - getControlAdapterEntityStates: () => CanvasV2State['controlAdapters']['entities']; - }) => - (): void => { - const { manager, getControlAdapterEntityStates } = arg; +export const getRenderControlAdapters = (manager: KonvaNodeManager) => { + const { getControlAdapterEntityStates } = manager.stateApi; + + function renderControlAdapters(): void { const entities = getControlAdapterEntityStates(); // Destroy nonexistent layers for (const adapters of manager.getAll('control_adapter')) { @@ -122,4 +113,7 @@ export const getRenderControlAdapters = for (const entity of entities) { renderControlAdapter(manager, entity); } - }; + } + + return renderControlAdapters; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts index ed1f155eaea..3c1c103ff46 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts @@ -16,7 +16,7 @@ import { getRectShape, } from 'features/controlLayers/konva/renderers/objects'; import { mapId } from 'features/controlLayers/konva/util'; -import type { CanvasEntity, CanvasV2State, InpaintMaskEntity, PosChangedArg } from 'features/controlLayers/store/types'; +import type { CanvasEntity, InpaintMaskEntity, PosChangedArg } from 'features/controlLayers/store/types'; import Konva from 'konva'; /** @@ -66,26 +66,14 @@ const getInpaintMask = ( }; /** - * Gets the inpaint mask render function. + * Gets a function to render the inpaint mask. * @param manager The konva node manager - * @param getEntityState A function to get the inpaint mask entity state - * @param getMaskOpacity A function to get the mask opacity - * @param getToolState A function to get the tool state - * @param getSelectedEntity A function to get the selected entity - * @param onPosChanged Callback for when the position changes (e.g. the entity is dragged) - * @returns The inpaint mask render function + * @returns A function to render the inpaint mask */ -export const getRenderInpaintMask = - (arg: { - manager: KonvaNodeManager; - getInpaintMaskEntityState: () => CanvasV2State['inpaintMask']; - getMaskOpacity: () => number; - getToolState: () => CanvasV2State['tool']; - getSelectedEntity: () => CanvasEntity | null; - onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void; - }) => - (): void => { - const { manager, getInpaintMaskEntityState, getMaskOpacity, getToolState, getSelectedEntity, onPosChanged } = arg; +export const getRenderInpaintMask = (manager: KonvaNodeManager) => { + const { getInpaintMaskEntityState, getMaskOpacity, getToolState, getSelectedEntity, onPosChanged } = manager.stateApi; + + function renderInpaintMask(): void { const entity = getInpaintMaskEntityState(); const globalMaskLayerOpacity = getMaskOpacity(); const toolState = getToolState(); @@ -228,4 +216,7 @@ export const getRenderInpaintMask = // } else { // bboxRect.visible(false); // } - }; + } + + return renderInpaintMask; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts index a2ddceeef82..eff2f7de858 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -15,13 +15,9 @@ import { getRectShape, } from 'features/controlLayers/konva/renderers/objects'; import { mapId } from 'features/controlLayers/konva/util'; -import type { CanvasEntity, CanvasV2State, LayerEntity, PosChangedArg, Tool } from 'features/controlLayers/store/types'; +import type { CanvasEntity, LayerEntity, PosChangedArg, Tool } from 'features/controlLayers/store/types'; import Konva from 'konva'; -/** - * Logic for creating and rendering raster layers. - */ - /** * Gets layer entity's konva nodes and entity adapter, creating them if they do not exist. * @param manager The konva node manager @@ -137,20 +133,12 @@ export const renderLayer = async ( /** * Gets a function to render all layers. * @param manager The konva node manager - * @param getLayerEntityStates A function to get all layer entities - * @param getToolState A function to get the current tool state - * @param onPosChanged Callback for when the layer's position changes * @returns A function to render all layers */ -export const getRenderLayers = - (arg: { - manager: KonvaNodeManager; - getLayerEntityStates: () => CanvasV2State['layers']['entities']; - getToolState: () => CanvasV2State['tool']; - onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void; - }) => - (): void => { - const { manager, getLayerEntityStates, getToolState, onPosChanged } = arg; +export const getRenderLayers = (manager: KonvaNodeManager) => { + const { getLayerEntityStates, getToolState, onPosChanged } = manager.stateApi; + + function renderLayers(): void { const entities = getLayerEntityStates(); const tool = getToolState(); // Destroy nonexistent layers @@ -162,4 +150,7 @@ export const getRenderLayers = for (const entity of entities) { renderLayer(manager, entity, tool.selected, onPosChanged); } - }; + } + + return renderLayers; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts index 18ff6bfb15f..b5dcfb1df48 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts @@ -19,9 +19,9 @@ import { PREVIEW_TOOL_GROUP_ID, } from 'features/controlLayers/konva/naming'; import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; -import type { CanvasEntity, CanvasV2State, RgbaColor } from 'features/controlLayers/store/types'; +import type { CanvasV2State } from 'features/controlLayers/store/types'; import Konva from 'konva'; -import type { IRect, Vector2d } from 'konva/lib/types'; +import type { IRect } from 'konva/lib/types'; import { atom } from 'nanostores'; /** @@ -245,13 +245,12 @@ const NO_ANCHORS: string[] = []; /** * Gets the bbox render function. * @param manager The konva node manager - * @param getBbox A function to get the bbox - * @param getToolState A function to get the tool state * @returns The bbox render function */ -export const getRenderBbox = - (manager: KonvaNodeManager, getBbox: () => CanvasV2State['bbox'], getToolState: () => CanvasV2State['tool']) => - (): void => { +export const getRenderBbox = (manager: KonvaNodeManager) => { + const { getBbox, getToolState } = manager.stateApi; + + return (): void => { const bbox = getBbox(); const toolState = getToolState(); manager.preview.bbox.group.listening(toolState.selected === 'bbox'); @@ -270,6 +269,7 @@ export const getRenderBbox = enabledAnchors: toolState.selected === 'bbox' ? ALL_ANCHORS : NO_ANCHORS, }); }; +}; /** * Gets the tool preview konva nodes. @@ -328,39 +328,21 @@ export const createToolPreviewNodes = (): KonvaNodeManager['preview']['tool'] => /** * Gets the tool preview (brush, eraser, rect) render function. - * @param arg.manager The konva node manager - * @param arg.getToolState The selected tool - * @param arg.currentFill The selected layer's color - * @param arg.selectedEntity The selected layer's type - * @param arg.globalMaskLayerOpacity The global mask layer opacity - * @param arg.cursorPos The cursor position - * @param arg.lastMouseDownPos The position of the last mouse down event - used for the rect tool - * @param arg.brushSize The brush size + * @param manager The konva node manager * @returns The tool preview render function */ -export const getRenderToolPreview = - (arg: { - manager: KonvaNodeManager; - getToolState: () => CanvasV2State['tool']; - getCurrentFill: () => RgbaColor; - getSelectedEntity: () => CanvasEntity | null; - getLastCursorPos: () => Vector2d | null; - getLastMouseDownPos: () => Vector2d | null; - getIsDrawing: () => boolean; - getIsMouseDown: () => boolean; - }) => - (): void => { - const { - manager, - getToolState, - getCurrentFill, - getSelectedEntity, - getLastCursorPos, - getLastMouseDownPos, - getIsDrawing, - getIsMouseDown, - } = arg; - +export const getRenderToolPreview = (manager: KonvaNodeManager) => { + const { + getToolState, + getCurrentFill, + getSelectedEntity, + getLastCursorPos, + getLastMouseDownPos, + getIsDrawing, + getIsMouseDown, + } = manager.stateApi; + + return (): void => { const stage = manager.stage; const layerCount = manager.adapters.size; const toolState = getToolState(); @@ -451,6 +433,7 @@ export const getRenderToolPreview = } } }; +}; /** * Scales the tool preview nodes. Depending on the scale of the stage, the border width and radius of the brush preview @@ -493,13 +476,13 @@ export const createDocumentOverlay = (): KonvaNodeManager['preview']['documentOv /** * Gets the document overlay render function. - * @param arg.manager The konva node manager - * @param arg.getDocument A function to get the document state + * @param manager The konva node manager * @returns The document overlay render function */ -export const getRenderDocumentOverlay = - (arg: { manager: KonvaNodeManager; getDocument: () => CanvasV2State['document'] }) => (): void => { - const { manager, getDocument } = arg; +export const getRenderDocumentOverlay = (manager: KonvaNodeManager) => { + const { getDocument } = manager.stateApi; + + function renderDocumentOverlay(): void { const document = getDocument(); const stage = manager.stage; @@ -524,4 +507,7 @@ export const getRenderDocumentOverlay = width: document.width, height: document.height, }); - }; + } + + return renderDocumentOverlay; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts index 79badb3e3ff..a6087b320be 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts @@ -19,7 +19,6 @@ import { mapId } from 'features/controlLayers/konva/util'; import type { CanvasEntity, CanvasEntityIdentifier, - CanvasV2State, PosChangedArg, RegionEntity, Tool, @@ -230,25 +229,13 @@ export const renderRegion = ( /** * Gets a function to render all regions. - * @param arg.manager The konva node manager - * @param arg.getRegionEntityStates A function to get all region entities - * @param arg.getMaskOpacity A function to get the mask opacity - * @param arg.getToolState A function to get the tool state - * @param arg.getSelectedEntity A function to get the selectedEntity - * @param arg.onPosChanged A callback for when the position of an entity changes + * @param manager The konva node manager * @returns A function to render all regions */ -export const getRenderRegions = - (arg: { - manager: KonvaNodeManager; - getRegionEntityStates: () => CanvasV2State['regions']['entities']; - getMaskOpacity: () => number; - getToolState: () => CanvasV2State['tool']; - getSelectedEntity: () => CanvasEntity | null; - onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void; - }) => - () => { - const { manager, getRegionEntityStates, getMaskOpacity, getToolState, getSelectedEntity, onPosChanged } = arg; +export const getRenderRegions = (manager: KonvaNodeManager) => { + const { getRegionEntityStates, getMaskOpacity, getToolState, getSelectedEntity, onPosChanged } = manager.stateApi; + + function renderRegions(): void { const entities = getRegionEntityStates(); const maskOpacity = getMaskOpacity(); const toolState = getToolState(); @@ -264,4 +251,7 @@ export const getRenderRegions = for (const entity of entities) { renderRegion(manager, entity, maskOpacity, toolState.selected, selectedEntity, onPosChanged); } - }; + } + + return renderRegions; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts index 99115297f79..dcd876de018 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -21,7 +21,7 @@ import { getRenderToolPreview, } from 'features/controlLayers/konva/renderers/preview'; import { getRenderRegions } from 'features/controlLayers/konva/renderers/regions'; -import { getFitDocumentToStage } from 'features/controlLayers/konva/renderers/stage'; +import { getFitDocumentToStage, getFitStageToContainer } from 'features/controlLayers/konva/renderers/stage'; import { $stageAttrs, bboxChanged, @@ -283,7 +283,7 @@ export const initializeRenderer = ( spaceKey = val; }; - const manager = new KonvaNodeManager(stage); + const manager = new KonvaNodeManager(stage, container); $nodeManager.set(manager); manager.background = { layer: createBackgroundLayer() }; @@ -298,17 +298,31 @@ export const initializeRenderer = ( manager.preview.layer.add(manager.preview.tool.group); manager.preview.layer.add(manager.preview.documentOverlay.group); manager.stage.add(manager.preview.layer); - - const cleanupListeners = setStageEventHandlers({ - manager, + manager.stateApi = { + // Read-only state getToolState, + getSelectedEntity, + getBbox, + getSettings, + getCurrentFill, + getAltKey: $alt.get, + getCtrlKey: $ctrl.get, + getMetaKey: $meta.get, + getShiftKey: $shift.get, + getControlAdapterEntityStates, + getDocument, + getLayerEntityStates, + getRegionEntityStates, + getMaskOpacity, + getInpaintMaskEntityState, + + // Read-write state setTool, setToolBuffer, getIsDrawing, setIsDrawing, getIsMouseDown, setIsMouseDown, - getSelectedEntity, getLastAddedPoint, setLastAddedPoint, getLastCursorPos, @@ -318,61 +332,37 @@ export const initializeRenderer = ( getSpaceKey, setSpaceKey, setStageAttrs: $stageAttrs.set, - getBbox, - getSettings, + + // Callbacks onBrushLineAdded, onEraserLineAdded, onPointAddedToLine, onRectShapeAdded, onBrushWidthChanged, onEraserWidthChanged, - getCurrentFill, - }); + onPosChanged, + onBboxTransformed, + }; + + const cleanupListeners = setStageEventHandlers(manager); // Calculating bounding boxes is expensive, must be debounced to not block the UI thread during a user interaction. // TODO(psyche): Figure out how to do this in a worker. Probably means running the renderer in a worker and sending // the entire state over when needed. const debouncedUpdateBboxes = debounce(updateBboxes, 300); - manager.renderers = { - renderRegions: getRenderRegions({ - manager, - getRegionEntityStates, - getMaskOpacity, - getToolState, - getSelectedEntity, - onPosChanged, - }), - renderLayers: getRenderLayers({ manager, getLayerEntityStates, getToolState, onPosChanged }), - renderControlAdapters: getRenderControlAdapters({ manager, getControlAdapterEntityStates }), - renderInpaintMask: getRenderInpaintMask({ - manager, - getInpaintMaskEntityState, - getMaskOpacity, - getToolState, - getSelectedEntity, - onPosChanged, - }), - renderBbox: getRenderBbox(manager, getBbox, getToolState), - renderToolPreview: getRenderToolPreview({ - manager, - getToolState, - getCurrentFill, - getSelectedEntity, - getLastCursorPos, - getLastMouseDownPos, - getIsDrawing, - getIsMouseDown, - }), - renderDocumentOverlay: getRenderDocumentOverlay({ manager, getDocument }), - renderBackground: getRenderBackground({ manager }), - fitDocumentToStage: getFitDocumentToStage({ manager, getDocument, setStageAttrs: $stageAttrs.set }), - arrangeEntities: getArrangeEntities({ - manager, - getLayerEntityStates, - getControlAdapterEntityStates, - getRegionEntityStates, - }), + manager.konvaApi = { + renderRegions: getRenderRegions(manager), + renderLayers: getRenderLayers(manager), + renderControlAdapters: getRenderControlAdapters(manager), + renderInpaintMask: getRenderInpaintMask(manager), + renderBbox: getRenderBbox(manager), + renderToolPreview: getRenderToolPreview(manager), + renderDocumentOverlay: getRenderDocumentOverlay(manager), + renderBackground: getRenderBackground(manager), + arrangeEntities: getArrangeEntities(manager), + fitDocumentToStage: getFitDocumentToStage(manager), + fitStageToContainer: getFitStageToContainer(manager), }; const renderCanvas = () => { @@ -392,7 +382,7 @@ export const initializeRenderer = ( canvasV2.tool.selected !== prevCanvasV2.tool.selected ) { logIfDebugging('Rendering layers'); - manager.renderers.renderLayers(); + manager.konvaApi.renderLayers(); } if ( @@ -402,7 +392,7 @@ export const initializeRenderer = ( canvasV2.tool.selected !== prevCanvasV2.tool.selected ) { logIfDebugging('Rendering regions'); - manager.renderers.renderRegions(); + manager.konvaApi.renderRegions(); } if ( @@ -412,22 +402,22 @@ export const initializeRenderer = ( canvasV2.tool.selected !== prevCanvasV2.tool.selected ) { logIfDebugging('Rendering inpaint mask'); - manager.renderers.renderInpaintMask(); + manager.konvaApi.renderInpaintMask(); } if (isFirstRender || canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities) { logIfDebugging('Rendering control adapters'); - manager.renderers.renderControlAdapters(); + manager.konvaApi.renderControlAdapters(); } if (isFirstRender || canvasV2.document !== prevCanvasV2.document) { logIfDebugging('Rendering document bounds overlay'); - manager.renderers.renderDocumentOverlay(); + manager.konvaApi.renderDocumentOverlay(); } if (isFirstRender || canvasV2.bbox !== prevCanvasV2.bbox || canvasV2.tool.selected !== prevCanvasV2.tool.selected) { logIfDebugging('Rendering generation bbox'); - manager.renderers.renderBbox(); + manager.konvaApi.renderBbox(); } if ( @@ -447,7 +437,7 @@ export const initializeRenderer = ( canvasV2.regions.entities !== prevCanvasV2.regions.entities ) { logIfDebugging('Arranging entities'); - manager.renderers.arrangeEntities(); + manager.konvaApi.arrangeEntities(); } prevCanvasV2 = canvasV2; @@ -461,30 +451,16 @@ export const initializeRenderer = ( // We can use a resize observer to ensure the stage always fits the container. We also need to re-render the bg and // document bounds overlay when the stage is resized. - const fitStageToContainer = () => { - stage.width(container.offsetWidth); - stage.height(container.offsetHeight); - $stageAttrs.set({ - x: stage.x(), - y: stage.y(), - width: stage.width(), - height: stage.height(), - scale: stage.scaleX(), - }); - manager.renderers.renderBackground(); - manager.renderers.renderDocumentOverlay(); - }; - - const resizeObserver = new ResizeObserver(fitStageToContainer); + const resizeObserver = new ResizeObserver(manager.konvaApi.fitStageToContainer); resizeObserver.observe(container); - fitStageToContainer(); + manager.konvaApi.fitStageToContainer(); const unsubscribeRenderer = subscribe(renderCanvas); logIfDebugging('First render of konva stage'); // On first render, the document should be fit to the stage. - manager.renderers.fitDocumentToStage(); - manager.renderers.renderToolPreview(); + manager.konvaApi.fitDocumentToStage(); + manager.konvaApi.renderToolPreview(); renderCanvas(); return () => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stage.ts index b4741c88ac2..d02ecd485a4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stage.ts @@ -1,23 +1,15 @@ import { DOCUMENT_FIT_PADDING_PX } from 'features/controlLayers/konva/constants'; import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; -import type { CanvasV2State, StageAttrs } from 'features/controlLayers/store/types'; /** * Gets a function to fit the document to the stage, resetting the stage scale to 100%. * If the document is smaller than the stage, the stage scale is increased to fit the document. - * @param arg.manager The konva node manager - * @param arg.getDocument A function to get the current document state - * @param arg.setStageAttrs A function to set the stage attributes + * @param manager The konva node manager * @returns A function to fit the document to the stage */ -export const getFitDocumentToStage = - (arg: { - manager: KonvaNodeManager; - getDocument: () => CanvasV2State['document']; - setStageAttrs: (stageAttrs: StageAttrs) => void; - }) => - (): void => { - const { manager, getDocument, setStageAttrs } = arg; +export const getFitDocumentToStage = (manager: KonvaNodeManager) => { + function fitDocumentToStage(): void { + const { getDocument, setStageAttrs } = manager.stateApi; const document = getDocument(); // Fit & center the document on the stage const width = manager.stage.width(); @@ -29,4 +21,32 @@ export const getFitDocumentToStage = const y = (height - docHeightWithBuffer * scale) / 2 + DOCUMENT_FIT_PADDING_PX * scale; manager.stage.setAttrs({ x, y, width, height, scaleX: scale, scaleY: scale }); setStageAttrs({ x, y, width, height, scale }); - }; + } + + return fitDocumentToStage; +}; + +/** + * Gets a function to fit the stage to its container element. Called during resize events. + * @param manager The konva node manager + * @returns A function to fit the stage to its container + */ +export const getFitStageToContainer = (manager: KonvaNodeManager) => { + const { stage, container } = manager; + const { setStageAttrs } = manager.stateApi; + function fitStageToContainer(): void { + stage.width(container.offsetWidth); + stage.height(container.offsetHeight); + setStageAttrs({ + x: stage.x(), + y: stage.y(), + width: stage.width(), + height: stage.height(), + scale: stage.scaleX(), + }); + manager.konvaApi.renderBackground(); + manager.konvaApi.renderDocumentOverlay(); + } + + return fitStageToContainer; +}; From 70f5231020bb8da917b54fabc0d14041d327c5fd Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 21 Jun 2024 16:00:42 +1000 Subject: [PATCH 109/678] feat(ui): add utils for getting images from canvas --- .../listeners/enqueueRequestedLinear.ts | 9 +- .../controlLayers/konva/nodeManager.ts | 190 +++++++++++++++++- .../controlLayers/konva/renderers/arrange.ts | 8 +- .../konva/renderers/controlAdapters.ts | 4 +- .../konva/renderers/inpaintMask.ts | 4 +- .../controlLayers/konva/renderers/layers.ts | 4 +- .../controlLayers/konva/renderers/regions.ts | 4 +- .../controlLayers/konva/renderers/renderer.ts | 36 +++- .../controlLayers/store/canvasV2Slice.ts | 8 +- .../controlLayers/store/layersReducers.ts | 39 ++-- .../controlLayers/store/regionsReducers.ts | 8 +- .../src/features/controlLayers/store/types.ts | 2 +- .../nodes/util/graph/generation/addLayers.ts | 89 +------- .../nodes/util/graph/generation/addRegions.ts | 100 +-------- .../web/src/services/api/endpoints/images.ts | 13 ++ 15 files changed, 277 insertions(+), 241 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index 6da0b82dc3b..b76d8acf85c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -1,10 +1,12 @@ import { enqueueRequested } from 'app/store/actions'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +import { $nodeManager } from 'features/controlLayers/konva/renderers/renderer'; import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; import { buildGenerationTabGraph } from 'features/nodes/util/graph/generation/buildGenerationTabGraph'; import { buildGenerationTabSDXLGraph } from 'features/nodes/util/graph/generation/buildGenerationTabSDXLGraph'; import { queueApi } from 'services/api/endpoints/queue'; +import { assert } from 'tsafe'; export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) => { startAppListening({ @@ -18,10 +20,13 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) let graph; + const manager = $nodeManager.get(); + assert(manager, 'Konva node manager not initialized'); + if (model?.base === 'sdxl') { - graph = await buildGenerationTabSDXLGraph(state); + graph = await buildGenerationTabSDXLGraph(state, manager); } else { - graph = await buildGenerationTabGraph(state); + graph = await buildGenerationTabGraph(state, manager); } const batchConfig = prepareLinearUIBatch(state, graph, prepend); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts index c093f87ee03..19e095703cd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts @@ -1,3 +1,5 @@ +import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; +import { blobToDataURL } from 'features/controlLayers/konva/util'; import type { BrushLine, BrushLineAddedArg, @@ -15,8 +17,11 @@ import type { StageAttrs, Tool, } from 'features/controlLayers/store/types'; +import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers'; import type Konva from 'konva'; import type { Vector2d } from 'konva/lib/types'; +import { getImageDTO as defaultGetImageDTO, uploadImage as defaultUploadImage } from 'services/api/endpoints/images'; +import type { ImageCategory, ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; export type BrushLineObjectRecord = { @@ -132,24 +137,53 @@ type StateApi = { getMetaKey: () => boolean; getAltKey: () => boolean; getDocument: () => CanvasV2State['document']; - getLayerEntityStates: () => CanvasV2State['layers']['entities']; - getControlAdapterEntityStates: () => CanvasV2State['controlAdapters']['entities']; - getRegionEntityStates: () => CanvasV2State['regions']['entities']; - getInpaintMaskEntityState: () => CanvasV2State['inpaintMask']; + getLayersState: () => CanvasV2State['layers']; + getControlAdaptersState: () => CanvasV2State['controlAdapters']; + getRegionsState: () => CanvasV2State['regions']; + getInpaintMaskState: () => CanvasV2State['inpaintMask']; + onInpaintMaskImageCached: (imageDTO: ImageDTO) => void; + onRegionMaskImageCached: (id: string, imageDTO: ImageDTO) => void; + onLayerImageCached: (imageDTO: ImageDTO) => void; +}; + +type Util = { + getImageDTO: (imageName: string) => Promise; + uploadImage: ( + blob: Blob, + fileName: string, + image_category: ImageCategory, + is_intermediate: boolean + ) => Promise; + getRegionMaskImage: (arg: { id: string; bbox?: Rect; preview?: boolean }) => Promise; + getInpaintMaskImage: (arg: { bbox?: Rect; preview?: boolean }) => Promise; + getImageSourceImage: (arg: { bbox?: Rect; preview?: boolean }) => Promise; }; export class KonvaNodeManager { stage: Konva.Stage; container: HTMLDivElement; adapters: Map; + util: Util; _background: BackgroundLayer | null; _preview: PreviewLayer | null; _konvaApi: KonvaApi | null; _stateApi: StateApi | null; - constructor(stage: Konva.Stage, container: HTMLDivElement) { + constructor( + stage: Konva.Stage, + container: HTMLDivElement, + getImageDTO: Util['getImageDTO'] = defaultGetImageDTO, + uploadImage: Util['uploadImage'] = defaultUploadImage + ) { this.stage = stage; this.container = container; + this.util = { + getImageDTO, + uploadImage, + getRegionMaskImage: this._getRegionMaskImage.bind(this), + getInpaintMaskImage: this._getInpaintMaskImage.bind(this), + getImageSourceImage: this._getImageSourceImage.bind(this), + }; this._konvaApi = null; this._preview = null; this._background = null; @@ -219,6 +253,152 @@ export class KonvaNodeManager { assert(this._stateApi !== null, 'State API has not been set'); return this._stateApi; } + + async _getRegionMaskImage(arg: { id: string; bbox?: Rect; preview?: boolean }): Promise { + const { id, bbox, preview = false } = arg; + const region = this.stateApi.getRegionsState().entities.find((entity) => entity.id === id); + assert(region, `Region entity state with id ${id} not found`); + const adapter = this.get(region.id); + assert(adapter, `Adapter for region ${region.id} not found`); + + if (region.imageCache) { + const imageDTO = await this.util.getImageDTO(region.imageCache.name); + if (imageDTO) { + return imageDTO; + } + } + + const layer = adapter.konvaLayer.clone(); + const objectGroup = adapter.konvaObjectGroup.clone(); + layer.destroyChildren(); + layer.add(objectGroup); + objectGroup.opacity(1); + objectGroup.cache(); + + const blob = await new Promise((resolve) => { + layer.toBlob({ + callback: (blob) => { + assert(blob, 'Blob is null'); + resolve(blob); + }, + ...bbox, + }); + }); + + if (preview) { + const base64 = await blobToDataURL(blob); + const caption = `${region.id}: ${region.positivePrompt} / ${region.negativePrompt}`; + openBase64ImageInTab([{ base64, caption }]); + } + + layer.destroy(); + + const imageDTO = await this.util.uploadImage(blob, `${region.id}_mask.png`, 'mask', true); + this.stateApi.onRegionMaskImageCached(region.id, imageDTO); + return imageDTO; + } + + async _getInpaintMaskImage(arg: { bbox?: Rect; preview?: boolean }): Promise { + const { bbox, preview = false } = arg; + const inpaintMask = this.stateApi.getInpaintMaskState(); + const adapter = this.get(inpaintMask.id); + assert(adapter, `Adapter for ${inpaintMask.id} not found`); + + if (inpaintMask.imageCache) { + const imageDTO = await this.util.getImageDTO(inpaintMask.imageCache.name); + if (imageDTO) { + return imageDTO; + } + } + + const layer = adapter.konvaLayer.clone(); + const objectGroup = adapter.konvaObjectGroup.clone(); + layer.destroyChildren(); + layer.add(objectGroup); + objectGroup.opacity(1); + objectGroup.cache(); + + const blob = await new Promise((resolve) => { + layer.toBlob({ + callback: (blob) => { + assert(blob, 'Blob is null'); + resolve(blob); + }, + ...bbox, + }); + }); + + if (preview) { + const base64 = await blobToDataURL(blob); + const caption = 'inpaint mask'; + openBase64ImageInTab([{ base64, caption }]); + } + + layer.destroy(); + + const imageDTO = await this.util.uploadImage(blob, 'inpaint_mask.png', 'mask', true); + this.stateApi.onInpaintMaskImageCached(imageDTO); + return imageDTO; + } + + async _getImageSourceImage(arg: { bbox?: Rect; preview?: boolean }): Promise { + const { bbox, preview = false } = arg; + const layersState = this.stateApi.getLayersState(); + const { entities, imageCache } = layersState; + if (imageCache) { + const imageDTO = await this.util.getImageDTO(imageCache.name); + if (imageDTO) { + return imageDTO; + } + } + + const stage = this.stage.clone(); + + stage.scaleX(1); + stage.scaleY(1); + stage.x(0); + stage.y(0); + + const validLayers = entities.filter(isValidLayer); + + // Konva bug (?) - when iterating over the array returned from `stage.getLayers()`, if you destroy a layer, the array + // is mutated in-place and the next iteration will skip the next layer. To avoid this, we first collect the layers + // to delete in a separate array and then destroy them. + // TODO(psyche): Maybe report this? + const toDelete: Konva.Layer[] = []; + + for (const konvaLayer of stage.getLayers()) { + const layer = validLayers.find((l) => l.id === konvaLayer.id()); + if (!layer) { + toDelete.push(konvaLayer); + } + } + + for (const konvaLayer of toDelete) { + konvaLayer.destroy(); + } + + const blob = await new Promise((resolve) => { + stage.toBlob({ + callback: (blob) => { + assert(blob, 'Blob is null'); + resolve(blob); + }, + ...bbox, + }); + }); + + if (preview) { + const base64 = await blobToDataURL(blob); + openBase64ImageInTab([{ base64, caption: 'base layer' }]); + } + + stage.destroy(); + + const imageDTO = await this.util.uploadImage(blob, 'base_layer.png', 'general', true); + this.stateApi.onLayerImageCached(imageDTO); + return imageDTO; + } } export class KonvaEntityAdapter { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts index dc640b10bbe..2fa30399325 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts @@ -6,12 +6,12 @@ import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager' * @returns An arrange entities function */ export const getArrangeEntities = (manager: KonvaNodeManager) => { - const { getLayerEntityStates, getControlAdapterEntityStates, getRegionEntityStates } = manager.stateApi; + const { getLayersState, getControlAdaptersState, getRegionsState } = manager.stateApi; function arrangeEntities(): void { - const layers = getLayerEntityStates(); - const controlAdapters = getControlAdapterEntityStates(); - const regions = getRegionEntityStates(); + const layers = getLayersState().entities; + const controlAdapters = getControlAdaptersState().entities; + const regions = getRegionsState().entities; let zIndex = 0; manager.background.layer.zIndex(++zIndex); for (const layer of layers) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts index d9db711147a..db61ced5db9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts @@ -100,10 +100,10 @@ export const renderControlAdapter = async (manager: KonvaNodeManager, entity: Co * @returns A function to render all control adapters */ export const getRenderControlAdapters = (manager: KonvaNodeManager) => { - const { getControlAdapterEntityStates } = manager.stateApi; + const { getControlAdaptersState } = manager.stateApi; function renderControlAdapters(): void { - const entities = getControlAdapterEntityStates(); + const { entities } = getControlAdaptersState(); // Destroy nonexistent layers for (const adapters of manager.getAll('control_adapter')) { if (!entities.find((ca) => ca.id === adapters.id)) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts index 3c1c103ff46..fc7bed9a641 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts @@ -71,10 +71,10 @@ const getInpaintMask = ( * @returns A function to render the inpaint mask */ export const getRenderInpaintMask = (manager: KonvaNodeManager) => { - const { getInpaintMaskEntityState, getMaskOpacity, getToolState, getSelectedEntity, onPosChanged } = manager.stateApi; + const { getInpaintMaskState, getMaskOpacity, getToolState, getSelectedEntity, onPosChanged } = manager.stateApi; function renderInpaintMask(): void { - const entity = getInpaintMaskEntityState(); + const entity = getInpaintMaskState(); const globalMaskLayerOpacity = getMaskOpacity(); const toolState = getToolState(); const selectedEntity = getSelectedEntity(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts index eff2f7de858..8dfb803c5bc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -136,10 +136,10 @@ export const renderLayer = async ( * @returns A function to render all layers */ export const getRenderLayers = (manager: KonvaNodeManager) => { - const { getLayerEntityStates, getToolState, onPosChanged } = manager.stateApi; + const { getLayersState, getToolState, onPosChanged } = manager.stateApi; function renderLayers(): void { - const entities = getLayerEntityStates(); + const { entities } = getLayersState(); const tool = getToolState(); // Destroy nonexistent layers for (const adapter of manager.getAll('layer')) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts index a6087b320be..70b8cfb51bc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts @@ -233,10 +233,10 @@ export const renderRegion = ( * @returns A function to render all regions */ export const getRenderRegions = (manager: KonvaNodeManager) => { - const { getRegionEntityStates, getMaskOpacity, getToolState, getSelectedEntity, onPosChanged } = manager.stateApi; + const { getRegionsState, getMaskOpacity, getToolState, getSelectedEntity, onPosChanged } = manager.stateApi; function renderRegions(): void { - const entities = getRegionEntityStates(); + const { entities } = getRegionsState(); const maskOpacity = getMaskOpacity(); const toolState = getToolState(); const selectedEntity = getSelectedEntity(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts index dcd876de018..939d9d776c5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -32,17 +32,20 @@ import { imBboxChanged, imBrushLineAdded, imEraserLineAdded, + imImageCacheChanged, imLinePointAdded, imTranslated, layerBboxChanged, layerBrushLineAdded, layerEraserLineAdded, + layerImageCacheChanged, layerLinePointAdded, layerRectAdded, layerTranslated, rgBboxChanged, rgBrushLineAdded, rgEraserLineAdded, + rgImageCacheChanged, rgLinePointAdded, rgRectAdded, rgTranslated, @@ -65,6 +68,7 @@ import type { IRect, Vector2d } from 'konva/lib/types'; import { debounce } from 'lodash-es'; import { atom } from 'nanostores'; import type { RgbaColor } from 'react-colorful'; +import type { ImageDTO } from 'services/api/types'; export const $nodeManager = atom(null); @@ -175,6 +179,19 @@ export const initializeRenderer = ( logIfDebugging('Eraser width changed'); dispatch(eraserWidthChanged(width)); }; + const onRegionMaskImageCached = (id: string, imageDTO: ImageDTO) => { + logIfDebugging('Region mask image cached'); + dispatch(rgImageCacheChanged({ id, imageDTO })); + }; + const onInpaintMaskImageCached = (imageDTO: ImageDTO) => { + logIfDebugging('Inpaint mask image cached'); + dispatch(imImageCacheChanged({ imageDTO })); + }; + const onLayerImageCached = (imageDTO: ImageDTO) => { + logIfDebugging('Layer image cached'); + dispatch(layerImageCacheChanged({ imageDTO })); + }; + const setTool = (tool: Tool) => { logIfDebugging('Tool selection changed'); dispatch(toolChanged(tool)); @@ -240,11 +257,11 @@ export const initializeRenderer = ( const getDocument = () => canvasV2.document; const getToolState = () => canvasV2.tool; const getSettings = () => canvasV2.settings; - const getRegionEntityStates = () => canvasV2.regions.entities; - const getLayerEntityStates = () => canvasV2.layers.entities; - const getControlAdapterEntityStates = () => canvasV2.controlAdapters.entities; + const getRegionsState = () => canvasV2.regions; + const getLayersState = () => canvasV2.layers; + const getControlAdaptersState = () => canvasV2.controlAdapters; + const getInpaintMaskState = () => canvasV2.inpaintMask; const getMaskOpacity = () => canvasV2.settings.maskOpacity; - const getInpaintMaskEntityState = () => canvasV2.inpaintMask; // Read-write state, ephemeral interaction state let isDrawing = false; @@ -309,12 +326,12 @@ export const initializeRenderer = ( getCtrlKey: $ctrl.get, getMetaKey: $meta.get, getShiftKey: $shift.get, - getControlAdapterEntityStates, + getControlAdaptersState, getDocument, - getLayerEntityStates, - getRegionEntityStates, + getLayersState, + getRegionsState, getMaskOpacity, - getInpaintMaskEntityState, + getInpaintMaskState, // Read-write state setTool, @@ -342,6 +359,9 @@ export const initializeRenderer = ( onEraserWidthChanged, onPosChanged, onBboxTransformed, + onRegionMaskImageCached, + onInpaintMaskImageCached, + onLayerImageCached, }; const cleanupListeners = setStageEventHandlers(manager); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 20bbeade0ae..60d7151815f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -24,7 +24,7 @@ import { DEFAULT_RGBA_COLOR } from './types'; const initialState: CanvasV2State = { _version: 3, selectedEntityIdentifier: { type: 'inpaint_mask', id: 'inpaint_mask' }, - layers: { entities: [], baseLayerImageCache: null }, + layers: { entities: [], imageCache: null }, controlAdapters: { entities: [] }, ipAdapters: { entities: [] }, regions: { entities: [] }, @@ -161,7 +161,7 @@ export const canvasV2Slice = createSlice({ allEntitiesDeleted: (state) => { state.regions.entities = []; state.layers.entities = []; - state.layers.baseLayerImageCache = null; + state.layers.imageCache = null; state.ipAdapters.entities = []; state.controlAdapters.entities = []; }, @@ -185,7 +185,6 @@ export const { scaledBboxChanged, bboxScaleMethodChanged, clipToBboxChanged, - baseLayerImageCacheChanged, // layers layerAdded, layerRecalled, @@ -205,6 +204,7 @@ export const { layerRectAdded, layerImageAdded, layerAllDeleted, + layerImageCacheChanged, // IP Adapters ipaAdded, ipaRecalled, @@ -255,7 +255,7 @@ export const { rgPositivePromptChanged, rgNegativePromptChanged, rgFillChanged, - rgMaskImageUploaded, + rgImageCacheChanged, rgAutoNegativeChanged, rgIPAdapterAdded, rgIPAdapterDeleted, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index c7303f00cc4..21cbbd4d85f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -40,7 +40,7 @@ export const layersReducers = { y: 0, }); state.selectedEntityIdentifier = { type: 'layer', id }; - state.layers.baseLayerImageCache = null; + state.layers.imageCache = null; }, prepare: () => ({ payload: { id: uuidv4() } }), }, @@ -48,7 +48,7 @@ export const layersReducers = { const { data } = action.payload; state.layers.entities.push(data); state.selectedEntityIdentifier = { type: 'layer', id: data.id }; - state.layers.baseLayerImageCache = null; + state.layers.imageCache = null; }, layerIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -57,7 +57,7 @@ export const layersReducers = { return; } layer.isEnabled = !layer.isEnabled; - state.layers.baseLayerImageCache = null; + state.layers.imageCache = null; }, layerTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => { const { id, x, y } = action.payload; @@ -67,7 +67,7 @@ export const layersReducers = { } layer.x = x; layer.y = y; - state.layers.baseLayerImageCache = null; + state.layers.imageCache = null; }, layerBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { const { id, bbox } = action.payload; @@ -93,16 +93,16 @@ export const layersReducers = { layer.objects = []; layer.bbox = null; layer.bboxNeedsUpdate = false; - state.layers.baseLayerImageCache = null; + state.layers.imageCache = null; }, layerDeleted: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; state.layers.entities = state.layers.entities.filter((l) => l.id !== id); - state.layers.baseLayerImageCache = null; + state.layers.imageCache = null; }, layerAllDeleted: (state) => { state.layers.entities = []; - state.layers.baseLayerImageCache = null; + state.layers.imageCache = null; }, layerOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => { const { id, opacity } = action.payload; @@ -111,7 +111,7 @@ export const layersReducers = { return; } layer.opacity = opacity; - state.layers.baseLayerImageCache = null; + state.layers.imageCache = null; }, layerMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -120,7 +120,7 @@ export const layersReducers = { return; } moveOneToEnd(state.layers.entities, layer); - state.layers.baseLayerImageCache = null; + state.layers.imageCache = null; }, layerMovedToFront: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -129,7 +129,7 @@ export const layersReducers = { return; } moveToEnd(state.layers.entities, layer); - state.layers.baseLayerImageCache = null; + state.layers.imageCache = null; }, layerMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -138,7 +138,7 @@ export const layersReducers = { return; } moveOneToStart(state.layers.entities, layer); - state.layers.baseLayerImageCache = null; + state.layers.imageCache = null; }, layerMovedToBack: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -147,7 +147,7 @@ export const layersReducers = { return; } moveToStart(state.layers.entities, layer); - state.layers.baseLayerImageCache = null; + state.layers.imageCache = null; }, layerBrushLineAdded: { reducer: (state, action: PayloadAction) => { @@ -166,7 +166,7 @@ export const layersReducers = { clip, }); layer.bboxNeedsUpdate = true; - state.layers.baseLayerImageCache = null; + state.layers.imageCache = null; }, prepare: (payload: BrushLineAddedArg) => ({ payload: { ...payload, lineId: uuidv4() }, @@ -188,7 +188,7 @@ export const layersReducers = { clip, }); layer.bboxNeedsUpdate = true; - state.layers.baseLayerImageCache = null; + state.layers.imageCache = null; }, prepare: (payload: EraserLineAddedArg) => ({ payload: { ...payload, lineId: uuidv4() }, @@ -206,7 +206,7 @@ export const layersReducers = { } lastObject.points.push(...point); layer.bboxNeedsUpdate = true; - state.layers.baseLayerImageCache = null; + state.layers.imageCache = null; }, layerRectAdded: { reducer: (state, action: PayloadAction) => { @@ -226,7 +226,7 @@ export const layersReducers = { color, }); layer.bboxNeedsUpdate = true; - state.layers.baseLayerImageCache = null; + state.layers.imageCache = null; }, prepare: (payload: RectShapeAddedArg) => ({ payload: { ...payload, rectId: uuidv4() } }), }, @@ -239,11 +239,12 @@ export const layersReducers = { } layer.objects.push(imageDTOToImageObject(id, objectId, imageDTO)); layer.bboxNeedsUpdate = true; - state.layers.baseLayerImageCache = null; + state.layers.imageCache = null; }, prepare: (payload: ImageObjectAddedArg) => ({ payload: { ...payload, objectId: uuidv4() } }), }, - baseLayerImageCacheChanged: (state, action: PayloadAction) => { - state.layers.baseLayerImageCache = action.payload ? imageDTOToImageWithDims(action.payload) : null; + layerImageCacheChanged: (state, action: PayloadAction<{ imageDTO: ImageDTO | null }>) => { + const { imageDTO } = action.payload; + state.layers.imageCache = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index 752e6a0af14..49aa7612460 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -1,11 +1,7 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; -import type { - CanvasV2State, - CLIPVisionModelV2, - IPMethodV2, -} from 'features/controlLayers/store/types'; +import type { CanvasV2State, CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types'; import { imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas'; @@ -182,7 +178,7 @@ export const regionsReducers = { } rg.fill = fill; }, - rgMaskImageUploaded: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO }>) => { + rgImageCacheChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO }>) => { const { id, imageDTO } = action.payload; const rg = selectRG(state, id); if (!rg) { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index f8410bb4b97..19642164899 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -797,7 +797,7 @@ export type CanvasV2State = { selectedEntityIdentifier: CanvasEntityIdentifier | null; inpaintMask: InpaintMaskEntity; layers: { - baseLayerImageCache: ImageWithDims | null; + imageCache: ImageWithDims | null; entities: LayerEntity[]; }; controlAdapters: { entities: ControlAdapterEntity[] }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts index d665d53d254..64cf8593832 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts @@ -1,96 +1,9 @@ -import { getStore } from 'app/store/nanostores/store'; -import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; -import { $nodeManager } from 'features/controlLayers/konva/renderers/renderer'; -import { blobToDataURL } from 'features/controlLayers/konva/util'; -import { baseLayerImageCacheChanged } from 'features/controlLayers/store/canvasV2Slice'; import type { LayerEntity } from 'features/controlLayers/store/types'; -import type Konva from 'konva'; -import type { IRect } from 'konva/lib/types'; -import { getImageDTO, imagesApi } from 'services/api/endpoints/images'; -import type { ImageDTO } from 'services/api/types'; -import { assert } from 'tsafe'; -const isValidLayer = (entity: LayerEntity) => { +export const isValidLayer = (entity: LayerEntity) => { return ( entity.isEnabled && // Boolean(entity.bbox) && TODO(psyche): Re-enable this check when we have a way to calculate bbox for all layers entity.objects.length > 0 ); }; - -/** - * Get the blobs of all regional prompt layers. Only visible layers are returned. - * @param layerIds The IDs of the layers to get blobs for. If not provided, all regional prompt layers are used. - * @param preview Whether to open a new tab displaying each layer. - * @returns A map of layer IDs to blobs. - */ - -const getBaseLayer = async (layers: LayerEntity[], bbox: IRect, preview: boolean = false): Promise => { - const manager = $nodeManager.get(); - assert(manager, 'Node manager is null'); - - const stage = manager.stage.clone(); - - stage.scaleX(1); - stage.scaleY(1); - stage.x(0); - stage.y(0); - - const validLayers = layers.filter(isValidLayer); - - // Konva bug (?) - when iterating over the array returned from `stage.getLayers()`, if you destroy a layer, the array - // is mutated in-place and the next iteration will skip the next layer. To avoid this, we first collect the layers - // to delete in a separate array and then destroy them. - // TODO(psyche): Maybe report this? - const toDelete: Konva.Layer[] = []; - - for (const konvaLayer of stage.getLayers()) { - const layer = validLayers.find((l) => l.id === konvaLayer.id()); - if (!layer) { - toDelete.push(konvaLayer); - } - } - - for (const konvaLayer of toDelete) { - konvaLayer.destroy(); - } - - const blob = await new Promise((resolve) => { - stage.toBlob({ - callback: (blob) => { - assert(blob, 'Blob is null'); - resolve(blob); - }, - ...bbox, - }); - }); - - if (preview) { - const base64 = await blobToDataURL(blob); - openBase64ImageInTab([{ base64, caption: 'base layer' }]); - } - - stage.destroy(); - - return blob; -}; - -export const getBaseLayerImage = async (): Promise => { - const { dispatch, getState } = getStore(); - const state = getState(); - if (state.canvasV2.layers.baseLayerImageCache) { - const imageDTO = await getImageDTO(state.canvasV2.layers.baseLayerImageCache.name); - if (imageDTO) { - return imageDTO; - } - } - const blob = await getBaseLayer(state.canvasV2.layers.entities, state.canvasV2.bbox, true); - const file = new File([blob], 'image.png', { type: 'image/png' }); - const req = dispatch( - imagesApi.endpoints.uploadImage.initiate({ file, image_category: 'general', is_intermediate: true }) - ); - req.reset(); - const imageDTO = await req.unwrap(); - dispatch(baseLayerImageCacheChanged(imageDTO)); - return imageDTO; -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts index ae99dd6e320..e5290422dab 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts @@ -1,10 +1,5 @@ -import { getStore } from 'app/store/nanostores/store'; import { deepClone } from 'common/util/deepClone'; -import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; -import type { KonvaEntityAdapter } from 'features/controlLayers/konva/nodeManager'; -import { $nodeManager } from 'features/controlLayers/konva/renderers/renderer'; -import { blobToDataURL } from 'features/controlLayers/konva/util'; -import { rgMaskImageUploaded } from 'features/controlLayers/store/canvasV2Slice'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import type { Dimensions, IPAdapterEntity, RegionEntity } from 'features/controlLayers/store/types'; import { PROMPT_REGION_INVERT_TENSOR_MASK_PREFIX, @@ -16,8 +11,7 @@ import { import { addIPAdapterCollectorSafe, isValidIPAdapter } from 'features/nodes/util/graph/generation/addIPAdapters'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import type { IRect } from 'konva/lib/types'; -import { getImageDTO, imagesApi } from 'services/api/endpoints/images'; -import type { BaseModelType, ImageDTO, Invocation } from 'services/api/types'; +import type { BaseModelType, Invocation } from 'services/api/types'; import { assert } from 'tsafe'; /** @@ -34,6 +28,7 @@ import { assert } from 'tsafe'; */ export const addRegions = async ( + manager: KonvaNodeManager, regions: RegionEntity[], g: Graph, documentSize: Dimensions, @@ -51,7 +46,7 @@ export const addRegions = async ( for (const region of validRegions) { // Upload the mask image, or get the cached image if it exists - const { image_name } = await getRegionMaskImage(region, bbox, true); + const { image_name } = await manager.util.getRegionMaskImage({ id: region.id, bbox, preview: true }); // The main mask-to-tensor node const maskToTensor = g.addNode({ @@ -217,90 +212,3 @@ export const isValidRegion = (rg: RegionEntity, base: BaseModelType) => { const hasIPAdapter = rg.ipAdapters.filter((ipa) => isValidIPAdapter(ipa, base)).length > 0; return hasTextPrompt || hasIPAdapter; }; - -export const getMaskImage = async (rg: RegionEntity, blob: Blob): Promise => { - const { id, imageCache } = rg; - if (imageCache) { - const imageDTO = await getImageDTO(imageCache.name); - if (imageDTO) { - return imageDTO; - } - } - const { dispatch } = getStore(); - // No cached mask, or the cached image no longer exists - we need to upload the mask image - const file = new File([blob], `${rg.id}_mask.png`, { type: 'image/png' }); - const req = dispatch( - imagesApi.endpoints.uploadImage.initiate({ file, image_category: 'mask', is_intermediate: true }) - ); - req.reset(); - - const imageDTO = await req.unwrap(); - dispatch(rgMaskImageUploaded({ id, imageDTO })); - return imageDTO; -}; - -export const uploadMaskImage = async ({ id }: RegionEntity, blob: Blob): Promise => { - const { dispatch } = getStore(); - // No cached mask, or the cached image no longer exists - we need to upload the mask image - const file = new File([blob], `${id}_mask.png`, { type: 'image/png' }); - const req = dispatch( - imagesApi.endpoints.uploadImage.initiate({ file, image_category: 'mask', is_intermediate: true }) - ); - req.reset(); - - const imageDTO = await req.unwrap(); - dispatch(rgMaskImageUploaded({ id, imageDTO })); - return imageDTO; -}; - -/** - * Get the blobs of all regional prompt layers. Only visible layers are returned. - * @param layerIds The IDs of the layers to get blobs for. If not provided, all regional prompt layers are used. - * @param preview Whether to open a new tab displaying each layer. - * @returns A map of layer IDs to blobs. - */ - -export const getRegionMaskImage = async ( - region: RegionEntity, - bbox: IRect, - preview: boolean = false -): Promise => { - const manager = $nodeManager.get(); - assert(manager, 'Node manager is null'); - - // TODO(psyche): Why do I need to annotate this? TS must have some kind of circular ref w/ this type but I can't figure it out... - const adapter: KonvaEntityAdapter | undefined = manager.get(region.id); - assert(adapter, `Adapter for region ${region.id} not found`); - if (region.imageCache) { - const imageDTO = await getImageDTO(region.imageCache.name); - if (imageDTO) { - return imageDTO; - } - } - const layer = adapter.konvaLayer.clone(); - const objectGroup = adapter.konvaObjectGroup.clone(); - layer.destroyChildren(); - layer.add(objectGroup); - objectGroup.opacity(1); - objectGroup.cache(); - - const blob = await new Promise((resolve) => { - layer.toBlob({ - callback: (blob) => { - assert(blob, 'Blob is null'); - resolve(blob); - }, - ...bbox, - }); - }); - - if (preview) { - const base64 = await blobToDataURL(blob); - const caption = `${region.id}: ${region.positivePrompt} / ${region.negativePrompt}`; - openBase64ImageInTab([{ base64, caption }]); - } - - layer.destroy(); - - return await uploadMaskImage(region, blob); -}; diff --git a/invokeai/frontend/web/src/services/api/endpoints/images.ts b/invokeai/frontend/web/src/services/api/endpoints/images.ts index 9672acc1490..9ba144ecd8e 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/images.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/images.ts @@ -588,3 +588,16 @@ export const getImageDTO = async (image_name: string, forceRefetch?: boolean): P return null; } }; + +export const uploadImage = async ( + blob: Blob, + fileName: string, + image_category: ImageCategory, + is_intermediate: boolean +): Promise => { + const { dispatch } = getStore(); + const file = new File([blob], fileName, { type: 'image/png' }); + const req = dispatch(imagesApi.endpoints.uploadImage.initiate({ file, image_category, is_intermediate })); + req.reset(); + return await req.unwrap(); +}; From 46f86a54c1a2391c2f3aca5670e4084974bb6e17 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 21 Jun 2024 22:30:57 +1000 Subject: [PATCH 110/678] feat(ui): generation mode calculation, fudged graphs --- .../listeners/enqueueRequestedLinear.ts | 2 + .../web/src/common/util/arrayBuffer.ts | 13 +- .../controlLayers/konva/nodeManager.ts | 177 +++++++++--------- .../src/features/controlLayers/konva/util.ts | 43 ++++- .../src/features/controlLayers/store/types.ts | 2 + .../generation/buildGenerationTabGraph.ts | 4 +- .../generation/buildGenerationTabSDXLGraph.ts | 16 +- 7 files changed, 152 insertions(+), 105 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index b76d8acf85c..29ff4b22248 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -23,6 +23,8 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) const manager = $nodeManager.get(); assert(manager, 'Konva node manager not initialized'); + console.log('generation mode', manager.util.getGenerationMode()); + if (model?.base === 'sdxl') { graph = await buildGenerationTabSDXLGraph(state, manager); } else { diff --git a/invokeai/frontend/web/src/common/util/arrayBuffer.ts b/invokeai/frontend/web/src/common/util/arrayBuffer.ts index a21c9d8a479..c3b13dac265 100644 --- a/invokeai/frontend/web/src/common/util/arrayBuffer.ts +++ b/invokeai/frontend/web/src/common/util/arrayBuffer.ts @@ -1,10 +1,9 @@ -export const getImageDataTransparency = (pixels: Uint8ClampedArray) => { +export const getImageDataTransparency = (imageData: ImageData) => { let isFullyTransparent = true; let isPartiallyTransparent = false; - const len = pixels.length; - let i = 3; - for (i; i < len; i += 4) { - if (pixels[i] === 255) { + const len = imageData.data.length; + for (let i = 3; i < len; i += 4) { + if (imageData.data[i] === 255) { isFullyTransparent = false; } else { isPartiallyTransparent = true; @@ -18,8 +17,8 @@ export const getImageDataTransparency = (pixels: Uint8ClampedArray) => { export const areAnyPixelsBlack = (pixels: Uint8ClampedArray) => { const len = pixels.length; - let i = 0; - for (i; i < len; ) { + const i = 0; + for (let i = 0; i < len; i) { if (pixels[i++] === 0 && pixels[i++] === 0 && pixels[i++] === 0 && pixels[i++] === 255) { return true; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts index 19e095703cd..f31a74b1f3d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts @@ -1,5 +1,5 @@ -import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; -import { blobToDataURL } from 'features/controlLayers/konva/util'; +import { getImageDataTransparency } from 'common/util/arrayBuffer'; +import { konvaNodeToBlob, konvaNodeToImageData, previewBlob } from 'features/controlLayers/konva/util'; import type { BrushLine, BrushLineAddedArg, @@ -7,6 +7,7 @@ import type { CanvasV2State, EraserLine, EraserLineAddedArg, + GenerationMode, ImageObject, PointAddedToLineArg, PosChangedArg, @@ -157,6 +158,9 @@ type Util = { getRegionMaskImage: (arg: { id: string; bbox?: Rect; preview?: boolean }) => Promise; getInpaintMaskImage: (arg: { bbox?: Rect; preview?: boolean }) => Promise; getImageSourceImage: (arg: { bbox?: Rect; preview?: boolean }) => Promise; + getMaskLayerClone: (arg: { id: string }) => Konva.Layer; + getCompositeLayerStageClone: () => Konva.Stage; + getGenerationMode: () => GenerationMode; }; export class KonvaNodeManager { @@ -183,6 +187,9 @@ export class KonvaNodeManager { getRegionMaskImage: this._getRegionMaskImage.bind(this), getInpaintMaskImage: this._getInpaintMaskImage.bind(this), getImageSourceImage: this._getImageSourceImage.bind(this), + getMaskLayerClone: this._getMaskLayerClone.bind(this), + getCompositeLayerStageClone: this._getCompositeLayerStageClone.bind(this), + getGenerationMode: this._getGenerationMode.bind(this), }; this._konvaApi = null; this._preview = null; @@ -254,12 +261,80 @@ export class KonvaNodeManager { return this._stateApi; } + _getMaskLayerClone(arg: { id: string }): Konva.Layer { + const { id } = arg; + const adapter = this.get(id); + assert(adapter, `Adapter for entity ${id} not found`); + + const layerClone = adapter.konvaLayer.clone(); + const objectGroupClone = adapter.konvaObjectGroup.clone(); + + layerClone.destroyChildren(); + layerClone.add(objectGroupClone); + + objectGroupClone.opacity(1); + objectGroupClone.cache(); + + return layerClone; + } + + _getCompositeLayerStageClone(): Konva.Stage { + const layersState = this.stateApi.getLayersState(); + + const stageClone = this.stage.clone(); + + stageClone.scaleX(1); + stageClone.scaleY(1); + stageClone.x(0); + stageClone.y(0); + + const validLayers = layersState.entities.filter(isValidLayer); + + // Konva bug (?) - when iterating over the array returned from `stage.getLayers()`, if you destroy a layer, the array + // is mutated in-place and the next iteration will skip the next layer. To avoid this, we first collect the layers + // to delete in a separate array and then destroy them. + // TODO(psyche): Maybe report this? + const toDelete: Konva.Layer[] = []; + + for (const konvaLayer of stageClone.getLayers()) { + const layer = validLayers.find((l) => l.id === konvaLayer.id()); + if (!layer) { + toDelete.push(konvaLayer); + } + } + + for (const konvaLayer of toDelete) { + konvaLayer.destroy(); + } + + return stageClone; + } + + _getGenerationMode(): GenerationMode { + const { x, y, width, height } = this.stateApi.getBbox(); + const inpaintMaskLayer = this.util.getMaskLayerClone({ id: 'inpaint_mask' }); + const inpaintMaskImageData = konvaNodeToImageData(inpaintMaskLayer, { x, y, width, height }); + const inpaintMaskTransparency = getImageDataTransparency(inpaintMaskImageData); + const compositeLayer = this.util.getCompositeLayerStageClone(); + const compositeLayerImageData = konvaNodeToImageData(compositeLayer, { x, y, width, height }); + const compositeLayerTransparency = getImageDataTransparency(compositeLayerImageData); + if (compositeLayerTransparency.isPartiallyTransparent) { + if (compositeLayerTransparency.isFullyTransparent) { + return 'txt2img'; + } + return 'outpaint'; + } else { + if (!inpaintMaskTransparency.isFullyTransparent) { + return 'inpaint'; + } + return 'img2img'; + } + } + async _getRegionMaskImage(arg: { id: string; bbox?: Rect; preview?: boolean }): Promise { const { id, bbox, preview = false } = arg; const region = this.stateApi.getRegionsState().entities.find((entity) => entity.id === id); assert(region, `Region entity state with id ${id} not found`); - const adapter = this.get(region.id); - assert(adapter, `Adapter for region ${region.id} not found`); if (region.imageCache) { const imageDTO = await this.util.getImageDTO(region.imageCache.name); @@ -268,30 +343,14 @@ export class KonvaNodeManager { } } - const layer = adapter.konvaLayer.clone(); - const objectGroup = adapter.konvaObjectGroup.clone(); - layer.destroyChildren(); - layer.add(objectGroup); - objectGroup.opacity(1); - objectGroup.cache(); - - const blob = await new Promise((resolve) => { - layer.toBlob({ - callback: (blob) => { - assert(blob, 'Blob is null'); - resolve(blob); - }, - ...bbox, - }); - }); + const layerClone = this.util.getMaskLayerClone({ id }); + const blob = await konvaNodeToBlob(layerClone, bbox); if (preview) { - const base64 = await blobToDataURL(blob); - const caption = `${region.id}: ${region.positivePrompt} / ${region.negativePrompt}`; - openBase64ImageInTab([{ base64, caption }]); + previewBlob(blob, `region ${region.id} mask`); } - layer.destroy(); + layerClone.destroy(); const imageDTO = await this.util.uploadImage(blob, `${region.id}_mask.png`, 'mask', true); this.stateApi.onRegionMaskImageCached(region.id, imageDTO); @@ -301,8 +360,6 @@ export class KonvaNodeManager { async _getInpaintMaskImage(arg: { bbox?: Rect; preview?: boolean }): Promise { const { bbox, preview = false } = arg; const inpaintMask = this.stateApi.getInpaintMaskState(); - const adapter = this.get(inpaintMask.id); - assert(adapter, `Adapter for ${inpaintMask.id} not found`); if (inpaintMask.imageCache) { const imageDTO = await this.util.getImageDTO(inpaintMask.imageCache.name); @@ -311,30 +368,14 @@ export class KonvaNodeManager { } } - const layer = adapter.konvaLayer.clone(); - const objectGroup = adapter.konvaObjectGroup.clone(); - layer.destroyChildren(); - layer.add(objectGroup); - objectGroup.opacity(1); - objectGroup.cache(); - - const blob = await new Promise((resolve) => { - layer.toBlob({ - callback: (blob) => { - assert(blob, 'Blob is null'); - resolve(blob); - }, - ...bbox, - }); - }); + const layerClone = this.util.getMaskLayerClone({ id: inpaintMask.id }); + const blob = await konvaNodeToBlob(layerClone, bbox); if (preview) { - const base64 = await blobToDataURL(blob); - const caption = 'inpaint mask'; - openBase64ImageInTab([{ base64, caption }]); + previewBlob(blob, 'inpaint mask'); } - layer.destroy(); + layerClone.destroy(); const imageDTO = await this.util.uploadImage(blob, 'inpaint_mask.png', 'mask', true); this.stateApi.onInpaintMaskImageCached(imageDTO); @@ -343,8 +384,7 @@ export class KonvaNodeManager { async _getImageSourceImage(arg: { bbox?: Rect; preview?: boolean }): Promise { const { bbox, preview = false } = arg; - const layersState = this.stateApi.getLayersState(); - const { entities, imageCache } = layersState; + const { imageCache } = this.stateApi.getLayersState(); if (imageCache) { const imageDTO = await this.util.getImageDTO(imageCache.name); if (imageDTO) { @@ -352,48 +392,15 @@ export class KonvaNodeManager { } } - const stage = this.stage.clone(); - - stage.scaleX(1); - stage.scaleY(1); - stage.x(0); - stage.y(0); - - const validLayers = entities.filter(isValidLayer); - - // Konva bug (?) - when iterating over the array returned from `stage.getLayers()`, if you destroy a layer, the array - // is mutated in-place and the next iteration will skip the next layer. To avoid this, we first collect the layers - // to delete in a separate array and then destroy them. - // TODO(psyche): Maybe report this? - const toDelete: Konva.Layer[] = []; - - for (const konvaLayer of stage.getLayers()) { - const layer = validLayers.find((l) => l.id === konvaLayer.id()); - if (!layer) { - toDelete.push(konvaLayer); - } - } - - for (const konvaLayer of toDelete) { - konvaLayer.destroy(); - } + const stageClone = this.util.getCompositeLayerStageClone(); - const blob = await new Promise((resolve) => { - stage.toBlob({ - callback: (blob) => { - assert(blob, 'Blob is null'); - resolve(blob); - }, - ...bbox, - }); - }); + const blob = await konvaNodeToBlob(stageClone, bbox); if (preview) { - const base64 = await blobToDataURL(blob); - openBase64ImageInTab([{ base64, caption: 'base layer' }]); + previewBlob(blob, 'image source'); } - stage.destroy(); + stageClone.destroy(); const imageDTO = await this.util.uploadImage(blob, 'base_layer.png', 'general', true); this.stateApi.onLayerImageCached(imageDTO); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index a4dd38627ea..d6ece23dd2e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -11,10 +11,11 @@ import { RG_LAYER_NAME, RG_LAYER_RECT_SHAPE_NAME, } from 'features/controlLayers/konva/naming'; -import type { RgbaColor } from 'features/controlLayers/store/types'; +import type { Rect, RgbaColor } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; -import type { IRect, Vector2d } from 'konva/lib/types'; +import type { Vector2d } from 'konva/lib/types'; +import { assert } from 'tsafe'; /** * Gets the scaled and floored cursor position on the stage. If the cursor is not currently over the stage, returns null. @@ -203,24 +204,33 @@ export const dataURLToImageData = async (dataURL: string, width: number, height: /** * Converts a Konva node to a Blob * @param node - The Konva node to convert to a Blob - * @param boundingBox - The bounding box to crop to + * @param bbox - The bounding box to crop to * @returns A Promise that resolves with Blob of the node cropped to the bounding box */ -export const konvaNodeToBlob = async (node: Konva.Node, boundingBox: IRect): Promise => { - return await canvasToBlob(node.toCanvas(boundingBox)); +export const konvaNodeToBlob = async (node: Konva.Node, bbox?: Rect): Promise => { + return await new Promise((resolve) => { + node.toBlob({ + callback: (blob) => { + assert(blob, 'Blob is null'); + resolve(blob); + }, + ...(bbox ?? {}), + }); + }); }; /** * Converts a Konva node to an ImageData object * @param node - The Konva node to convert to an ImageData object - * @param boundingBox - The bounding box to crop to + * @param bbox - The bounding box to crop to * @returns A Promise that resolves with ImageData object of the node cropped to the bounding box */ -export const konvaNodeToImageData = async (node: Konva.Node, boundingBox: IRect): Promise => { +export const konvaNodeToImageData = (node: Konva.Node, bbox?: Rect): ImageData => { // get a dataURL of the bbox'd region - const dataURL = node.toDataURL(boundingBox); - - return await dataURLToImageData(dataURL, boundingBox.width, boundingBox.height); + const canvas = node.toCanvas({ ...(bbox ?? {}) }); + const ctx = canvas.getContext('2d'); + assert(ctx, 'ctx is null'); + return ctx.getImageData(0, 0, canvas.width, canvas.height); }; /** @@ -246,3 +256,16 @@ export const getPixelUnderCursor = (stage: Konva.Stage): RgbaColor | null => { return { r, g, b, a }; }; + +export const previewBlob = async (blob: Blob, label?: string) => { + const url = URL.createObjectURL(blob); + const w = window.open(''); + if (!w) { + return; + } + if (label) { + w.document.write(label); + w.document.write('
'); + } + w.document.write(``); +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 19642164899..482ffe1a5f7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -906,3 +906,5 @@ export const isLine = (obj: RenderableObject): obj is BrushLine | EraserLine => export type RemoveIndexString = { [K in keyof T as string extends K ? never : K]: T[K]; }; + +export type GenerationMode = 'txt2img' | 'img2img' | 'inpaint' | 'outpaint'; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts index d9adb21cd58..cc859780f27 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts @@ -1,4 +1,5 @@ import type { RootState } from 'app/store/store'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { CLIP_SKIP, @@ -29,7 +30,7 @@ import { assert } from 'tsafe'; import { addRegions } from './addRegions'; -export const buildGenerationTabGraph = async (state: RootState): Promise => { +export const buildGenerationTabGraph = async (state: RootState, manager: KonvaNodeManager): Promise => { const { model, cfgScale: cfg_scale, @@ -159,6 +160,7 @@ export const buildGenerationTabGraph = async (state: RootState): Promise => { +export const buildGenerationTabSDXLGraph = async ( + state: RootState, + manager: KonvaNodeManager +): Promise => { const { model, cfgScale: cfg_scale, @@ -42,6 +46,7 @@ export const buildGenerationTabSDXLGraph = async (state: RootState): Promise Date: Mon, 24 Jun 2024 11:11:20 +1000 Subject: [PATCH 111/678] feat(ui): node manager getter/setter --- .../listeners/enqueueRequestedLinear.ts | 6 ++---- .../src/features/controlLayers/konva/nodeManager.ts | 11 +++++++++++ .../controlLayers/konva/renderers/renderer.ts | 7 ++----- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index 29ff4b22248..7748e359989 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -1,12 +1,11 @@ import { enqueueRequested } from 'app/store/actions'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { $nodeManager } from 'features/controlLayers/konva/renderers/renderer'; +import { getNodeManager } from 'features/controlLayers/konva/nodeManager'; import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; import { buildGenerationTabGraph } from 'features/nodes/util/graph/generation/buildGenerationTabGraph'; import { buildGenerationTabSDXLGraph } from 'features/nodes/util/graph/generation/buildGenerationTabSDXLGraph'; import { queueApi } from 'services/api/endpoints/queue'; -import { assert } from 'tsafe'; export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) => { startAppListening({ @@ -20,8 +19,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) let graph; - const manager = $nodeManager.get(); - assert(manager, 'Konva node manager not initialized'); + const manager = getNodeManager(); console.log('generation mode', manager.util.getGenerationMode()); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts index f31a74b1f3d..2fa562b44f6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts @@ -21,6 +21,7 @@ import type { import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers'; import type Konva from 'konva'; import type { Vector2d } from 'konva/lib/types'; +import { atom } from 'nanostores'; import { getImageDTO as defaultGetImageDTO, uploadImage as defaultUploadImage } from 'services/api/endpoints/images'; import type { ImageCategory, ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; @@ -466,3 +467,13 @@ export class KonvaEntityAdapter { return this.objectRecords.delete(id); } } + +const $nodeManager = atom(null); +export const setNodeManager = (manager: KonvaNodeManager) => { + $nodeManager.set(manager); +}; +export const getNodeManager = () => { + const nodeManager = $nodeManager.get(); + assert(nodeManager, 'Konva node manager not initialized'); + return nodeManager; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts index 939d9d776c5..11fb09e57f6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -4,7 +4,7 @@ import { logger } from 'app/logging/logger'; import { $isDebugging } from 'app/store/nanostores/isDebugging'; import type { RootState } from 'app/store/store'; import { setStageEventHandlers } from 'features/controlLayers/konva/events'; -import { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import { KonvaNodeManager, setNodeManager } from 'features/controlLayers/konva/nodeManager'; import { getArrangeEntities } from 'features/controlLayers/konva/renderers/arrange'; import { createBackgroundLayer, getRenderBackground } from 'features/controlLayers/konva/renderers/background'; import { updateBboxes } from 'features/controlLayers/konva/renderers/bbox'; @@ -66,12 +66,9 @@ import type { import type Konva from 'konva'; import type { IRect, Vector2d } from 'konva/lib/types'; import { debounce } from 'lodash-es'; -import { atom } from 'nanostores'; import type { RgbaColor } from 'react-colorful'; import type { ImageDTO } from 'services/api/types'; -export const $nodeManager = atom(null); - /** * Initializes the canvas renderer. It subscribes to the redux store and listens for changes directly, bypassing the * react rendering cycle entirely, improving canvas performance. @@ -301,7 +298,7 @@ export const initializeRenderer = ( }; const manager = new KonvaNodeManager(stage, container); - $nodeManager.set(manager); + setNodeManager(manager); manager.background = { layer: createBackgroundLayer() }; manager.stage.add(manager.background.layer); From 055737a6e868892fb0a4b8381d13572a0f4f5657 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 24 Jun 2024 11:11:53 +1000 Subject: [PATCH 112/678] feat(ui): simplified konva node to blob/imagedata utils --- .../src/features/controlLayers/konva/util.ts | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index d6ece23dd2e..233c6f6dc80 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -159,21 +159,6 @@ export const downloadBlob = (blob: Blob, fileName: string) => { a.remove(); }; -/** - * Gets a Blob from a HTMLCanvasElement. - */ -export const canvasToBlob = async (canvas: HTMLCanvasElement): Promise => { - return new Promise((resolve, reject) => { - canvas.toBlob((blob) => { - if (blob) { - resolve(blob); - return; - } - reject('Unable to create Blob'); - }); - }); -}; - /** * Gets an ImageData object from an image dataURL by drawing it to a canvas. */ @@ -201,24 +186,31 @@ export const dataURLToImageData = async (dataURL: string, width: number, height: }); }; +export const konvaNodeToCanvas = (node: Konva.Node, bbox?: Rect): HTMLCanvasElement => { + return node.toCanvas({ ...(bbox ?? {}) }); +}; + /** * Converts a Konva node to a Blob * @param node - The Konva node to convert to a Blob * @param bbox - The bounding box to crop to * @returns A Promise that resolves with Blob of the node cropped to the bounding box */ -export const konvaNodeToBlob = async (node: Konva.Node, bbox?: Rect): Promise => { - return await new Promise((resolve) => { - node.toBlob({ - callback: (blob) => { - assert(blob, 'Blob is null'); - resolve(blob); - }, - ...(bbox ?? {}), +export const canvasToBlob = (canvas: HTMLCanvasElement): Promise => { + return new Promise((resolve) => { + canvas.toBlob((blob) => { + assert(blob, 'blob is null'); + resolve(blob); }); }); }; +export const canvasToImageData = (canvas: HTMLCanvasElement): ImageData => { + const ctx = canvas.getContext('2d'); + assert(ctx, 'ctx is null'); + return ctx.getImageData(0, 0, canvas.width, canvas.height); +}; + /** * Converts a Konva node to an ImageData object * @param node - The Konva node to convert to an ImageData object @@ -226,11 +218,19 @@ export const konvaNodeToBlob = async (node: Konva.Node, bbox?: Rect): Promise { - // get a dataURL of the bbox'd region - const canvas = node.toCanvas({ ...(bbox ?? {}) }); - const ctx = canvas.getContext('2d'); - assert(ctx, 'ctx is null'); - return ctx.getImageData(0, 0, canvas.width, canvas.height); + const canvas = konvaNodeToCanvas(node, bbox); + return canvasToImageData(canvas); +}; + +/** + * Converts a Konva node to a Blob + * @param node - The Konva node to convert to a Blob + * @param bbox - The bounding box to crop to + * @returns A Promise that resolves to the Blob or null, + */ +export const konvaNodeToBlob = (node: Konva.Node, bbox?: Rect): Promise => { + const canvas = konvaNodeToCanvas(node, bbox); + return canvasToBlob(canvas); }; /** From e839765ddc15da5474dd7af53218a34db0c24fb4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 24 Jun 2024 12:22:25 +1000 Subject: [PATCH 113/678] feat(ui): minor change to canvas bbox state type --- .../controlLayers/store/bboxReducers.ts | 26 +++++++------------ .../controlLayers/store/canvasV2Slice.ts | 6 +++-- .../controlLayers/store/paramsReducers.ts | 4 +-- .../src/features/controlLayers/store/types.ts | 6 +++-- .../InfillAndScaling/ParamScaledHeight.tsx | 2 +- .../InfillAndScaling/ParamScaledWidth.tsx | 2 +- 6 files changed, 21 insertions(+), 25 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts index dc75e2e8e66..f2e814290ec 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts @@ -3,37 +3,31 @@ import type { BoundingBoxScaleMethod, CanvasV2State, Dimensions } from 'features import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; import type { IRect } from 'konva/lib/types'; +import { pick } from 'lodash-es'; export const bboxReducers = { scaledBboxChanged: (state, action: PayloadAction>) => { - const { width, height } = action.payload; - state.bbox.scaledWidth = width ?? state.bbox.scaledWidth; - state.bbox.scaledHeight = height ?? state.bbox.scaledHeight; + state.layers.imageCache = null; + state.bbox.scaledSize = { ...state.bbox.scaledSize, ...action.payload }; }, bboxScaleMethodChanged: (state, action: PayloadAction) => { state.bbox.scaleMethod = action.payload; + state.layers.imageCache = null; if (action.payload === 'auto') { - const bboxDims = { width: state.bbox.width, height: state.bbox.height }; const optimalDimension = getOptimalDimension(state.params.model); - const scaledBboxDims = getScaledBoundingBoxDimensions(bboxDims, optimalDimension); - state.bbox.scaledWidth = scaledBboxDims.width; - state.bbox.scaledHeight = scaledBboxDims.height; + const size = pick(state.bbox, 'width', 'height'); + state.bbox.scaledSize = getScaledBoundingBoxDimensions(size, optimalDimension); } }, bboxChanged: (state, action: PayloadAction) => { - const { x, y, width, height } = action.payload; - state.bbox.x = x; - state.bbox.y = y; - state.bbox.width = width; - state.bbox.height = height; + state.bbox = { ...state.bbox, ...action.payload }; + state.layers.imageCache = null; if (state.bbox.scaleMethod === 'auto') { - const bboxDims = { width: state.bbox.width, height: state.bbox.height }; const optimalDimension = getOptimalDimension(state.params.model); - const scaledBboxDims = getScaledBoundingBoxDimensions(bboxDims, optimalDimension); - state.bbox.scaledWidth = scaledBboxDims.width; - state.bbox.scaledHeight = scaledBboxDims.height; + const size = pick(state.bbox, 'width', 'height'); + state.bbox.scaledSize = getScaledBoundingBoxDimensions(size, optimalDimension); } }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 60d7151815f..2e28fe6370c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -64,8 +64,10 @@ const initialState: CanvasV2State = { width: 512, height: 512, scaleMethod: 'auto', - scaledWidth: 512, - scaledHeight: 512, + scaledSize: { + width: 512, + height: 512, + }, }, settings: { maskOpacity: 0.3, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts index ba36deaa323..fe7f895651e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts @@ -68,9 +68,7 @@ export const paramsReducers = { state.bbox.height = bboxDims.height; if (state.bbox.scaleMethod === 'auto') { - const scaledBboxDims = getScaledBoundingBoxDimensions(bboxDims, optimalDimension); - state.bbox.scaledWidth = scaledBboxDims.width; - state.bbox.scaledHeight = scaledBboxDims.height; + state.bbox.scaledSize = getScaledBoundingBoxDimensions(bboxDims, optimalDimension); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 482ffe1a5f7..1d2b57922cb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -831,9 +831,11 @@ export type CanvasV2State = { y: number; width: ParameterWidth; height: ParameterHeight; + scaledSize: { + width: ParameterWidth; + height: ParameterHeight; + }; scaleMethod: BoundingBoxScaleMethod; - scaledWidth: ParameterWidth; - scaledHeight: ParameterHeight; }; compositing: { maskBlur: number; diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx index 71a5dc28c9f..d9871bc78fc 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx @@ -10,7 +10,7 @@ const ParamScaledHeight = () => { const dispatch = useAppDispatch(); const optimalDimension = useAppSelector(selectOptimalDimension); const isManual = useAppSelector((s) => s.canvasV2.bbox.scaleMethod === 'manual'); - const height = useAppSelector((s) => s.canvasV2.bbox.scaledHeight); + const height = useAppSelector((s) => s.canvasV2.bbox.scaledSize.height); const sliderMin = useAppSelector((s) => s.config.sd.scaledBoundingBoxHeight.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.scaledBoundingBoxHeight.sliderMax); const numberInputMin = useAppSelector((s) => s.config.sd.scaledBoundingBoxHeight.numberInputMin); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx index ed09e4599aa..6f5338c9ef0 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx @@ -10,7 +10,7 @@ const ParamScaledWidth = () => { const dispatch = useAppDispatch(); const optimalDimension = useAppSelector(selectOptimalDimension); const isManual = useAppSelector((s) => s.canvasV2.bbox.scaleMethod === 'manual'); - const width = useAppSelector((s) => s.canvasV2.bbox.scaledWidth); + const width = useAppSelector((s) => s.canvasV2.bbox.scaledSize.width); const sliderMin = useAppSelector((s) => s.config.sd.scaledBoundingBoxWidth.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.scaledBoundingBoxWidth.sliderMax); const numberInputMin = useAppSelector((s) => s.config.sd.scaledBoundingBoxWidth.numberInputMin); From 30c4ed87b5bc43d28a520d0a444ff47cfbcbe3b5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 24 Jun 2024 15:19:51 +1000 Subject: [PATCH 114/678] feat(ui): txt2img & img2img graphs --- .../listeners/enqueueRequestedLinear.ts | 17 +- .../util/graph/generation/addNSFWChecker.ts | 2 +- .../util/graph/generation/addSDXLLoRAs.ts | 2 +- .../util/graph/generation/addWatermarker.ts | 2 +- ...Graph.ts => buildImageToImageSDXLGraph.ts} | 41 +-- .../util/graph/generation/buildSD1Graph.ts | 246 ++++++++++++++++++ .../util/graph/generation/buildSDXLGraph.ts | 246 ++++++++++++++++++ ...raph.ts => buildTextToImageSD1SD2Graph.ts} | 36 ++- .../nodes/util/graph/graphBuilderUtils.ts | 10 +- 9 files changed, 562 insertions(+), 40 deletions(-) rename invokeai/frontend/web/src/features/nodes/util/graph/generation/{buildGenerationTabSDXLGraph.ts => buildImageToImageSDXLGraph.ts} (85%) create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts rename invokeai/frontend/web/src/features/nodes/util/graph/generation/{buildGenerationTabGraph.ts => buildTextToImageSD1SD2Graph.ts} (85%) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index 7748e359989..0e1544b17bf 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -3,9 +3,10 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware' import { getNodeManager } from 'features/controlLayers/konva/nodeManager'; import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; -import { buildGenerationTabGraph } from 'features/nodes/util/graph/generation/buildGenerationTabGraph'; -import { buildGenerationTabSDXLGraph } from 'features/nodes/util/graph/generation/buildGenerationTabSDXLGraph'; +import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph'; +import { buildSDXLGraph } from 'features/nodes/util/graph/generation/buildSDXLGraph'; import { queueApi } from 'services/api/endpoints/queue'; +import { assert } from 'tsafe'; export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) => { startAppListening({ @@ -20,13 +21,15 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) let graph; const manager = getNodeManager(); + assert(model, 'No model found in state'); + const base = model.base; - console.log('generation mode', manager.util.getGenerationMode()); - - if (model?.base === 'sdxl') { - graph = await buildGenerationTabSDXLGraph(state, manager); + if (base === 'sdxl') { + graph = await buildSDXLGraph(state, manager); + } else if (base === 'sd-1' || base === 'sd-2') { + graph = await buildSD1Graph(state, manager); } else { - graph = await buildGenerationTabGraph(state, manager); + assert(false, `No graph builders for base ${base}`); } const batchConfig = prepareLinearUIBatch(state, graph, prepend); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addNSFWChecker.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addNSFWChecker.ts index 7850413195c..939aa6894c8 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addNSFWChecker.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addNSFWChecker.ts @@ -10,7 +10,7 @@ import type { Invocation } from 'services/api/types'; */ export const addNSFWChecker = ( g: Graph, - imageOutput: Invocation<'l2i'> | Invocation<'img_nsfw'> | Invocation<'img_watermark'> + imageOutput: Invocation<'l2i'> | Invocation<'img_nsfw'> | Invocation<'img_watermark'> | Invocation<'img_resize'> ): Invocation<'img_nsfw'> => { const nsfw = g.addNode({ id: NSFW_CHECKER, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts index d7377da4b06..f274ec9a099 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts @@ -4,7 +4,7 @@ import { LORA_LOADER } from 'features/nodes/util/graph/constants'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import type { Invocation, S } from 'services/api/types'; -export const addSDXLLoRas = ( +export const addSDXLLoRAs = ( state: RootState, g: Graph, denoise: Invocation<'denoise_latents'> | Invocation<'tiled_multi_diffusion_denoise_latents'>, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addWatermarker.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addWatermarker.ts index 2a7af866f82..9111a77630d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addWatermarker.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addWatermarker.ts @@ -10,7 +10,7 @@ import type { Invocation } from 'services/api/types'; */ export const addWatermarker = ( g: Graph, - imageOutput: Invocation<'l2i'> | Invocation<'img_nsfw'> | Invocation<'img_watermark'> + imageOutput: Invocation<'l2i'> | Invocation<'img_nsfw'> | Invocation<'img_watermark'> | Invocation<'img_resize'> ): Invocation<'img_watermark'> => { const watermark = g.addNode({ id: WATERMARKER, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImageToImageSDXLGraph.ts similarity index 85% rename from invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabSDXLGraph.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImageToImageSDXLGraph.ts index 9dc82bb237a..4dd0e1e0565 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImageToImageSDXLGraph.ts @@ -16,22 +16,23 @@ import { import { addControlAdapters } from 'features/nodes/util/graph/generation/addControlAdapters'; import { addIPAdapters } from 'features/nodes/util/graph/generation/addIPAdapters'; import { addNSFWChecker } from 'features/nodes/util/graph/generation/addNSFWChecker'; -import { addSDXLLoRas } from 'features/nodes/util/graph/generation/addSDXLLoRAs'; +import { addSDXLLoRAs } from 'features/nodes/util/graph/generation/addSDXLLoRAs'; import { addSDXLRefiner } from 'features/nodes/util/graph/generation/addSDXLRefiner'; import { addSeamless } from 'features/nodes/util/graph/generation/addSeamless'; import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermarker'; import { Graph } from 'features/nodes/util/graph/generation/Graph'; -import { getBoardField, getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils'; +import { getBoardField, getPresetModifiedPrompts , getSizes } from 'features/nodes/util/graph/graphBuilderUtils'; import type { Invocation, NonNullableGraph } from 'services/api/types'; import { isNonRefinerMainModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; import { addRegions } from './addRegions'; -export const buildGenerationTabSDXLGraph = async ( +export const buildImageToImageSDXLGraph = async ( state: RootState, manager: KonvaNodeManager ): Promise => { + const { bbox, params } = state.canvasV2; const { model, cfgScale: cfg_scale, @@ -42,17 +43,17 @@ export const buildGenerationTabSDXLGraph = async ( shouldUseCpuNoise, vaePrecision, vae, - positivePrompt, - negativePrompt, refinerModel, refinerStart, img2imgStrength, - } = state.canvasV2.params; - const { width, height } = state.canvasV2.bbox; + } = params; assert(model, 'No model found in state'); const { positivePrompt, negativePrompt, positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state); + const { originalSize, scaledSize } = getSizes(bbox); + + const g = new Graph(SDXL_CONTROL_LAYERS_GRAPH); const modelLoader = g.addNode({ @@ -80,8 +81,14 @@ export const buildGenerationTabSDXLGraph = async ( type: 'collect', id: NEGATIVE_CONDITIONING_COLLECT, }); - const noise = g.addNode({ type: 'noise', id: NOISE, seed, width, height, use_cpu: shouldUseCpuNoise }); - const i2l = g.addNode({ type: 'i2l', id: 'i2l' }); + const noise = g.addNode({ + type: 'noise', + id: NOISE, + seed, + width: scaledSize.width, + height: scaledSize.height, + use_cpu: shouldUseCpuNoise, + }); const denoise = g.addNode({ type: 'denoise_latents', id: SDXL_DENOISE_LATENTS, @@ -110,7 +117,8 @@ export const buildGenerationTabSDXLGraph = async ( }) : null; - let imageOutput: Invocation<'l2i'> | Invocation<'img_nsfw'> | Invocation<'img_watermark'> = l2i; + let imageOutput: Invocation<'l2i'> | Invocation<'img_nsfw'> | Invocation<'img_watermark'> | Invocation<'img_resize'> = + l2i; g.addEdge(modelLoader, 'unet', denoise, 'unet'); g.addEdge(modelLoader, 'clip', posCond, 'clip'); @@ -122,7 +130,6 @@ export const buildGenerationTabSDXLGraph = async ( g.addEdge(posCondCollect, 'collection', denoise, 'positive_conditioning'); g.addEdge(negCondCollect, 'collection', denoise, 'negative_conditioning'); g.addEdge(noise, 'noise', denoise, 'noise'); - g.addEdge(i2l, 'latents', denoise, 'latents'); g.addEdge(denoise, 'latents', l2i, 'latents'); const modelConfig = await fetchModelConfigWithTypeGuard(model.key, isNonRefinerMainModelConfig); @@ -132,8 +139,8 @@ export const buildGenerationTabSDXLGraph = async ( generation_mode: 'sdxl_txt2img', cfg_scale, cfg_rescale_multiplier, - height, - width, + width: scaledSize.width, + height: scaledSize.height, positive_prompt: positivePrompt, negative_prompt: negativePrompt, model: Graph.getModelMetadataField(modelConfig), @@ -148,18 +155,19 @@ export const buildGenerationTabSDXLGraph = async ( const seamless = addSeamless(state, g, denoise, modelLoader, vaeLoader); - addSDXLLoRas(state, g, denoise, modelLoader, seamless, posCond, negCond); + addSDXLLoRAs(state, g, denoise, modelLoader, seamless, posCond, negCond); // We might get the VAE from the main model, custom VAE, or seamless node. const vaeSource = seamless ?? vaeLoader ?? modelLoader; g.addEdge(vaeSource, 'vae', l2i, 'vae'); - g.addEdge(vaeSource, 'vae', i2l, 'vae'); // Add Refiner if enabled if (refinerModel) { await addSDXLRefiner(state, g, denoise, seamless, posCond, negCond, l2i); } + + const _addedCAs = addControlAdapters(state.canvasV2.controlAdapters.entities, g, denoise, modelConfig.base); const _addedIPAs = addIPAdapters(state.canvasV2.ipAdapters.entities, g, denoise, modelConfig.base); const _addedRegions = await addRegions( @@ -175,9 +183,6 @@ export const buildGenerationTabSDXLGraph = async ( posCondCollect, negCondCollect ); - const { image_name } = await manager.util.getImageSourceImage({ bbox: state.canvasV2.bbox, preview: true }); - await manager.util.getInpaintMaskImage({ bbox: state.canvasV2.bbox, preview: true }); - i2l.image = { image_name }; if (state.system.shouldUseNSFWChecker) { imageOutput = addNSFWChecker(g, imageOutput); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts new file mode 100644 index 00000000000..bf219cd1604 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -0,0 +1,246 @@ +import type { RootState } from 'app/store/store'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; +import { + CLIP_SKIP, + CONTROL_LAYERS_GRAPH, + DENOISE_LATENTS, + LATENTS_TO_IMAGE, + MAIN_MODEL_LOADER, + NEGATIVE_CONDITIONING, + NEGATIVE_CONDITIONING_COLLECT, + NOISE, + POSITIVE_CONDITIONING, + POSITIVE_CONDITIONING_COLLECT, + VAE_LOADER, +} from 'features/nodes/util/graph/constants'; +import { addControlAdapters } from 'features/nodes/util/graph/generation/addControlAdapters'; +// import { addHRF } from 'features/nodes/util/graph/generation/addHRF'; +import { addIPAdapters } from 'features/nodes/util/graph/generation/addIPAdapters'; +import { addLoRAs } from 'features/nodes/util/graph/generation/addLoRAs'; +import { addNSFWChecker } from 'features/nodes/util/graph/generation/addNSFWChecker'; +import { addSeamless } from 'features/nodes/util/graph/generation/addSeamless'; +import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermarker'; +import type { GraphType } from 'features/nodes/util/graph/generation/Graph'; +import { Graph } from 'features/nodes/util/graph/generation/Graph'; +import { getBoardField, getSizes } from 'features/nodes/util/graph/graphBuilderUtils'; +import { isEqual, pick } from 'lodash-es'; +import type { Invocation } from 'services/api/types'; +import { isNonRefinerMainModelConfig } from 'services/api/types'; +import { assert } from 'tsafe'; + +import { addRegions } from './addRegions'; + +export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager): Promise => { + const generationMode = manager.util.getGenerationMode(); + + const { bbox, params } = state.canvasV2; + + const { + model, + cfgScale: cfg_scale, + cfgRescaleMultiplier: cfg_rescale_multiplier, + scheduler, + steps, + clipSkip: skipped_layers, + shouldUseCpuNoise, + vaePrecision, + seed, + vae, + positivePrompt, + negativePrompt, + img2imgStrength, + } = params; + + assert(model, 'No model found in state'); + + const { originalSize, scaledSize } = getSizes(bbox); + + const g = new Graph(CONTROL_LAYERS_GRAPH); + const modelLoader = g.addNode({ + type: 'main_model_loader', + id: MAIN_MODEL_LOADER, + model, + }); + const clipSkip = g.addNode({ + type: 'clip_skip', + id: CLIP_SKIP, + skipped_layers, + }); + const posCond = g.addNode({ + type: 'compel', + id: POSITIVE_CONDITIONING, + prompt: positivePrompt, + }); + const posCondCollect = g.addNode({ + type: 'collect', + id: POSITIVE_CONDITIONING_COLLECT, + }); + const negCond = g.addNode({ + type: 'compel', + id: NEGATIVE_CONDITIONING, + prompt: negativePrompt, + }); + const negCondCollect = g.addNode({ + type: 'collect', + id: NEGATIVE_CONDITIONING_COLLECT, + }); + const noise = g.addNode({ + type: 'noise', + id: NOISE, + seed, + width: scaledSize.width, + height: scaledSize.height, + use_cpu: shouldUseCpuNoise, + }); + const denoise = g.addNode({ + type: 'denoise_latents', + id: DENOISE_LATENTS, + cfg_scale, + cfg_rescale_multiplier, + scheduler, + steps, + denoising_start: 0, + denoising_end: 1, + }); + const l2i = g.addNode({ + type: 'l2i', + id: LATENTS_TO_IMAGE, + fp32: vaePrecision === 'fp32', + board: getBoardField(state), + // This is the terminal node and must always save to gallery. + is_intermediate: false, + use_cache: false, + }); + const vaeLoader = + vae?.base === model.base + ? g.addNode({ + type: 'vae_loader', + id: VAE_LOADER, + vae_model: vae, + }) + : null; + + let imageOutput: Invocation<'l2i'> | Invocation<'img_nsfw'> | Invocation<'img_watermark'> | Invocation<'img_resize'> = + l2i; + + g.addEdge(modelLoader, 'unet', denoise, 'unet'); + g.addEdge(modelLoader, 'clip', clipSkip, 'clip'); + g.addEdge(clipSkip, 'clip', posCond, 'clip'); + g.addEdge(clipSkip, 'clip', negCond, 'clip'); + g.addEdge(posCond, 'conditioning', posCondCollect, 'item'); + g.addEdge(negCond, 'conditioning', negCondCollect, 'item'); + g.addEdge(posCondCollect, 'collection', denoise, 'positive_conditioning'); + g.addEdge(negCondCollect, 'collection', denoise, 'negative_conditioning'); + g.addEdge(noise, 'noise', denoise, 'noise'); + g.addEdge(denoise, 'latents', l2i, 'latents'); + + const modelConfig = await fetchModelConfigWithTypeGuard(model.key, isNonRefinerMainModelConfig); + assert(modelConfig.base === 'sd-1' || modelConfig.base === 'sd-2'); + + g.upsertMetadata({ + generation_mode: 'txt2img', + cfg_scale, + cfg_rescale_multiplier, + width: scaledSize.width, + height: scaledSize.height, + positive_prompt: positivePrompt, + negative_prompt: negativePrompt, + model: Graph.getModelMetadataField(modelConfig), + seed, + steps, + rand_device: shouldUseCpuNoise ? 'cpu' : 'cuda', + scheduler, + clip_skip: skipped_layers, + vae: vae ?? undefined, + }); + + const seamless = addSeamless(state, g, denoise, modelLoader, vaeLoader); + + addLoRAs(state, g, denoise, modelLoader, seamless, clipSkip, posCond, negCond); + + // We might get the VAE from the main model, custom VAE, or seamless node. + const vaeSource = seamless ?? vaeLoader ?? modelLoader; + g.addEdge(vaeSource, 'vae', l2i, 'vae'); + + if (generationMode === 'txt2img') { + if (!isEqual(scaledSize, originalSize)) { + // We are using scaled bbox and need to resize the output image back to the original size. + imageOutput = g.addNode({ + id: 'img_resize', + type: 'img_resize', + ...originalSize, + is_intermediate: false, + use_cache: false, + }); + g.addEdge(l2i, 'image', imageOutput, 'image'); + } + } else if (generationMode === 'img2img') { + const { image_name } = await manager.util.getImageSourceImage({ + bbox: pick(bbox, ['x', 'y', 'width', 'height']), + preview: true, + }); + + denoise.denoising_start = 1 - img2imgStrength; + + if (!isEqual(scaledSize, originalSize)) { + // We are using scaled bbox and need to resize the output image back to the original size. + const initialImageResize = g.addNode({ + id: 'initial_image_resize', + type: 'img_resize', + ...scaledSize, + image: { image_name }, + }); + const i2l = g.addNode({ id: 'i2l', type: 'i2l' }); + + g.addEdge(vaeSource, 'vae', i2l, 'vae'); + g.addEdge(initialImageResize, 'image', i2l, 'image'); + g.addEdge(i2l, 'latents', denoise, 'latents'); + + imageOutput = g.addNode({ + id: 'img_resize', + type: 'img_resize', + ...originalSize, + is_intermediate: false, + use_cache: false, + }); + g.addEdge(l2i, 'image', imageOutput, 'image'); + } else { + const i2l = g.addNode({ id: 'i2l', type: 'i2l', image: { image_name } }); + g.addEdge(vaeSource, 'vae', i2l, 'vae'); + g.addEdge(i2l, 'latents', denoise, 'latents'); + } + } + + const _addedCAs = addControlAdapters(state.canvasV2.controlAdapters.entities, g, denoise, modelConfig.base); + const _addedIPAs = addIPAdapters(state.canvasV2.ipAdapters.entities, g, denoise, modelConfig.base); + const _addedRegions = await addRegions( + manager, + state.canvasV2.regions.entities, + g, + state.canvasV2.document, + state.canvasV2.bbox, + modelConfig.base, + denoise, + posCond, + negCond, + posCondCollect, + negCondCollect + ); + + // const isHRFAllowed = !addedLayers.some((l) => isInitialImageLayer(l) || isRegionalGuidanceLayer(l)); + // if (isHRFAllowed && state.hrf.hrfEnabled) { + // imageOutput = addHRF(state, g, denoise, noise, l2i, vaeSource); + // } + + if (state.system.shouldUseNSFWChecker) { + imageOutput = addNSFWChecker(g, imageOutput); + } + + if (state.system.shouldUseWatermarker) { + imageOutput = addWatermarker(g, imageOutput); + } + + g.setMetadataReceivingNode(imageOutput); + return g.getGraph(); +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts new file mode 100644 index 00000000000..5523f777957 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -0,0 +1,246 @@ +import type { RootState } from 'app/store/store'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; +import { + LATENTS_TO_IMAGE, + NEGATIVE_CONDITIONING, + NEGATIVE_CONDITIONING_COLLECT, + NOISE, + POSITIVE_CONDITIONING, + POSITIVE_CONDITIONING_COLLECT, + SDXL_CONTROL_LAYERS_GRAPH, + SDXL_DENOISE_LATENTS, + SDXL_MODEL_LOADER, + VAE_LOADER, +} from 'features/nodes/util/graph/constants'; +import { addControlAdapters } from 'features/nodes/util/graph/generation/addControlAdapters'; +import { addIPAdapters } from 'features/nodes/util/graph/generation/addIPAdapters'; +import { addNSFWChecker } from 'features/nodes/util/graph/generation/addNSFWChecker'; +import { addSDXLLoRAs } from 'features/nodes/util/graph/generation/addSDXLLoRAs'; +import { addSDXLRefiner } from 'features/nodes/util/graph/generation/addSDXLRefiner'; +import { addSeamless } from 'features/nodes/util/graph/generation/addSeamless'; +import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermarker'; +import { Graph } from 'features/nodes/util/graph/generation/Graph'; +import { getBoardField, getSDXLStylePrompts, getSizes } from 'features/nodes/util/graph/graphBuilderUtils'; +import { isEqual, pick } from 'lodash-es'; +import type { Invocation, NonNullableGraph } from 'services/api/types'; +import { isNonRefinerMainModelConfig } from 'services/api/types'; +import { assert } from 'tsafe'; + +import { addRegions } from './addRegions'; + +export const buildSDXLGraph = async (state: RootState, manager: KonvaNodeManager): Promise => { + const generationMode = manager.util.getGenerationMode(); + + const { bbox, params } = state.canvasV2; + + const { + model, + cfgScale: cfg_scale, + cfgRescaleMultiplier: cfg_rescale_multiplier, + scheduler, + seed, + steps, + shouldUseCpuNoise, + vaePrecision, + vae, + positivePrompt, + negativePrompt, + refinerModel, + refinerStart, + img2imgStrength, + } = params; + + assert(model, 'No model found in state'); + + const { originalSize, scaledSize } = getSizes(bbox); + + const { positiveStylePrompt, negativeStylePrompt } = getSDXLStylePrompts(state); + + const g = new Graph(SDXL_CONTROL_LAYERS_GRAPH); + const modelLoader = g.addNode({ + type: 'sdxl_model_loader', + id: SDXL_MODEL_LOADER, + model, + }); + const posCond = g.addNode({ + type: 'sdxl_compel_prompt', + id: POSITIVE_CONDITIONING, + prompt: positivePrompt, + style: positiveStylePrompt, + }); + const posCondCollect = g.addNode({ + type: 'collect', + id: POSITIVE_CONDITIONING_COLLECT, + }); + const negCond = g.addNode({ + type: 'sdxl_compel_prompt', + id: NEGATIVE_CONDITIONING, + prompt: negativePrompt, + style: negativeStylePrompt, + }); + const negCondCollect = g.addNode({ + type: 'collect', + id: NEGATIVE_CONDITIONING_COLLECT, + }); + const noise = g.addNode({ + type: 'noise', + id: NOISE, + seed, + width: scaledSize.width, + height: scaledSize.height, + use_cpu: shouldUseCpuNoise, + }); + const denoise = g.addNode({ + type: 'denoise_latents', + id: SDXL_DENOISE_LATENTS, + cfg_scale, + cfg_rescale_multiplier, + scheduler, + steps, + denoising_start: 0, + denoising_end: refinerModel ? refinerStart : 1, + }); + const l2i = g.addNode({ + type: 'l2i', + id: LATENTS_TO_IMAGE, + fp32: vaePrecision === 'fp32', + board: getBoardField(state), + // This is the terminal node and must always save to gallery. + is_intermediate: false, + use_cache: false, + }); + const vaeLoader = + vae?.base === model.base + ? g.addNode({ + type: 'vae_loader', + id: VAE_LOADER, + vae_model: vae, + }) + : null; + + let imageOutput: Invocation<'l2i'> | Invocation<'img_nsfw'> | Invocation<'img_watermark'> | Invocation<'img_resize'> = + l2i; + + g.addEdge(modelLoader, 'unet', denoise, 'unet'); + g.addEdge(modelLoader, 'clip', posCond, 'clip'); + g.addEdge(modelLoader, 'clip', negCond, 'clip'); + g.addEdge(modelLoader, 'clip2', posCond, 'clip2'); + g.addEdge(modelLoader, 'clip2', negCond, 'clip2'); + g.addEdge(posCond, 'conditioning', posCondCollect, 'item'); + g.addEdge(negCond, 'conditioning', negCondCollect, 'item'); + g.addEdge(posCondCollect, 'collection', denoise, 'positive_conditioning'); + g.addEdge(negCondCollect, 'collection', denoise, 'negative_conditioning'); + g.addEdge(noise, 'noise', denoise, 'noise'); + g.addEdge(denoise, 'latents', l2i, 'latents'); + + const modelConfig = await fetchModelConfigWithTypeGuard(model.key, isNonRefinerMainModelConfig); + assert(modelConfig.base === 'sdxl'); + + g.upsertMetadata({ + generation_mode: 'sdxl_txt2img', + cfg_scale, + cfg_rescale_multiplier, + width: scaledSize.width, + height: scaledSize.height, + positive_prompt: positivePrompt, + negative_prompt: negativePrompt, + model: Graph.getModelMetadataField(modelConfig), + seed, + steps, + rand_device: shouldUseCpuNoise ? 'cpu' : 'cuda', + scheduler, + positive_style_prompt: positiveStylePrompt, + negative_style_prompt: negativeStylePrompt, + vae: vae ?? undefined, + }); + + const seamless = addSeamless(state, g, denoise, modelLoader, vaeLoader); + + addSDXLLoRAs(state, g, denoise, modelLoader, seamless, posCond, negCond); + + // We might get the VAE from the main model, custom VAE, or seamless node. + const vaeSource = seamless ?? vaeLoader ?? modelLoader; + g.addEdge(vaeSource, 'vae', l2i, 'vae'); + + // Add Refiner if enabled + if (refinerModel) { + await addSDXLRefiner(state, g, denoise, seamless, posCond, negCond, l2i); + } + + if (generationMode === 'txt2img') { + if (!isEqual(scaledSize, originalSize)) { + // We are using scaled bbox and need to resize the output image back to the original size. + imageOutput = g.addNode({ + id: 'img_resize', + type: 'img_resize', + ...originalSize, + is_intermediate: false, + use_cache: false, + }); + g.addEdge(l2i, 'image', imageOutput, 'image'); + } + } else if (generationMode === 'img2img') { + denoise.denoising_start = refinerModel ? Math.min(refinerStart, 1 - img2imgStrength) : 1 - img2imgStrength; + + const { image_name } = await manager.util.getImageSourceImage({ + bbox: pick(bbox, ['x', 'y', 'width', 'height']), + preview: true, + }); + + if (!isEqual(scaledSize, originalSize)) { + // We are using scaled bbox and need to resize the output image back to the original size. + const initialImageResize = g.addNode({ + id: 'initial_image_resize', + type: 'img_resize', + ...scaledSize, + image: { image_name }, + }); + const i2l = g.addNode({ id: 'i2l', type: 'i2l' }); + + g.addEdge(vaeSource, 'vae', i2l, 'vae'); + g.addEdge(initialImageResize, 'image', i2l, 'image'); + g.addEdge(i2l, 'latents', denoise, 'latents'); + + imageOutput = g.addNode({ + id: 'img_resize', + type: 'img_resize', + ...originalSize, + is_intermediate: false, + use_cache: false, + }); + g.addEdge(l2i, 'image', imageOutput, 'image'); + } else { + const i2l = g.addNode({ id: 'i2l', type: 'i2l', image: { image_name } }); + g.addEdge(vaeSource, 'vae', i2l, 'vae'); + g.addEdge(i2l, 'latents', denoise, 'latents'); + } + } + + const _addedCAs = addControlAdapters(state.canvasV2.controlAdapters.entities, g, denoise, modelConfig.base); + const _addedIPAs = addIPAdapters(state.canvasV2.ipAdapters.entities, g, denoise, modelConfig.base); + const _addedRegions = await addRegions( + manager, + state.canvasV2.regions.entities, + g, + state.canvasV2.document, + state.canvasV2.bbox, + modelConfig.base, + denoise, + posCond, + negCond, + posCondCollect, + negCondCollect + ); + + if (state.system.shouldUseNSFWChecker) { + imageOutput = addNSFWChecker(g, imageOutput); + } + + if (state.system.shouldUseWatermarker) { + imageOutput = addWatermarker(g, imageOutput); + } + + g.setMetadataReceivingNode(imageOutput); + return g.getGraph(); +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildTextToImageSD1SD2Graph.ts similarity index 85% rename from invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/generation/buildTextToImageSD1SD2Graph.ts index cc859780f27..044cca05ce2 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildGenerationTabGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildTextToImageSD1SD2Graph.ts @@ -23,14 +23,17 @@ import { addSeamless } from 'features/nodes/util/graph/generation/addSeamless'; import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermarker'; import type { GraphType } from 'features/nodes/util/graph/generation/Graph'; import { Graph } from 'features/nodes/util/graph/generation/Graph'; -import { getBoardField, getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils'; +import { getBoardField, getPresetModifiedPrompts , getSizes } from 'features/nodes/util/graph/graphBuilderUtils'; +import { isEqual } from 'lodash-es'; import type { Invocation } from 'services/api/types'; import { isNonRefinerMainModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; import { addRegions } from './addRegions'; -export const buildGenerationTabGraph = async (state: RootState, manager: KonvaNodeManager): Promise => { +export const buildTextToImageSD1SD2Graph = async (state: RootState, manager: KonvaNodeManager): Promise => { + const { bbox, params } = state.canvasV2; + const { model, cfgScale: cfg_scale, @@ -42,14 +45,12 @@ export const buildGenerationTabGraph = async (state: RootState, manager: KonvaNo vaePrecision, seed, vae, - positivePrompt, - negativePrompt, - } = state.canvasV2.params; - const { width, height } = state.canvasV2.document; + } = params; assert(model, 'No model found in state'); const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state); + const { originalSize, scaledSize } = getSizes(bbox); const g = new Graph(CONTROL_LAYERS_GRAPH); const modelLoader = g.addNode({ @@ -84,8 +85,8 @@ export const buildGenerationTabGraph = async (state: RootState, manager: KonvaNo type: 'noise', id: NOISE, seed, - width, - height, + width: scaledSize.width, + height: scaledSize.height, use_cpu: shouldUseCpuNoise, }); const denoise = g.addNode({ @@ -116,7 +117,8 @@ export const buildGenerationTabGraph = async (state: RootState, manager: KonvaNo }) : null; - let imageOutput: Invocation<'l2i'> | Invocation<'img_nsfw'> | Invocation<'img_watermark'> = l2i; + let imageOutput: Invocation<'l2i'> | Invocation<'img_nsfw'> | Invocation<'img_watermark'> | Invocation<'img_resize'> = + l2i; g.addEdge(modelLoader, 'unet', denoise, 'unet'); g.addEdge(modelLoader, 'clip', clipSkip, 'clip'); @@ -136,8 +138,8 @@ export const buildGenerationTabGraph = async (state: RootState, manager: KonvaNo generation_mode: 'txt2img', cfg_scale, cfg_rescale_multiplier, - height, - width, + width: scaledSize.width, + height: scaledSize.height, positive_prompt: positivePrompt, negative_prompt: negativePrompt, model: Graph.getModelMetadataField(modelConfig), @@ -157,6 +159,18 @@ export const buildGenerationTabGraph = async (state: RootState, manager: KonvaNo const vaeSource = seamless ?? vaeLoader ?? modelLoader; g.addEdge(vaeSource, 'vae', l2i, 'vae'); + if (!isEqual(scaledSize, originalSize)) { + // We are using scaled bbox and need to resize the output image back to the original size. + imageOutput = g.addNode({ + id: 'img_resize', + type: 'img_resize', + ...originalSize, + is_intermediate: false, + use_cache: false, + }); + g.addEdge(l2i, 'image', imageOutput, 'image'); + } + const _addedCAs = addControlAdapters(state.canvasV2.controlAdapters.entities, g, denoise, modelConfig.base); const _addedIPAs = addIPAdapters(state.canvasV2.ipAdapters.entities, g, denoise, modelConfig.base); const _addedRegions = await addRegions( diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts index 90151797ea0..419c7aac289 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts @@ -1,7 +1,9 @@ import type { RootState } from 'app/store/store'; +import type { CanvasV2State } from 'features/controlLayers/store/types'; import type { BoardField } from 'features/nodes/types/common'; import { buildPresetModifiedPrompt } from 'features/stylePresets/hooks/usePresetModifiedPrompts'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; +import { pick } from 'lodash-es'; import { stylePresetsApi } from 'services/api/endpoints/stylePresets'; /** @@ -22,7 +24,7 @@ export const getPresetModifiedPrompts = ( state: RootState ): { positivePrompt: string; negativePrompt: string; positiveStylePrompt?: string; negativeStylePrompt?: string } => { const { positivePrompt, negativePrompt, positivePrompt2, negativePrompt2, shouldConcatPrompts } = - state.generation; + state.canvasV2.params; const { activeStylePresetId } = state.stylePreset; if (activeStylePresetId) { @@ -68,3 +70,9 @@ export const getIsIntermediate = (state: RootState) => { } return false; }; + +export const getSizes = (bboxState: CanvasV2State['bbox']) => { + const originalSize = pick(bboxState, 'width', 'height'); + const scaledSize = ['auto', 'manual'].includes(bboxState.scaleMethod) ? bboxState.scaledSize : originalSize; + return { originalSize, scaledSize }; +}; From 9497a75c953d3f0392a2201ee7a11162a76fdb50 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 24 Jun 2024 17:03:07 +1000 Subject: [PATCH 115/678] feat(ui): temp disable image caching while testing --- .../controlLayers/konva/nodeManager.ts | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts index 2fa562b44f6..7e64696ad4e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts @@ -337,12 +337,12 @@ export class KonvaNodeManager { const region = this.stateApi.getRegionsState().entities.find((entity) => entity.id === id); assert(region, `Region entity state with id ${id} not found`); - if (region.imageCache) { - const imageDTO = await this.util.getImageDTO(region.imageCache.name); - if (imageDTO) { - return imageDTO; - } - } + // if (region.imageCache) { + // const imageDTO = await this.util.getImageDTO(region.imageCache.name); + // if (imageDTO) { + // return imageDTO; + // } + // } const layerClone = this.util.getMaskLayerClone({ id }); const blob = await konvaNodeToBlob(layerClone, bbox); @@ -362,12 +362,12 @@ export class KonvaNodeManager { const { bbox, preview = false } = arg; const inpaintMask = this.stateApi.getInpaintMaskState(); - if (inpaintMask.imageCache) { - const imageDTO = await this.util.getImageDTO(inpaintMask.imageCache.name); - if (imageDTO) { - return imageDTO; - } - } + // if (inpaintMask.imageCache) { + // const imageDTO = await this.util.getImageDTO(inpaintMask.imageCache.name); + // if (imageDTO) { + // return imageDTO; + // } + // } const layerClone = this.util.getMaskLayerClone({ id: inpaintMask.id }); const blob = await konvaNodeToBlob(layerClone, bbox); @@ -386,12 +386,13 @@ export class KonvaNodeManager { async _getImageSourceImage(arg: { bbox?: Rect; preview?: boolean }): Promise { const { bbox, preview = false } = arg; const { imageCache } = this.stateApi.getLayersState(); - if (imageCache) { - const imageDTO = await this.util.getImageDTO(imageCache.name); - if (imageDTO) { - return imageDTO; - } - } + + // if (imageCache) { + // const imageDTO = await this.util.getImageDTO(imageCache.name); + // if (imageDTO) { + // return imageDTO; + // } + // } const stageClone = this.util.getCompositeLayerStageClone(); From 656dbbb9f1b16229fb0e3a2a3f287b5bd6ad41c3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 24 Jun 2024 17:03:18 +1000 Subject: [PATCH 116/678] feat(ui): inpaint sd1 graph --- .../util/graph/generation/buildSD1Graph.ts | 177 +++++++++++++++--- 1 file changed, 149 insertions(+), 28 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index bf219cd1604..fe427a13c79 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -49,7 +49,6 @@ export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager) vae, positivePrompt, negativePrompt, - img2imgStrength, } = params; assert(model, 'No model found in state'); @@ -108,9 +107,6 @@ export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager) id: LATENTS_TO_IMAGE, fp32: vaePrecision === 'fp32', board: getBoardField(state), - // This is the terminal node and must always save to gallery. - is_intermediate: false, - use_cache: false, }); const vaeLoader = vae?.base === model.base @@ -121,8 +117,7 @@ export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager) }) : null; - let imageOutput: Invocation<'l2i'> | Invocation<'img_nsfw'> | Invocation<'img_watermark'> | Invocation<'img_resize'> = - l2i; + let imageOutput: Invocation<'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_paste_back'> = l2i; g.addEdge(modelLoader, 'unet', denoise, 'unet'); g.addEdge(modelLoader, 'clip', clipSkip, 'clip'); @@ -165,50 +160,172 @@ export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager) if (generationMode === 'txt2img') { if (!isEqual(scaledSize, originalSize)) { - // We are using scaled bbox and need to resize the output image back to the original size. - imageOutput = g.addNode({ - id: 'img_resize', + // We need to resize the output image back to the original size + const resizeImageToOriginalSize = g.addNode({ + id: 'resize_image_to_original_size', type: 'img_resize', ...originalSize, - is_intermediate: false, - use_cache: false, }); - g.addEdge(l2i, 'image', imageOutput, 'image'); + g.addEdge(l2i, 'image', resizeImageToOriginalSize, 'image'); + + // This is the new output node + imageOutput = resizeImageToOriginalSize; } } else if (generationMode === 'img2img') { - const { image_name } = await manager.util.getImageSourceImage({ - bbox: pick(bbox, ['x', 'y', 'width', 'height']), + denoise.denoising_start = 1 - params.img2imgStrength; + + const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']); + const initialImage = await manager.util.getImageSourceImage({ + bbox: cropBbox, preview: true, }); - denoise.denoising_start = 1 - img2imgStrength; - if (!isEqual(scaledSize, originalSize)) { - // We are using scaled bbox and need to resize the output image back to the original size. - const initialImageResize = g.addNode({ - id: 'initial_image_resize', + // Resize the initial image to the scaled size, denoise, then resize back to the original size + const resizeImageToScaledSize = g.addNode({ + id: 'initial_image_resize_in', type: 'img_resize', + image: { image_name: initialImage.image_name }, ...scaledSize, - image: { image_name }, }); const i2l = g.addNode({ id: 'i2l', type: 'i2l' }); + const resizeImageToOriginalSize = g.addNode({ + id: 'initial_image_resize_out', + type: 'img_resize', + ...originalSize, + }); g.addEdge(vaeSource, 'vae', i2l, 'vae'); - g.addEdge(initialImageResize, 'image', i2l, 'image'); + g.addEdge(resizeImageToScaledSize, 'image', i2l, 'image'); g.addEdge(i2l, 'latents', denoise, 'latents'); + g.addEdge(l2i, 'image', resizeImageToOriginalSize, 'image'); + + // This is the new output node + imageOutput = resizeImageToOriginalSize; + } else { + // No need to resize, just denoise + const i2l = g.addNode({ id: 'i2l', type: 'i2l', image: { image_name: initialImage.image_name } }); + g.addEdge(vaeSource, 'vae', i2l, 'vae'); + g.addEdge(i2l, 'latents', denoise, 'latents'); + } + } else if (generationMode === 'inpaint') { + denoise.denoising_start = 1 - params.img2imgStrength; + + const { compositing } = state.canvasV2; - imageOutput = g.addNode({ - id: 'img_resize', + const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']); + const initialImage = await manager.util.getImageSourceImage({ + bbox: cropBbox, + preview: true, + }); + const maskImage = await manager.util.getInpaintMaskImage({ + bbox: cropBbox, + preview: true, + }); + + if (!isEqual(scaledSize, originalSize)) { + // Scale before processing requires some resizing + const i2l = g.addNode({ id: 'i2l', type: 'i2l' }); + const resizeImageToScaledSize = g.addNode({ + id: 'resize_image_to_scaled_size', + type: 'img_resize', + image: { image_name: initialImage.image_name }, + ...scaledSize, + }); + const alphaToMask = g.addNode({ + id: 'alpha_to_mask', + type: 'tomask', + image: { image_name: maskImage.image_name }, + invert: true, + }); + const resizeMaskToScaledSize = g.addNode({ + id: 'resize_mask_to_scaled_size', + type: 'img_resize', + ...scaledSize, + }); + const resizeImageToOriginalSize = g.addNode({ + id: 'resize_image_to_original_size', type: 'img_resize', ...originalSize, - is_intermediate: false, - use_cache: false, }); - g.addEdge(l2i, 'image', imageOutput, 'image'); - } else { - const i2l = g.addNode({ id: 'i2l', type: 'i2l', image: { image_name } }); + const resizeMaskToOriginalSize = g.addNode({ + id: 'resize_mask_to_original_size', + type: 'img_resize', + ...originalSize, + }); + const createGradientMask = g.addNode({ + id: 'create_gradient_mask', + type: 'create_gradient_mask', + coherence_mode: compositing.canvasCoherenceMode, + minimum_denoise: compositing.canvasCoherenceMinDenoise, + edge_radius: compositing.canvasCoherenceEdgeSize, + fp32: vaePrecision === 'fp32', + }); + const canvasPasteBack = g.addNode({ + id: 'canvas_paste_back', + type: 'canvas_paste_back', + board: getBoardField(state), + mask_blur: compositing.maskBlur, + source_image: { image_name: initialImage.image_name }, + }); + + // Resize initial image and mask to scaled size, feed into to gradient mask + g.addEdge(alphaToMask, 'image', resizeMaskToScaledSize, 'image'); + g.addEdge(resizeImageToScaledSize, 'image', i2l, 'image'); + g.addEdge(i2l, 'latents', denoise, 'latents'); g.addEdge(vaeSource, 'vae', i2l, 'vae'); + + g.addEdge(vaeSource, 'vae', createGradientMask, 'vae'); + g.addEdge(modelLoader, 'unet', createGradientMask, 'unet'); + g.addEdge(resizeImageToScaledSize, 'image', createGradientMask, 'image'); + g.addEdge(resizeMaskToScaledSize, 'image', createGradientMask, 'mask'); + + g.addEdge(createGradientMask, 'denoise_mask', denoise, 'denoise_mask'); + + // After denoising, resize the image and mask back to original size + g.addEdge(l2i, 'image', resizeImageToOriginalSize, 'image'); + g.addEdge(createGradientMask, 'expanded_mask_area', resizeMaskToOriginalSize, 'image'); + + // Finally, paste the generated masked image back onto the original image + g.addEdge(resizeImageToOriginalSize, 'image', canvasPasteBack, 'target_image'); + g.addEdge(resizeMaskToOriginalSize, 'image', canvasPasteBack, 'mask'); + + imageOutput = canvasPasteBack; + } else { + // No scale before processing, much simpler + const i2l = g.addNode({ id: 'i2l', type: 'i2l', image: { image_name: initialImage.image_name } }); + const alphaToMask = g.addNode({ + id: 'alpha_to_mask', + type: 'tomask', + image: { image_name: maskImage.image_name }, + invert: true, + }); + const createGradientMask = g.addNode({ + id: 'create_gradient_mask', + type: 'create_gradient_mask', + coherence_mode: compositing.canvasCoherenceMode, + minimum_denoise: compositing.canvasCoherenceMinDenoise, + edge_radius: compositing.canvasCoherenceEdgeSize, + fp32: vaePrecision === 'fp32', + image: { image_name: initialImage.image_name }, + }); + const canvasPasteBack = g.addNode({ + id: 'canvas_paste_back', + type: 'canvas_paste_back', + board: getBoardField(state), + mask_blur: compositing.maskBlur, + source_image: { image_name: initialImage.image_name }, + mask: { image_name: maskImage.image_name }, + }); + g.addEdge(alphaToMask, 'image', createGradientMask, 'mask'); g.addEdge(i2l, 'latents', denoise, 'latents'); + g.addEdge(vaeSource, 'vae', i2l, 'vae'); + g.addEdge(vaeSource, 'vae', createGradientMask, 'vae'); + g.addEdge(modelLoader, 'unet', createGradientMask, 'unet'); + g.addEdge(createGradientMask, 'denoise_mask', denoise, 'denoise_mask'); + g.addEdge(l2i, 'image', canvasPasteBack, 'target_image'); + + imageOutput = canvasPasteBack; } } @@ -241,6 +358,10 @@ export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager) imageOutput = addWatermarker(g, imageOutput); } + // This is the terminal node and must always save to gallery. + imageOutput.is_intermediate = false; + imageOutput.use_cache = false; + g.setMetadataReceivingNode(imageOutput); return g.getGraph(); }; From 0f709cb06aef99d738aca751909665a2ffd4d5fd Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 24 Jun 2024 19:31:58 +1000 Subject: [PATCH 117/678] feat(ui): outpaint graph, organize builder a bit --- .../util/graph/generation/addImageToImage.ts | 56 +++++ .../nodes/util/graph/generation/addInpaint.ts | 137 +++++++++++ .../util/graph/generation/addOutpaint.ts | 152 ++++++++++++ .../util/graph/generation/addTextToImage.ts | 25 ++ .../util/graph/generation/buildSD1Graph.ts | 217 ++++-------------- .../nodes/util/graph/graphBuilderUtils.ts | 51 ++++ 6 files changed, 471 insertions(+), 167 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/generation/addTextToImage.ts diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts new file mode 100644 index 00000000000..c06ffa5ca6d --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts @@ -0,0 +1,56 @@ +import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import type { CanvasV2State, Dimensions } from 'features/controlLayers/store/types'; +import type { Graph } from 'features/nodes/util/graph/generation/Graph'; +import type { ParameterStrength } from 'features/parameters/types/parameterSchemas'; +import { isEqual, pick } from 'lodash-es'; +import type { Invocation } from 'services/api/types'; + +export const addImageToImage = async ( + g: Graph, + manager: KonvaNodeManager, + l2i: Invocation<'l2i'>, + denoise: Invocation<'denoise_latents'>, + vaeSource: Invocation<'main_model_loader' | 'seamless' | 'vae_loader'>, + imageOutput: Invocation<'canvas_paste_back' | 'img_nsfw' | 'img_resize' | 'img_watermark' | 'l2i'>, + originalSize: Dimensions, + scaledSize: Dimensions, + bbox: CanvasV2State['bbox'], + strength: ParameterStrength +) => { + denoise.denoising_start = 1 - strength; + + const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']); + const initialImage = await manager.util.getImageSourceImage({ + bbox: cropBbox, + preview: true, + }); + + if (!isEqual(scaledSize, originalSize)) { + // Resize the initial image to the scaled size, denoise, then resize back to the original size + const resizeImageToScaledSize = g.addNode({ + id: 'initial_image_resize_in', + type: 'img_resize', + image: { image_name: initialImage.image_name }, + ...scaledSize, + }); + const i2l = g.addNode({ id: 'i2l', type: 'i2l' }); + const resizeImageToOriginalSize = g.addNode({ + id: 'initial_image_resize_out', + type: 'img_resize', + ...originalSize, + }); + + g.addEdge(vaeSource, 'vae', i2l, 'vae'); + g.addEdge(resizeImageToScaledSize, 'image', i2l, 'image'); + g.addEdge(i2l, 'latents', denoise, 'latents'); + g.addEdge(l2i, 'image', resizeImageToOriginalSize, 'image'); + + // This is the new output node + imageOutput = resizeImageToOriginalSize; + } else { + // No need to resize, just denoise + const i2l = g.addNode({ id: 'i2l', type: 'i2l', image: { image_name: initialImage.image_name } }); + g.addEdge(vaeSource, 'vae', i2l, 'vae'); + g.addEdge(i2l, 'latents', denoise, 'latents'); + } +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts new file mode 100644 index 00000000000..9d946d20020 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts @@ -0,0 +1,137 @@ +import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import type { CanvasV2State, Dimensions } from 'features/controlLayers/store/types'; +import type { Graph } from 'features/nodes/util/graph/generation/Graph'; +import type { ParameterPrecision, ParameterStrength } from 'features/parameters/types/parameterSchemas'; +import { isEqual, pick } from 'lodash-es'; +import type { Invocation } from 'services/api/types'; + +export const addInpaint = async ( + g: Graph, + manager: KonvaNodeManager, + l2i: Invocation<'l2i'>, + denoise: Invocation<'denoise_latents'>, + vaeSource: Invocation<'main_model_loader' | 'seamless' | 'vae_loader'>, + modelLoader: Invocation<'main_model_loader'>, + imageOutput: Invocation<'canvas_paste_back' | 'img_nsfw' | 'img_resize' | 'img_watermark' | 'l2i'>, + originalSize: Dimensions, + scaledSize: Dimensions, + bbox: CanvasV2State['bbox'], + compositing: CanvasV2State['compositing'], + strength: ParameterStrength, + vaePrecision: ParameterPrecision +) => { + denoise.denoising_start = 1 - strength; + + const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']); + const initialImage = await manager.util.getImageSourceImage({ + bbox: cropBbox, + preview: true, + }); + const maskImage = await manager.util.getInpaintMaskImage({ + bbox: cropBbox, + preview: true, + }); + + if (!isEqual(scaledSize, originalSize)) { + // Scale before processing requires some resizing + const i2l = g.addNode({ id: 'i2l', type: 'i2l' }); + const resizeImageToScaledSize = g.addNode({ + id: 'resize_image_to_scaled_size', + type: 'img_resize', + image: { image_name: initialImage.image_name }, + ...scaledSize, + }); + const alphaToMask = g.addNode({ + id: 'alpha_to_mask', + type: 'tomask', + image: { image_name: maskImage.image_name }, + invert: true, + }); + const resizeMaskToScaledSize = g.addNode({ + id: 'resize_mask_to_scaled_size', + type: 'img_resize', + ...scaledSize, + }); + const resizeImageToOriginalSize = g.addNode({ + id: 'resize_image_to_original_size', + type: 'img_resize', + ...originalSize, + }); + const resizeMaskToOriginalSize = g.addNode({ + id: 'resize_mask_to_original_size', + type: 'img_resize', + ...originalSize, + }); + const createGradientMask = g.addNode({ + id: 'create_gradient_mask', + type: 'create_gradient_mask', + coherence_mode: compositing.canvasCoherenceMode, + minimum_denoise: compositing.canvasCoherenceMinDenoise, + edge_radius: compositing.canvasCoherenceEdgeSize, + fp32: vaePrecision === 'fp32', + }); + const canvasPasteBack = g.addNode({ + id: 'canvas_paste_back', + type: 'canvas_paste_back', + mask_blur: compositing.maskBlur, + source_image: { image_name: initialImage.image_name }, + }); + + // Resize initial image and mask to scaled size, feed into to gradient mask + g.addEdge(alphaToMask, 'image', resizeMaskToScaledSize, 'image'); + g.addEdge(resizeImageToScaledSize, 'image', i2l, 'image'); + g.addEdge(i2l, 'latents', denoise, 'latents'); + g.addEdge(vaeSource, 'vae', i2l, 'vae'); + + g.addEdge(vaeSource, 'vae', createGradientMask, 'vae'); + g.addEdge(modelLoader, 'unet', createGradientMask, 'unet'); + g.addEdge(resizeImageToScaledSize, 'image', createGradientMask, 'image'); + g.addEdge(resizeMaskToScaledSize, 'image', createGradientMask, 'mask'); + + g.addEdge(createGradientMask, 'denoise_mask', denoise, 'denoise_mask'); + + // After denoising, resize the image and mask back to original size + g.addEdge(l2i, 'image', resizeImageToOriginalSize, 'image'); + g.addEdge(createGradientMask, 'expanded_mask_area', resizeMaskToOriginalSize, 'image'); + + // Finally, paste the generated masked image back onto the original image + g.addEdge(resizeImageToOriginalSize, 'image', canvasPasteBack, 'target_image'); + g.addEdge(resizeMaskToOriginalSize, 'image', canvasPasteBack, 'mask'); + + imageOutput = canvasPasteBack; + } else { + // No scale before processing, much simpler + const i2l = g.addNode({ id: 'i2l', type: 'i2l', image: { image_name: initialImage.image_name } }); + const alphaToMask = g.addNode({ + id: 'alpha_to_mask', + type: 'tomask', + image: { image_name: maskImage.image_name }, + invert: true, + }); + const createGradientMask = g.addNode({ + id: 'create_gradient_mask', + type: 'create_gradient_mask', + coherence_mode: compositing.canvasCoherenceMode, + minimum_denoise: compositing.canvasCoherenceMinDenoise, + edge_radius: compositing.canvasCoherenceEdgeSize, + fp32: vaePrecision === 'fp32', + image: { image_name: initialImage.image_name }, + }); + const canvasPasteBack = g.addNode({ + id: 'canvas_paste_back', + type: 'canvas_paste_back', + mask_blur: compositing.maskBlur, + source_image: { image_name: initialImage.image_name }, + mask: { image_name: maskImage.image_name }, + }); + g.addEdge(alphaToMask, 'image', createGradientMask, 'mask'); + g.addEdge(i2l, 'latents', denoise, 'latents'); + g.addEdge(vaeSource, 'vae', i2l, 'vae'); + g.addEdge(vaeSource, 'vae', createGradientMask, 'vae'); + g.addEdge(modelLoader, 'unet', createGradientMask, 'unet'); + g.addEdge(createGradientMask, 'denoise_mask', denoise, 'denoise_mask'); + g.addEdge(l2i, 'image', canvasPasteBack, 'target_image'); + + imageOutput = canvasPasteBack; + } +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts new file mode 100644 index 00000000000..440eeda9345 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts @@ -0,0 +1,152 @@ +import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import type { CanvasV2State, Dimensions } from 'features/controlLayers/store/types'; +import type { Graph } from 'features/nodes/util/graph/generation/Graph'; +import { getInfill } from 'features/nodes/util/graph/graphBuilderUtils'; +import type { ParameterPrecision, ParameterStrength } from 'features/parameters/types/parameterSchemas'; +import { isEqual, pick } from 'lodash-es'; +import type { Invocation } from 'services/api/types'; + +export const addOutpaint = async ( + g: Graph, + manager: KonvaNodeManager, + l2i: Invocation<'l2i'>, + denoise: Invocation<'denoise_latents'>, + vaeSource: Invocation<'main_model_loader' | 'seamless' | 'vae_loader'>, + modelLoader: Invocation<'main_model_loader'>, + imageOutput: Invocation<'canvas_paste_back' | 'img_nsfw' | 'img_resize' | 'img_watermark' | 'l2i'>, + originalSize: Dimensions, + scaledSize: Dimensions, + bbox: CanvasV2State['bbox'], + compositing: CanvasV2State['compositing'], + strength: ParameterStrength, + vaePrecision: ParameterPrecision +) => { + denoise.denoising_start = 1 - strength; + + const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']); + const initialImage = await manager.util.getImageSourceImage({ + bbox: cropBbox, + preview: true, + }); + const maskImage = await manager.util.getInpaintMaskImage({ + bbox: cropBbox, + preview: true, + }); + const infill = getInfill(g, compositing); + + if (!isEqual(scaledSize, originalSize)) { + // Scale before processing requires some resizing + const i2l = g.addNode({ id: 'i2l', type: 'i2l' }); + const resizeImageToScaledSize = g.addNode({ + id: 'resize_image_to_scaled_size', + type: 'img_resize', + image: { image_name: initialImage.image_name }, + ...scaledSize, + }); + const alphaToMask = g.addNode({ + id: 'alpha_to_mask', + type: 'tomask', + image: { image_name: maskImage.image_name }, + invert: true, + }); + const resizeMaskToScaledSize = g.addNode({ + id: 'resize_mask_to_scaled_size', + type: 'img_resize', + ...scaledSize, + }); + const resizeImageToOriginalSize = g.addNode({ + id: 'resize_image_to_original_size', + type: 'img_resize', + ...originalSize, + }); + const resizeMaskToOriginalSize = g.addNode({ + id: 'resize_mask_to_original_size', + type: 'img_resize', + ...originalSize, + }); + const createGradientMask = g.addNode({ + id: 'create_gradient_mask', + type: 'create_gradient_mask', + coherence_mode: compositing.canvasCoherenceMode, + minimum_denoise: compositing.canvasCoherenceMinDenoise, + edge_radius: compositing.canvasCoherenceEdgeSize, + fp32: vaePrecision === 'fp32', + }); + const canvasPasteBack = g.addNode({ + id: 'canvas_paste_back', + type: 'canvas_paste_back', + mask_blur: compositing.maskBlur, + source_image: { image_name: initialImage.image_name }, + }); + + // Resize initial image and mask to scaled size, feed into to gradient mask + g.addEdge(alphaToMask, 'image', resizeMaskToScaledSize, 'image'); + g.addEdge(resizeImageToScaledSize, 'image', i2l, 'image'); + g.addEdge(i2l, 'latents', denoise, 'latents'); + g.addEdge(vaeSource, 'vae', i2l, 'vae'); + + g.addEdge(vaeSource, 'vae', createGradientMask, 'vae'); + g.addEdge(modelLoader, 'unet', createGradientMask, 'unet'); + g.addEdge(resizeImageToScaledSize, 'image', createGradientMask, 'image'); + g.addEdge(resizeMaskToScaledSize, 'image', createGradientMask, 'mask'); + + g.addEdge(createGradientMask, 'denoise_mask', denoise, 'denoise_mask'); + + // After denoising, resize the image and mask back to original size + g.addEdge(l2i, 'image', resizeImageToOriginalSize, 'image'); + g.addEdge(createGradientMask, 'expanded_mask_area', resizeMaskToOriginalSize, 'image'); + + // Finally, paste the generated masked image back onto the original image + g.addEdge(resizeImageToOriginalSize, 'image', canvasPasteBack, 'target_image'); + g.addEdge(resizeMaskToOriginalSize, 'image', canvasPasteBack, 'mask'); + + imageOutput = canvasPasteBack; + } else { + infill.image = { image_name: initialImage.image_name }; + // No scale before processing, much simpler + const i2l = g.addNode({ id: 'i2l', type: 'i2l' }); + const maskAlphaToMask = g.addNode({ + id: 'mask_alpha_to_mask', + type: 'tomask', + image: { image_name: maskImage.image_name }, + invert: true, + }); + const initialImageAlphaToMask = g.addNode({ + id: 'image_alpha_to_mask', + type: 'tomask', + image: { image_name: initialImage.image_name }, + }); + const maskCombine = g.addNode({ + id: 'mask_combine', + type: 'mask_combine', + }); + const createGradientMask = g.addNode({ + id: 'create_gradient_mask', + type: 'create_gradient_mask', + coherence_mode: compositing.canvasCoherenceMode, + minimum_denoise: compositing.canvasCoherenceMinDenoise, + edge_radius: compositing.canvasCoherenceEdgeSize, + fp32: vaePrecision === 'fp32', + image: { image_name: initialImage.image_name }, + }); + const canvasPasteBack = g.addNode({ + id: 'canvas_paste_back', + type: 'canvas_paste_back', + mask_blur: compositing.maskBlur, + mask: { image_name: maskImage.image_name }, + }); + g.addEdge(maskAlphaToMask, 'image', maskCombine, 'mask1'); + g.addEdge(initialImageAlphaToMask, 'image', maskCombine, 'mask2'); + g.addEdge(maskCombine, 'image', createGradientMask, 'mask'); + g.addEdge(infill, 'image', i2l, 'image'); + g.addEdge(i2l, 'latents', denoise, 'latents'); + g.addEdge(vaeSource, 'vae', i2l, 'vae'); + g.addEdge(vaeSource, 'vae', createGradientMask, 'vae'); + g.addEdge(modelLoader, 'unet', createGradientMask, 'unet'); + g.addEdge(createGradientMask, 'denoise_mask', denoise, 'denoise_mask'); + g.addEdge(infill, 'image', canvasPasteBack, 'source_image'); + g.addEdge(l2i, 'image', canvasPasteBack, 'target_image'); + + imageOutput = canvasPasteBack; + } +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addTextToImage.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addTextToImage.ts new file mode 100644 index 00000000000..00792efe5c6 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addTextToImage.ts @@ -0,0 +1,25 @@ +import type { Dimensions } from 'features/controlLayers/store/types'; +import type { Graph } from 'features/nodes/util/graph/generation/Graph'; +import { isEqual } from 'lodash-es'; +import type { Invocation } from 'services/api/types'; + +export const addTextToImage = ( + g: Graph, + l2i: Invocation<'l2i'>, + imageOutput: Invocation<'canvas_paste_back' | 'img_nsfw' | 'img_resize' | 'img_watermark' | 'l2i'>, + originalSize: Dimensions, + scaledSize: Dimensions +) => { + if (!isEqual(scaledSize, originalSize)) { + // We need to resize the output image back to the original size + const resizeImageToOriginalSize = g.addNode({ + id: 'resize_image_to_original_size', + type: 'img_resize', + ...originalSize, + }); + g.addEdge(l2i, 'image', resizeImageToOriginalSize, 'image'); + + // This is the new output node + imageOutput = resizeImageToOriginalSize; + } +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index fe427a13c79..428b8d72d17 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -15,16 +15,19 @@ import { VAE_LOADER, } from 'features/nodes/util/graph/constants'; import { addControlAdapters } from 'features/nodes/util/graph/generation/addControlAdapters'; +import { addImageToImage } from 'features/nodes/util/graph/generation/addImageToImage'; +import { addInpaint } from 'features/nodes/util/graph/generation/addInpaint'; // import { addHRF } from 'features/nodes/util/graph/generation/addHRF'; import { addIPAdapters } from 'features/nodes/util/graph/generation/addIPAdapters'; import { addLoRAs } from 'features/nodes/util/graph/generation/addLoRAs'; import { addNSFWChecker } from 'features/nodes/util/graph/generation/addNSFWChecker'; +import { addOutpaint } from 'features/nodes/util/graph/generation/addOutpaint'; import { addSeamless } from 'features/nodes/util/graph/generation/addSeamless'; +import { addTextToImage } from 'features/nodes/util/graph/generation/addTextToImage'; import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermarker'; import type { GraphType } from 'features/nodes/util/graph/generation/Graph'; import { Graph } from 'features/nodes/util/graph/generation/Graph'; import { getBoardField, getSizes } from 'features/nodes/util/graph/graphBuilderUtils'; -import { isEqual, pick } from 'lodash-es'; import type { Invocation } from 'services/api/types'; import { isNonRefinerMainModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; @@ -155,178 +158,58 @@ export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager) addLoRAs(state, g, denoise, modelLoader, seamless, clipSkip, posCond, negCond); // We might get the VAE from the main model, custom VAE, or seamless node. - const vaeSource = seamless ?? vaeLoader ?? modelLoader; + const vaeSource: Invocation<'main_model_loader' | 'seamless' | 'vae_loader'> = seamless ?? vaeLoader ?? modelLoader; g.addEdge(vaeSource, 'vae', l2i, 'vae'); if (generationMode === 'txt2img') { - if (!isEqual(scaledSize, originalSize)) { - // We need to resize the output image back to the original size - const resizeImageToOriginalSize = g.addNode({ - id: 'resize_image_to_original_size', - type: 'img_resize', - ...originalSize, - }); - g.addEdge(l2i, 'image', resizeImageToOriginalSize, 'image'); - - // This is the new output node - imageOutput = resizeImageToOriginalSize; - } + addTextToImage(g, l2i, imageOutput, originalSize, scaledSize); } else if (generationMode === 'img2img') { - denoise.denoising_start = 1 - params.img2imgStrength; - - const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']); - const initialImage = await manager.util.getImageSourceImage({ - bbox: cropBbox, - preview: true, - }); - - if (!isEqual(scaledSize, originalSize)) { - // Resize the initial image to the scaled size, denoise, then resize back to the original size - const resizeImageToScaledSize = g.addNode({ - id: 'initial_image_resize_in', - type: 'img_resize', - image: { image_name: initialImage.image_name }, - ...scaledSize, - }); - const i2l = g.addNode({ id: 'i2l', type: 'i2l' }); - const resizeImageToOriginalSize = g.addNode({ - id: 'initial_image_resize_out', - type: 'img_resize', - ...originalSize, - }); - - g.addEdge(vaeSource, 'vae', i2l, 'vae'); - g.addEdge(resizeImageToScaledSize, 'image', i2l, 'image'); - g.addEdge(i2l, 'latents', denoise, 'latents'); - g.addEdge(l2i, 'image', resizeImageToOriginalSize, 'image'); - - // This is the new output node - imageOutput = resizeImageToOriginalSize; - } else { - // No need to resize, just denoise - const i2l = g.addNode({ id: 'i2l', type: 'i2l', image: { image_name: initialImage.image_name } }); - g.addEdge(vaeSource, 'vae', i2l, 'vae'); - g.addEdge(i2l, 'latents', denoise, 'latents'); - } + addImageToImage( + g, + manager, + l2i, + denoise, + vaeSource, + imageOutput, + originalSize, + scaledSize, + bbox, + params.img2imgStrength + ); } else if (generationMode === 'inpaint') { - denoise.denoising_start = 1 - params.img2imgStrength; - const { compositing } = state.canvasV2; - - const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']); - const initialImage = await manager.util.getImageSourceImage({ - bbox: cropBbox, - preview: true, - }); - const maskImage = await manager.util.getInpaintMaskImage({ - bbox: cropBbox, - preview: true, - }); - - if (!isEqual(scaledSize, originalSize)) { - // Scale before processing requires some resizing - const i2l = g.addNode({ id: 'i2l', type: 'i2l' }); - const resizeImageToScaledSize = g.addNode({ - id: 'resize_image_to_scaled_size', - type: 'img_resize', - image: { image_name: initialImage.image_name }, - ...scaledSize, - }); - const alphaToMask = g.addNode({ - id: 'alpha_to_mask', - type: 'tomask', - image: { image_name: maskImage.image_name }, - invert: true, - }); - const resizeMaskToScaledSize = g.addNode({ - id: 'resize_mask_to_scaled_size', - type: 'img_resize', - ...scaledSize, - }); - const resizeImageToOriginalSize = g.addNode({ - id: 'resize_image_to_original_size', - type: 'img_resize', - ...originalSize, - }); - const resizeMaskToOriginalSize = g.addNode({ - id: 'resize_mask_to_original_size', - type: 'img_resize', - ...originalSize, - }); - const createGradientMask = g.addNode({ - id: 'create_gradient_mask', - type: 'create_gradient_mask', - coherence_mode: compositing.canvasCoherenceMode, - minimum_denoise: compositing.canvasCoherenceMinDenoise, - edge_radius: compositing.canvasCoherenceEdgeSize, - fp32: vaePrecision === 'fp32', - }); - const canvasPasteBack = g.addNode({ - id: 'canvas_paste_back', - type: 'canvas_paste_back', - board: getBoardField(state), - mask_blur: compositing.maskBlur, - source_image: { image_name: initialImage.image_name }, - }); - - // Resize initial image and mask to scaled size, feed into to gradient mask - g.addEdge(alphaToMask, 'image', resizeMaskToScaledSize, 'image'); - g.addEdge(resizeImageToScaledSize, 'image', i2l, 'image'); - g.addEdge(i2l, 'latents', denoise, 'latents'); - g.addEdge(vaeSource, 'vae', i2l, 'vae'); - - g.addEdge(vaeSource, 'vae', createGradientMask, 'vae'); - g.addEdge(modelLoader, 'unet', createGradientMask, 'unet'); - g.addEdge(resizeImageToScaledSize, 'image', createGradientMask, 'image'); - g.addEdge(resizeMaskToScaledSize, 'image', createGradientMask, 'mask'); - - g.addEdge(createGradientMask, 'denoise_mask', denoise, 'denoise_mask'); - - // After denoising, resize the image and mask back to original size - g.addEdge(l2i, 'image', resizeImageToOriginalSize, 'image'); - g.addEdge(createGradientMask, 'expanded_mask_area', resizeMaskToOriginalSize, 'image'); - - // Finally, paste the generated masked image back onto the original image - g.addEdge(resizeImageToOriginalSize, 'image', canvasPasteBack, 'target_image'); - g.addEdge(resizeMaskToOriginalSize, 'image', canvasPasteBack, 'mask'); - - imageOutput = canvasPasteBack; - } else { - // No scale before processing, much simpler - const i2l = g.addNode({ id: 'i2l', type: 'i2l', image: { image_name: initialImage.image_name } }); - const alphaToMask = g.addNode({ - id: 'alpha_to_mask', - type: 'tomask', - image: { image_name: maskImage.image_name }, - invert: true, - }); - const createGradientMask = g.addNode({ - id: 'create_gradient_mask', - type: 'create_gradient_mask', - coherence_mode: compositing.canvasCoherenceMode, - minimum_denoise: compositing.canvasCoherenceMinDenoise, - edge_radius: compositing.canvasCoherenceEdgeSize, - fp32: vaePrecision === 'fp32', - image: { image_name: initialImage.image_name }, - }); - const canvasPasteBack = g.addNode({ - id: 'canvas_paste_back', - type: 'canvas_paste_back', - board: getBoardField(state), - mask_blur: compositing.maskBlur, - source_image: { image_name: initialImage.image_name }, - mask: { image_name: maskImage.image_name }, - }); - g.addEdge(alphaToMask, 'image', createGradientMask, 'mask'); - g.addEdge(i2l, 'latents', denoise, 'latents'); - g.addEdge(vaeSource, 'vae', i2l, 'vae'); - g.addEdge(vaeSource, 'vae', createGradientMask, 'vae'); - g.addEdge(modelLoader, 'unet', createGradientMask, 'unet'); - g.addEdge(createGradientMask, 'denoise_mask', denoise, 'denoise_mask'); - g.addEdge(l2i, 'image', canvasPasteBack, 'target_image'); - - imageOutput = canvasPasteBack; - } + addInpaint( + g, + manager, + l2i, + denoise, + vaeSource, + modelLoader, + imageOutput, + originalSize, + scaledSize, + bbox, + compositing, + params.img2imgStrength, + vaePrecision + ); + } else if (generationMode === 'outpaint') { + const { compositing } = state.canvasV2; + addOutpaint( + g, + manager, + l2i, + denoise, + vaeSource, + modelLoader, + imageOutput, + originalSize, + scaledSize, + bbox, + compositing, + params.img2imgStrength, + vaePrecision + ); } const _addedCAs = addControlAdapters(state.canvasV2.controlAdapters.entities, g, denoise, modelConfig.base); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts index 419c7aac289..7097029ca7e 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts @@ -1,10 +1,13 @@ import type { RootState } from 'app/store/store'; import type { CanvasV2State } from 'features/controlLayers/store/types'; import type { BoardField } from 'features/nodes/types/common'; +import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { buildPresetModifiedPrompt } from 'features/stylePresets/hooks/usePresetModifiedPrompts'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { pick } from 'lodash-es'; import { stylePresetsApi } from 'services/api/endpoints/stylePresets'; +import type { Invocation } from 'services/api/types'; +import { assert } from 'tsafe'; /** * Gets the board field, based on the autoAddBoardId setting. @@ -76,3 +79,51 @@ export const getSizes = (bboxState: CanvasV2State['bbox']) => { const scaledSize = ['auto', 'manual'].includes(bboxState.scaleMethod) ? bboxState.scaledSize : originalSize; return { originalSize, scaledSize }; }; + +export const getInfill = ( + g: Graph, + compositing: CanvasV2State['compositing'] +): Invocation<'infill_patchmatch' | 'infill_cv2' | 'infill_lama' | 'infill_rgba' | 'infill_tile'> => { + const { infillMethod, infillColorValue, infillPatchmatchDownscaleSize, infillTileSize } = compositing; + + // Add Infill Nodes + if (infillMethod === 'patchmatch') { + return g.addNode({ + id: 'infill_patchmatch', + type: 'infill_patchmatch', + downscale: infillPatchmatchDownscaleSize, + }); + } + + if (infillMethod === 'lama') { + return g.addNode({ + id: 'infill_lama', + type: 'infill_lama', + }); + } + + if (infillMethod === 'cv2') { + return g.addNode({ + id: 'infill_cv2', + type: 'infill_cv2', + }); + } + + if (infillMethod === 'tile') { + return g.addNode({ + id: 'infill_tile', + type: 'infill_tile', + tile_size: infillTileSize, + }); + } + + if (infillMethod === 'color') { + return g.addNode({ + id: 'infill_rgba', + type: 'infill_rgba', + color: infillColorValue, + }); + } + + assert(false, 'Unknown infill method'); +}; From 0edff49957b64f9a98ad2eb164a08f5142347a72 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 24 Jun 2024 19:58:32 +1000 Subject: [PATCH 118/678] feat(ui): add Graph.getid() util --- .../src/features/nodes/util/graph/generation/Graph.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.ts index 41142e56285..213adac4b87 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.ts @@ -430,5 +430,16 @@ export class Graph { assert(fromField !== undefined && toNodeId !== undefined && toField !== undefined, 'Invalid edge arguments'); return `${fromNodeId}.${fromField} -> ${toNodeId}.${toField}`; } + /** + * Gets a unique id. + * @param prefix An optional prefix + */ + static getId(prefix?: string): string { + if (prefix) { + return `${prefix}_${uuidv4()}`; + } else { + return uuidv4(); + } + } //#endregion } From 9c646712e0616d149df572f31168f15895e8a346 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 24 Jun 2024 19:58:46 +1000 Subject: [PATCH 119/678] tests(ui): add missing tests for Graph class --- .../nodes/util/graph/generation/Graph.test.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.test.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.test.ts index e5a97bb50b9..67fabbc158e 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.test.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.test.ts @@ -3,6 +3,7 @@ import type { AnyInvocation, Invocation } from 'services/api/types'; import { assert, AssertionError, is } from 'tsafe'; import { validate } from 'uuid'; import { describe, expect, it } from 'vitest'; +import { z } from 'zod'; describe('Graph', () => { describe('constructor', () => { @@ -591,4 +592,31 @@ describe('Graph', () => { }); }); }); + + describe('other utils', () => { + describe('edgeToString', () => { + it('should return a string representation of the edge given the edge fields', () => { + expect(Graph.edgeToString('from-node', 'value', 'to-node', 'b')).toBe('from-node.value -> to-node.b'); + }); + it('should return a string representation of the edge given an edge object', () => { + expect( + Graph.edgeToString({ + source: { node_id: 'from-node', field: 'value' }, + destination: { node_id: 'to-node', field: 'b' }, + }) + ).toBe('from-node.value -> to-node.b'); + }); + }); + + describe('getId', () => { + it('should create a new uuid v4 id', () => { + const id = Graph.getId(); + expect(() => z.string().uuid().parse(id)).not.toThrow(); + }); + it('should prepend the prefix if provided', () => { + const id = Graph.getId('prefix'); + expect(id.startsWith('prefix_')).toBe(true); + }); + }); + }); }); From 05992130d024f283238f28b8e966b2119f699c63 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 24 Jun 2024 21:07:47 +1000 Subject: [PATCH 120/678] feat(ui): sd1 outpaint graph --- .../util/graph/generation/addImageToImage.ts | 5 +- .../nodes/util/graph/generation/addInpaint.ts | 7 +- .../util/graph/generation/addOutpaint.ts | 83 ++++++++++++------- .../util/graph/generation/addTextToImage.ts | 8 +- .../util/graph/generation/buildSD1Graph.ts | 12 ++- 5 files changed, 67 insertions(+), 48 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts index c06ffa5ca6d..a57036d366d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts @@ -16,7 +16,7 @@ export const addImageToImage = async ( scaledSize: Dimensions, bbox: CanvasV2State['bbox'], strength: ParameterStrength -) => { +): Promise> => { denoise.denoising_start = 1 - strength; const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']); @@ -46,11 +46,12 @@ export const addImageToImage = async ( g.addEdge(l2i, 'image', resizeImageToOriginalSize, 'image'); // This is the new output node - imageOutput = resizeImageToOriginalSize; + return resizeImageToOriginalSize; } else { // No need to resize, just denoise const i2l = g.addNode({ id: 'i2l', type: 'i2l', image: { image_name: initialImage.image_name } }); g.addEdge(vaeSource, 'vae', i2l, 'vae'); g.addEdge(i2l, 'latents', denoise, 'latents'); + return l2i; } }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts index 9d946d20020..48cf3249011 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts @@ -12,14 +12,13 @@ export const addInpaint = async ( denoise: Invocation<'denoise_latents'>, vaeSource: Invocation<'main_model_loader' | 'seamless' | 'vae_loader'>, modelLoader: Invocation<'main_model_loader'>, - imageOutput: Invocation<'canvas_paste_back' | 'img_nsfw' | 'img_resize' | 'img_watermark' | 'l2i'>, originalSize: Dimensions, scaledSize: Dimensions, bbox: CanvasV2State['bbox'], compositing: CanvasV2State['compositing'], strength: ParameterStrength, vaePrecision: ParameterPrecision -) => { +): Promise> => { denoise.denoising_start = 1 - strength; const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']); @@ -98,7 +97,7 @@ export const addInpaint = async ( g.addEdge(resizeImageToOriginalSize, 'image', canvasPasteBack, 'target_image'); g.addEdge(resizeMaskToOriginalSize, 'image', canvasPasteBack, 'mask'); - imageOutput = canvasPasteBack; + return canvasPasteBack; } else { // No scale before processing, much simpler const i2l = g.addNode({ id: 'i2l', type: 'i2l', image: { image_name: initialImage.image_name } }); @@ -132,6 +131,6 @@ export const addInpaint = async ( g.addEdge(createGradientMask, 'denoise_mask', denoise, 'denoise_mask'); g.addEdge(l2i, 'image', canvasPasteBack, 'target_image'); - imageOutput = canvasPasteBack; + return canvasPasteBack; } }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index 440eeda9345..dad18653c11 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts @@ -13,14 +13,13 @@ export const addOutpaint = async ( denoise: Invocation<'denoise_latents'>, vaeSource: Invocation<'main_model_loader' | 'seamless' | 'vae_loader'>, modelLoader: Invocation<'main_model_loader'>, - imageOutput: Invocation<'canvas_paste_back' | 'img_nsfw' | 'img_resize' | 'img_watermark' | 'l2i'>, originalSize: Dimensions, scaledSize: Dimensions, bbox: CanvasV2State['bbox'], compositing: CanvasV2State['compositing'], strength: ParameterStrength, vaePrecision: ParameterPrecision -) => { +): Promise> => { denoise.denoising_start = 1 - strength; const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']); @@ -36,34 +35,44 @@ export const addOutpaint = async ( if (!isEqual(scaledSize, originalSize)) { // Scale before processing requires some resizing - const i2l = g.addNode({ id: 'i2l', type: 'i2l' }); - const resizeImageToScaledSize = g.addNode({ - id: 'resize_image_to_scaled_size', - type: 'img_resize', - image: { image_name: initialImage.image_name }, - ...scaledSize, - }); - const alphaToMask = g.addNode({ + + // Combine the inpaint mask and the initial image's alpha channel into a single mask + const maskAlphaToMask = g.addNode({ id: 'alpha_to_mask', type: 'tomask', image: { image_name: maskImage.image_name }, invert: true, }); + const initialImageAlphaToMask = g.addNode({ + id: 'image_alpha_to_mask', + type: 'tomask', + image: { image_name: initialImage.image_name }, + }); + const maskCombine = g.addNode({ + id: 'mask_combine', + type: 'mask_combine', + }); + g.addEdge(maskAlphaToMask, 'image', maskCombine, 'mask1'); + g.addEdge(initialImageAlphaToMask, 'image', maskCombine, 'mask2'); + + // Resize the combined and initial image to the scaled size const resizeMaskToScaledSize = g.addNode({ id: 'resize_mask_to_scaled_size', type: 'img_resize', ...scaledSize, }); - const resizeImageToOriginalSize = g.addNode({ - id: 'resize_image_to_original_size', - type: 'img_resize', - ...originalSize, - }); - const resizeMaskToOriginalSize = g.addNode({ - id: 'resize_mask_to_original_size', + g.addEdge(maskCombine, 'image', resizeMaskToScaledSize, 'image'); + + // Resize the initial image to the scaled size and infill + const resizeImageToScaledSize = g.addNode({ + id: 'resize_image_to_scaled_size', type: 'img_resize', - ...originalSize, + image: { image_name: initialImage.image_name }, + ...scaledSize, }); + g.addEdge(resizeImageToScaledSize, 'image', infill, 'image'); + + // Create the gradient denoising mask from the combined mask const createGradientMask = g.addNode({ id: 'create_gradient_mask', type: 'create_gradient_mask', @@ -72,6 +81,29 @@ export const addOutpaint = async ( edge_radius: compositing.canvasCoherenceEdgeSize, fp32: vaePrecision === 'fp32', }); + g.addEdge(infill, 'image', createGradientMask, 'image'); + g.addEdge(maskCombine, 'image', createGradientMask, 'mask'); + g.addEdge(vaeSource, 'vae', createGradientMask, 'vae'); + g.addEdge(modelLoader, 'unet', createGradientMask, 'unet'); + g.addEdge(createGradientMask, 'denoise_mask', denoise, 'denoise_mask'); + + // Decode infilled image and connect to denoise + const i2l = g.addNode({ id: 'i2l', type: 'i2l' }); + g.addEdge(infill, 'image', i2l, 'image'); + g.addEdge(vaeSource, 'vae', i2l, 'vae'); + g.addEdge(i2l, 'latents', denoise, 'latents'); + + // Resize the output image back to the original size + const resizeImageToOriginalSize = g.addNode({ + id: 'resize_image_to_original_size', + type: 'img_resize', + ...originalSize, + }); + const resizeMaskToOriginalSize = g.addNode({ + id: 'resize_mask_to_original_size', + type: 'img_resize', + ...originalSize, + }); const canvasPasteBack = g.addNode({ id: 'canvas_paste_back', type: 'canvas_paste_back', @@ -80,17 +112,6 @@ export const addOutpaint = async ( }); // Resize initial image and mask to scaled size, feed into to gradient mask - g.addEdge(alphaToMask, 'image', resizeMaskToScaledSize, 'image'); - g.addEdge(resizeImageToScaledSize, 'image', i2l, 'image'); - g.addEdge(i2l, 'latents', denoise, 'latents'); - g.addEdge(vaeSource, 'vae', i2l, 'vae'); - - g.addEdge(vaeSource, 'vae', createGradientMask, 'vae'); - g.addEdge(modelLoader, 'unet', createGradientMask, 'unet'); - g.addEdge(resizeImageToScaledSize, 'image', createGradientMask, 'image'); - g.addEdge(resizeMaskToScaledSize, 'image', createGradientMask, 'mask'); - - g.addEdge(createGradientMask, 'denoise_mask', denoise, 'denoise_mask'); // After denoising, resize the image and mask back to original size g.addEdge(l2i, 'image', resizeImageToOriginalSize, 'image'); @@ -100,7 +121,7 @@ export const addOutpaint = async ( g.addEdge(resizeImageToOriginalSize, 'image', canvasPasteBack, 'target_image'); g.addEdge(resizeMaskToOriginalSize, 'image', canvasPasteBack, 'mask'); - imageOutput = canvasPasteBack; + return canvasPasteBack; } else { infill.image = { image_name: initialImage.image_name }; // No scale before processing, much simpler @@ -147,6 +168,6 @@ export const addOutpaint = async ( g.addEdge(infill, 'image', canvasPasteBack, 'source_image'); g.addEdge(l2i, 'image', canvasPasteBack, 'target_image'); - imageOutput = canvasPasteBack; + return canvasPasteBack; } }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addTextToImage.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addTextToImage.ts index 00792efe5c6..bc11f76be2b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addTextToImage.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addTextToImage.ts @@ -6,10 +6,9 @@ import type { Invocation } from 'services/api/types'; export const addTextToImage = ( g: Graph, l2i: Invocation<'l2i'>, - imageOutput: Invocation<'canvas_paste_back' | 'img_nsfw' | 'img_resize' | 'img_watermark' | 'l2i'>, originalSize: Dimensions, scaledSize: Dimensions -) => { +): Invocation<'img_resize' | 'l2i'> => { if (!isEqual(scaledSize, originalSize)) { // We need to resize the output image back to the original size const resizeImageToOriginalSize = g.addNode({ @@ -19,7 +18,8 @@ export const addTextToImage = ( }); g.addEdge(l2i, 'image', resizeImageToOriginalSize, 'image'); - // This is the new output node - imageOutput = resizeImageToOriginalSize; + return resizeImageToOriginalSize; + } else { + return l2i; } }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index 428b8d72d17..410ce19cdca 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -109,7 +109,6 @@ export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager) type: 'l2i', id: LATENTS_TO_IMAGE, fp32: vaePrecision === 'fp32', - board: getBoardField(state), }); const vaeLoader = vae?.base === model.base @@ -162,9 +161,9 @@ export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager) g.addEdge(vaeSource, 'vae', l2i, 'vae'); if (generationMode === 'txt2img') { - addTextToImage(g, l2i, imageOutput, originalSize, scaledSize); + imageOutput = addTextToImage(g, l2i, originalSize, scaledSize); } else if (generationMode === 'img2img') { - addImageToImage( + imageOutput = await addImageToImage( g, manager, l2i, @@ -178,14 +177,13 @@ export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager) ); } else if (generationMode === 'inpaint') { const { compositing } = state.canvasV2; - addInpaint( + imageOutput = await addInpaint( g, manager, l2i, denoise, vaeSource, modelLoader, - imageOutput, originalSize, scaledSize, bbox, @@ -195,14 +193,13 @@ export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager) ); } else if (generationMode === 'outpaint') { const { compositing } = state.canvasV2; - addOutpaint( + imageOutput = await addOutpaint( g, manager, l2i, denoise, vaeSource, modelLoader, - imageOutput, originalSize, scaledSize, bbox, @@ -244,6 +241,7 @@ export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager) // This is the terminal node and must always save to gallery. imageOutput.is_intermediate = false; imageOutput.use_cache = false; + imageOutput.board = getBoardField(state); g.setMetadataReceivingNode(imageOutput); return g.getGraph(); From c1f5345987ce536e8485025aee7a26b196d40898 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 25 Jun 2024 15:14:54 +1000 Subject: [PATCH 121/678] feat(ui): sdxl graphs --- .../util/graph/generation/addImageToImage.ts | 8 +- .../nodes/util/graph/generation/addInpaint.ts | 13 ++- .../util/graph/generation/addNSFWChecker.ts | 10 +- .../util/graph/generation/addOutpaint.ts | 34 +++--- .../util/graph/generation/addWatermarker.ts | 10 +- .../util/graph/generation/buildSD1Graph.ts | 11 +- .../util/graph/generation/buildSDXLGraph.ts | 107 +++++++++--------- 7 files changed, 88 insertions(+), 105 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts index a57036d366d..3a839bf3a80 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts @@ -1,7 +1,6 @@ import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import type { CanvasV2State, Dimensions } from 'features/controlLayers/store/types'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; -import type { ParameterStrength } from 'features/parameters/types/parameterSchemas'; import { isEqual, pick } from 'lodash-es'; import type { Invocation } from 'services/api/types'; @@ -10,14 +9,13 @@ export const addImageToImage = async ( manager: KonvaNodeManager, l2i: Invocation<'l2i'>, denoise: Invocation<'denoise_latents'>, - vaeSource: Invocation<'main_model_loader' | 'seamless' | 'vae_loader'>, - imageOutput: Invocation<'canvas_paste_back' | 'img_nsfw' | 'img_resize' | 'img_watermark' | 'l2i'>, + vaeSource: Invocation<'main_model_loader' | 'sdxl_model_loader' | 'seamless' | 'vae_loader'>, originalSize: Dimensions, scaledSize: Dimensions, bbox: CanvasV2State['bbox'], - strength: ParameterStrength + denoising_start: number ): Promise> => { - denoise.denoising_start = 1 - strength; + denoise.denoising_start = denoising_start; const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']); const initialImage = await manager.util.getImageSourceImage({ diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts index 48cf3249011..8c7c7b7d7c0 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts @@ -1,7 +1,7 @@ import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import type { CanvasV2State, Dimensions } from 'features/controlLayers/store/types'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; -import type { ParameterPrecision, ParameterStrength } from 'features/parameters/types/parameterSchemas'; +import type { ParameterPrecision } from 'features/parameters/types/parameterSchemas'; import { isEqual, pick } from 'lodash-es'; import type { Invocation } from 'services/api/types'; @@ -10,16 +10,16 @@ export const addInpaint = async ( manager: KonvaNodeManager, l2i: Invocation<'l2i'>, denoise: Invocation<'denoise_latents'>, - vaeSource: Invocation<'main_model_loader' | 'seamless' | 'vae_loader'>, - modelLoader: Invocation<'main_model_loader'>, + vaeSource: Invocation<'main_model_loader' | 'sdxl_model_loader' | 'seamless' | 'vae_loader'>, + modelLoader: Invocation<'main_model_loader' | 'sdxl_model_loader'>, originalSize: Dimensions, scaledSize: Dimensions, bbox: CanvasV2State['bbox'], compositing: CanvasV2State['compositing'], - strength: ParameterStrength, + denoising_start: number, vaePrecision: ParameterPrecision ): Promise> => { - denoise.denoising_start = 1 - strength; + denoise.denoising_start = denoising_start; const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']); const initialImage = await manager.util.getImageSourceImage({ @@ -121,7 +121,6 @@ export const addInpaint = async ( type: 'canvas_paste_back', mask_blur: compositing.maskBlur, source_image: { image_name: initialImage.image_name }, - mask: { image_name: maskImage.image_name }, }); g.addEdge(alphaToMask, 'image', createGradientMask, 'mask'); g.addEdge(i2l, 'latents', denoise, 'latents'); @@ -129,6 +128,8 @@ export const addInpaint = async ( g.addEdge(vaeSource, 'vae', createGradientMask, 'vae'); g.addEdge(modelLoader, 'unet', createGradientMask, 'unet'); g.addEdge(createGradientMask, 'denoise_mask', denoise, 'denoise_mask'); + g.addEdge(createGradientMask, 'expanded_mask_area', canvasPasteBack, 'mask'); + g.addEdge(l2i, 'image', canvasPasteBack, 'target_image'); return canvasPasteBack; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addNSFWChecker.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addNSFWChecker.ts index 939aa6894c8..5a3bb741f51 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addNSFWChecker.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addNSFWChecker.ts @@ -10,21 +10,13 @@ import type { Invocation } from 'services/api/types'; */ export const addNSFWChecker = ( g: Graph, - imageOutput: Invocation<'l2i'> | Invocation<'img_nsfw'> | Invocation<'img_watermark'> | Invocation<'img_resize'> + imageOutput: Invocation<'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_paste_back'> ): Invocation<'img_nsfw'> => { const nsfw = g.addNode({ id: NSFW_CHECKER, type: 'img_nsfw', - is_intermediate: imageOutput.is_intermediate, - board: imageOutput.board, - use_cache: false, }); - // The NSFW checker node is the new image output - make the previous one intermediate - imageOutput.is_intermediate = true; - imageOutput.use_cache = true; - imageOutput.board = undefined; - g.addEdge(imageOutput, 'image', nsfw, 'image'); return nsfw; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index dad18653c11..86a37f85df7 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts @@ -2,7 +2,7 @@ import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager' import type { CanvasV2State, Dimensions } from 'features/controlLayers/store/types'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { getInfill } from 'features/nodes/util/graph/graphBuilderUtils'; -import type { ParameterPrecision, ParameterStrength } from 'features/parameters/types/parameterSchemas'; +import type { ParameterPrecision } from 'features/parameters/types/parameterSchemas'; import { isEqual, pick } from 'lodash-es'; import type { Invocation } from 'services/api/types'; @@ -11,17 +11,15 @@ export const addOutpaint = async ( manager: KonvaNodeManager, l2i: Invocation<'l2i'>, denoise: Invocation<'denoise_latents'>, - vaeSource: Invocation<'main_model_loader' | 'seamless' | 'vae_loader'>, - modelLoader: Invocation<'main_model_loader'>, + vaeSource: Invocation<'main_model_loader' | 'sdxl_model_loader' | 'seamless' | 'vae_loader'>, + modelLoader: Invocation<'main_model_loader' | 'sdxl_model_loader'>, originalSize: Dimensions, scaledSize: Dimensions, bbox: CanvasV2State['bbox'], compositing: CanvasV2State['compositing'], - strength: ParameterStrength, + denoising_start: number, vaePrecision: ParameterPrecision ): Promise> => { - denoise.denoising_start = 1 - strength; - const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']); const initialImage = await manager.util.getImageSourceImage({ bbox: cropBbox, @@ -56,21 +54,21 @@ export const addOutpaint = async ( g.addEdge(initialImageAlphaToMask, 'image', maskCombine, 'mask2'); // Resize the combined and initial image to the scaled size - const resizeMaskToScaledSize = g.addNode({ + const resizeInputMaskToScaledSize = g.addNode({ id: 'resize_mask_to_scaled_size', type: 'img_resize', ...scaledSize, }); - g.addEdge(maskCombine, 'image', resizeMaskToScaledSize, 'image'); + g.addEdge(maskCombine, 'image', resizeInputMaskToScaledSize, 'image'); // Resize the initial image to the scaled size and infill - const resizeImageToScaledSize = g.addNode({ + const resizeInputImageToScaledSize = g.addNode({ id: 'resize_image_to_scaled_size', type: 'img_resize', image: { image_name: initialImage.image_name }, ...scaledSize, }); - g.addEdge(resizeImageToScaledSize, 'image', infill, 'image'); + g.addEdge(resizeInputImageToScaledSize, 'image', infill, 'image'); // Create the gradient denoising mask from the combined mask const createGradientMask = g.addNode({ @@ -82,7 +80,7 @@ export const addOutpaint = async ( fp32: vaePrecision === 'fp32', }); g.addEdge(infill, 'image', createGradientMask, 'image'); - g.addEdge(maskCombine, 'image', createGradientMask, 'mask'); + g.addEdge(resizeInputMaskToScaledSize, 'image', createGradientMask, 'mask'); g.addEdge(vaeSource, 'vae', createGradientMask, 'vae'); g.addEdge(modelLoader, 'unet', createGradientMask, 'unet'); g.addEdge(createGradientMask, 'denoise_mask', denoise, 'denoise_mask'); @@ -94,12 +92,12 @@ export const addOutpaint = async ( g.addEdge(i2l, 'latents', denoise, 'latents'); // Resize the output image back to the original size - const resizeImageToOriginalSize = g.addNode({ + const resizeOutputImageToOriginalSize = g.addNode({ id: 'resize_image_to_original_size', type: 'img_resize', ...originalSize, }); - const resizeMaskToOriginalSize = g.addNode({ + const resizeOutputMaskToOriginalSize = g.addNode({ id: 'resize_mask_to_original_size', type: 'img_resize', ...originalSize, @@ -114,12 +112,12 @@ export const addOutpaint = async ( // Resize initial image and mask to scaled size, feed into to gradient mask // After denoising, resize the image and mask back to original size - g.addEdge(l2i, 'image', resizeImageToOriginalSize, 'image'); - g.addEdge(createGradientMask, 'expanded_mask_area', resizeMaskToOriginalSize, 'image'); + g.addEdge(l2i, 'image', resizeOutputImageToOriginalSize, 'image'); + g.addEdge(createGradientMask, 'expanded_mask_area', resizeOutputMaskToOriginalSize, 'image'); // Finally, paste the generated masked image back onto the original image - g.addEdge(resizeImageToOriginalSize, 'image', canvasPasteBack, 'target_image'); - g.addEdge(resizeMaskToOriginalSize, 'image', canvasPasteBack, 'mask'); + g.addEdge(resizeOutputImageToOriginalSize, 'image', canvasPasteBack, 'target_image'); + g.addEdge(resizeOutputMaskToOriginalSize, 'image', canvasPasteBack, 'mask'); return canvasPasteBack; } else { @@ -154,7 +152,6 @@ export const addOutpaint = async ( id: 'canvas_paste_back', type: 'canvas_paste_back', mask_blur: compositing.maskBlur, - mask: { image_name: maskImage.image_name }, }); g.addEdge(maskAlphaToMask, 'image', maskCombine, 'mask1'); g.addEdge(initialImageAlphaToMask, 'image', maskCombine, 'mask2'); @@ -165,6 +162,7 @@ export const addOutpaint = async ( g.addEdge(vaeSource, 'vae', createGradientMask, 'vae'); g.addEdge(modelLoader, 'unet', createGradientMask, 'unet'); g.addEdge(createGradientMask, 'denoise_mask', denoise, 'denoise_mask'); + g.addEdge(createGradientMask, 'expanded_mask_area', canvasPasteBack, 'mask'); g.addEdge(infill, 'image', canvasPasteBack, 'source_image'); g.addEdge(l2i, 'image', canvasPasteBack, 'target_image'); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addWatermarker.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addWatermarker.ts index 9111a77630d..9cd197a38cb 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addWatermarker.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addWatermarker.ts @@ -10,21 +10,13 @@ import type { Invocation } from 'services/api/types'; */ export const addWatermarker = ( g: Graph, - imageOutput: Invocation<'l2i'> | Invocation<'img_nsfw'> | Invocation<'img_watermark'> | Invocation<'img_resize'> + imageOutput: Invocation<'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_paste_back'> ): Invocation<'img_watermark'> => { const watermark = g.addNode({ id: WATERMARKER, type: 'img_watermark', - is_intermediate: imageOutput.is_intermediate, - board: imageOutput.board, - use_cache: false, }); - // The watermarker node is the new image output - make the previous one intermediate - imageOutput.is_intermediate = true; - imageOutput.use_cache = true; - imageOutput.board = undefined; - g.addEdge(imageOutput, 'image', watermark, 'image'); return watermark; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index 410ce19cdca..894ea19d84c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -157,7 +157,9 @@ export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager) addLoRAs(state, g, denoise, modelLoader, seamless, clipSkip, posCond, negCond); // We might get the VAE from the main model, custom VAE, or seamless node. - const vaeSource: Invocation<'main_model_loader' | 'seamless' | 'vae_loader'> = seamless ?? vaeLoader ?? modelLoader; + const vaeSource: Invocation< + 'main_model_loader' | 'sdxl_model_loader' | 'sdxl_model_loader' | 'seamless' | 'vae_loader' + > = seamless ?? vaeLoader ?? modelLoader; g.addEdge(vaeSource, 'vae', l2i, 'vae'); if (generationMode === 'txt2img') { @@ -169,11 +171,10 @@ export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager) l2i, denoise, vaeSource, - imageOutput, originalSize, scaledSize, bbox, - params.img2imgStrength + 1 - params.img2imgStrength ); } else if (generationMode === 'inpaint') { const { compositing } = state.canvasV2; @@ -188,7 +189,7 @@ export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager) scaledSize, bbox, compositing, - params.img2imgStrength, + 1 - params.img2imgStrength, vaePrecision ); } else if (generationMode === 'outpaint') { @@ -204,7 +205,7 @@ export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager) scaledSize, bbox, compositing, - params.img2imgStrength, + 1 - params.img2imgStrength, vaePrecision ); } diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index 5523f777957..e8591a9b95b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -14,15 +14,18 @@ import { VAE_LOADER, } from 'features/nodes/util/graph/constants'; import { addControlAdapters } from 'features/nodes/util/graph/generation/addControlAdapters'; +import { addImageToImage } from 'features/nodes/util/graph/generation/addImageToImage'; +import { addInpaint } from 'features/nodes/util/graph/generation/addInpaint'; import { addIPAdapters } from 'features/nodes/util/graph/generation/addIPAdapters'; import { addNSFWChecker } from 'features/nodes/util/graph/generation/addNSFWChecker'; +import { addOutpaint } from 'features/nodes/util/graph/generation/addOutpaint'; import { addSDXLLoRAs } from 'features/nodes/util/graph/generation/addSDXLLoRAs'; import { addSDXLRefiner } from 'features/nodes/util/graph/generation/addSDXLRefiner'; import { addSeamless } from 'features/nodes/util/graph/generation/addSeamless'; +import { addTextToImage } from 'features/nodes/util/graph/generation/addTextToImage'; import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermarker'; import { Graph } from 'features/nodes/util/graph/generation/Graph'; import { getBoardField, getSDXLStylePrompts, getSizes } from 'features/nodes/util/graph/graphBuilderUtils'; -import { isEqual, pick } from 'lodash-es'; import type { Invocation, NonNullableGraph } from 'services/api/types'; import { isNonRefinerMainModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; @@ -48,7 +51,6 @@ export const buildSDXLGraph = async (state: RootState, manager: KonvaNodeManager negativePrompt, refinerModel, refinerStart, - img2imgStrength, } = params; assert(model, 'No model found in state'); @@ -105,10 +107,6 @@ export const buildSDXLGraph = async (state: RootState, manager: KonvaNodeManager type: 'l2i', id: LATENTS_TO_IMAGE, fp32: vaePrecision === 'fp32', - board: getBoardField(state), - // This is the terminal node and must always save to gallery. - is_intermediate: false, - use_cache: false, }); const vaeLoader = vae?.base === model.base @@ -119,8 +117,7 @@ export const buildSDXLGraph = async (state: RootState, manager: KonvaNodeManager }) : null; - let imageOutput: Invocation<'l2i'> | Invocation<'img_nsfw'> | Invocation<'img_watermark'> | Invocation<'img_resize'> = - l2i; + let imageOutput: Invocation<'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_paste_back'> = l2i; g.addEdge(modelLoader, 'unet', denoise, 'unet'); g.addEdge(modelLoader, 'clip', posCond, 'clip'); @@ -169,52 +166,51 @@ export const buildSDXLGraph = async (state: RootState, manager: KonvaNodeManager } if (generationMode === 'txt2img') { - if (!isEqual(scaledSize, originalSize)) { - // We are using scaled bbox and need to resize the output image back to the original size. - imageOutput = g.addNode({ - id: 'img_resize', - type: 'img_resize', - ...originalSize, - is_intermediate: false, - use_cache: false, - }); - g.addEdge(l2i, 'image', imageOutput, 'image'); - } + imageOutput = addTextToImage(g, l2i, originalSize, scaledSize); } else if (generationMode === 'img2img') { - denoise.denoising_start = refinerModel ? Math.min(refinerStart, 1 - img2imgStrength) : 1 - img2imgStrength; - - const { image_name } = await manager.util.getImageSourceImage({ - bbox: pick(bbox, ['x', 'y', 'width', 'height']), - preview: true, - }); - - if (!isEqual(scaledSize, originalSize)) { - // We are using scaled bbox and need to resize the output image back to the original size. - const initialImageResize = g.addNode({ - id: 'initial_image_resize', - type: 'img_resize', - ...scaledSize, - image: { image_name }, - }); - const i2l = g.addNode({ id: 'i2l', type: 'i2l' }); - - g.addEdge(vaeSource, 'vae', i2l, 'vae'); - g.addEdge(initialImageResize, 'image', i2l, 'image'); - g.addEdge(i2l, 'latents', denoise, 'latents'); - - imageOutput = g.addNode({ - id: 'img_resize', - type: 'img_resize', - ...originalSize, - is_intermediate: false, - use_cache: false, - }); - g.addEdge(l2i, 'image', imageOutput, 'image'); - } else { - const i2l = g.addNode({ id: 'i2l', type: 'i2l', image: { image_name } }); - g.addEdge(vaeSource, 'vae', i2l, 'vae'); - g.addEdge(i2l, 'latents', denoise, 'latents'); - } + imageOutput = await addImageToImage( + g, + manager, + l2i, + denoise, + vaeSource, + originalSize, + scaledSize, + bbox, + refinerModel ? Math.min(refinerStart, 1 - params.img2imgStrength) : 1 - params.img2imgStrength + ); + } else if (generationMode === 'inpaint') { + const { compositing } = state.canvasV2; + imageOutput = await addInpaint( + g, + manager, + l2i, + denoise, + vaeSource, + modelLoader, + originalSize, + scaledSize, + bbox, + compositing, + refinerModel ? Math.min(refinerStart, 1 - params.img2imgStrength) : 1 - params.img2imgStrength, + vaePrecision + ); + } else if (generationMode === 'outpaint') { + const { compositing } = state.canvasV2; + imageOutput = await addOutpaint( + g, + manager, + l2i, + denoise, + vaeSource, + modelLoader, + originalSize, + scaledSize, + bbox, + compositing, + refinerModel ? Math.min(refinerStart, 1 - params.img2imgStrength) : 1 - params.img2imgStrength, + vaePrecision + ); } const _addedCAs = addControlAdapters(state.canvasV2.controlAdapters.entities, g, denoise, modelConfig.base); @@ -241,6 +237,11 @@ export const buildSDXLGraph = async (state: RootState, manager: KonvaNodeManager imageOutput = addWatermarker(g, imageOutput); } + // This is the terminal node and must always save to gallery. + imageOutput.is_intermediate = false; + imageOutput.use_cache = false; + imageOutput.board = getBoardField(state); + g.setMetadataReceivingNode(imageOutput); return g.getGraph(); }; From f4fceac372684d8708d98f2a2aa0cc06a67eab37 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 25 Jun 2024 19:21:15 +1000 Subject: [PATCH 122/678] feat(ui): add updateNode to Graph --- .../nodes/util/graph/generation/Graph.test.ts | 68 +++++++++++++++++++ .../nodes/util/graph/generation/Graph.ts | 24 +++++++ 2 files changed, 92 insertions(+) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.test.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.test.ts index 67fabbc158e..c3c3ca23488 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.test.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.test.ts @@ -1,3 +1,4 @@ +import { deepClone } from 'common/util/deepClone'; import { Graph } from 'features/nodes/util/graph/generation/Graph'; import type { AnyInvocation, Invocation } from 'services/api/types'; import { assert, AssertionError, is } from 'tsafe'; @@ -70,6 +71,73 @@ describe('Graph', () => { }); }); + describe('updateNode', () => { + const initialNode: Invocation<'add'> = { + id: 'old-id', + type: 'add', + a: 1, + }; + + it('should update node properties correctly', () => { + const g = new Graph(); + const n = g.addNode(deepClone(initialNode)); + const updates = { is_intermediate: true, use_cache: true }; + const updatedNode = g.updateNode(n, updates); + expect(updatedNode.is_intermediate).toBe(true); + expect(updatedNode.use_cache).toBe(true); + }); + + it('should allow updating the node id and update related edges', () => { + const g = new Graph(); + const n = g.addNode(deepClone(initialNode)); + const n2 = g.addNode({ id: 'node-2', type: 'add' }); + const n3 = g.addNode({ id: 'node-4', type: 'add' }); + const oldId = n.id; + const newId = 'new-id'; + const e1 = g.addEdge(n, 'value', n2, 'a'); + const e2 = g.addEdge(n3, 'value', n, 'a'); + g.updateNode(n, { id: newId }); + expect(g.hasNode(newId)).toBe(true); + expect(g.hasNode(oldId)).toBe(false); + expect(e1.source.node_id).toBe(newId); + expect(e2.destination.node_id).toBe(newId); + }); + + it('should throw an error if updated id already exists', () => { + const g = new Graph(); + const n = g.addNode(deepClone(initialNode)); + const n2 = g.addNode({ + id: 'other-id', + type: 'add', + }); + expect(() => g.updateNode(n, { id: n2.id })).toThrowError(AssertionError); + }); + + it('should preserve other fields not specified in updates', () => { + const g = new Graph(); + const n = g.addNode(deepClone(initialNode)); + const updatedNode = g.updateNode(n, { b: 3 }); + expect(updatedNode.b).toBe(3); + expect(updatedNode.a).toBe(initialNode.a); + }); + + it('should allow changing multiple properties at once', () => { + const g = new Graph(); + const n = g.addNode(deepClone(initialNode)); + const updatedNode = g.updateNode(n, { a: 2, b: 3 }); + expect(updatedNode.a).toBe(2); + expect(updatedNode.b).toBe(3); + }); + + it('should handle updates with no changes gracefully', () => { + const g = new Graph(); + const n = g.addNode(deepClone(initialNode)); + const updates = {}; + const updatedNode = g.updateNode(n, updates); + expect(updatedNode).toEqual(n); + }); + }); + describe('addEdge', () => { const add: Invocation<'add'> = { id: 'from-node', diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.ts index 213adac4b87..db96a5f7d0e 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.ts @@ -100,6 +100,30 @@ export class Graph { } } + updateNode(node: Invocation, changes: Partial>): Invocation { + if (changes.id) { + assert(!this.hasNode(changes.id), `Node with id ${changes.id} already exists`); + const oldId = node.id; + const newId = changes.id; + this._graph.nodes[newId] = node; + delete this._graph.nodes[node.id]; + node.id = newId; + + this._graph.edges.forEach((edge) => { + if (edge.source.node_id === oldId) { + edge.source.node_id = newId; + } + if (edge.destination.node_id === oldId) { + edge.destination.node_id = newId; + } + }); + } + + Object.assign(node, changes); + + return node; + } + /** * Get the immediate incomers of a node. * @param node The node to get the incomers of. From c0bfa07ea78ec0865fab4bdf7c31dbae020080ea Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 25 Jun 2024 19:23:18 +1000 Subject: [PATCH 123/678] tidy(ui): type "Dimensions" -> "Size" --- .../features/controlLayers/store/bboxReducers.ts | 4 ++-- .../web/src/features/controlLayers/store/types.ts | 2 +- .../util/getScaledBoundingBoxDimensions.ts | 4 ++-- .../components/ImageViewer/ImageComparison.tsx | 4 ++-- .../ImageViewer/ImageComparisonHover.tsx | 6 +++--- .../ImageViewer/ImageComparisonSlider.tsx | 6 +++--- .../gallery/components/ImageViewer/common.ts | 14 +++++++------- .../nodes/util/graph/generation/addImageToImage.ts | 6 +++--- .../nodes/util/graph/generation/addInpaint.ts | 6 +++--- .../nodes/util/graph/generation/addOutpaint.ts | 6 +++--- .../nodes/util/graph/generation/addTextToImage.ts | 6 +++--- 11 files changed, 32 insertions(+), 32 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts index f2e814290ec..ded7d30a490 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts @@ -1,12 +1,12 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import type { BoundingBoxScaleMethod, CanvasV2State, Dimensions } from 'features/controlLayers/store/types'; +import type { BoundingBoxScaleMethod, CanvasV2State, Size } from 'features/controlLayers/store/types'; import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; import type { IRect } from 'konva/lib/types'; import { pick } from 'lodash-es'; export const bboxReducers = { - scaledBboxChanged: (state, action: PayloadAction>) => { + scaledBboxChanged: (state, action: PayloadAction>) => { state.layers.imageCache = null; state.bbox.scaledSize = { ...state.bbox.scaledSize, ...action.payload }; }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 1d2b57922cb..0f8b048a6c3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -780,7 +780,7 @@ export const isBoundingBoxScaleMethod = (v: unknown): v is BoundingBoxScaleMetho export type CanvasEntity = LayerEntity | ControlAdapterEntity | RegionEntity | InpaintMaskEntity | IPAdapterEntity; export type CanvasEntityIdentifier = Pick; -export type Dimensions = { +export type Size = { width: number; height: number; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/util/getScaledBoundingBoxDimensions.ts b/invokeai/frontend/web/src/features/controlLayers/util/getScaledBoundingBoxDimensions.ts index d98d03f33ec..e35e11e226c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/getScaledBoundingBoxDimensions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/getScaledBoundingBoxDimensions.ts @@ -1,6 +1,6 @@ import { roundToMultiple } from 'common/util/roundDownToMultiple'; import { CANVAS_GRID_SIZE_FINE } from 'features/controlLayers/konva/constants'; -import type { Dimensions } from 'features/controlLayers/store/types'; +import type { Size } from 'features/controlLayers/store/types'; /** * Scales the bounding box dimensions to the optimal dimension. The optimal dimensions should be the trained dimension @@ -8,7 +8,7 @@ import type { Dimensions } from 'features/controlLayers/store/types'; * @param dimensions The un-scaled bbox dimensions * @param optimalDimension The optimal dimension to scale the bbox to */ -export const getScaledBoundingBoxDimensions = (dimensions: Dimensions, optimalDimension: number): Dimensions => { +export const getScaledBoundingBoxDimensions = (dimensions: Size, optimalDimension: number): Size => { const { width, height } = dimensions; const scaledDimensions = { width, height }; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparison.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparison.tsx index ff97a5a687a..3e1583cf6e3 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparison.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparison.tsx @@ -1,6 +1,6 @@ import { useAppSelector } from 'app/store/storeHooks'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; -import type { Dimensions } from 'features/controlLayers/store/types'; +import type { Size } from 'features/controlLayers/store/types'; import { selectComparisonImages } from 'features/gallery/components/ImageViewer/common'; import { ImageComparisonHover } from 'features/gallery/components/ImageViewer/ImageComparisonHover'; import { ImageComparisonSideBySide } from 'features/gallery/components/ImageViewer/ImageComparisonSideBySide'; @@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next'; import { PiImagesBold } from 'react-icons/pi'; type Props = { - containerDims: Dimensions; + containerDims: Size; }; export const ImageComparison = memo(({ containerDims }: Props) => { diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonHover.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonHover.tsx index 11f9da928be..83afa376cdb 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonHover.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonHover.tsx @@ -3,7 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { useBoolean } from 'common/hooks/useBoolean'; import { preventDefault } from 'common/util/stopPropagation'; import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants'; -import type { Dimensions } from 'features/controlLayers/store/types'; +import type { Size } from 'features/controlLayers/store/types'; import { ImageComparisonLabel } from 'features/gallery/components/ImageViewer/ImageComparisonLabel'; import { memo, useMemo, useRef } from 'react'; @@ -14,11 +14,11 @@ export const ImageComparisonHover = memo(({ firstImage, secondImage, containerDi const comparisonFit = useAppSelector((s) => s.gallery.comparisonFit); const imageContainerRef = useRef(null); const mouseOver = useBoolean(false); - const fittedDims = useMemo( + const fittedDims = useMemo( () => fitDimsToContainer(containerDims, firstImage), [containerDims, firstImage] ); - const compareImageDims = useMemo( + const compareImageDims = useMemo( () => getSecondImageDims(comparisonFit, fittedDims, firstImage, secondImage), [comparisonFit, fittedDims, firstImage, secondImage] ); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx index 00b25b1b326..2a9da095168 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx @@ -2,7 +2,7 @@ import { Box, Flex, Icon, Image } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { preventDefault } from 'common/util/stopPropagation'; import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants'; -import type { Dimensions } from 'features/controlLayers/store/types'; +import type { Size } from 'features/controlLayers/store/types'; import { ImageComparisonLabel } from 'features/gallery/components/ImageViewer/ImageComparisonLabel'; import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { PiCaretLeftBold, PiCaretRightBold } from 'react-icons/pi'; @@ -31,12 +31,12 @@ export const ImageComparisonSlider = memo(({ firstImage, secondImage, containerD const rafRef = useRef(null); const lastMoveTimeRef = useRef(0); - const fittedDims = useMemo( + const fittedDims = useMemo( () => fitDimsToContainer(containerDims, firstImage), [containerDims, firstImage] ); - const compareImageDims = useMemo( + const compareImageDims = useMemo( () => getSecondImageDims(comparisonFit, fittedDims, firstImage, secondImage), [comparisonFit, fittedDims, firstImage, secondImage] ); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/common.ts b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/common.ts index ac3d7b172b4..7c58ad2aff5 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/common.ts +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/common.ts @@ -1,5 +1,5 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import type { Dimensions } from 'features/controlLayers/store/types'; +import type { Size } from 'features/controlLayers/store/types'; import { selectGallerySlice } from 'features/gallery/store/gallerySlice'; import type { ComparisonFit } from 'features/gallery/store/types'; import type { ImageDTO } from 'services/api/types'; @@ -9,10 +9,10 @@ export const DROP_SHADOW = 'drop-shadow(0px 0px 4px rgb(0, 0, 0)) drop-shadow(0p export type ComparisonProps = { firstImage: ImageDTO; secondImage: ImageDTO; - containerDims: Dimensions; + containerDims: Size; }; -export const fitDimsToContainer = (containerDims: Dimensions, imageDims: Dimensions): Dimensions => { +export const fitDimsToContainer = (containerDims: Size, imageDims: Size): Size => { // Fall back to the image's dimensions if the container has no dimensions if (containerDims.width === 0 || containerDims.height === 0) { return { width: imageDims.width, height: imageDims.height }; @@ -46,10 +46,10 @@ export const fitDimsToContainer = (containerDims: Dimensions, imageDims: Dimensi */ export const getSecondImageDims = ( comparisonFit: ComparisonFit, - fittedDims: Dimensions, - firstImageDims: Dimensions, - secondImageDims: Dimensions -): Dimensions => { + fittedDims: Size, + firstImageDims: Size, + secondImageDims: Size +): Size => { const width = comparisonFit === 'fill' ? fittedDims.width : (fittedDims.width * secondImageDims.width) / firstImageDims.width; const height = diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts index 3a839bf3a80..32e36a5c826 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts @@ -1,5 +1,5 @@ import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; -import type { CanvasV2State, Dimensions } from 'features/controlLayers/store/types'; +import type { CanvasV2State, Size } from 'features/controlLayers/store/types'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { isEqual, pick } from 'lodash-es'; import type { Invocation } from 'services/api/types'; @@ -10,8 +10,8 @@ export const addImageToImage = async ( l2i: Invocation<'l2i'>, denoise: Invocation<'denoise_latents'>, vaeSource: Invocation<'main_model_loader' | 'sdxl_model_loader' | 'seamless' | 'vae_loader'>, - originalSize: Dimensions, - scaledSize: Dimensions, + originalSize: Size, + scaledSize: Size, bbox: CanvasV2State['bbox'], denoising_start: number ): Promise> => { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts index 8c7c7b7d7c0..41e18071b5b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts @@ -1,5 +1,5 @@ import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; -import type { CanvasV2State, Dimensions } from 'features/controlLayers/store/types'; +import type { CanvasV2State, Size } from 'features/controlLayers/store/types'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import type { ParameterPrecision } from 'features/parameters/types/parameterSchemas'; import { isEqual, pick } from 'lodash-es'; @@ -12,8 +12,8 @@ export const addInpaint = async ( denoise: Invocation<'denoise_latents'>, vaeSource: Invocation<'main_model_loader' | 'sdxl_model_loader' | 'seamless' | 'vae_loader'>, modelLoader: Invocation<'main_model_loader' | 'sdxl_model_loader'>, - originalSize: Dimensions, - scaledSize: Dimensions, + originalSize: Size, + scaledSize: Size, bbox: CanvasV2State['bbox'], compositing: CanvasV2State['compositing'], denoising_start: number, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index 86a37f85df7..29102cc2e3d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts @@ -1,5 +1,5 @@ import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; -import type { CanvasV2State, Dimensions } from 'features/controlLayers/store/types'; +import type { CanvasV2State, Size } from 'features/controlLayers/store/types'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { getInfill } from 'features/nodes/util/graph/graphBuilderUtils'; import type { ParameterPrecision } from 'features/parameters/types/parameterSchemas'; @@ -13,8 +13,8 @@ export const addOutpaint = async ( denoise: Invocation<'denoise_latents'>, vaeSource: Invocation<'main_model_loader' | 'sdxl_model_loader' | 'seamless' | 'vae_loader'>, modelLoader: Invocation<'main_model_loader' | 'sdxl_model_loader'>, - originalSize: Dimensions, - scaledSize: Dimensions, + originalSize: Size, + scaledSize: Size, bbox: CanvasV2State['bbox'], compositing: CanvasV2State['compositing'], denoising_start: number, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addTextToImage.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addTextToImage.ts index bc11f76be2b..6a80c325faa 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addTextToImage.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addTextToImage.ts @@ -1,4 +1,4 @@ -import type { Dimensions } from 'features/controlLayers/store/types'; +import type { Size } from 'features/controlLayers/store/types'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { isEqual } from 'lodash-es'; import type { Invocation } from 'services/api/types'; @@ -6,8 +6,8 @@ import type { Invocation } from 'services/api/types'; export const addTextToImage = ( g: Graph, l2i: Invocation<'l2i'>, - originalSize: Dimensions, - scaledSize: Dimensions + originalSize: Size, + scaledSize: Size ): Invocation<'img_resize' | 'l2i'> => { if (!isEqual(scaledSize, originalSize)) { // We need to resize the output image back to the original size From e1ace99e05cd2218ebbe939a08befa8a1405dc96 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:48:16 +1000 Subject: [PATCH 124/678] feat(ui): staging area (rendering wip) --- .../middleware/listenerMiddleware/index.ts | 2 + .../addCommitStagingAreaImageListener.ts | 65 ++++++-- .../listeners/enqueueRequestedLinear.ts | 12 +- .../socketio/socketInvocationComplete.ts | 12 +- .../components/ControlLayersEditor.tsx | 4 + .../StagingArea/StagingAreaToolbar.tsx | 151 ++++++++++++++++++ .../controlLayers/konva/nodeManager.ts | 7 + .../controlLayers/konva/renderers/objects.ts | 7 +- .../controlLayers/konva/renderers/renderer.ts | 5 + .../konva/renderers/stagingArea.ts | 41 +++++ .../controlLayers/store/canvasV2Slice.ts | 12 ++ .../controlLayers/store/layersReducers.ts | 18 ++- .../store/stagingAreaReducers.ts | 73 +++++++++ .../src/features/controlLayers/store/types.ts | 13 +- .../nodes/util/graph/generation/addRegions.ts | 6 +- .../util/graph/generation/buildSD1Graph.ts | 27 ++-- .../util/graph/generation/buildSDXLGraph.ts | 27 ++-- 17 files changed, 428 insertions(+), 54 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/renderers/stagingArea.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/stagingAreaReducers.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts index 7709770d81f..6bb27c0eaf0 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -1,6 +1,7 @@ import type { TypedStartListening } from '@reduxjs/toolkit'; import { createListenerMiddleware } from '@reduxjs/toolkit'; import { addAdHocPostProcessingRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener'; +import { addStagingListeners } from 'app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener'; import { addAnyEnqueuedListener } from 'app/store/middleware/listenerMiddleware/listeners/anyEnqueued'; import { addAppConfigReceivedListener } from 'app/store/middleware/listenerMiddleware/listeners/appConfigReceived'; import { addAppStartedListener } from 'app/store/middleware/listenerMiddleware/listeners/appStarted'; @@ -87,6 +88,7 @@ addBatchEnqueuedListener(startAppListening); // addCanvasMergedListener(startAppListening); // addStagingAreaImageSavedListener(startAppListening); // addCommitStagingAreaImageListener(startAppListening); +addStagingListeners(startAppListening); // Socket.IO addGeneratorProgressEventListener(startAppListening); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts index 9095a08431e..48e52a46aa9 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts @@ -1,30 +1,38 @@ -import { isAnyOf } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { - canvasBatchIdsReset, - commitStagingAreaImage, - discardStagedImages, - resetCanvas, - setInitialCanvasImage, -} from 'features/canvas/store/canvasSlice'; + layerAdded, + layerImageAdded, + stagingAreaImageAccepted, + stagingAreaReset, +} from 'features/controlLayers/store/canvasV2Slice'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { queueApi } from 'services/api/endpoints/queue'; +import { assert } from 'tsafe'; -const matcher = isAnyOf(commitStagingAreaImage, discardStagedImages, resetCanvas, setInitialCanvasImage); - -export const addCommitStagingAreaImageListener = (startAppListening: AppStartListening) => { +export const addStagingListeners = (startAppListening: AppStartListening) => { startAppListening({ - matcher, + actionCreator: stagingAreaReset, effect: async (_, { dispatch, getState }) => { const log = logger('canvas'); - const state = getState(); - const { batchIds } = state.canvas; + const stagingArea = getState().canvasV2.stagingArea; + + if (!stagingArea) { + // Should not happen + return; + } + + if (stagingArea.batchIds.length === 0) { + return; + } try { const req = dispatch( - queueApi.endpoints.cancelByBatchIds.initiate({ batch_ids: batchIds }, { fixedCacheKey: 'cancelByBatchIds' }) + queueApi.endpoints.cancelByBatchIds.initiate( + { batch_ids: stagingArea.batchIds }, + { fixedCacheKey: 'cancelByBatchIds' } + ) ); const { canceled } = await req.unwrap(); req.reset(); @@ -36,7 +44,6 @@ export const addCommitStagingAreaImageListener = (startAppListening: AppStartLis status: 'success', }); } - dispatch(canvasBatchIdsReset()); } catch { log.error('Failed to cancel canvas batches'); toast({ @@ -47,4 +54,32 @@ export const addCommitStagingAreaImageListener = (startAppListening: AppStartLis } }, }); + + startAppListening({ + actionCreator: stagingAreaImageAccepted, + effect: async (action, api) => { + const { imageDTO } = action.payload; + const { layers, stagingArea, selectedEntityIdentifier } = api.getState().canvasV2; + let layer = layers.entities.find((layer) => layer.id === selectedEntityIdentifier?.id); + + if (!layer) { + layer = layers.entities[0]; + } + + if (!layer) { + // We need to create a new layer to add the accepted image + api.dispatch(layerAdded()); + layer = layers.entities[0]; + } + + assert(layer, 'No layer found to stage image'); + assert(stagingArea, 'Staging should be defined'); + + const { x, y } = stagingArea.bbox; + const { id } = layer; + + api.dispatch(layerImageAdded({ id, imageDTO, pos: { x, y } })); + api.dispatch(stagingAreaReset()); + }, + }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index 0e1544b17bf..bbac10e2a14 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -1,6 +1,7 @@ import { enqueueRequested } from 'app/store/actions'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { getNodeManager } from 'features/controlLayers/konva/nodeManager'; +import { stagingAreaInitialized } from 'features/controlLayers/store/canvasV2Slice'; import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph'; @@ -40,10 +41,19 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) }) ); try { - await req.unwrap(); + const enqueueResult = await req.unwrap(); if (shouldShowProgressInViewer) { dispatch(isImageViewerOpenChanged(true)); } + // TODO(psyche): update the backend schema, this is always provided + const batchId = enqueueResult.batch.batch_id; + assert(batchId, 'No batch ID found in enqueue result'); + dispatch( + stagingAreaInitialized({ + batchIds: [batchId], + bbox: getState().canvasV2.bbox, + }) + ); } finally { req.reset(); } diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts index a82f3f265c4..53aa9acf0ed 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts @@ -2,6 +2,7 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { deepClone } from 'common/util/deepClone'; import { parseify } from 'common/util/serialize'; +import { stagingAreaImageAdded } from 'features/controlLayers/store/canvasV2Slice'; import { boardIdSelected, galleryViewChanged, @@ -11,10 +12,12 @@ import { } from 'features/gallery/store/gallerySlice'; import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; import { zNodeStatus } from 'features/nodes/types/invocation'; +import { CANVAS_OUTPUT } from 'features/nodes/util/graph/constants'; import { boardsApi } from 'services/api/endpoints/boards'; import { imagesApi } from 'services/api/endpoints/images'; import { getCategories, getListImagesUrl } from 'services/api/util'; import { socketInvocationComplete } from 'services/events/actions'; +import { assert } from 'tsafe'; // These nodes output an image, but do not actually *save* an image, so we don't want to handle the gallery logic on them const nodeTypeDenylist = ['load_image', 'image']; @@ -45,10 +48,11 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi imageDTORequest.unsubscribe(); // Add canvas images to the staging area - // TODO(psyche): canvas batchid processing - // if (canvas.batchIds.includes(data.batch_id) && data.invocation_source_id === CANVAS_OUTPUT) { - // dispatch(addImageToStagingArea(imageDTO)); - // } + if (canvasV2.stagingArea?.batchIds.includes(data.batch_id) && data.invocation_source_id === CANVAS_OUTPUT) { + const stagingArea = getState().canvasV2.stagingArea; + assert(stagingArea, 'Staging should be defined'); + dispatch(stagingAreaImageAdded({ imageDTO })); + } if (!imageDTO.is_intermediate) { // update the total images for the board diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx index 41ee961b590..1a3b0c20f99 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx @@ -2,6 +2,7 @@ import { Flex } from '@invoke-ai/ui-library'; import { ControlLayersToolbar } from 'features/controlLayers/components/ControlLayersToolbar'; import { StageComponent } from 'features/controlLayers/components/StageComponent'; +import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar'; import { memo } from 'react'; export const ControlLayersEditor = memo(() => { @@ -17,6 +18,9 @@ export const ControlLayersEditor = memo(() => { > + + +
); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx new file mode 100644 index 00000000000..e7faac86e8a --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx @@ -0,0 +1,151 @@ +import { Button, ButtonGroup, IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { + stagingAreaImageAccepted, + stagingAreaImageDiscarded, + stagingAreaNextImageSelected, + stagingAreaPreviousImageSelected, + stagingAreaReset, +} from 'features/controlLayers/store/canvasV2Slice'; +import type { CanvasV2State } from 'features/controlLayers/store/types'; +import { memo, useCallback, useMemo } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; +import { PiArrowLeftBold, PiArrowRightBold, PiCheckBold, PiTrashSimpleBold, PiXBold } from 'react-icons/pi'; + +export const StagingAreaToolbar = memo(() => { + const stagingArea = useAppSelector((s) => s.canvasV2.stagingArea); + + if (!stagingArea || stagingArea.images.length === 0) { + return null; + } + + return ; +}); + +StagingAreaToolbar.displayName = 'StagingAreaToolbar'; + +type Props = { + stagingArea: NonNullable; +}; + +export const StagingAreaToolbarContent = memo(({ stagingArea }: Props) => { + const dispatch = useAppDispatch(); + const images = useMemo(() => stagingArea.images, [stagingArea]); + const imageDTO = useMemo(() => { + if (stagingArea.selectedImageIndex === null) { + return null; + } + return images[stagingArea.selectedImageIndex] ?? null; + }, [images, stagingArea.selectedImageIndex]); + + const { t } = useTranslation(); + + const onPrev = useCallback(() => { + dispatch(stagingAreaPreviousImageSelected()); + }, [dispatch]); + + const onNext = useCallback(() => { + dispatch(stagingAreaNextImageSelected()); + }, [dispatch]); + + const onAccept = useCallback(() => { + if (!imageDTO || !stagingArea) { + return; + } + dispatch(stagingAreaImageAccepted({ imageDTO })); + }, [dispatch, imageDTO, stagingArea]); + + const onDiscardOne = useCallback(() => { + if (!imageDTO || !stagingArea) { + return; + } + if (images.length === 1) { + dispatch(stagingAreaReset()); + } else { + dispatch(stagingAreaImageDiscarded({ imageDTO })); + } + }, [dispatch, imageDTO, images.length, stagingArea]); + + const onDiscardAll = useCallback(() => { + if (!stagingArea) { + return; + } + dispatch(stagingAreaReset()); + }, [dispatch, stagingArea]); + + useHotkeys(['left'], onPrev, { + preventDefault: true, + }); + + useHotkeys(['right'], onNext, { + preventDefault: true, + }); + + useHotkeys(['enter'], onAccept, { + preventDefault: true, + }); + + return ( + <> + + } + onClick={onPrev} + colorScheme="invokeBlue" + /> + + } + onClick={onNext} + colorScheme="invokeBlue" + /> + + + } + onClick={onAccept} + colorScheme="invokeBlue" + /> + {/* } + onClick={handleSaveToGallery} + colorScheme="invokeBlue" + /> */} + } + onClick={onDiscardOne} + colorScheme="invokeBlue" + fontSize={16} + isDisabled={images.length <= 1} + /> + } + onClick={onDiscardAll} + colorScheme="error" + fontSize={16} + isDisabled={images.length === 0} + /> + + + ); +}); + +StagingAreaToolbarContent.displayName = 'StagingAreaToolbarContent'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts index 7e64696ad4e..c927dd5edbb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts @@ -53,6 +53,7 @@ export type ImageObjectRecord = { konvaPlaceholderGroup: Konva.Group; konvaPlaceholderRect: Konva.Rect; konvaPlaceholderText: Konva.Text; + imageName: string | null; konvaImage: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately isLoading: boolean; isError: boolean; @@ -69,6 +70,7 @@ type KonvaApi = { renderDocumentOverlay: () => void; renderBackground: () => void; renderToolPreview: () => void; + renderStagingArea: () => void; arrangeEntities: () => void; fitDocumentToStage: () => void; fitStageToContainer: () => void; @@ -102,6 +104,10 @@ type PreviewLayer = { innerRect: Konva.Rect; outerRect: Konva.Rect; }; + stagingArea: { + group: Konva.Group; + image: ImageObjectRecord | null; + }; }; type StateApi = { @@ -143,6 +149,7 @@ type StateApi = { getControlAdaptersState: () => CanvasV2State['controlAdapters']; getRegionsState: () => CanvasV2State['regions']; getInpaintMaskState: () => CanvasV2State['inpaintMask']; + getStagingAreaState: () => CanvasV2State['stagingArea']; onInpaintMaskImageCached: (imageDTO: ImageDTO) => void; onRegionMaskImageCached: (id: string, imageDTO: ImageDTO) => void; onLayerImageCached: (imageDTO: ImageDTO) => void; 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 5e5df4e96a3..60d5038dbf3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -153,6 +153,7 @@ export const updateImageSource = async (arg: { const imageDTO = await getImageDTO(image.name); if (!imageDTO) { + objectRecord.imageName = null; objectRecord.isLoading = false; objectRecord.isError = true; objectRecord.konvaPlaceholderGroup.visible(true); @@ -173,6 +174,7 @@ export const updateImageSource = async (arg: { image: imageEl, }); objectRecord.konvaImageGroup.add(objectRecord.konvaImage); + objectRecord.imageName = image.name; } objectRecord.isLoading = false; objectRecord.isError = false; @@ -180,6 +182,7 @@ export const updateImageSource = async (arg: { onLoad?.(objectRecord.konvaImage); }; imageEl.onerror = () => { + objectRecord.imageName = null; objectRecord.isLoading = false; objectRecord.isError = true; objectRecord.konvaPlaceholderGroup.visible(true); @@ -189,6 +192,7 @@ export const updateImageSource = async (arg: { imageEl.id = image.name; imageEl.src = imageDTO.image_url; } catch { + objectRecord.imageName = null; objectRecord.isLoading = false; objectRecord.isError = true; objectRecord.konvaPlaceholderGroup.visible(true); @@ -218,7 +222,7 @@ export const createImageObjectGroup = (arg: { } const { id, image } = obj; const { width, height } = obj; - const konvaImageGroup = new Konva.Group({ id, name, listening: false }); + const konvaImageGroup = new Konva.Group({ id, name, listening: false, x: obj.x, y: obj.y }); const konvaPlaceholderGroup = new Konva.Group({ name: IMAGE_PLACEHOLDER_NAME, listening: false }); const konvaPlaceholderRect = new Konva.Rect({ fill: 'hsl(220 12% 45% / 1)', // 'base.500' @@ -246,6 +250,7 @@ export const createImageObjectGroup = (arg: { konvaPlaceholderRect, konvaPlaceholderText, konvaImage: null, + imageName: null, isLoading: false, isError: false, }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts index 11fb09e57f6..890c93a580f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -22,6 +22,7 @@ import { } from 'features/controlLayers/konva/renderers/preview'; import { getRenderRegions } from 'features/controlLayers/konva/renderers/regions'; import { getFitDocumentToStage, getFitStageToContainer } from 'features/controlLayers/konva/renderers/stage'; +import { createStagingArea, getRenderStagingArea } from 'features/controlLayers/konva/renderers/stagingArea'; import { $stageAttrs, bboxChanged, @@ -259,6 +260,7 @@ export const initializeRenderer = ( const getControlAdaptersState = () => canvasV2.controlAdapters; const getInpaintMaskState = () => canvasV2.inpaintMask; const getMaskOpacity = () => canvasV2.settings.maskOpacity; + const getStagingAreaState = () => canvasV2.stagingArea; // Read-write state, ephemeral interaction state let isDrawing = false; @@ -307,6 +309,7 @@ export const initializeRenderer = ( bbox: createBboxNodes(stage, getBbox, onBboxTransformed, $shift.get, $ctrl.get, $meta.get, $alt.get), tool: createToolPreviewNodes(), documentOverlay: createDocumentOverlay(), + stagingArea: createStagingArea(), }; manager.preview.layer.add(manager.preview.bbox.group); manager.preview.layer.add(manager.preview.tool.group); @@ -329,6 +332,7 @@ export const initializeRenderer = ( getRegionsState, getMaskOpacity, getInpaintMaskState, + getStagingAreaState, // Read-write state setTool, @@ -376,6 +380,7 @@ export const initializeRenderer = ( renderBbox: getRenderBbox(manager), renderToolPreview: getRenderToolPreview(manager), renderDocumentOverlay: getRenderDocumentOverlay(manager), + renderStagingArea: getRenderStagingArea(manager), renderBackground: getRenderBackground(manager), arrangeEntities: getArrangeEntities(manager), fitDocumentToStage: getFitDocumentToStage(manager), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stagingArea.ts new file mode 100644 index 00000000000..d4178e67396 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stagingArea.ts @@ -0,0 +1,41 @@ +import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import { createImageObjectGroup, updateImageSource } from 'features/controlLayers/konva/renderers/objects'; +import { imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/types'; +import Konva from 'konva'; +import { assert } from 'tsafe'; + +export const createStagingArea = (): KonvaNodeManager['preview']['stagingArea'] => { + const group = new Konva.Group({ id: 'staging_area_group', listening: false }); + return { group, image: null }; +}; + +export const getRenderStagingArea = async (manager: KonvaNodeManager) => { + const { getStagingAreaState } = manager.stateApi; + const stagingArea = getStagingAreaState(); + + if (!stagingArea || stagingArea.selectedImageIndex === null) { + if (manager.preview.stagingArea.image) { + manager.preview.stagingArea.image.konvaImageGroup.visible(false); + manager.preview.stagingArea.image = null; + } + return; + } + + if (stagingArea.selectedImageIndex) { + const imageDTO = stagingArea.images[stagingArea.selectedImageIndex]; + assert(imageDTO, 'Image must exist'); + if (manager.preview.stagingArea.image) { + if (manager.preview.stagingArea.image.imageName !== imageDTO.image_name) { + await updateImageSource({ + objectRecord: manager.preview.stagingArea.image, + image: imageDTOToImageWithDims(imageDTO), + }); + } + } else { + manager.preview.stagingArea.image = await createImageObjectGroup({ + obj: imageDTOToImageObject(imageDTO), + name: imageDTO.image_name, + }); + } + } +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 2e28fe6370c..8c579c1cb4f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -13,6 +13,7 @@ import { lorasReducers } from 'features/controlLayers/store/lorasReducers'; import { paramsReducers } from 'features/controlLayers/store/paramsReducers'; import { regionsReducers } from 'features/controlLayers/store/regionsReducers'; import { settingsReducers } from 'features/controlLayers/store/settingsReducers'; +import { stagingAreaReducers } from 'features/controlLayers/store/stagingAreaReducers'; import { toolReducers } from 'features/controlLayers/store/toolReducers'; import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; @@ -119,6 +120,7 @@ const initialState: CanvasV2State = { refinerNegativeAestheticScore: 2.5, refinerStart: 0.8, }, + stagingArea: null, }; export const canvasV2Slice = createSlice({ @@ -136,6 +138,7 @@ export const canvasV2Slice = createSlice({ ...toolReducers, ...bboxReducers, ...inpaintMaskReducers, + ...stagingAreaReducers, widthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }>) => { const { width, updateAspectRatio, clamp } = action.payload; state.document.width = clamp ? Math.max(roundDownToMultiple(width, 8), 64) : width; @@ -327,6 +330,15 @@ export const { imEraserLineAdded, imLinePointAdded, imRectAdded, + // Staging + stagingAreaInitialized, + stagingAreaImageAdded, + stagingAreaBatchIdAdded, + stagingAreaImageDiscarded, + stagingAreaImageAccepted, + stagingAreaReset, + stagingAreaNextImageSelected, + stagingAreaPreviousImageSelected, } = canvasV2Slice.actions; export const selectCanvasV2Slice = (state: RootState) => state.canvasV2; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index 21cbbd4d85f..9edabb1acfc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -231,17 +231,27 @@ export const layersReducers = { prepare: (payload: RectShapeAddedArg) => ({ payload: { ...payload, rectId: uuidv4() } }), }, layerImageAdded: { - reducer: (state, action: PayloadAction) => { - const { id, objectId, imageDTO } = action.payload; + reducer: ( + state, + action: PayloadAction + ) => { + const { id, objectId, imageDTO, pos } = action.payload; const layer = selectLayer(state, id); if (!layer) { return; } - layer.objects.push(imageDTOToImageObject(id, objectId, imageDTO)); + const imageObject = imageDTOToImageObject(id, objectId, imageDTO); + if (pos) { + imageObject.x = pos.x; + imageObject.y = pos.y; + } + layer.objects.push(imageObject); layer.bboxNeedsUpdate = true; state.layers.imageCache = null; }, - prepare: (payload: ImageObjectAddedArg) => ({ payload: { ...payload, objectId: uuidv4() } }), + prepare: (payload: ImageObjectAddedArg & { pos?: { x: number; y: number } }) => ({ + payload: { ...payload, objectId: uuidv4() }, + }), }, layerImageCacheChanged: (state, action: PayloadAction<{ imageDTO: ImageDTO | null }>) => { const { imageDTO } = action.payload; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/stagingAreaReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/stagingAreaReducers.ts new file mode 100644 index 00000000000..8b22d56b653 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/stagingAreaReducers.ts @@ -0,0 +1,73 @@ +import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import type { CanvasV2State, Rect } from 'features/controlLayers/store/types'; +import type { ImageDTO } from 'services/api/types'; + +export const stagingAreaReducers = { + stagingAreaInitialized: (state, action: PayloadAction<{ bbox: Rect; batchIds: string[] }>) => { + const { bbox, batchIds } = action.payload; + state.stagingArea = { + bbox, + batchIds, + selectedImageIndex: null, + images: [], + }; + }, + stagingAreaImageAdded: (state, action: PayloadAction<{ imageDTO: ImageDTO }>) => { + const { imageDTO } = action.payload; + if (!state.stagingArea) { + // Should not happen + return; + } + state.stagingArea.images.push(imageDTO); + if (!state.stagingArea.selectedImageIndex) { + state.stagingArea.selectedImageIndex = state.stagingArea.images.length - 1; + } + }, + stagingAreaNextImageSelected: (state) => { + if (!state.stagingArea) { + // Should not happen + return; + } + if (state.stagingArea.selectedImageIndex === null) { + if (state.stagingArea.images.length > 0) { + state.stagingArea.selectedImageIndex = 0; + } + return; + } + state.stagingArea.selectedImageIndex = (state.stagingArea.selectedImageIndex + 1) % state.stagingArea.images.length; + }, + stagingAreaPreviousImageSelected: (state) => { + if (!state.stagingArea) { + // Should not happen + return; + } + if (state.stagingArea.selectedImageIndex === null) { + if (state.stagingArea.images.length > 0) { + state.stagingArea.selectedImageIndex = 0; + } + return; + } + state.stagingArea.selectedImageIndex = + (state.stagingArea.selectedImageIndex - 1 + state.stagingArea.images.length) % state.stagingArea.images.length; + }, + stagingAreaBatchIdAdded: (state, action: PayloadAction<{ batchId: string }>) => { + const { batchId } = action.payload; + if (!state.stagingArea) { + // Should not happen + return; + } + state.stagingArea.batchIds.push(batchId); + }, + stagingAreaImageDiscarded: (state, action: PayloadAction<{ imageDTO: ImageDTO }>) => { + const { imageDTO } = action.payload; + if (!state.stagingArea) { + // Should not happen + return; + } + state.stagingArea.images = state.stagingArea.images.filter((image) => image.image_name !== imageDTO.image_name); + }, + stagingAreaImageAccepted: (state, _: PayloadAction<{ imageDTO: ImageDTO }>) => state, + stagingAreaReset: (state) => { + state.stagingArea = null; + }, +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 0f8b048a6c3..4f27d721862 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -785,6 +785,11 @@ export type Size = { height: number; }; +export type Position = { + x: number; + y: number; +}; + export type LoRA = { id: string; isEnabled: boolean; @@ -877,6 +882,12 @@ export type CanvasV2State = { refinerNegativeAestheticScore: number; refinerStart: number; }; + stagingArea: { + bbox: Rect; + images: ImageDTO[]; + selectedImageIndex: number | null; + batchIds: string[]; + } | null; }; export type StageAttrs = { x: number; y: number; width: number; height: number; scale: number }; @@ -891,7 +902,7 @@ export type EraserLineAddedArg = { export type BrushLineAddedArg = EraserLineAddedArg & { color: RgbaColor }; export type PointAddedToLineArg = { id: string; point: [number, number] }; export type RectShapeAddedArg = { id: string; rect: IRect; color: RgbaColor }; -export type ImageObjectAddedArg = { id: string; imageDTO: ImageDTO }; +export type ImageObjectAddedArg = { id: string; imageDTO: ImageDTO; pos?: Position }; //#region Type guards export const isLine = (obj: RenderableObject): obj is BrushLine | EraserLine => { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts index e5290422dab..c7a8a3a002a 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts @@ -1,6 +1,6 @@ import { deepClone } from 'common/util/deepClone'; import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; -import type { Dimensions, IPAdapterEntity, RegionEntity } from 'features/controlLayers/store/types'; +import type { IPAdapterEntity, Rect, RegionEntity } from 'features/controlLayers/store/types'; import { PROMPT_REGION_INVERT_TENSOR_MASK_PREFIX, PROMPT_REGION_MASK_TO_TENSOR_PREFIX, @@ -10,7 +10,6 @@ import { } from 'features/nodes/util/graph/constants'; import { addIPAdapterCollectorSafe, isValidIPAdapter } from 'features/nodes/util/graph/generation/addIPAdapters'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; -import type { IRect } from 'konva/lib/types'; import type { BaseModelType, Invocation } from 'services/api/types'; import { assert } from 'tsafe'; @@ -31,8 +30,7 @@ export const addRegions = async ( manager: KonvaNodeManager, regions: RegionEntity[], g: Graph, - documentSize: Dimensions, - bbox: IRect, + bbox: Rect, base: BaseModelType, denoise: Invocation<'denoise_latents'>, posCond: Invocation<'compel'> | Invocation<'sdxl_compel_prompt'>, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index 894ea19d84c..a954938c15d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -2,6 +2,7 @@ import type { RootState } from 'app/store/store'; import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { + CANVAS_OUTPUT, CLIP_SKIP, CONTROL_LAYERS_GRAPH, DENOISE_LATENTS, @@ -119,7 +120,7 @@ export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager) }) : null; - let imageOutput: Invocation<'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_paste_back'> = l2i; + let canvasOutput: Invocation<'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_paste_back'> = l2i; g.addEdge(modelLoader, 'unet', denoise, 'unet'); g.addEdge(modelLoader, 'clip', clipSkip, 'clip'); @@ -163,9 +164,9 @@ export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager) g.addEdge(vaeSource, 'vae', l2i, 'vae'); if (generationMode === 'txt2img') { - imageOutput = addTextToImage(g, l2i, originalSize, scaledSize); + canvasOutput = addTextToImage(g, l2i, originalSize, scaledSize); } else if (generationMode === 'img2img') { - imageOutput = await addImageToImage( + canvasOutput = await addImageToImage( g, manager, l2i, @@ -178,7 +179,7 @@ export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager) ); } else if (generationMode === 'inpaint') { const { compositing } = state.canvasV2; - imageOutput = await addInpaint( + canvasOutput = await addInpaint( g, manager, l2i, @@ -194,7 +195,7 @@ export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager) ); } else if (generationMode === 'outpaint') { const { compositing } = state.canvasV2; - imageOutput = await addOutpaint( + canvasOutput = await addOutpaint( g, manager, l2i, @@ -216,7 +217,6 @@ export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager) manager, state.canvasV2.regions.entities, g, - state.canvasV2.document, state.canvasV2.bbox, modelConfig.base, denoise, @@ -232,18 +232,21 @@ export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager) // } if (state.system.shouldUseNSFWChecker) { - imageOutput = addNSFWChecker(g, imageOutput); + canvasOutput = addNSFWChecker(g, canvasOutput); } if (state.system.shouldUseWatermarker) { - imageOutput = addWatermarker(g, imageOutput); + canvasOutput = addWatermarker(g, canvasOutput); } // This is the terminal node and must always save to gallery. - imageOutput.is_intermediate = false; - imageOutput.use_cache = false; - imageOutput.board = getBoardField(state); + g.updateNode(canvasOutput, { + id: CANVAS_OUTPUT, + is_intermediate: false, + use_cache: false, + board: getBoardField(state), + }); - g.setMetadataReceivingNode(imageOutput); + g.setMetadataReceivingNode(canvasOutput); return g.getGraph(); }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index e8591a9b95b..3d780a2f3df 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -2,6 +2,7 @@ import type { RootState } from 'app/store/store'; import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { + CANVAS_OUTPUT, LATENTS_TO_IMAGE, NEGATIVE_CONDITIONING, NEGATIVE_CONDITIONING_COLLECT, @@ -117,7 +118,7 @@ export const buildSDXLGraph = async (state: RootState, manager: KonvaNodeManager }) : null; - let imageOutput: Invocation<'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_paste_back'> = l2i; + let canvasOutput: Invocation<'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_paste_back'> = l2i; g.addEdge(modelLoader, 'unet', denoise, 'unet'); g.addEdge(modelLoader, 'clip', posCond, 'clip'); @@ -166,9 +167,9 @@ export const buildSDXLGraph = async (state: RootState, manager: KonvaNodeManager } if (generationMode === 'txt2img') { - imageOutput = addTextToImage(g, l2i, originalSize, scaledSize); + canvasOutput = addTextToImage(g, l2i, originalSize, scaledSize); } else if (generationMode === 'img2img') { - imageOutput = await addImageToImage( + canvasOutput = await addImageToImage( g, manager, l2i, @@ -181,7 +182,7 @@ export const buildSDXLGraph = async (state: RootState, manager: KonvaNodeManager ); } else if (generationMode === 'inpaint') { const { compositing } = state.canvasV2; - imageOutput = await addInpaint( + canvasOutput = await addInpaint( g, manager, l2i, @@ -197,7 +198,7 @@ export const buildSDXLGraph = async (state: RootState, manager: KonvaNodeManager ); } else if (generationMode === 'outpaint') { const { compositing } = state.canvasV2; - imageOutput = await addOutpaint( + canvasOutput = await addOutpaint( g, manager, l2i, @@ -219,7 +220,6 @@ export const buildSDXLGraph = async (state: RootState, manager: KonvaNodeManager manager, state.canvasV2.regions.entities, g, - state.canvasV2.document, state.canvasV2.bbox, modelConfig.base, denoise, @@ -230,18 +230,21 @@ export const buildSDXLGraph = async (state: RootState, manager: KonvaNodeManager ); if (state.system.shouldUseNSFWChecker) { - imageOutput = addNSFWChecker(g, imageOutput); + canvasOutput = addNSFWChecker(g, canvasOutput); } if (state.system.shouldUseWatermarker) { - imageOutput = addWatermarker(g, imageOutput); + canvasOutput = addWatermarker(g, canvasOutput); } // This is the terminal node and must always save to gallery. - imageOutput.is_intermediate = false; - imageOutput.use_cache = false; - imageOutput.board = getBoardField(state); + g.updateNode(canvasOutput, { + id: CANVAS_OUTPUT, + is_intermediate: false, + use_cache: false, + board: getBoardField(state), + }); - g.setMetadataReceivingNode(imageOutput); + g.setMetadataReceivingNode(canvasOutput); return g.getGraph(); }; From 330acb55f4c157bd1617f1d262c77fa171374c28 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 26 Jun 2024 15:58:09 +1000 Subject: [PATCH 125/678] feat(ui): consolidate konva API --- .../features/controlLayers/konva/events.ts | 36 +- .../controlLayers/konva/nodeManager.ts | 380 ++++++-------- .../controlLayers/konva/renderers/arrange.ts | 2 +- .../konva/renderers/background.ts | 83 +++ .../konva/renderers/controlAdapters.ts | 147 ++---- .../konva/renderers/inpaintMask.ts | 223 ++++---- .../controlLayers/konva/renderers/layers.ts | 245 ++++----- .../controlLayers/konva/renderers/objects.ts | 481 ++++++++---------- .../controlLayers/konva/renderers/preview.ts | 232 ++++++++- .../controlLayers/konva/renderers/regions.ts | 394 ++++++-------- .../controlLayers/konva/renderers/renderer.ts | 94 ++-- .../controlLayers/konva/renderers/stage.ts | 2 +- .../controlLayers/store/regionsReducers.ts | 10 +- 13 files changed, 1170 insertions(+), 1159 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 6b8aa4bf7d9..97e887067a5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -130,7 +130,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) = stage.on('mouseenter', () => { const tool = getToolState().selected; stage.findOne(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); - manager.konvaApi.renderToolPreview(); + manager.renderToolPreview(); }); //#region mousedown @@ -251,7 +251,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) = setLastAddedPoint(pos); } } - manager.konvaApi.renderToolPreview(); + manager.renderToolPreview(); }); //#region mouseup @@ -290,7 +290,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) = setLastMouseDownPos(null); } - manager.konvaApi.renderToolPreview(); + manager.renderToolPreview(); }); //#region mousemove @@ -396,7 +396,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) = } } } - manager.konvaApi.renderToolPreview(); + manager.renderToolPreview(); }); //#region mouseleave @@ -425,7 +425,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) = } } - manager.konvaApi.renderToolPreview(); + manager.renderToolPreview(); }); //#region wheel @@ -466,11 +466,11 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) = stage.scaleY(newScale); stage.position(newPos); setStageAttrs({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale }); - manager.konvaApi.renderBackground(); - manager.konvaApi.renderDocumentOverlay(); + manager.renderBackground(); + manager.renderDocumentOverlay(); } } - manager.konvaApi.renderToolPreview(); + manager.renderToolPreview(); }); //#region dragmove @@ -482,9 +482,9 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) = height: stage.height(), scale: stage.scaleX(), }); - manager.konvaApi.renderBackground(); - manager.konvaApi.renderDocumentOverlay(); - manager.konvaApi.renderToolPreview(); + manager.renderBackground(); + manager.renderDocumentOverlay(); + manager.renderToolPreview(); }); //#region dragend @@ -497,7 +497,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) = height: stage.height(), scale: stage.scaleX(), }); - manager.konvaApi.renderToolPreview(); + manager.renderToolPreview(); }); //#region key @@ -518,12 +518,12 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) = setTool('view'); setSpaceKey(true); } else if (e.key === 'r') { - manager.konvaApi.fitDocumentToStage(); - manager.konvaApi.renderToolPreview(); - manager.konvaApi.renderBackground(); - manager.konvaApi.renderDocumentOverlay(); + manager.fitDocumentToStage(); + manager.renderToolPreview(); + manager.renderBackground(); + manager.renderDocumentOverlay(); } - manager.konvaApi.renderToolPreview(); + manager.renderToolPreview(); }; window.addEventListener('keydown', onKeyDown); @@ -541,7 +541,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) = setToolBuffer(null); setSpaceKey(false); } - manager.konvaApi.renderToolPreview(); + manager.renderToolPreview(); }; window.addEventListener('keyup', onKeyUp); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts index c927dd5edbb..56c26d2cb30 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts @@ -1,18 +1,17 @@ import { getImageDataTransparency } from 'common/util/arrayBuffer'; +import { DOCUMENT_FIT_PADDING_PX } from 'features/controlLayers/konva/constants'; +import { KonvaBackground } from 'features/controlLayers/konva/renderers/background'; +import { KonvaPreview } from 'features/controlLayers/konva/renderers/preview'; import { konvaNodeToBlob, konvaNodeToImageData, previewBlob } from 'features/controlLayers/konva/util'; import type { - BrushLine, BrushLineAddedArg, CanvasEntity, CanvasV2State, - EraserLine, EraserLineAddedArg, GenerationMode, - ImageObject, PointAddedToLineArg, PosChangedArg, Rect, - RectShape, RectShapeAddedArg, RgbaColor, StageAttrs, @@ -21,96 +20,16 @@ import type { import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers'; import type Konva from 'konva'; import type { Vector2d } from 'konva/lib/types'; -import { atom } from 'nanostores'; import { getImageDTO as defaultGetImageDTO, uploadImage as defaultUploadImage } from 'services/api/endpoints/images'; import type { ImageCategory, ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; -export type BrushLineObjectRecord = { - id: string; - type: BrushLine['type']; - konvaLine: Konva.Line; - konvaLineGroup: Konva.Group; -}; - -export type EraserLineObjectRecord = { - id: string; - type: EraserLine['type']; - konvaLine: Konva.Line; - konvaLineGroup: Konva.Group; -}; - -export type RectShapeObjectRecord = { - id: string; - type: RectShape['type']; - konvaRect: Konva.Rect; -}; - -export type ImageObjectRecord = { - id: string; - type: ImageObject['type']; - konvaImageGroup: Konva.Group; - konvaPlaceholderGroup: Konva.Group; - konvaPlaceholderRect: Konva.Rect; - konvaPlaceholderText: Konva.Text; - imageName: string | null; - konvaImage: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately - isLoading: boolean; - isError: boolean; -}; +import { KonvaControlAdapter } from './renderers/controlAdapters'; +import { KonvaInpaintMask } from './renderers/inpaintMask'; +import { KonvaLayerAdapter } from './renderers/layers'; +import { KonvaRegion } from './renderers/regions'; -type ObjectRecord = BrushLineObjectRecord | EraserLineObjectRecord | RectShapeObjectRecord | ImageObjectRecord; - -type KonvaApi = { - renderRegions: () => void; - renderLayers: () => void; - renderControlAdapters: () => void; - renderInpaintMask: () => void; - renderBbox: () => void; - renderDocumentOverlay: () => void; - renderBackground: () => void; - renderToolPreview: () => void; - renderStagingArea: () => void; - arrangeEntities: () => void; - fitDocumentToStage: () => void; - fitStageToContainer: () => void; -}; - -type BackgroundLayer = { - layer: Konva.Layer; -}; - -type PreviewLayer = { - layer: Konva.Layer; - bbox: { - group: Konva.Group; - rect: Konva.Rect; - transformer: Konva.Transformer; - }; - tool: { - group: Konva.Group; - brush: { - group: Konva.Group; - fill: Konva.Circle; - innerBorder: Konva.Circle; - outerBorder: Konva.Circle; - }; - rect: { - rect: Konva.Rect; - }; - }; - documentOverlay: { - group: Konva.Group; - innerRect: Konva.Rect; - outerRect: Konva.Rect; - }; - stagingArea: { - group: Konva.Group; - image: ImageObjectRecord | null; - }; -}; - -type StateApi = { +export type StateApi = { getToolState: () => CanvasV2State['tool']; getCurrentFill: () => RgbaColor; setTool: (tool: Tool) => void; @@ -174,21 +93,25 @@ type Util = { export class KonvaNodeManager { stage: Konva.Stage; container: HTMLDivElement; - adapters: Map; + controlAdapters: Map; + layers: Map; + regions: Map; + inpaintMask: KonvaInpaintMask | null; util: Util; - _background: BackgroundLayer | null; - _preview: PreviewLayer | null; - _konvaApi: KonvaApi | null; - _stateApi: StateApi | null; + stateApi: StateApi; + preview: KonvaPreview; + background: KonvaBackground; constructor( stage: Konva.Stage, container: HTMLDivElement, + stateApi: StateApi, getImageDTO: Util['getImageDTO'] = defaultGetImageDTO, uploadImage: Util['uploadImage'] = defaultUploadImage ) { this.stage = stage; this.container = container; + this.stateApi = stateApi; this.util = { getImageDTO, uploadImage, @@ -199,83 +122,183 @@ export class KonvaNodeManager { getCompositeLayerStageClone: this._getCompositeLayerStageClone.bind(this), getGenerationMode: this._getGenerationMode.bind(this), }; - this._konvaApi = null; - this._preview = null; - this._background = null; - this._stateApi = null; - this.adapters = new Map(); + this.preview = new KonvaPreview( + this.stage, + this.stateApi.getBbox, + this.stateApi.onBboxTransformed, + this.stateApi.getShiftKey, + this.stateApi.getCtrlKey, + this.stateApi.getMetaKey, + this.stateApi.getAltKey + ); + this.background = new KonvaBackground(); + this.layers = new Map(); + this.regions = new Map(); + this.controlAdapters = new Map(); + this.inpaintMask = null; } - add(entity: CanvasEntity, konvaLayer: Konva.Layer, konvaObjectGroup: Konva.Group): KonvaEntityAdapter { - const adapter = new KonvaEntityAdapter(entity, konvaLayer, konvaObjectGroup, this); - this.adapters.set(adapter.id, adapter); - return adapter; - } + renderLayers() { + const { entities } = this.stateApi.getLayersState(); + const toolState = this.stateApi.getToolState(); - get(id: string): KonvaEntityAdapter | undefined { - return this.adapters.get(id); + for (const adapter of this.layers.values()) { + if (!entities.find((l) => l.id === adapter.id)) { + adapter.destroy(); + this.layers.delete(adapter.id); + } + } + + for (const entity of entities) { + let adapter = this.layers.get(entity.id); + if (!adapter) { + adapter = new KonvaLayerAdapter(entity, this.stateApi.onPosChanged); + this.layers.set(adapter.id, adapter); + this.stage.add(adapter.konvaLayer); + } + adapter.render(entity, toolState.selected); + } } - getAll(type?: CanvasEntity['type']): KonvaEntityAdapter[] { - if (type) { - return Array.from(this.adapters.values()).filter((adapter) => adapter.entityType === type); - } else { - return Array.from(this.adapters.values()); + renderRegions() { + const { entities } = this.stateApi.getRegionsState(); + const maskOpacity = this.stateApi.getMaskOpacity(); + const toolState = this.stateApi.getToolState(); + const selectedEntity = this.stateApi.getSelectedEntity(); + + // Destroy the konva nodes for nonexistent entities + for (const adapter of this.regions.values()) { + if (!entities.find((rg) => rg.id === adapter.id)) { + adapter.destroy(); + this.regions.delete(adapter.id); + } + } + + for (const entity of entities) { + let adapter = this.regions.get(entity.id); + if (!adapter) { + adapter = new KonvaRegion(entity, this.stateApi.onPosChanged); + this.regions.set(adapter.id, adapter); + this.stage.add(adapter.konvaLayer); + } + adapter.render(entity, toolState.selected, selectedEntity, maskOpacity); } } - destroy(id: string): boolean { - const adapter = this.get(id); - if (!adapter) { - return false; + renderInpaintMask() { + const inpaintMaskState = this.stateApi.getInpaintMaskState(); + if (!this.inpaintMask) { + this.inpaintMask = new KonvaInpaintMask(inpaintMaskState, this.stateApi.onPosChanged); + this.stage.add(this.inpaintMask.konvaLayer); } - adapter.konvaLayer.destroy(); - return this.adapters.delete(id); + const toolState = this.stateApi.getToolState(); + const selectedEntity = this.stateApi.getSelectedEntity(); + const maskOpacity = this.stateApi.getMaskOpacity(); + + this.inpaintMask.render(inpaintMaskState, toolState.selected, selectedEntity, maskOpacity); } - set konvaApi(konvaApi: KonvaApi) { - this._konvaApi = konvaApi; + renderControlAdapters() { + const { entities } = this.stateApi.getControlAdaptersState(); + + for (const adapter of this.controlAdapters.values()) { + if (!entities.find((ca) => ca.id === adapter.id)) { + adapter.destroy(); + this.controlAdapters.delete(adapter.id); + } + } + + for (const entity of entities) { + let adapter = this.controlAdapters.get(entity.id); + if (!adapter) { + adapter = new KonvaControlAdapter(entity); + this.controlAdapters.set(adapter.id, adapter); + this.stage.add(adapter.konvaLayer); + } + adapter.render(entity); + } } - get konvaApi(): KonvaApi { - assert(this._konvaApi !== null, 'Konva API has not been set'); - return this._konvaApi; + arrangeEntities() { + const { getLayersState, getControlAdaptersState, getRegionsState } = this.stateApi; + const layers = getLayersState().entities; + const controlAdapters = getControlAdaptersState().entities; + const regions = getRegionsState().entities; + let zIndex = 0; + this.background.konvaLayer.zIndex(++zIndex); + for (const layer of layers) { + this.layers.get(layer.id)?.konvaLayer.zIndex(++zIndex); + } + for (const ca of controlAdapters) { + this.controlAdapters.get(ca.id)?.konvaLayer.zIndex(++zIndex); + } + for (const rg of regions) { + this.regions.get(rg.id)?.konvaLayer.zIndex(++zIndex); + } + this.inpaintMask?.konvaLayer.zIndex(++zIndex); + this.preview.konvaLayer.zIndex(++zIndex); } - set preview(preview: PreviewLayer) { - this._preview = preview; + renderDocumentOverlay() { + this.preview.renderDocumentOverlay(this.stage, this.stateApi.getDocument()); } - get preview(): PreviewLayer { - assert(this._preview !== null, 'Konva preview layer has not been set'); - return this._preview; + renderBbox() { + this.preview.renderBbox(this.stateApi.getBbox(), this.stateApi.getToolState()); } - set background(background: BackgroundLayer) { - this._background = background; + renderToolPreview() { + this.preview.renderToolPreview( + this.stage, + 1, + this.stateApi.getToolState(), + this.stateApi.getCurrentFill(), + this.stateApi.getSelectedEntity(), + this.stateApi.getLastCursorPos(), + this.stateApi.getLastMouseDownPos(), + this.stateApi.getIsDrawing(), + this.stateApi.getIsMouseDown() + ); } - get background(): BackgroundLayer { - assert(this._background !== null, 'Konva background layer has not been set'); - return this._background; + fitDocumentToStage(): void { + const { getDocument, setStageAttrs } = this.stateApi; + const document = getDocument(); + // Fit & center the document on the stage + const width = this.stage.width(); + const height = this.stage.height(); + const docWidthWithBuffer = document.width + DOCUMENT_FIT_PADDING_PX * 2; + const docHeightWithBuffer = document.height + DOCUMENT_FIT_PADDING_PX * 2; + const scale = Math.min(Math.min(width / docWidthWithBuffer, height / docHeightWithBuffer), 1); + const x = (width - docWidthWithBuffer * scale) / 2 + DOCUMENT_FIT_PADDING_PX * scale; + const y = (height - docHeightWithBuffer * scale) / 2 + DOCUMENT_FIT_PADDING_PX * scale; + this.stage.setAttrs({ x, y, width, height, scaleX: scale, scaleY: scale }); + setStageAttrs({ x, y, width, height, scale }); } - set stateApi(stateApi: StateApi) { - this._stateApi = stateApi; + fitStageToContainer(): void { + this.stage.width(this.container.offsetWidth); + this.stage.height(this.container.offsetHeight); + this.stateApi.setStageAttrs({ + x: this.stage.x(), + y: this.stage.y(), + width: this.stage.width(), + height: this.stage.height(), + scale: this.stage.scaleX(), + }); + this.renderBackground(); + this.renderDocumentOverlay(); } - get stateApi(): StateApi { - assert(this._stateApi !== null, 'State API has not been set'); - return this._stateApi; + renderBackground() { + this.background.renderBackground(this.stage); } - _getMaskLayerClone(arg: { id: string }): Konva.Layer { - const { id } = arg; - const adapter = this.get(id); - assert(adapter, `Adapter for entity ${id} not found`); + _getMaskLayerClone(): Konva.Layer { + assert(this.inpaintMask, 'Inpaint mask layer has not been set'); - const layerClone = adapter.konvaLayer.clone(); - const objectGroupClone = adapter.konvaObjectGroup.clone(); + const layerClone = this.inpaintMask.konvaLayer.clone(); + const objectGroupClone = this.inpaintMask.konvaObjectGroup.clone(); layerClone.destroyChildren(); layerClone.add(objectGroupClone); @@ -392,7 +415,7 @@ export class KonvaNodeManager { async _getImageSourceImage(arg: { bbox?: Rect; preview?: boolean }): Promise { const { bbox, preview = false } = arg; - const { imageCache } = this.stateApi.getLayersState(); + // const { imageCache } = this.stateApi.getLayersState(); // if (imageCache) { // const imageDTO = await this.util.getImageDTO(imageCache.name); @@ -416,72 +439,3 @@ export class KonvaNodeManager { return imageDTO; } } - -export class KonvaEntityAdapter { - id: string; - entityType: CanvasEntity['type']; - konvaLayer: Konva.Layer; // Every entity is associated with a konva layer - konvaObjectGroup: Konva.Group; // Every entity's nodes are part of an object group - objectRecords: Map; - manager: KonvaNodeManager; - - constructor(entity: CanvasEntity, konvaLayer: Konva.Layer, konvaObjectGroup: Konva.Group, manager: KonvaNodeManager) { - this.id = entity.id; - this.entityType = entity.type; - this.konvaLayer = konvaLayer; - this.konvaObjectGroup = konvaObjectGroup; - this.objectRecords = new Map(); - this.manager = manager; - this.konvaLayer.add(this.konvaObjectGroup); - this.manager.stage.add(this.konvaLayer); - } - - add(objectRecord: T): T { - this.objectRecords.set(objectRecord.id, objectRecord); - if (objectRecord.type === 'brush_line' || objectRecord.type === 'eraser_line') { - objectRecord.konvaLineGroup.add(objectRecord.konvaLine); - this.konvaObjectGroup.add(objectRecord.konvaLineGroup); - } else if (objectRecord.type === 'rect_shape') { - this.konvaObjectGroup.add(objectRecord.konvaRect); - } else if (objectRecord.type === 'image') { - objectRecord.konvaPlaceholderGroup.add(objectRecord.konvaPlaceholderRect); - objectRecord.konvaPlaceholderGroup.add(objectRecord.konvaPlaceholderText); - objectRecord.konvaImageGroup.add(objectRecord.konvaPlaceholderGroup); - this.konvaObjectGroup.add(objectRecord.konvaImageGroup); - } - return objectRecord; - } - - get(id: string): T | undefined { - return this.objectRecords.get(id) as T | undefined; - } - - getAll(): T[] { - return Array.from(this.objectRecords.values()) as T[]; - } - - destroy(id: string): boolean { - const record = this.get(id); - if (!record) { - return false; - } - if (record.type === 'brush_line' || record.type === 'eraser_line') { - record.konvaLineGroup.destroy(); - } else if (record.type === 'rect_shape') { - record.konvaRect.destroy(); - } else if (record.type === 'image') { - record.konvaImageGroup.destroy(); - } - return this.objectRecords.delete(id); - } -} - -const $nodeManager = atom(null); -export const setNodeManager = (manager: KonvaNodeManager) => { - $nodeManager.set(manager); -}; -export const getNodeManager = () => { - const nodeManager = $nodeManager.get(); - assert(nodeManager, 'Konva node manager not initialized'); - return nodeManager; -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts index 2fa30399325..dea5aba2a3d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts @@ -24,7 +24,7 @@ export const getArrangeEntities = (manager: KonvaNodeManager) => { manager.get(rg.id)?.konvaLayer.zIndex(++zIndex); } manager.get('inpaint_mask')?.konvaLayer.zIndex(++zIndex); - manager.preview.layer.zIndex(++zIndex); + manager.preview.konvaLayer.zIndex(++zIndex); } return arrangeEntities; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts index 15aa97e096d..3e1dd6de064 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts @@ -120,3 +120,86 @@ export const getRenderBackground = (manager: KonvaNodeManager) => { return renderBackground; }; + +export class KonvaBackground { + konvaLayer: Konva.Layer; + + constructor() { + this.konvaLayer = new Konva.Layer({ listening: false }); + } + + renderBackground(stage: Konva.Stage): void { + this.konvaLayer.zIndex(0); + const scale = stage.scaleX(); + const gridSpacing = getGridSpacing(scale); + const x = stage.x(); + const y = stage.y(); + const width = stage.width(); + const height = stage.height(); + const stageRect = { + x1: 0, + y1: 0, + x2: width, + y2: height, + }; + + const gridOffset = { + x: Math.ceil(x / scale / gridSpacing) * gridSpacing, + y: Math.ceil(y / scale / gridSpacing) * gridSpacing, + }; + + const gridRect = { + x1: -gridOffset.x, + y1: -gridOffset.y, + x2: width / scale - gridOffset.x + gridSpacing, + y2: height / scale - gridOffset.y + gridSpacing, + }; + + const gridFullRect = { + x1: Math.min(stageRect.x1, gridRect.x1), + y1: Math.min(stageRect.y1, gridRect.y1), + x2: Math.max(stageRect.x2, gridRect.x2), + y2: Math.max(stageRect.y2, gridRect.y2), + }; + + // find the x & y size of the grid + const xSize = gridFullRect.x2 - gridFullRect.x1; + const ySize = gridFullRect.y2 - gridFullRect.y1; + // compute the number of steps required on each axis. + const xSteps = Math.round(xSize / gridSpacing) + 1; + const ySteps = Math.round(ySize / gridSpacing) + 1; + + const strokeWidth = 1 / scale; + let _x = 0; + let _y = 0; + + this.konvaLayer.destroyChildren(); + + for (let i = 0; i < xSteps; i++) { + _x = gridFullRect.x1 + i * gridSpacing; + this.konvaLayer.add( + new Konva.Line({ + x: _x, + y: gridFullRect.y1, + points: [0, 0, 0, ySize], + stroke: _x % 64 ? fineGridLineColor : baseGridLineColor, + strokeWidth, + listening: false, + }) + ); + } + for (let i = 0; i < ySteps; i++) { + _y = gridFullRect.y1 + i * gridSpacing; + this.konvaLayer.add( + new Konva.Line({ + x: gridFullRect.x1, + y: _y, + points: [0, 0, xSize, 0], + stroke: _y % 64 ? fineGridLineColor : baseGridLineColor, + strokeWidth, + listening: false, + }) + ); + } + } +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts index db61ced5db9..81c392cb9ed 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts @@ -1,81 +1,50 @@ import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; -import { CA_LAYER_IMAGE_NAME, CA_LAYER_NAME, CA_LAYER_OBJECT_GROUP_NAME } from 'features/controlLayers/konva/naming'; -import type { ImageObjectRecord, KonvaEntityAdapter, KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; -import { - createImageObjectGroup, - createObjectGroup, - updateImageSource, -} from 'features/controlLayers/konva/renderers/objects'; +import { getObjectGroupId } from 'features/controlLayers/konva/naming'; import type { ControlAdapterEntity } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { isEqual } from 'lodash-es'; -import { assert } from 'tsafe'; +import { v4 as uuidv4 } from 'uuid'; -/** - * Gets a control adapter entity's konva nodes and entity adapter, creating them if they do not exist. - * @param manager The konva node manager - * @param entity The control adapter layer state - */ -const getControlAdapter = (manager: KonvaNodeManager, entity: ControlAdapterEntity): KonvaEntityAdapter => { - const adapter = manager.get(entity.id); - if (adapter) { - return adapter; - } - const konvaLayer = new Konva.Layer({ - id: entity.id, - name: CA_LAYER_NAME, - imageSmoothingEnabled: false, - listening: false, - }); - const konvaObjectGroup = createObjectGroup(konvaLayer, CA_LAYER_OBJECT_GROUP_NAME); - return manager.add(entity, konvaLayer, konvaObjectGroup); -}; +import { KonvaImage } from './objects'; -/** - * Renders a control adapter. - * @param manager The konva node manager - * @param entity The control adapter entity state - */ -export const renderControlAdapter = async (manager: KonvaNodeManager, entity: ControlAdapterEntity): Promise => { - const adapter = getControlAdapter(manager, entity); - const imageObject = entity.processedImageObject ?? entity.imageObject; +export class KonvaControlAdapter { + id: string; + konvaLayer: Konva.Layer; + konvaObjectGroup: Konva.Group; + konvaImageObject: KonvaImage | null; - if (!imageObject) { - // The user has deleted/reset the image - adapter.getAll().forEach((entry) => { - adapter.destroy(entry.id); + constructor(entity: ControlAdapterEntity) { + const { id } = entity; + this.id = id; + this.konvaLayer = new Konva.Layer({ + id, + imageSmoothingEnabled: false, + listening: false, + }); + this.konvaObjectGroup = new Konva.Group({ + id: getObjectGroupId(this.konvaLayer.id(), uuidv4()), + listening: false, }); - return; + this.konvaLayer.add(this.konvaObjectGroup); + this.konvaImageObject = null; } - let entry = adapter.getAll()[0]; - const opacity = entity.opacity; - const visible = entity.isEnabled; - const filters = entity.filter === 'LightnessToAlphaFilter' ? [LightnessToAlphaFilter] : []; - - if (!entry) { - entry = await createImageObjectGroup({ - adapter: adapter, - obj: imageObject, - name: CA_LAYER_IMAGE_NAME, - onLoad: (konvaImage) => { - konvaImage.filters(filters); - konvaImage.cache(); - konvaImage.opacity(opacity); - konvaImage.visible(visible); - }, - }); - } else { - if (entry.isLoading || entry.isError) { + async render(entity: ControlAdapterEntity) { + const imageObject = entity.processedImageObject ?? entity.imageObject; + if (!imageObject) { + if (this.konvaImageObject) { + this.konvaImageObject.destroy(); + } return; } - assert(entry.konvaImage, `Image entry ${entry.id} must have a konva image if it is not loading or in error state`); - const imageSource = entry.konvaImage.image(); - assert(imageSource instanceof HTMLImageElement, `Image source must be an HTMLImageElement`); - if (imageSource.id !== imageObject.image.name) { - updateImageSource({ - objectRecord: entry, - image: imageObject.image, + + const opacity = entity.opacity; + const visible = entity.isEnabled; + const filters = entity.filter === 'LightnessToAlphaFilter' ? [LightnessToAlphaFilter] : []; + + if (!this.konvaImageObject) { + this.konvaImageObject = await new KonvaImage({ + imageObject, onLoad: (konvaImage) => { konvaImage.filters(filters); konvaImage.cache(); @@ -83,37 +52,25 @@ export const renderControlAdapter = async (manager: KonvaNodeManager, entity: Co konvaImage.visible(visible); }, }); - } else { - if (!isEqual(entry.konvaImage.filters(), filters)) { - entry.konvaImage.filters(filters); - entry.konvaImage.cache(); - } - entry.konvaImage.opacity(opacity); - entry.konvaImage.visible(visible); + this.konvaObjectGroup.add(this.konvaImageObject.konvaImageGroup); } - } -}; - -/** - * Gets a function to render all control adapters. - * @param manager The konva node manager - * @returns A function to render all control adapters - */ -export const getRenderControlAdapters = (manager: KonvaNodeManager) => { - const { getControlAdaptersState } = manager.stateApi; - - function renderControlAdapters(): void { - const { entities } = getControlAdaptersState(); - // Destroy nonexistent layers - for (const adapters of manager.getAll('control_adapter')) { - if (!entities.find((ca) => ca.id === adapters.id)) { - manager.destroy(adapters.id); - } + if (this.konvaImageObject.isLoading || this.konvaImageObject.isError) { + return; + } + if (this.konvaImageObject.imageName !== imageObject.image.name) { + this.konvaImageObject.updateImageSource(imageObject.image.name); } - for (const entity of entities) { - renderControlAdapter(manager, entity); + if (this.konvaImageObject.konvaImage) { + if (!isEqual(this.konvaImageObject.konvaImage.filters(), filters)) { + this.konvaImageObject.konvaImage.filters(filters); + this.konvaImageObject.konvaImage.cache(); + } + this.konvaImageObject.konvaImage.opacity(opacity); + this.konvaImageObject.konvaImage.visible(visible); } } - return renderControlAdapters; -}; + destroy(): void { + this.konvaLayer.destroy(); + } +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts index fc7bed9a641..780121b20a4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts @@ -1,162 +1,134 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; -import { - COMPOSITING_RECT_NAME, - INPAINT_MASK_LAYER_BRUSH_LINE_NAME, - INPAINT_MASK_LAYER_ERASER_LINE_NAME, - INPAINT_MASK_LAYER_NAME, - INPAINT_MASK_LAYER_OBJECT_GROUP_NAME, - INPAINT_MASK_LAYER_RECT_SHAPE_NAME, -} from 'features/controlLayers/konva/naming'; -import type { KonvaEntityAdapter, KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import { getObjectGroupId } from 'features/controlLayers/konva/naming'; +import type { StateApi } from 'features/controlLayers/konva/nodeManager'; import { getLayerBboxFast } from 'features/controlLayers/konva/renderers/bbox'; -import { - createObjectGroup, - getBrushLine, - getEraserLine, - getRectShape, -} from 'features/controlLayers/konva/renderers/objects'; +import { KonvaBrushLine, KonvaEraserLine, KonvaRect } from 'features/controlLayers/konva/renderers/objects'; import { mapId } from 'features/controlLayers/konva/util'; -import type { CanvasEntity, InpaintMaskEntity, PosChangedArg } from 'features/controlLayers/store/types'; +import type { CanvasEntityIdentifier, InpaintMaskEntity, Tool } from 'features/controlLayers/store/types'; import Konva from 'konva'; +import { assert } from 'tsafe'; +import { v4 as uuidv4 } from 'uuid'; + +export class KonvaInpaintMask { + id: string; + konvaLayer: Konva.Layer; + konvaObjectGroup: Konva.Group; + compositingRect: Konva.Rect; + objects: Map; + + constructor(entity: InpaintMaskEntity, onPosChanged: StateApi['onPosChanged']) { + this.id = entity.id; + + this.konvaLayer = new Konva.Layer({ + id: entity.id, + draggable: true, + dragDistance: 0, + }); -/** - * Creates the "compositing rect" for the inpaint mask. - * @param konvaLayer The konva layer - */ -const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => { - const compositingRect = new Konva.Rect({ name: COMPOSITING_RECT_NAME, listening: false }); - konvaLayer.add(compositingRect); - return compositingRect; -}; - -/** - * Gets the singleton inpaint mask entity's konva nodes and entity adapter, creating them if they do not exist. - * @param manager The konva node manager - * @param entityState The inpaint mask entity state - * @param onPosChanged Callback for when the position changes (e.g. the entity is dragged) - * @returns The konva entity adapter for the inpaint mask - */ -const getInpaintMask = ( - manager: KonvaNodeManager, - entityState: InpaintMaskEntity, - onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void -): KonvaEntityAdapter => { - const adapter = manager.get(entityState.id); - if (adapter) { - return adapter; - } - // This layer hasn't been added to the konva state yet - const konvaLayer = new Konva.Layer({ - id: entityState.id, - name: INPAINT_MASK_LAYER_NAME, - draggable: true, - dragDistance: 0, - }); - - // 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 (onPosChanged) { - konvaLayer.on('dragend', function (e) { - onPosChanged({ id: entityState.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'inpaint_mask'); + // 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. + this.konvaLayer.on('dragend', function (e) { + onPosChanged({ id: entity.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'inpaint_mask'); }); + this.konvaObjectGroup = new Konva.Group({ + id: getObjectGroupId(this.konvaLayer.id(), uuidv4()), + listening: false, + }); + this.konvaLayer.add(this.konvaObjectGroup); + this.compositingRect = new Konva.Rect({ listening: false }); + this.konvaLayer.add(this.compositingRect); + this.objects = new Map(); } - const konvaObjectGroup = createObjectGroup(konvaLayer, INPAINT_MASK_LAYER_OBJECT_GROUP_NAME); - return manager.add(entityState, konvaLayer, konvaObjectGroup); -}; - -/** - * Gets a function to render the inpaint mask. - * @param manager The konva node manager - * @returns A function to render the inpaint mask - */ -export const getRenderInpaintMask = (manager: KonvaNodeManager) => { - const { getInpaintMaskState, getMaskOpacity, getToolState, getSelectedEntity, onPosChanged } = manager.stateApi; - - function renderInpaintMask(): void { - const entity = getInpaintMaskState(); - const globalMaskLayerOpacity = getMaskOpacity(); - const toolState = getToolState(); - const selectedEntity = getSelectedEntity(); - const adapter = getInpaintMask(manager, entity, onPosChanged); + destroy(): void { + this.konvaLayer.destroy(); + } + async render( + inpaintMaskState: InpaintMaskEntity, + selectedTool: Tool, + selectedEntityIdentifier: CanvasEntityIdentifier | null, + maskOpacity: number + ) { // Update the layer's position and listening state - adapter.konvaLayer.setAttrs({ - listening: toolState.selected === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events - x: Math.floor(entity.x), - y: Math.floor(entity.y), + this.konvaLayer.setAttrs({ + listening: selectedTool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events + x: Math.floor(inpaintMaskState.x), + y: Math.floor(inpaintMaskState.y), }); // Convert the color to a string, stripping the alpha - the object group will handle opacity. - const rgbColor = rgbColorToString(entity.fill); + const rgbColor = rgbColorToString(inpaintMaskState.fill); // We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required. let groupNeedsCache = false; - const objectIds = entity.objects.map(mapId); + const objectIds = inpaintMaskState.objects.map(mapId); // Destroy any objects that are no longer in state - for (const objectRecord of adapter.getAll()) { - if (!objectIds.includes(objectRecord.id)) { - adapter.destroy(objectRecord.id); + for (const object of this.objects.values()) { + if (!objectIds.includes(object.id)) { + object.destroy(); groupNeedsCache = true; } } - for (const obj of entity.objects) { + for (const obj of inpaintMaskState.objects) { if (obj.type === 'brush_line') { - const objectRecord = getBrushLine(adapter, obj, INPAINT_MASK_LAYER_BRUSH_LINE_NAME); + let brushLine = this.objects.get(obj.id); + assert(brushLine instanceof KonvaBrushLine || brushLine === undefined); - // Only update the points if they have changed. The point values are never mutated, they are only added to the - // array, so checking the length is sufficient to determine if we need to re-cache. - if (objectRecord.konvaLine.points().length !== obj.points.length) { - objectRecord.konvaLine.points(obj.points); + if (!brushLine) { + brushLine = new KonvaBrushLine({ brushLine: obj }); + this.objects.set(brushLine.id, brushLine); + this.konvaLayer.add(brushLine.konvaLineGroup); groupNeedsCache = true; } - // Only update the color if it has changed. - if (objectRecord.konvaLine.stroke() !== rgbColor) { - objectRecord.konvaLine.stroke(rgbColor); + + if (obj.points.length !== brushLine.konvaLine.points().length) { + brushLine.konvaLine.points(obj.points); groupNeedsCache = true; } } else if (obj.type === 'eraser_line') { - const objectRecord = getEraserLine(adapter, obj, INPAINT_MASK_LAYER_ERASER_LINE_NAME); + let eraserLine = this.objects.get(obj.id); + assert(eraserLine instanceof KonvaEraserLine || eraserLine === undefined); - // Only update the points if they have changed. The point values are never mutated, they are only added to the - // array, so checking the length is sufficient to determine if we need to re-cache. - if (objectRecord.konvaLine.points().length !== obj.points.length) { - objectRecord.konvaLine.points(obj.points); + if (!eraserLine) { + eraserLine = new KonvaEraserLine({ eraserLine: obj }); + this.objects.set(eraserLine.id, eraserLine); + this.konvaLayer.add(eraserLine.konvaLineGroup); groupNeedsCache = true; } - // Only update the color if it has changed. - if (objectRecord.konvaLine.stroke() !== rgbColor) { - objectRecord.konvaLine.stroke(rgbColor); + + if (obj.points.length !== eraserLine.konvaLine.points().length) { + eraserLine.konvaLine.points(obj.points); groupNeedsCache = true; } } else if (obj.type === 'rect_shape') { - const objectRecord = getRectShape(adapter, obj, INPAINT_MASK_LAYER_RECT_SHAPE_NAME); + let rect = this.objects.get(obj.id); + assert(rect instanceof KonvaRect || rect === undefined); - // Only update the color if it has changed. - if (objectRecord.konvaRect.fill() !== rgbColor) { - objectRecord.konvaRect.fill(rgbColor); + if (!rect) { + rect = new KonvaRect({ rectShape: obj }); + this.objects.set(rect.id, rect); + this.konvaLayer.add(rect.konvaRect); groupNeedsCache = true; } } } // Only update layer visibility if it has changed. - if (adapter.konvaLayer.visible() !== entity.isEnabled) { - adapter.konvaLayer.visible(entity.isEnabled); + if (this.konvaLayer.visible() !== inpaintMaskState.isEnabled) { + this.konvaLayer.visible(inpaintMaskState.isEnabled); groupNeedsCache = true; } - if (adapter.konvaObjectGroup.getChildren().length === 0) { + if (this.objects.size === 0) { // No objects - clear the cache to reset the previous pixel data - adapter.konvaObjectGroup.clearCache(); + this.konvaObjectGroup.clearCache(); return; } - const compositingRect = - adapter.konvaLayer.findOne(`.${COMPOSITING_RECT_NAME}`) ?? createCompositingRect(adapter.konvaLayer); - const isSelected = selectedEntity?.id === entity.id; + const isSelected = selectedEntityIdentifier?.id === inpaintMaskState.id; /** * When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows @@ -169,39 +141,40 @@ export const getRenderInpaintMask = (manager: KonvaNodeManager) => { * 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. */ - if (isSelected && toolState.selected !== 'move') { + if (isSelected && selectedTool !== 'move') { // We must clear the cache first so Konva will re-draw the group with the new compositing rect - if (adapter.konvaObjectGroup.isCached()) { - adapter.konvaObjectGroup.clearCache(); + if (this.konvaObjectGroup.isCached()) { + this.konvaObjectGroup.clearCache(); } // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work - adapter.konvaObjectGroup.opacity(1); + this.konvaObjectGroup.opacity(1); - compositingRect.setAttrs({ + this.compositingRect.setAttrs({ // The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already - ...(!entity.bboxNeedsUpdate && entity.bbox ? entity.bbox : getLayerBboxFast(adapter.konvaLayer)), + ...(!inpaintMaskState.bboxNeedsUpdate && inpaintMaskState.bbox + ? inpaintMaskState.bbox + : getLayerBboxFast(this.konvaLayer)), fill: rgbColor, - opacity: globalMaskLayerOpacity, + opacity: maskOpacity, // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) globalCompositeOperation: 'source-in', visible: true, // This rect must always be on top of all other shapes - zIndex: adapter.konvaObjectGroup.getChildren().length, + zIndex: this.objects.size + 1, }); } else { // The compositing rect should only be shown when the layer is selected. - compositingRect.visible(false); + this.compositingRect.visible(false); // Cache only if needed - or if we are on this code path and _don't_ have a cache - if (groupNeedsCache || !adapter.konvaObjectGroup.isCached()) { - adapter.konvaObjectGroup.cache(); + if (groupNeedsCache || !this.konvaObjectGroup.isCached()) { + this.konvaObjectGroup.cache(); } // Updating group opacity does not require re-caching - adapter.konvaObjectGroup.opacity(globalMaskLayerOpacity); + this.konvaObjectGroup.opacity(maskOpacity); } // const bboxRect = // regionMap.konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rg, regionMap.konvaLayer); - // if (rg.bbox) { // const active = !rg.bboxNeedsUpdate && isSelected && tool === 'move'; // bboxRect.setAttrs({ @@ -217,6 +190,4 @@ export const getRenderInpaintMask = (manager: KonvaNodeManager) => { // bboxRect.visible(false); // } } - - return renderInpaintMask; -}; +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts index 8dfb803c5bc..d034078d64d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -1,156 +1,131 @@ -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, -} from 'features/controlLayers/konva/naming'; -import type { KonvaEntityAdapter, KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; -import { - createImageObjectGroup, - createObjectGroup, - getBrushLine, - getEraserLine, - getRectShape, -} from 'features/controlLayers/konva/renderers/objects'; +import { getObjectGroupId } from 'features/controlLayers/konva/naming'; +import type { StateApi } from 'features/controlLayers/konva/nodeManager'; +import { KonvaBrushLine, KonvaEraserLine, KonvaImage, KonvaRect } from 'features/controlLayers/konva/renderers/objects'; import { mapId } from 'features/controlLayers/konva/util'; -import type { CanvasEntity, LayerEntity, PosChangedArg, Tool } from 'features/controlLayers/store/types'; +import type { LayerEntity, Tool } from 'features/controlLayers/store/types'; import Konva from 'konva'; +import { assert } from 'tsafe'; +import { v4 as uuidv4 } from 'uuid'; -/** - * Gets layer entity's konva nodes and entity adapter, creating them if they do not exist. - * @param manager The konva node manager - * @param entity The layer entity state - * @param onPosChanged Callback for when the layer's position changes - * @returns The konva entity adapter for the layer - */ -const getLayer = ( - manager: KonvaNodeManager, - entity: LayerEntity, - onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void -): KonvaEntityAdapter => { - const adapter = manager.get(entity.id); - if (adapter) { - return adapter; - } - // This layer hasn't been added to the konva state yet - const konvaLayer = new Konva.Layer({ - id: entity.id, - name: RASTER_LAYER_NAME, - draggable: true, - dragDistance: 0, - }); +export class KonvaLayerAdapter { + id: string; + konvaLayer: Konva.Layer; + konvaObjectGroup: Konva.Group; + objects: Map; + + constructor(entity: LayerEntity, onPosChanged: StateApi['onPosChanged']) { + this.id = entity.id; - // 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 (onPosChanged) { - konvaLayer.on('dragend', function (e) { + this.konvaLayer = new Konva.Layer({ + id: entity.id, + draggable: true, + dragDistance: 0, + }); + + // 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. + this.konvaLayer.on('dragend', function (e) { onPosChanged({ id: entity.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'layer'); }); + const konvaObjectGroup = new Konva.Group({ + id: getObjectGroupId(this.konvaLayer.id(), uuidv4()), + listening: false, + }); + this.konvaObjectGroup = konvaObjectGroup; + this.konvaLayer.add(this.konvaObjectGroup); + this.objects = new Map(); } - const konvaObjectGroup = createObjectGroup(konvaLayer, RASTER_LAYER_OBJECT_GROUP_NAME); - return manager.add(entity, konvaLayer, konvaObjectGroup); -}; - -/** - * Renders a layer. - * @param manager The konva node manager - * @param entity The layer entity state - * @param tool The current tool - * @param onPosChanged Callback for when the layer's position changes - */ -export const renderLayer = async ( - manager: KonvaNodeManager, - entity: LayerEntity, - tool: Tool, - onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void -) => { - const adapter = getLayer(manager, entity, onPosChanged); - - // Update the layer's position and listening state - adapter.konvaLayer.setAttrs({ - listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events - x: Math.floor(entity.x), - y: Math.floor(entity.y), - }); - - const objectIds = entity.objects.map(mapId); - // Destroy any objects that are no longer in state - for (const objectRecord of adapter.getAll()) { - if (!objectIds.includes(objectRecord.id)) { - adapter.destroy(objectRecord.id); - } + destroy(): void { + this.konvaLayer.destroy(); } - for (const obj of entity.objects) { - if (obj.type === 'brush_line') { - const objectRecord = getBrushLine(adapter, obj, RASTER_LAYER_BRUSH_LINE_NAME); - // Only update the points if they have changed. - if (objectRecord.konvaLine.points().length !== obj.points.length) { - objectRecord.konvaLine.points(obj.points); - } - } else if (obj.type === 'eraser_line') { - const objectRecord = getEraserLine(adapter, obj, RASTER_LAYER_ERASER_LINE_NAME); - // Only update the points if they have changed. - if (objectRecord.konvaLine.points().length !== obj.points.length) { - objectRecord.konvaLine.points(obj.points); + async render(layerState: LayerEntity, selectedTool: Tool) { + // Update the layer's position and listening state + this.konvaLayer.setAttrs({ + listening: selectedTool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events + x: Math.floor(layerState.x), + y: Math.floor(layerState.y), + }); + + const objectIds = layerState.objects.map(mapId); + // Destroy any objects that are no longer in state + for (const object of this.objects.values()) { + if (!objectIds.includes(object.id)) { + object.destroy(); } - } else if (obj.type === 'rect_shape') { - getRectShape(adapter, obj, RASTER_LAYER_RECT_SHAPE_NAME); - } else if (obj.type === 'image') { - createImageObjectGroup({ adapter, obj, name: RASTER_LAYER_IMAGE_NAME }); } - } - // Only update layer visibility if it has changed. - if (adapter.konvaLayer.visible() !== entity.isEnabled) { - adapter.konvaLayer.visible(entity.isEnabled); - } + for (const obj of layerState.objects) { + if (obj.type === 'brush_line') { + let brushLine = this.objects.get(obj.id); + assert(brushLine instanceof KonvaBrushLine || brushLine === undefined); - // const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer); + if (!brushLine) { + brushLine = new KonvaBrushLine({ brushLine: obj }); + this.objects.set(brushLine.id, brushLine); + this.konvaLayer.add(brushLine.konvaLineGroup); + } + if (obj.points.length !== brushLine.konvaLine.points().length) { + brushLine.konvaLine.points(obj.points); + } + } else if (obj.type === 'eraser_line') { + let eraserLine = this.objects.get(obj.id); + assert(eraserLine instanceof KonvaEraserLine || eraserLine === undefined); - // if (layerState.bbox) { - // const active = !layerState.bboxNeedsUpdate && layerState.isSelected && tool === 'move'; - // bboxRect.setAttrs({ - // visible: active, - // listening: active, - // x: layerState.bbox.x, - // y: layerState.bbox.y, - // width: layerState.bbox.width, - // height: layerState.bbox.height, - // stroke: layerState.isSelected ? BBOX_SELECTED_STROKE : '', - // strokeWidth: 1 / stage.scaleX(), - // }); - // } else { - // bboxRect.visible(false); - // } + if (!eraserLine) { + eraserLine = new KonvaEraserLine({ eraserLine: obj }); + this.objects.set(eraserLine.id, eraserLine); + this.konvaLayer.add(eraserLine.konvaLineGroup); + } + if (obj.points.length !== eraserLine.konvaLine.points().length) { + eraserLine.konvaLine.points(obj.points); + } + } else if (obj.type === 'rect_shape') { + let rect = this.objects.get(obj.id); + assert(rect instanceof KonvaRect || rect === undefined); - adapter.konvaObjectGroup.opacity(entity.opacity); -}; + if (!rect) { + rect = new KonvaRect({ rectShape: obj }); + this.objects.set(rect.id, rect); + this.konvaLayer.add(rect.konvaRect); + } + } else if (obj.type === 'image') { + let image = this.objects.get(obj.id); + assert(image instanceof KonvaImage || image === undefined); -/** - * Gets a function to render all layers. - * @param manager The konva node manager - * @returns A function to render all layers - */ -export const getRenderLayers = (manager: KonvaNodeManager) => { - const { getLayersState, getToolState, onPosChanged } = manager.stateApi; - - function renderLayers(): void { - const { entities } = getLayersState(); - const tool = getToolState(); - // Destroy nonexistent layers - for (const adapter of manager.getAll('layer')) { - if (!entities.find((l) => l.id === adapter.id)) { - manager.destroy(adapter.id); + if (!image) { + image = await new KonvaImage({ imageObject: obj }); + this.objects.set(image.id, image); + this.konvaLayer.add(image.konvaImageGroup); + } + if (image.imageName !== obj.image.name) { + image.updateImageSource(obj.image.name); + } } } - for (const entity of entities) { - renderLayer(manager, entity, tool.selected, onPosChanged); + + // Only update layer visibility if it has changed. + if (this.konvaLayer.visible() !== layerState.isEnabled) { + this.konvaLayer.visible(layerState.isEnabled); } - } - return renderLayers; -}; + // const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer); + // if (layerState.bbox) { + // const active = !layerState.bboxNeedsUpdate && layerState.isSelected && tool === 'move'; + // bboxRect.setAttrs({ + // visible: active, + // listening: active, + // x: layerState.bbox.x, + // y: layerState.bbox.y, + // width: layerState.bbox.width, + // height: layerState.bbox.height, + // stroke: layerState.isSelected ? BBOX_SELECTED_STROKE : '', + // strokeWidth: 1 / stage.scaleX(), + // }); + // } else { + // bboxRect.visible(false); + // } + this.konvaObjectGroup.opacity(layerState.opacity); + } +} 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 60d5038dbf3..bd540b1f203 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -1,306 +1,239 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; -import { - getLayerBboxId, - getObjectGroupId, - IMAGE_PLACEHOLDER_NAME, - LAYER_BBOX_NAME, - PREVIEW_GENERATION_BBOX_DUMMY_RECT, -} from 'features/controlLayers/konva/naming'; -import type { - BrushLineObjectRecord, - EraserLineObjectRecord, - ImageObjectRecord, - KonvaEntityAdapter, - RectShapeObjectRecord, -} from 'features/controlLayers/konva/nodeManager'; -import type { - BrushLine, - CanvasEntity, - EraserLine, - ImageObject, - ImageWithDims, - RectShape, -} from 'features/controlLayers/store/types'; +import { getLayerBboxId, LAYER_BBOX_NAME } from 'features/controlLayers/konva/naming'; +import type { BrushLine, CanvasEntity, EraserLine, ImageObject, RectShape } from 'features/controlLayers/store/types'; import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types'; import { t } from 'i18next'; import Konva from 'konva'; import { getImageDTO as defaultGetImageDTO } from 'services/api/endpoints/images'; import type { ImageDTO } from 'services/api/types'; -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 - * layers types. - */ - -/** - * 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 + * Creates a bounding box rect for a layer. + * @param entity The layer state for the layer to create the bounding box for + * @param konvaLayer The konva layer to attach the bounding box to */ -export const getBrushLine = ( - adapter: KonvaEntityAdapter, - brushLine: BrushLine, - name: string -): BrushLineObjectRecord => { - const objectRecord = adapter.get(brushLine.id); - if (objectRecord) { - return objectRecord; - } - const { id, strokeWidth, clip, color } = brushLine; - const konvaLineGroup = new Konva.Group({ - clip, - listening: false, - }); - const konvaLine = new Konva.Line({ - id, - name, - strokeWidth, - tension: 0, - lineCap: 'round', - lineJoin: 'round', - shadowForStrokeEnabled: false, - globalCompositeOperation: 'source-over', - listening: false, - stroke: rgbaColorToString(color), +export const createBboxRect = (entity: CanvasEntity, konvaLayer: Konva.Layer): Konva.Rect => { + const rect = new Konva.Rect({ + id: getLayerBboxId(entity.id), + name: LAYER_BBOX_NAME, + strokeWidth: 1, + visible: false, }); - return adapter.add({ id, type: 'brush_line', konvaLine, konvaLineGroup }); + konvaLayer.add(rect); + return rect; }; -/** - * 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 getEraserLine = ( - adapter: KonvaEntityAdapter, - eraserLine: EraserLine, - name: string -): EraserLineObjectRecord => { - const objectRecord = adapter.get(eraserLine.id); - if (objectRecord) { - return objectRecord; +export class KonvaBrushLine { + id: string; + konvaLineGroup: Konva.Group; + konvaLine: Konva.Line; + + constructor(arg: { brushLine: BrushLine }) { + const { brushLine } = arg; + const { id, strokeWidth, clip, color } = brushLine; + this.id = id; + this.konvaLineGroup = new Konva.Group({ + clip, + listening: false, + }); + this.konvaLine = new Konva.Line({ + id, + listening: false, + shadowForStrokeEnabled: false, + strokeWidth, + tension: 0, + lineCap: 'round', + lineJoin: 'round', + globalCompositeOperation: 'source-over', + stroke: rgbaColorToString(color), + }); + this.konvaLineGroup.add(this.konvaLine); } - const { id, strokeWidth, clip } = eraserLine; - const konvaLineGroup = new Konva.Group({ - clip, - listening: false, - }); - const konvaLine = new Konva.Line({ - id, - name, - strokeWidth, - tension: 0, - lineCap: 'round', - lineJoin: 'round', - shadowForStrokeEnabled: false, - globalCompositeOperation: 'destination-out', - listening: false, - stroke: rgbaColorToString(DEFAULT_RGBA_COLOR), - }); - return adapter.add({ id, type: 'eraser_line', konvaLine, konvaLineGroup }); -}; + destroy() { + this.konvaLineGroup.destroy(); + } +} -/** - * 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 rect to - * @param name The konva name for the rect - */ -export const getRectShape = ( - adapter: KonvaEntityAdapter, - rectShape: RectShape, - name: string -): RectShapeObjectRecord => { - const objectRecord = adapter.get(rectShape.id); - if (objectRecord) { - return objectRecord; +export class KonvaEraserLine { + id: string; + konvaLineGroup: Konva.Group; + konvaLine: Konva.Line; + + constructor(arg: { eraserLine: EraserLine }) { + const { eraserLine } = arg; + const { id, strokeWidth, clip } = eraserLine; + this.id = id; + this.konvaLineGroup = new Konva.Group({ + clip, + listening: false, + }); + this.konvaLine = new Konva.Line({ + id, + listening: false, + shadowForStrokeEnabled: false, + strokeWidth, + tension: 0, + lineCap: 'round', + lineJoin: 'round', + globalCompositeOperation: 'destination-out', + stroke: rgbaColorToString(DEFAULT_RGBA_COLOR), + }); + this.konvaLineGroup.add(this.konvaLine); } - const { id, x, y, width, height } = rectShape; - const konvaRect = new Konva.Rect({ - id, - name, - x, - y, - width, - height, - listening: false, - fill: rgbaColorToString(rectShape.color), - }); - return adapter.add({ id: rectShape.id, type: 'rect_shape', konvaRect }); -}; -export const updateImageSource = async (arg: { - objectRecord: ImageObjectRecord; - image: ImageWithDims; - getImageDTO?: (imageName: string) => Promise; - onLoading?: () => void; - onLoad?: (konvaImage: Konva.Image) => void; - onError?: () => void; -}) => { - const { objectRecord, image, getImageDTO = defaultGetImageDTO, onLoading, onLoad, onError } = arg; + destroy() { + this.konvaLineGroup.destroy(); + } +} - try { - objectRecord.isLoading = true; - if (!objectRecord.konvaImage) { - objectRecord.konvaPlaceholderGroup.visible(true); - objectRecord.konvaPlaceholderText.text(t('common.loadingImage', 'Loading Image')); - } - onLoading?.(); +export class KonvaRect { + id: string; + konvaRect: Konva.Rect; - const imageDTO = await getImageDTO(image.name); - if (!imageDTO) { - objectRecord.imageName = null; - objectRecord.isLoading = false; - objectRecord.isError = true; - objectRecord.konvaPlaceholderGroup.visible(true); - objectRecord.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load')); - onError?.(); - return; - } - const imageEl = new Image(); - imageEl.onload = () => { - if (objectRecord.konvaImage) { - objectRecord.konvaImage.setAttrs({ + constructor(arg: { rectShape: RectShape }) { + const { rectShape } = arg; + const { id, x, y, width, height } = rectShape; + this.id = id; + const konvaRect = new Konva.Rect({ + id, + x, + y, + width, + height, + listening: false, + fill: rgbaColorToString(rectShape.color), + }); + this.konvaRect = konvaRect; + } + + destroy() { + this.konvaRect.destroy(); + } +} + +export class KonvaImage { + id: string; + konvaImageGroup: Konva.Group; + konvaPlaceholderGroup: Konva.Group; + konvaPlaceholderRect: Konva.Rect; + konvaPlaceholderText: Konva.Text; + imageName: string | null; + konvaImage: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately + isLoading: boolean; + isError: boolean; + getImageDTO: (imageName: string) => Promise; + onLoading: () => void; + onLoad: (imageName: string, imageEl: HTMLImageElement) => void; + onError: () => void; + + constructor(arg: { + imageObject: ImageObject; + getImageDTO?: (imageName: string) => Promise; + onLoading?: () => void; + onLoad?: (konvaImage: Konva.Image) => void; + onError?: () => void; + }) { + const { imageObject, getImageDTO, onLoading, onLoad, onError } = arg; + const { id, width, height, x, y } = imageObject; + this.konvaImageGroup = new Konva.Group({ id, listening: false, x, y }); + this.konvaPlaceholderGroup = new Konva.Group({ listening: false }); + this.konvaPlaceholderRect = new Konva.Rect({ + fill: 'hsl(220 12% 45% / 1)', // 'base.500' + width, + height, + listening: false, + }); + this.konvaPlaceholderText = new Konva.Text({ + fill: 'hsl(220 12% 10% / 1)', // 'base.900' + width, + height, + align: 'center', + verticalAlign: 'middle', + fontFamily: '"Inter Variable", sans-serif', + fontSize: width / 16, + fontStyle: '600', + text: t('common.loadingImage', 'Loading Image'), + listening: false, + }); + + this.konvaPlaceholderGroup.add(this.konvaPlaceholderRect); + this.konvaPlaceholderGroup.add(this.konvaPlaceholderText); + this.konvaImageGroup.add(this.konvaPlaceholderGroup); + + this.id = id; + this.imageName = null; + this.konvaImage = null; + this.isLoading = false; + this.isError = false; + this.getImageDTO = getImageDTO ?? defaultGetImageDTO; + this.onLoading = function () { + this.isLoading = true; + if (!this.konvaImage) { + this.konvaPlaceholderGroup.visible(true); + this.konvaPlaceholderText.text(t('common.loadingImage', 'Loading Image')); + } + if (onLoading) { + onLoading(); + } + }; + this.onLoad = function (imageName: string, imageEl: HTMLImageElement) { + if (this.konvaImage) { + this.konvaImage.setAttrs({ image: imageEl, }); } else { - objectRecord.konvaImage = new Konva.Image({ - id: objectRecord.id, + this.konvaImage = new Konva.Image({ + id: this.id, listening: false, image: imageEl, }); - objectRecord.konvaImageGroup.add(objectRecord.konvaImage); - objectRecord.imageName = image.name; + this.konvaImageGroup.add(this.konvaImage); + this.imageName = imageName; + } + this.isLoading = false; + this.isError = false; + this.konvaPlaceholderGroup.visible(false); + if (onLoad) { + onLoad(this.konvaImage); } - objectRecord.isLoading = false; - objectRecord.isError = false; - objectRecord.konvaPlaceholderGroup.visible(false); - onLoad?.(objectRecord.konvaImage); }; - imageEl.onerror = () => { - objectRecord.imageName = null; - objectRecord.isLoading = false; - objectRecord.isError = true; - objectRecord.konvaPlaceholderGroup.visible(true); - objectRecord.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load')); - onError?.(); + this.onError = function () { + this.imageName = null; + this.isLoading = false; + this.isError = true; + this.konvaPlaceholderGroup.visible(true); + this.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load')); + if (onError) { + onError(); + } }; - imageEl.id = image.name; - imageEl.src = imageDTO.image_url; - } catch { - objectRecord.imageName = null; - objectRecord.isLoading = false; - objectRecord.isError = true; - objectRecord.konvaPlaceholderGroup.visible(true); - objectRecord.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load')); - onError?.(); } -}; -/** - * Creates an image placeholder group for an image object. - * @param image The image object state - * @returns The konva group for the image placeholder, and callbacks to handle loading and error states - */ -export const createImageObjectGroup = (arg: { - adapter: KonvaEntityAdapter; - obj: ImageObject; - name: string; - getImageDTO?: (imageName: string) => Promise; - onLoad?: (konvaImage: Konva.Image) => void; - onLoading?: () => void; - onError?: () => void; -}): ImageObjectRecord => { - const { adapter, obj, name, getImageDTO = defaultGetImageDTO, onLoad, onLoading, onError } = arg; - let objectRecord = adapter.get(obj.id); - if (objectRecord) { - return objectRecord; - } - const { id, image } = obj; - const { width, height } = obj; - const konvaImageGroup = new Konva.Group({ id, name, listening: false, x: obj.x, y: obj.y }); - const konvaPlaceholderGroup = new Konva.Group({ name: IMAGE_PLACEHOLDER_NAME, listening: false }); - const konvaPlaceholderRect = new Konva.Rect({ - fill: 'hsl(220 12% 45% / 1)', // 'base.500' - width, - height, - listening: false, - }); - const konvaPlaceholderText = new Konva.Text({ - fill: 'hsl(220 12% 10% / 1)', // 'base.900' - width, - height, - align: 'center', - verticalAlign: 'middle', - fontFamily: '"Inter Variable", sans-serif', - fontSize: width / 16, - fontStyle: '600', - text: t('common.loadingImage', 'Loading Image'), - listening: false, - }); - objectRecord = adapter.add({ - id, - type: 'image', - konvaImageGroup, - konvaPlaceholderGroup, - konvaPlaceholderRect, - konvaPlaceholderText, - konvaImage: null, - imageName: null, - isLoading: false, - isError: false, - }); - updateImageSource({ objectRecord, image, getImageDTO, onLoad, onLoading, onError }); - return objectRecord; -}; - -/** - * Creates a bounding box rect for a layer. - * @param entity The layer state for the layer to create the bounding box for - * @param konvaLayer The konva layer to attach the bounding box to - */ -export const createBboxRect = (entity: CanvasEntity, konvaLayer: Konva.Layer): Konva.Rect => { - const rect = new Konva.Rect({ - id: getLayerBboxId(entity.id), - name: LAYER_BBOX_NAME, - strokeWidth: 1, - visible: false, - }); - konvaLayer.add(rect); - return rect; -}; + async updateImageSource(imageName: string) { + try { + this.onLoading(); -/** - * 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; -}; + const imageDTO = await this.getImageDTO(imageName); + if (!imageDTO) { + this.onError(); + return; + } + const imageEl = new Image(); + imageEl.onload = () => { + this.onLoad(imageName, imageEl); + }; + imageEl.onerror = () => { + this.onError(); + }; + imageEl.id = imageName; + imageEl.src = imageDTO.image_url; + } catch { + this.onError(); + } + } -export const createImageDimsPreview = (konvaLayer: Konva.Layer, width: number, height: number): Konva.Rect => { - const imageDimsPreview = new Konva.Rect({ - id: PREVIEW_GENERATION_BBOX_DUMMY_RECT, - x: 0, - y: 0, - width, - height, - stroke: 'rgb(255,0,255)', - strokeWidth: 1 / konvaLayer.getStage().scaleX(), - listening: false, - }); - konvaLayer.add(imageDimsPreview); - return imageDimsPreview; -}; + destroy() { + this.konvaImageGroup.destroy(); + } +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts index b5dcfb1df48..5605b4f40a9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts @@ -19,10 +19,13 @@ import { PREVIEW_TOOL_GROUP_ID, } from 'features/controlLayers/konva/naming'; import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; -import type { CanvasV2State } from 'features/controlLayers/store/types'; +import { createImageObjectGroup, updateImageSource } from 'features/controlLayers/konva/renderers/objects'; +import type { CanvasEntity, CanvasV2State, Position, RgbaColor } from 'features/controlLayers/store/types'; +import { imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; import { atom } from 'nanostores'; +import { assert } from 'tsafe'; /** * Creates the konva preview layer. @@ -511,3 +514,230 @@ export const getRenderDocumentOverlay = (manager: KonvaNodeManager) => { return renderDocumentOverlay; }; + +export const createStagingArea = (): KonvaNodeManager['preview']['stagingArea'] => { + const group = new Konva.Group({ id: 'staging_area_group', listening: false }); + return { group, image: null }; +}; + +export const getRenderStagingArea = async (manager: KonvaNodeManager) => { + const { getStagingAreaState } = manager.stateApi; + const stagingArea = getStagingAreaState(); + + if (!stagingArea || stagingArea.selectedImageIndex === null) { + if (manager.preview.stagingArea.image) { + manager.preview.stagingArea.image.konvaImageGroup.visible(false); + manager.preview.stagingArea.image = null; + } + return; + } + + if (stagingArea.selectedImageIndex) { + const imageDTO = stagingArea.images[stagingArea.selectedImageIndex]; + assert(imageDTO, 'Image must exist'); + if (manager.preview.stagingArea.image) { + if (manager.preview.stagingArea.image.imageName !== imageDTO.image_name) { + await updateImageSource({ + objectRecord: manager.preview.stagingArea.image, + image: imageDTOToImageWithDims(imageDTO), + }); + } + } else { + manager.preview.stagingArea.image = await createImageObjectGroup({ + obj: imageDTOToImageObject(imageDTO), + name: imageDTO.image_name, + }); + } + } +}; + +export class KonvaPreview { + konvaLayer: Konva.Layer; + bbox: { + group: Konva.Group; + rect: Konva.Rect; + transformer: Konva.Transformer; + }; + tool: { + group: Konva.Group; + brush: { + group: Konva.Group; + fill: Konva.Circle; + innerBorder: Konva.Circle; + outerBorder: Konva.Circle; + }; + rect: { + rect: Konva.Rect; + }; + }; + documentOverlay: { + group: Konva.Group; + innerRect: Konva.Rect; + outerRect: Konva.Rect; + }; + stagingArea: { + group: Konva.Group; + // image: KonvaImage | null; + }; + + constructor( + stage: Konva.Stage, + getBbox: () => IRect, + onBboxTransformed: (bbox: IRect) => void, + getShiftKey: () => boolean, + getCtrlKey: () => boolean, + getMetaKey: () => boolean, + getAltKey: () => boolean + ) { + this.konvaLayer = createPreviewLayer(); + this.bbox = createBboxNodes(stage, getBbox, onBboxTransformed, getShiftKey, getCtrlKey, getMetaKey, getAltKey); + this.tool = createToolPreviewNodes(); + this.documentOverlay = createDocumentOverlay(); + this.stagingArea = createStagingArea(); + } + + renderBbox(bbox: CanvasV2State['bbox'], toolState: CanvasV2State['tool']) { + this.bbox.group.listening(toolState.selected === 'bbox'); + // This updates the bbox during transformation + this.bbox.rect.setAttrs({ + x: bbox.x, + y: bbox.y, + width: bbox.width, + height: bbox.height, + scaleX: 1, + scaleY: 1, + listening: toolState.selected === 'bbox', + }); + this.bbox.transformer.setAttrs({ + listening: toolState.selected === 'bbox', + enabledAnchors: toolState.selected === 'bbox' ? ALL_ANCHORS : NO_ANCHORS, + }); + } + + scaleToolPreview(stage: Konva.Stage, toolState: CanvasV2State['tool']) { + const scale = stage.scaleX(); + const radius = (toolState.selected === 'brush' ? toolState.brush.width : toolState.eraser.width) / 2; + this.tool.brush.innerBorder.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale); + this.tool.brush.outerBorder.setAttrs({ + strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, + radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, + }); + } + + renderToolPreview( + stage: Konva.Stage, + renderedEntityCount: number, + toolState: CanvasV2State['tool'], + currentFill: RgbaColor, + selectedEntity: CanvasEntity | null, + cursorPos: Position | null, + lastMouseDownPos: Position | null, + isDrawing: boolean, + isMouseDown: boolean + ) { + const tool = toolState.selected; + const isDrawableEntity = + selectedEntity?.type === 'regional_guidance' || + selectedEntity?.type === 'layer' || + selectedEntity?.type === 'inpaint_mask'; + + // Update the stage's pointer style + if (tool === 'view') { + // View gets a hand + stage.container().style.cursor = isMouseDown ? 'grabbing' : 'grab'; + } else if (renderedEntityCount === 0) { + // We have no layers, so we should not render any tool + stage.container().style.cursor = 'default'; + } else if (!isDrawableEntity) { + // Non-drawable layers don't have tools + stage.container().style.cursor = 'not-allowed'; + } else if (tool === 'move') { + // Move tool gets a pointer + stage.container().style.cursor = 'default'; + } else if (tool === 'rect') { + // Rect gets a crosshair + stage.container().style.cursor = 'crosshair'; + } else if (tool === 'brush' || tool === 'eraser') { + // Hide the native cursor and use the konva-rendered brush preview + stage.container().style.cursor = 'none'; + } else if (tool === 'bbox') { + stage.container().style.cursor = 'default'; + } + + stage.draggable(tool === 'view'); + + if (!cursorPos || renderedEntityCount === 0 || !isDrawableEntity) { + // We can bail early if the mouse isn't over the stage or there are no layers + this.tool.group.visible(false); + } else { + this.tool.group.visible(true); + + // No need to render the brush preview if the cursor position or color is missing + if (cursorPos && (tool === 'brush' || tool === 'eraser')) { + const scale = stage.scaleX(); + // Update the fill circle + const radius = (tool === 'brush' ? toolState.brush.width : toolState.eraser.width) / 2; + this.tool.brush.fill.setAttrs({ + x: cursorPos.x, + y: cursorPos.y, + radius, + fill: isDrawing ? '' : rgbaColorToString(currentFill), + globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out', + }); + + // Update the inner border of the brush preview + this.tool.brush.innerBorder.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius }); + + // Update the outer border of the brush preview + this.tool.brush.outerBorder.setAttrs({ + x: cursorPos.x, + y: cursorPos.y, + radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, + }); + + this.scaleToolPreview(stage, toolState); + + this.tool.brush.group.visible(true); + } else { + this.tool.brush.group.visible(false); + } + + if (cursorPos && lastMouseDownPos && tool === 'rect') { + this.tool.rect.rect.setAttrs({ + x: Math.min(cursorPos.x, lastMouseDownPos.x), + y: Math.min(cursorPos.y, lastMouseDownPos.y), + width: Math.abs(cursorPos.x - lastMouseDownPos.x), + height: Math.abs(cursorPos.y - lastMouseDownPos.y), + fill: rgbaColorToString(currentFill), + visible: true, + }); + } else { + this.tool.rect.rect.visible(false); + } + } + } + + renderDocumentOverlay(stage: Konva.Stage, document: CanvasV2State['document']) { + this.documentOverlay.group.zIndex(0); + + const x = stage.x(); + const y = stage.y(); + const width = stage.width(); + const height = stage.height(); + const scale = stage.scaleX(); + + this.documentOverlay.outerRect.setAttrs({ + offsetX: x / scale, + offsetY: y / scale, + width: width / scale, + height: height / scale, + }); + + this.documentOverlay.innerRect.setAttrs({ + x: 0, + y: 0, + width: document.width, + height: document.height, + }); + } +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts index 70b8cfb51bc..a7263df9f40 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts @@ -1,257 +1,191 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; -import { - COMPOSITING_RECT_NAME, - 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 type { KonvaEntityAdapter, KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import { getObjectGroupId } from 'features/controlLayers/konva/naming'; +import type { StateApi } from 'features/controlLayers/konva/nodeManager'; import { getLayerBboxFast } from 'features/controlLayers/konva/renderers/bbox'; -import { - createObjectGroup, - getBrushLine, - getEraserLine, - getRectShape, -} from 'features/controlLayers/konva/renderers/objects'; +import { KonvaBrushLine, KonvaEraserLine, KonvaRect } from 'features/controlLayers/konva/renderers/objects'; import { mapId } from 'features/controlLayers/konva/util'; -import type { - CanvasEntity, - CanvasEntityIdentifier, - PosChangedArg, - RegionEntity, - Tool, -} from 'features/controlLayers/store/types'; +import type { CanvasEntityIdentifier, RegionEntity, Tool } from 'features/controlLayers/store/types'; import Konva from 'konva'; +import { assert } from 'tsafe'; +import { v4 as uuidv4 } from 'uuid'; + +export class KonvaRegion { + id: string; + konvaLayer: Konva.Layer; + konvaObjectGroup: Konva.Group; + compositingRect: Konva.Rect; + objects: Map; + + constructor(entity: RegionEntity, onPosChanged: StateApi['onPosChanged']) { + this.id = entity.id; + + this.konvaLayer = new Konva.Layer({ + id: entity.id, + draggable: true, + dragDistance: 0, + }); -/** - * Creates the "compositing rect" for a regional guidance layer. - * @param konvaLayer The konva layer - */ -const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => { - const compositingRect = new Konva.Rect({ name: COMPOSITING_RECT_NAME, listening: false }); - konvaLayer.add(compositingRect); - return compositingRect; -}; - -/** - * Gets a region's konva nodes and entity adapter, creating them if they do not exist. - * @param stage The konva stage - * @param entity The regional guidance layer state - * @param onLayerPosChanged Callback for when the layer's position changes - * @returns The konva entity adapter for the region - */ -const getRegion = ( - manager: KonvaNodeManager, - entity: RegionEntity, - onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void -): KonvaEntityAdapter => { - const adapter = manager.get(entity.id); - if (adapter) { - return adapter; - } - // This layer hasn't been added to the konva state yet - const konvaLayer = new Konva.Layer({ - id: entity.id, - name: RG_LAYER_NAME, - draggable: true, - dragDistance: 0, - }); - - // 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 (onPosChanged) { - konvaLayer.on('dragend', function (e) { + // 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. + this.konvaLayer.on('dragend', function (e) { onPosChanged({ id: entity.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'regional_guidance'); }); + this.konvaObjectGroup = new Konva.Group({ + id: getObjectGroupId(this.konvaLayer.id(), uuidv4()), + listening: false, + }); + this.konvaLayer.add(this.konvaObjectGroup); + this.compositingRect = new Konva.Rect({ listening: false }); + this.konvaLayer.add(this.compositingRect); + this.objects = new Map(); } - const konvaObjectGroup = createObjectGroup(konvaLayer, RG_LAYER_OBJECT_GROUP_NAME); - return manager.add(entity, konvaLayer, konvaObjectGroup); -}; - -/** - * Renders a region. - * @param stage The konva stage - * @param entity The regional guidance layer state - * @param globalMaskLayerOpacity The global mask layer opacity - * @param tool The current tool - * @param onPosChanged Callback for when the layer's position changes - */ -export const renderRegion = ( - manager: KonvaNodeManager, - entity: RegionEntity, - globalMaskLayerOpacity: number, - tool: Tool, - selectedEntityIdentifier: CanvasEntityIdentifier | null, - onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void -): void => { - const adapter = getRegion(manager, entity, onPosChanged); - - // Update the layer's position and listening state - adapter.konvaLayer.setAttrs({ - listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events - x: Math.floor(entity.x), - y: Math.floor(entity.y), - }); - - // Convert the color to a string, stripping the alpha - the object group will handle opacity. - const rgbColor = rgbColorToString(entity.fill); - - // We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required. - let groupNeedsCache = false; - - const objectIds = entity.objects.map(mapId); - // Destroy any objects that are no longer in state - for (const objectRecord of adapter.getAll()) { - if (!objectIds.includes(objectRecord.id)) { - adapter.destroy(objectRecord.id); - groupNeedsCache = true; - } + destroy(): void { + this.konvaLayer.destroy(); } - for (const obj of entity.objects) { - if (obj.type === 'brush_line') { - const objectRecord = getBrushLine(adapter, obj, RG_LAYER_BRUSH_LINE_NAME); + async render( + regionState: RegionEntity, + selectedTool: Tool, + selectedEntityIdentifier: CanvasEntityIdentifier | null, + maskOpacity: number + ) { + // Update the layer's position and listening state + this.konvaLayer.setAttrs({ + listening: selectedTool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events + x: Math.floor(regionState.x), + y: Math.floor(regionState.y), + }); - // Only update the points if they have changed. The point values are never mutated, they are only added to the - // array, so checking the length is sufficient to determine if we need to re-cache. - if (objectRecord.konvaLine.points().length !== obj.points.length) { - objectRecord.konvaLine.points(obj.points); - groupNeedsCache = true; - } - // Only update the color if it has changed. - if (objectRecord.konvaLine.stroke() !== rgbColor) { - objectRecord.konvaLine.stroke(rgbColor); - groupNeedsCache = true; - } - } else if (obj.type === 'eraser_line') { - const objectRecord = getEraserLine(adapter, obj, RG_LAYER_ERASER_LINE_NAME); + // Convert the color to a string, stripping the alpha - the object group will handle opacity. + const rgbColor = rgbColorToString(regionState.fill); - // Only update the points if they have changed. The point values are never mutated, they are only added to the - // array, so checking the length is sufficient to determine if we need to re-cache. - if (objectRecord.konvaLine.points().length !== obj.points.length) { - objectRecord.konvaLine.points(obj.points); - groupNeedsCache = true; - } - // Only update the color if it has changed. - if (objectRecord.konvaLine.stroke() !== rgbColor) { - objectRecord.konvaLine.stroke(rgbColor); - groupNeedsCache = true; - } - } else if (obj.type === 'rect_shape') { - const objectRecord = getRectShape(adapter, obj, RG_LAYER_RECT_SHAPE_NAME); + // We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required. + let groupNeedsCache = false; - // Only update the color if it has changed. - if (objectRecord.konvaRect.fill() !== rgbColor) { - objectRecord.konvaRect.fill(rgbColor); + const objectIds = regionState.objects.map(mapId); + // Destroy any objects that are no longer in state + for (const object of this.objects.values()) { + if (!objectIds.includes(object.id)) { + object.destroy(); groupNeedsCache = true; } } - } - - // Only update layer visibility if it has changed. - if (adapter.konvaLayer.visible() !== entity.isEnabled) { - adapter.konvaLayer.visible(entity.isEnabled); - groupNeedsCache = true; - } - if (adapter.konvaObjectGroup.getChildren().length === 0) { - // No objects - clear the cache to reset the previous pixel data - adapter.konvaObjectGroup.clearCache(); - return; - } - - const compositingRect = - adapter.konvaLayer.findOne(`.${COMPOSITING_RECT_NAME}`) ?? createCompositingRect(adapter.konvaLayer); - const isSelected = selectedEntityIdentifier?.id === entity.id; - - /** - * When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows - * shapes to render as a "raster" layer with all pixels drawn at the same color and opacity. - * - * Without this special handling, each shape is drawn individually with the given opacity, atop the other shapes. The - * effect is like if you have a Photoshop Group consisting of many shapes, each of which has the given opacity. - * Overlapping shapes will have their colors blended together, and the final color is the result of all the shapes. - * - * 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. - */ - if (isSelected && tool !== 'move') { - // We must clear the cache first so Konva will re-draw the group with the new compositing rect - if (adapter.konvaObjectGroup.isCached()) { - adapter.konvaObjectGroup.clearCache(); + for (const obj of regionState.objects) { + if (obj.type === 'brush_line') { + let brushLine = this.objects.get(obj.id); + assert(brushLine instanceof KonvaBrushLine || brushLine === undefined); + + if (!brushLine) { + brushLine = new KonvaBrushLine({ brushLine: obj }); + this.objects.set(brushLine.id, brushLine); + this.konvaLayer.add(brushLine.konvaLineGroup); + groupNeedsCache = true; + } + + if (obj.points.length !== brushLine.konvaLine.points().length) { + brushLine.konvaLine.points(obj.points); + groupNeedsCache = true; + } + } else if (obj.type === 'eraser_line') { + let eraserLine = this.objects.get(obj.id); + assert(eraserLine instanceof KonvaEraserLine || eraserLine === undefined); + + if (!eraserLine) { + eraserLine = new KonvaEraserLine({ eraserLine: obj }); + this.objects.set(eraserLine.id, eraserLine); + this.konvaLayer.add(eraserLine.konvaLineGroup); + groupNeedsCache = true; + } + + if (obj.points.length !== eraserLine.konvaLine.points().length) { + eraserLine.konvaLine.points(obj.points); + groupNeedsCache = true; + } + } else if (obj.type === 'rect_shape') { + let rect = this.objects.get(obj.id); + assert(rect instanceof KonvaRect || rect === undefined); + + if (!rect) { + rect = new KonvaRect({ rectShape: obj }); + this.objects.set(rect.id, rect); + this.konvaLayer.add(rect.konvaRect); + groupNeedsCache = true; + } + } } - // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work - adapter.konvaObjectGroup.opacity(1); - compositingRect.setAttrs({ - // The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already - ...(!entity.bboxNeedsUpdate && entity.bbox ? entity.bbox : getLayerBboxFast(adapter.konvaLayer)), - fill: rgbColor, - opacity: globalMaskLayerOpacity, - // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) - globalCompositeOperation: 'source-in', - visible: true, - // This rect must always be on top of all other shapes - zIndex: adapter.konvaObjectGroup.getChildren().length, - }); - } else { - // The compositing rect should only be shown when the layer is selected. - compositingRect.visible(false); - // Cache only if needed - or if we are on this code path and _don't_ have a cache - if (groupNeedsCache || !adapter.konvaObjectGroup.isCached()) { - adapter.konvaObjectGroup.cache(); + // Only update layer visibility if it has changed. + if (this.konvaLayer.visible() !== regionState.isEnabled) { + this.konvaLayer.visible(regionState.isEnabled); + groupNeedsCache = true; } - // Updating group opacity does not require re-caching - adapter.konvaObjectGroup.opacity(globalMaskLayerOpacity); - } - - // const bboxRect = - // regionMap.konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rg, regionMap.konvaLayer); - - // if (rg.bbox) { - // const active = !rg.bboxNeedsUpdate && isSelected && tool === 'move'; - // bboxRect.setAttrs({ - // visible: active, - // listening: active, - // x: rg.bbox.x, - // y: rg.bbox.y, - // width: rg.bbox.width, - // height: rg.bbox.height, - // stroke: isSelected ? BBOX_SELECTED_STROKE : '', - // }); - // } else { - // bboxRect.visible(false); - // } -}; - -/** - * Gets a function to render all regions. - * @param manager The konva node manager - * @returns A function to render all regions - */ -export const getRenderRegions = (manager: KonvaNodeManager) => { - const { getRegionsState, getMaskOpacity, getToolState, getSelectedEntity, onPosChanged } = manager.stateApi; - function renderRegions(): void { - const { entities } = getRegionsState(); - const maskOpacity = getMaskOpacity(); - const toolState = getToolState(); - const selectedEntity = getSelectedEntity(); + if (this.objects.size === 0) { + // No objects - clear the cache to reset the previous pixel data + this.konvaObjectGroup.clearCache(); + return; + } - // Destroy the konva nodes for nonexistent entities - for (const adapter of manager.getAll('regional_guidance')) { - if (!entities.find((rg) => rg.id === adapter.id)) { - manager.destroy(adapter.id); + const isSelected = selectedEntityIdentifier?.id === regionState.id; + + /** + * When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows + * shapes to render as a "raster" layer with all pixels drawn at the same color and opacity. + * + * Without this special handling, each shape is drawn individually with the given opacity, atop the other shapes. The + * effect is like if you have a Photoshop Group consisting of many shapes, each of which has the given opacity. + * Overlapping shapes will have their colors blended together, and the final color is the result of all the shapes. + * + * 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. + */ + if (isSelected && selectedTool !== 'move') { + // We must clear the cache first so Konva will re-draw the group with the new compositing rect + if (this.konvaObjectGroup.isCached()) { + this.konvaObjectGroup.clearCache(); + } + // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work + this.konvaObjectGroup.opacity(1); + + this.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 + ...(!regionState.bboxNeedsUpdate && regionState.bbox ? regionState.bbox : getLayerBboxFast(this.konvaLayer)), + fill: rgbColor, + opacity: maskOpacity, + // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) + globalCompositeOperation: 'source-in', + visible: true, + // This rect must always be on top of all other shapes + zIndex: this.objects.size + 1, + }); + } else { + // The compositing rect should only be shown when the layer is selected. + this.compositingRect.visible(false); + // Cache only if needed - or if we are on this code path and _don't_ have a cache + if (groupNeedsCache || !this.konvaObjectGroup.isCached()) { + this.konvaObjectGroup.cache(); } + // Updating group opacity does not require re-caching + this.konvaObjectGroup.opacity(maskOpacity); } - for (const entity of entities) { - renderRegion(manager, entity, maskOpacity, toolState.selected, selectedEntity, onPosChanged); - } + // const bboxRect = + // regionMap.konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rg, regionMap.konvaLayer); + // if (rg.bbox) { + // const active = !rg.bboxNeedsUpdate && isSelected && tool === 'move'; + // bboxRect.setAttrs({ + // visible: active, + // listening: active, + // x: rg.bbox.x, + // y: rg.bbox.y, + // width: rg.bbox.width, + // height: rg.bbox.height, + // stroke: isSelected ? BBOX_SELECTED_STROKE : '', + // }); + // } else { + // bboxRect.visible(false); + // } } - - return renderRegions; -}; +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts index 890c93a580f..dad2e4a51cb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -5,24 +5,9 @@ import { $isDebugging } from 'app/store/nanostores/isDebugging'; import type { RootState } from 'app/store/store'; import { setStageEventHandlers } from 'features/controlLayers/konva/events'; import { KonvaNodeManager, setNodeManager } from 'features/controlLayers/konva/nodeManager'; -import { getArrangeEntities } from 'features/controlLayers/konva/renderers/arrange'; -import { createBackgroundLayer, getRenderBackground } from 'features/controlLayers/konva/renderers/background'; +import { KonvaBackground } from 'features/controlLayers/konva/renderers/background'; import { updateBboxes } from 'features/controlLayers/konva/renderers/bbox'; -import { getRenderControlAdapters } from 'features/controlLayers/konva/renderers/controlAdapters'; -import { getRenderInpaintMask } from 'features/controlLayers/konva/renderers/inpaintMask'; -import { getRenderLayers } from 'features/controlLayers/konva/renderers/layers'; -import { - createBboxNodes, - createDocumentOverlay, - createPreviewLayer, - createToolPreviewNodes, - getRenderBbox, - getRenderDocumentOverlay, - getRenderToolPreview, -} from 'features/controlLayers/konva/renderers/preview'; -import { getRenderRegions } from 'features/controlLayers/konva/renderers/regions'; -import { getFitDocumentToStage, getFitStageToContainer } from 'features/controlLayers/konva/renderers/stage'; -import { createStagingArea, getRenderStagingArea } from 'features/controlLayers/konva/renderers/stagingArea'; +import { KonvaPreview } from 'features/controlLayers/konva/renderers/preview'; import { $stageAttrs, bboxChanged, @@ -299,23 +284,7 @@ export const initializeRenderer = ( spaceKey = val; }; - const manager = new KonvaNodeManager(stage, container); - setNodeManager(manager); - - manager.background = { layer: createBackgroundLayer() }; - manager.stage.add(manager.background.layer); - manager.preview = { - layer: createPreviewLayer(), - bbox: createBboxNodes(stage, getBbox, onBboxTransformed, $shift.get, $ctrl.get, $meta.get, $alt.get), - tool: createToolPreviewNodes(), - documentOverlay: createDocumentOverlay(), - stagingArea: createStagingArea(), - }; - manager.preview.layer.add(manager.preview.bbox.group); - manager.preview.layer.add(manager.preview.tool.group); - manager.preview.layer.add(manager.preview.documentOverlay.group); - manager.stage.add(manager.preview.layer); - manager.stateApi = { + const stateApi: KonvaNodeManager['stateApi'] = { // Read-only state getToolState, getSelectedEntity, @@ -365,6 +334,26 @@ export const initializeRenderer = ( onLayerImageCached, }; + const manager = new KonvaNodeManager(stage, container, stateApi); + setNodeManager(manager); + console.log(manager); + + manager.background = new KonvaBackground(); + manager.stage.add(manager.background.konvaLayer); + manager.preview = new KonvaPreview({ + stage, + getBbox, + onBboxTransformed, + getShiftKey: $shift.get, + getCtrlKey: $ctrl.get, + getMetaKey: $meta.get, + getAltKey: $alt.get, + }); + manager.preview.konvaLayer.add(manager.preview.bbox.group); + manager.preview.konvaLayer.add(manager.preview.tool.group); + manager.preview.konvaLayer.add(manager.preview.documentOverlay.group); + manager.stage.add(manager.preview.konvaLayer); + const cleanupListeners = setStageEventHandlers(manager); // Calculating bounding boxes is expensive, must be debounced to not block the UI thread during a user interaction. @@ -372,21 +361,6 @@ export const initializeRenderer = ( // the entire state over when needed. const debouncedUpdateBboxes = debounce(updateBboxes, 300); - manager.konvaApi = { - renderRegions: getRenderRegions(manager), - renderLayers: getRenderLayers(manager), - renderControlAdapters: getRenderControlAdapters(manager), - renderInpaintMask: getRenderInpaintMask(manager), - renderBbox: getRenderBbox(manager), - renderToolPreview: getRenderToolPreview(manager), - renderDocumentOverlay: getRenderDocumentOverlay(manager), - renderStagingArea: getRenderStagingArea(manager), - renderBackground: getRenderBackground(manager), - arrangeEntities: getArrangeEntities(manager), - fitDocumentToStage: getFitDocumentToStage(manager), - fitStageToContainer: getFitStageToContainer(manager), - }; - const renderCanvas = () => { canvasV2 = store.getState().canvasV2; @@ -404,7 +378,7 @@ export const initializeRenderer = ( canvasV2.tool.selected !== prevCanvasV2.tool.selected ) { logIfDebugging('Rendering layers'); - manager.konvaApi.renderLayers(); + manager.renderLayers(); } if ( @@ -414,7 +388,7 @@ export const initializeRenderer = ( canvasV2.tool.selected !== prevCanvasV2.tool.selected ) { logIfDebugging('Rendering regions'); - manager.konvaApi.renderRegions(); + manager.renderRegions(); } if ( @@ -424,22 +398,22 @@ export const initializeRenderer = ( canvasV2.tool.selected !== prevCanvasV2.tool.selected ) { logIfDebugging('Rendering inpaint mask'); - manager.konvaApi.renderInpaintMask(); + manager.renderInpaintMask(); } if (isFirstRender || canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities) { logIfDebugging('Rendering control adapters'); - manager.konvaApi.renderControlAdapters(); + manager.renderControlAdapters(); } if (isFirstRender || canvasV2.document !== prevCanvasV2.document) { logIfDebugging('Rendering document bounds overlay'); - manager.konvaApi.renderDocumentOverlay(); + manager.renderDocumentOverlay(); } if (isFirstRender || canvasV2.bbox !== prevCanvasV2.bbox || canvasV2.tool.selected !== prevCanvasV2.tool.selected) { logIfDebugging('Rendering generation bbox'); - manager.konvaApi.renderBbox(); + manager.renderBbox(); } if ( @@ -459,7 +433,7 @@ export const initializeRenderer = ( canvasV2.regions.entities !== prevCanvasV2.regions.entities ) { logIfDebugging('Arranging entities'); - manager.konvaApi.arrangeEntities(); + manager.arrangeEntities(); } prevCanvasV2 = canvasV2; @@ -473,16 +447,16 @@ export const initializeRenderer = ( // We can use a resize observer to ensure the stage always fits the container. We also need to re-render the bg and // document bounds overlay when the stage is resized. - const resizeObserver = new ResizeObserver(manager.konvaApi.fitStageToContainer); + const resizeObserver = new ResizeObserver(manager.fitStageToContainer); resizeObserver.observe(container); - manager.konvaApi.fitStageToContainer(); + manager.fitStageToContainer(); const unsubscribeRenderer = subscribe(renderCanvas); logIfDebugging('First render of konva stage'); // On first render, the document should be fit to the stage. - manager.konvaApi.fitDocumentToStage(); - manager.konvaApi.renderToolPreview(); + manager.fitDocumentToStage(); + manager.renderToolPreview(); renderCanvas(); return () => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stage.ts index d02ecd485a4..70b05cf104a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stage.ts @@ -45,7 +45,7 @@ export const getFitStageToContainer = (manager: KonvaNodeManager) => { scale: stage.scaleX(), }); manager.konvaApi.renderBackground(); - manager.konvaApi.renderDocumentOverlay(); + manager.renderDocumentOverlay(); } return fitStageToContainer; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index 49aa7612460..54e73bc582d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -2,7 +2,7 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; import type { CanvasV2State, CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types'; -import { imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/types'; +import { DEFAULT_RGBA_COLOR, imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas'; import type { IRect } from 'konva/lib/types'; @@ -309,7 +309,7 @@ export const regionsReducers = { }, rgBrushLineAdded: { reducer: (state, action: PayloadAction) => { - const { id, points, lineId, color, width, clip } = action.payload; + const { id, points, lineId, width, clip } = action.payload; const rg = selectRG(state, id); if (!rg) { return; @@ -319,7 +319,7 @@ export const regionsReducers = { type: 'brush_line', points, strokeWidth: width, - color, + color: DEFAULT_RGBA_COLOR, clip, }); rg.bboxNeedsUpdate = true; @@ -366,7 +366,7 @@ export const regionsReducers = { }, rgRectAdded: { reducer: (state, action: PayloadAction) => { - const { id, rect, rectId, color } = action.payload; + const { id, rect, rectId } = action.payload; if (rect.height === 0 || rect.width === 0) { // Ignore zero-area rectangles return; @@ -379,7 +379,7 @@ export const regionsReducers = { type: 'rect_shape', id: getRectShapeId(id, rectId), ...rect, - color, + color: DEFAULT_RGBA_COLOR, }); rg.bboxNeedsUpdate = true; rg.imageCache = null; From e084655e694994e0d76a784c471a9855e1b97dee Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 26 Jun 2024 17:28:13 +1000 Subject: [PATCH 126/678] feat(ui): consolidate konva API --- .../features/controlLayers/konva/events.ts | 9 +- .../controlLayers/konva/nodeManager.ts | 90 +- .../controlLayers/konva/renderers/arrange.ts | 31 - .../konva/renderers/background.ts | 95 +- .../konva/renderers/controlAdapters.ts | 2 +- .../konva/renderers/inpaintMask.ts | 2 +- .../controlLayers/konva/renderers/layers.ts | 2 +- .../controlLayers/konva/renderers/preview.ts | 1056 ++++++++--------- .../controlLayers/konva/renderers/regions.ts | 2 +- .../controlLayers/konva/renderers/renderer.ts | 27 +- .../controlLayers/konva/renderers/stage.ts | 52 - 11 files changed, 524 insertions(+), 844 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts delete mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/renderers/stage.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 97e887067a5..c0901e4f0dd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -467,7 +467,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) = stage.position(newPos); setStageAttrs({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale }); manager.renderBackground(); - manager.renderDocumentOverlay(); + manager.renderDocumentSizeOverlay(); } } manager.renderToolPreview(); @@ -483,7 +483,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) = scale: stage.scaleX(), }); manager.renderBackground(); - manager.renderDocumentOverlay(); + manager.renderDocumentSizeOverlay(); manager.renderToolPreview(); }); @@ -518,10 +518,9 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) = setTool('view'); setSpaceKey(true); } else if (e.key === 'r') { - manager.fitDocumentToStage(); - manager.renderToolPreview(); + manager.fitDocument(); manager.renderBackground(); - manager.renderDocumentOverlay(); + manager.renderDocumentSizeOverlay(); } manager.renderToolPreview(); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts index 56c26d2cb30..4f0b043bb45 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts @@ -1,7 +1,6 @@ import { getImageDataTransparency } from 'common/util/arrayBuffer'; -import { DOCUMENT_FIT_PADDING_PX } from 'features/controlLayers/konva/constants'; -import { KonvaBackground } from 'features/controlLayers/konva/renderers/background'; -import { KonvaPreview } from 'features/controlLayers/konva/renderers/preview'; +import { CanvasBackground } from 'features/controlLayers/konva/renderers/background'; +import { CanvasPreview } from 'features/controlLayers/konva/renderers/preview'; import { konvaNodeToBlob, konvaNodeToImageData, previewBlob } from 'features/controlLayers/konva/util'; import type { BrushLineAddedArg, @@ -20,14 +19,15 @@ import type { import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers'; import type Konva from 'konva'; import type { Vector2d } from 'konva/lib/types'; +import { atom } from 'nanostores'; import { getImageDTO as defaultGetImageDTO, uploadImage as defaultUploadImage } from 'services/api/endpoints/images'; import type { ImageCategory, ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; -import { KonvaControlAdapter } from './renderers/controlAdapters'; -import { KonvaInpaintMask } from './renderers/inpaintMask'; -import { KonvaLayerAdapter } from './renderers/layers'; -import { KonvaRegion } from './renderers/regions'; +import { CanvasControlAdapter } from './renderers/controlAdapters'; +import { CanvasInpaintMask } from './renderers/inpaintMask'; +import { CanvasLayer } from './renderers/layers'; +import { CanvasRegion } from './renderers/regions'; export type StateApi = { getToolState: () => CanvasV2State['tool']; @@ -90,17 +90,27 @@ type Util = { getGenerationMode: () => GenerationMode; }; +const $nodeManager = atom(null); +export function getNodeManager() { + const nodeManager = $nodeManager.get(); + assert(nodeManager !== null, 'Node manager not initialized'); + return nodeManager; +} +export function setNodeManager(nodeManager: KonvaNodeManager) { + $nodeManager.set(nodeManager); +} + export class KonvaNodeManager { stage: Konva.Stage; container: HTMLDivElement; - controlAdapters: Map; - layers: Map; - regions: Map; - inpaintMask: KonvaInpaintMask | null; + controlAdapters: Map; + layers: Map; + regions: Map; + inpaintMask: CanvasInpaintMask | null; util: Util; stateApi: StateApi; - preview: KonvaPreview; - background: KonvaBackground; + preview: CanvasPreview; + background: CanvasBackground; constructor( stage: Konva.Stage, @@ -122,7 +132,8 @@ export class KonvaNodeManager { getCompositeLayerStageClone: this._getCompositeLayerStageClone.bind(this), getGenerationMode: this._getGenerationMode.bind(this), }; - this.preview = new KonvaPreview( + + this.preview = new CanvasPreview( this.stage, this.stateApi.getBbox, this.stateApi.onBboxTransformed, @@ -131,7 +142,11 @@ export class KonvaNodeManager { this.stateApi.getMetaKey, this.stateApi.getAltKey ); - this.background = new KonvaBackground(); + this.stage.add(this.preview.konvaLayer); + + this.background = new CanvasBackground(); + this.stage.add(this.background.konvaLayer); + this.layers = new Map(); this.regions = new Map(); this.controlAdapters = new Map(); @@ -152,7 +167,7 @@ export class KonvaNodeManager { for (const entity of entities) { let adapter = this.layers.get(entity.id); if (!adapter) { - adapter = new KonvaLayerAdapter(entity, this.stateApi.onPosChanged); + adapter = new CanvasLayer(entity, this.stateApi.onPosChanged); this.layers.set(adapter.id, adapter); this.stage.add(adapter.konvaLayer); } @@ -177,7 +192,7 @@ export class KonvaNodeManager { for (const entity of entities) { let adapter = this.regions.get(entity.id); if (!adapter) { - adapter = new KonvaRegion(entity, this.stateApi.onPosChanged); + adapter = new CanvasRegion(entity, this.stateApi.onPosChanged); this.regions.set(adapter.id, adapter); this.stage.add(adapter.konvaLayer); } @@ -188,7 +203,7 @@ export class KonvaNodeManager { renderInpaintMask() { const inpaintMaskState = this.stateApi.getInpaintMaskState(); if (!this.inpaintMask) { - this.inpaintMask = new KonvaInpaintMask(inpaintMaskState, this.stateApi.onPosChanged); + this.inpaintMask = new CanvasInpaintMask(inpaintMaskState, this.stateApi.onPosChanged); this.stage.add(this.inpaintMask.konvaLayer); } const toolState = this.stateApi.getToolState(); @@ -211,7 +226,7 @@ export class KonvaNodeManager { for (const entity of entities) { let adapter = this.controlAdapters.get(entity.id); if (!adapter) { - adapter = new KonvaControlAdapter(entity); + adapter = new CanvasControlAdapter(entity); this.controlAdapters.set(adapter.id, adapter); this.stage.add(adapter.konvaLayer); } @@ -239,18 +254,18 @@ export class KonvaNodeManager { this.preview.konvaLayer.zIndex(++zIndex); } - renderDocumentOverlay() { - this.preview.renderDocumentOverlay(this.stage, this.stateApi.getDocument()); + renderDocumentSizeOverlay() { + this.preview.documentSizeOverlay.render(this.stage, this.stateApi.getDocument()); } renderBbox() { - this.preview.renderBbox(this.stateApi.getBbox(), this.stateApi.getToolState()); + this.preview.bbox.render(this.stateApi.getBbox(), this.stateApi.getToolState()); } renderToolPreview() { - this.preview.renderToolPreview( + this.preview.tool.render( this.stage, - 1, + 1, // TODO(psyche): this should be renderable entity count this.stateApi.getToolState(), this.stateApi.getCurrentFill(), this.stateApi.getSelectedEntity(), @@ -261,22 +276,15 @@ export class KonvaNodeManager { ); } - fitDocumentToStage(): void { - const { getDocument, setStageAttrs } = this.stateApi; - const document = getDocument(); - // Fit & center the document on the stage - const width = this.stage.width(); - const height = this.stage.height(); - const docWidthWithBuffer = document.width + DOCUMENT_FIT_PADDING_PX * 2; - const docHeightWithBuffer = document.height + DOCUMENT_FIT_PADDING_PX * 2; - const scale = Math.min(Math.min(width / docWidthWithBuffer, height / docHeightWithBuffer), 1); - const x = (width - docWidthWithBuffer * scale) / 2 + DOCUMENT_FIT_PADDING_PX * scale; - const y = (height - docHeightWithBuffer * scale) / 2 + DOCUMENT_FIT_PADDING_PX * scale; - this.stage.setAttrs({ x, y, width, height, scaleX: scale, scaleY: scale }); - setStageAttrs({ x, y, width, height, scale }); + renderBackground() { + this.background.renderBackground(this.stage); + } + + fitDocument() { + this.preview.documentSizeOverlay.fitToStage(this.stage, this.stateApi.getDocument(), this.stateApi.setStageAttrs); } - fitStageToContainer(): void { + fitStageToContainer() { this.stage.width(this.container.offsetWidth); this.stage.height(this.container.offsetHeight); this.stateApi.setStageAttrs({ @@ -287,11 +295,7 @@ export class KonvaNodeManager { scale: this.stage.scaleX(), }); this.renderBackground(); - this.renderDocumentOverlay(); - } - - renderBackground() { - this.background.renderBackground(this.stage); + this.renderDocumentSizeOverlay(); } _getMaskLayerClone(): Konva.Layer { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts deleted file mode 100644 index dea5aba2a3d..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/arrange.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; - -/** - * Gets a function to arrange the entities in the konva stage. - * @param manager The konva node manager - * @returns An arrange entities function - */ -export const getArrangeEntities = (manager: KonvaNodeManager) => { - const { getLayersState, getControlAdaptersState, getRegionsState } = manager.stateApi; - - function arrangeEntities(): void { - const layers = getLayersState().entities; - const controlAdapters = getControlAdaptersState().entities; - const regions = getRegionsState().entities; - let zIndex = 0; - manager.background.layer.zIndex(++zIndex); - for (const layer of layers) { - manager.get(layer.id)?.konvaLayer.zIndex(++zIndex); - } - for (const ca of controlAdapters) { - manager.get(ca.id)?.konvaLayer.zIndex(++zIndex); - } - for (const rg of regions) { - manager.get(rg.id)?.konvaLayer.zIndex(++zIndex); - } - manager.get('inpaint_mask')?.konvaLayer.zIndex(++zIndex); - manager.preview.konvaLayer.zIndex(++zIndex); - } - - return arrangeEntities; -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts index 3e1dd6de064..a4d75214636 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts @@ -1,6 +1,4 @@ import { getArbitraryBaseColor } from '@invoke-ai/ui-library'; -import { BACKGROUND_LAYER_ID } from 'features/controlLayers/konva/naming'; -import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import Konva from 'konva'; const baseGridLineColor = getArbitraryBaseColor(27); @@ -30,98 +28,7 @@ const getGridSpacing = (scale: number): number => { return 256; }; -/** - * Creates the background konva layer. - * @returns The background konva layer - */ -export const createBackgroundLayer = (): Konva.Layer => new Konva.Layer({ id: BACKGROUND_LAYER_ID, listening: false }); - -/** - * Gets a render function for the background layer. - * @param manager The konva node manager - * @returns A function to render the background grid - */ -export const getRenderBackground = (manager: KonvaNodeManager) => { - function renderBackground(): void { - const background = manager.background.layer; - background.zIndex(0); - const scale = manager.stage.scaleX(); - const gridSpacing = getGridSpacing(scale); - const x = manager.stage.x(); - const y = manager.stage.y(); - const width = manager.stage.width(); - const height = manager.stage.height(); - const stageRect = { - x1: 0, - y1: 0, - x2: width, - y2: height, - }; - - const gridOffset = { - x: Math.ceil(x / scale / gridSpacing) * gridSpacing, - y: Math.ceil(y / scale / gridSpacing) * gridSpacing, - }; - - const gridRect = { - x1: -gridOffset.x, - y1: -gridOffset.y, - x2: width / scale - gridOffset.x + gridSpacing, - y2: height / scale - gridOffset.y + gridSpacing, - }; - - const gridFullRect = { - x1: Math.min(stageRect.x1, gridRect.x1), - y1: Math.min(stageRect.y1, gridRect.y1), - x2: Math.max(stageRect.x2, gridRect.x2), - y2: Math.max(stageRect.y2, gridRect.y2), - }; - - // find the x & y size of the grid - const xSize = gridFullRect.x2 - gridFullRect.x1; - const ySize = gridFullRect.y2 - gridFullRect.y1; - // compute the number of steps required on each axis. - const xSteps = Math.round(xSize / gridSpacing) + 1; - const ySteps = Math.round(ySize / gridSpacing) + 1; - - const strokeWidth = 1 / scale; - let _x = 0; - let _y = 0; - - background.destroyChildren(); - - for (let i = 0; i < xSteps; i++) { - _x = gridFullRect.x1 + i * gridSpacing; - background.add( - new Konva.Line({ - x: _x, - y: gridFullRect.y1, - points: [0, 0, 0, ySize], - stroke: _x % 64 ? fineGridLineColor : baseGridLineColor, - strokeWidth, - listening: false, - }) - ); - } - for (let i = 0; i < ySteps; i++) { - _y = gridFullRect.y1 + i * gridSpacing; - background.add( - new Konva.Line({ - x: gridFullRect.x1, - y: _y, - points: [0, 0, xSize, 0], - stroke: _y % 64 ? fineGridLineColor : baseGridLineColor, - strokeWidth, - listening: false, - }) - ); - } - } - - return renderBackground; -}; - -export class KonvaBackground { +export class CanvasBackground { konvaLayer: Konva.Layer; constructor() { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts index 81c392cb9ed..a63ff802697 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts @@ -7,7 +7,7 @@ import { v4 as uuidv4 } from 'uuid'; import { KonvaImage } from './objects'; -export class KonvaControlAdapter { +export class CanvasControlAdapter { id: string; konvaLayer: Konva.Layer; konvaObjectGroup: Konva.Group; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts index 780121b20a4..86639898043 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts @@ -9,7 +9,7 @@ import Konva from 'konva'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; -export class KonvaInpaintMask { +export class CanvasInpaintMask { id: string; konvaLayer: Konva.Layer; konvaObjectGroup: Konva.Group; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts index d034078d64d..ad138a66ab4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -7,7 +7,7 @@ import Konva from 'konva'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; -export class KonvaLayerAdapter { +export class CanvasLayer { id: string; konvaLayer: Konva.Layer; konvaObjectGroup: Konva.Group; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts index 5605b4f40a9..798052c7a10 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts @@ -5,491 +5,48 @@ import { BRUSH_BORDER_INNER_COLOR, BRUSH_BORDER_OUTER_COLOR, BRUSH_ERASER_BORDER_WIDTH, + DOCUMENT_FIT_PADDING_PX, } from 'features/controlLayers/konva/constants'; import { - PREVIEW_BRUSH_BORDER_INNER_ID, - PREVIEW_BRUSH_BORDER_OUTER_ID, - PREVIEW_BRUSH_FILL_ID, - PREVIEW_BRUSH_GROUP_ID, PREVIEW_GENERATION_BBOX_DUMMY_RECT, PREVIEW_GENERATION_BBOX_GROUP, PREVIEW_GENERATION_BBOX_TRANSFORMER, - PREVIEW_LAYER_ID, PREVIEW_RECT_ID, - PREVIEW_TOOL_GROUP_ID, } from 'features/controlLayers/konva/naming'; -import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; -import { createImageObjectGroup, updateImageSource } from 'features/controlLayers/konva/renderers/objects'; -import type { CanvasEntity, CanvasV2State, Position, RgbaColor } from 'features/controlLayers/store/types'; -import { imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/types'; +import { KonvaImage } from 'features/controlLayers/konva/renderers/objects'; +import type { CanvasEntity, CanvasV2State, Position, RgbaColor, StageAttrs } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; import { atom } from 'nanostores'; import { assert } from 'tsafe'; -/** - * Creates the konva preview layer. - * @returns The konva preview layer - */ -export const createPreviewLayer = (): Konva.Layer => new Konva.Layer({ id: PREVIEW_LAYER_ID, listening: true }); - -/** - * Creates the bbox konva nodes. - * @param stage The konva stage - * @param getBbox A function to get the bbox - * @param onBboxTransformed A callback for when the bbox is transformed - * @param getShiftKey A function to get the shift key state - * @param getCtrlKey A function to get the ctrl key state - * @param getMetaKey A function to get the meta key state - * @param getAltKey A function to get the alt key state - * @returns The bbox nodes - */ -export const createBboxNodes = ( - stage: Konva.Stage, - getBbox: () => IRect, - onBboxTransformed: (bbox: IRect) => void, - getShiftKey: () => boolean, - getCtrlKey: () => boolean, - getMetaKey: () => boolean, - getAltKey: () => boolean -): { group: Konva.Group; rect: Konva.Rect; transformer: Konva.Transformer } => { - // Create a stash to hold onto the last aspect ratio of the bbox - this allows for locking the aspect ratio when - // transforming the bbox. - const bbox = getBbox(); - const $aspectRatioBuffer = atom(bbox.width / bbox.height); - - // Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully - // transparent rect for this purpose. - const group = new Konva.Group({ id: PREVIEW_GENERATION_BBOX_GROUP, listening: false }); - const rect = new Konva.Rect({ - id: PREVIEW_GENERATION_BBOX_DUMMY_RECT, - listening: false, - strokeEnabled: false, - draggable: true, - ...getBbox(), - }); - rect.on('dragmove', () => { - const gridSize = getCtrlKey() || getMetaKey() ? 8 : 64; - const oldBbox = getBbox(); - const newBbox: IRect = { - ...oldBbox, - x: roundToMultiple(rect.x(), gridSize), - y: roundToMultiple(rect.y(), gridSize), - }; - rect.setAttrs(newBbox); - if (oldBbox.x !== newBbox.x || oldBbox.y !== newBbox.y) { - onBboxTransformed(newBbox); - } - }); - const transformer = new Konva.Transformer({ - id: PREVIEW_GENERATION_BBOX_TRANSFORMER, - borderDash: [5, 5], - borderStroke: 'rgba(212,216,234,1)', - borderEnabled: true, - rotateEnabled: false, - keepRatio: false, - ignoreStroke: true, - listening: false, - flipEnabled: false, - anchorFill: 'rgba(212,216,234,1)', - anchorStroke: 'rgb(42,42,42)', - anchorSize: 12, - anchorCornerRadius: 3, - shiftBehavior: 'none', // we will implement our own shift behavior - centeredScaling: false, - anchorStyleFunc: (anchor) => { - // Make the x/y resize anchors little bars - if (anchor.hasName('top-center') || anchor.hasName('bottom-center')) { - anchor.height(8); - anchor.offsetY(4); - anchor.width(30); - anchor.offsetX(15); - } - if (anchor.hasName('middle-left') || anchor.hasName('middle-right')) { - anchor.height(30); - anchor.offsetY(15); - anchor.width(8); - anchor.offsetX(4); - } - }, - anchorDragBoundFunc: (_oldAbsPos, newAbsPos) => { - // This function works with absolute position - that is, a position in "physical" pixels on the screen, as opposed - // to konva's internal coordinate system. - - // We need to snap the anchors to the grid. If the user is holding ctrl/meta, we use the finer 8px grid. - const gridSize = getCtrlKey() || getMetaKey() ? 8 : 64; - // Because we are working in absolute coordinates, we need to scale the grid size by the stage scale. - const scaledGridSize = gridSize * stage.scaleX(); - // To snap the anchor to the grid, we need to calculate an offset from the stage's absolute position. - const stageAbsPos = stage.getAbsolutePosition(); - // The offset is the remainder of the stage's absolute position divided by the scaled grid size. - const offsetX = stageAbsPos.x % scaledGridSize; - const offsetY = stageAbsPos.y % scaledGridSize; - // Finally, calculate the position by rounding to the grid and adding the offset. - return { - x: roundToMultiple(newAbsPos.x, scaledGridSize) + offsetX, - y: roundToMultiple(newAbsPos.y, scaledGridSize) + offsetY, - }; - }, - }); - - transformer.on('transform', () => { - // In the transform callback, we calculate the bbox's new dims and pos and update the konva object. - - // Some special handling is needed depending on the anchor being dragged. - const anchor = transformer.getActiveAnchor(); - if (!anchor) { - // Pretty sure we should always have an anchor here? - return; - } - - const alt = getAltKey(); - const ctrl = getCtrlKey(); - const meta = getMetaKey(); - const shift = getShiftKey(); - - // Grid size depends on the modifier keys - let gridSize = ctrl || meta ? 8 : 64; - - // Alt key indicates we are using centered scaling. We need to double the gride size used when calculating the - // new dimensions so that each size scales in the correct increments and doesn't mis-place the bbox. For example, if - // we snapped the width and height to 8px increments, the bbox would be mis-placed by 4px in the x and y axes. - // Doubling the grid size ensures the bbox's coords remain aligned to the 8px/64px grid. - if (getAltKey()) { - gridSize = gridSize * 2; - } - - // The coords should be correct per the anchorDragBoundFunc. - let x = rect.x(); - let y = rect.y(); - - // Konva transforms by scaling the dims, not directly changing width and height. At this point, the width and height - // *have not changed*, only the scale has changed. To get the final height, we need to scale the dims and then snap - // them to the grid. - let width = roundToMultipleMin(rect.width() * rect.scaleX(), gridSize); - let height = roundToMultipleMin(rect.height() * rect.scaleY(), gridSize); - - // If shift is held and we are resizing from a corner, retain aspect ratio - needs special handling. We skip this - // if alt/opt is held - this requires math too big for my brain. - if (shift && CORNER_ANCHORS.includes(anchor) && !alt) { - // Fit the bbox to the last aspect ratio - let fittedWidth = Math.sqrt(width * height * $aspectRatioBuffer.get()); - let fittedHeight = fittedWidth / $aspectRatioBuffer.get(); - fittedWidth = roundToMultipleMin(fittedWidth, gridSize); - fittedHeight = roundToMultipleMin(fittedHeight, gridSize); - - // We need to adjust the x and y coords to have the resize occur from the right origin. - if (anchor === 'top-left') { - // The transform origin is the bottom-right anchor. Both x and y need to be updated. - x = x - (fittedWidth - width); - y = y - (fittedHeight - height); - } - if (anchor === 'top-right') { - // The transform origin is the bottom-left anchor. Only y needs to be updated. - y = y - (fittedHeight - height); - } - if (anchor === 'bottom-left') { - // The transform origin is the top-right anchor. Only x needs to be updated. - x = x - (fittedWidth - width); - } - // Update the width and height to the fitted dims. - width = fittedWidth; - height = fittedHeight; - } - - const bbox = { - x: Math.round(x), - y: Math.round(y), - width, - height, - }; - - // Update the bboxRect's attrs directly with the new transform, and reset its scale to 1. - // TODO(psyche): In `renderBboxPreview()` we also call setAttrs, need to do it twice to ensure it renders correctly. - // Gotta be a way to avoid setting it twice... - rect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1 }); - - // Update the bbox in internal state. - onBboxTransformed(bbox); - - // Update the aspect ratio buffer whenever the shift key is not held - this allows for a nice UX where you can start - // a transform, get the right aspect ratio, then hold shift to lock it in. - if (!shift) { - $aspectRatioBuffer.set(bbox.width / bbox.height); - } - }); - - transformer.on('transformend', () => { - // Always update the aspect ratio buffer when the transform ends, so if the next transform starts with shift held, - // we have the correct aspect ratio to start from. - $aspectRatioBuffer.set(rect.width() / rect.height()); - }); - - // The transformer will always be transforming the dummy rect - transformer.nodes([rect]); - group.add(rect); - group.add(transformer); - return { group, rect, transformer }; -}; - -const ALL_ANCHORS: string[] = [ - 'top-left', - 'top-center', - 'top-right', - 'middle-right', - 'middle-left', - 'bottom-left', - 'bottom-center', - 'bottom-right', -]; -const CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right']; -const NO_ANCHORS: string[] = []; - -/** - * Gets the bbox render function. - * @param manager The konva node manager - * @returns The bbox render function - */ -export const getRenderBbox = (manager: KonvaNodeManager) => { - const { getBbox, getToolState } = manager.stateApi; - - return (): void => { - const bbox = getBbox(); - const toolState = getToolState(); - manager.preview.bbox.group.listening(toolState.selected === 'bbox'); - // This updates the bbox during transformation - manager.preview.bbox.rect.setAttrs({ - x: bbox.x, - y: bbox.y, - width: bbox.width, - height: bbox.height, - scaleX: 1, - scaleY: 1, - listening: toolState.selected === 'bbox', +export class CanvasDocumentSizeOverlay { + group: Konva.Group; + outerRect: Konva.Rect; + innerRect: Konva.Rect; + padding: number; + + constructor(padding?: number) { + this.padding = padding ?? DOCUMENT_FIT_PADDING_PX; + this.group = new Konva.Group({ id: 'document_overlay_group', listening: false }); + this.outerRect = new Konva.Rect({ + id: 'document_overlay_outer_rect', + listening: false, + fill: getArbitraryBaseColor(10), + opacity: 0.7, }); - manager.preview.bbox.transformer.setAttrs({ - listening: toolState.selected === 'bbox', - enabledAnchors: toolState.selected === 'bbox' ? ALL_ANCHORS : NO_ANCHORS, + this.innerRect = new Konva.Rect({ + id: 'document_overlay_inner_rect', + listening: false, + fill: 'white', + globalCompositeOperation: 'destination-out', }); - }; -}; - -/** - * Gets the tool preview konva nodes. - * @returns The tool preview konva nodes - */ -export const createToolPreviewNodes = (): KonvaNodeManager['preview']['tool'] => { - const group = new Konva.Group({ id: PREVIEW_TOOL_GROUP_ID }); - - // Create the brush preview group & circles - const brushGroup = new Konva.Group({ id: PREVIEW_BRUSH_GROUP_ID }); - const brushFill = new Konva.Circle({ - id: PREVIEW_BRUSH_FILL_ID, - listening: false, - strokeEnabled: false, - }); - brushGroup.add(brushFill); - const brushBorderInner = new Konva.Circle({ - id: PREVIEW_BRUSH_BORDER_INNER_ID, - listening: false, - stroke: BRUSH_BORDER_INNER_COLOR, - strokeWidth: BRUSH_ERASER_BORDER_WIDTH, - strokeEnabled: true, - }); - brushGroup.add(brushBorderInner); - const brushBorderOuter = new Konva.Circle({ - id: PREVIEW_BRUSH_BORDER_OUTER_ID, - listening: false, - stroke: BRUSH_BORDER_OUTER_COLOR, - strokeWidth: BRUSH_ERASER_BORDER_WIDTH, - strokeEnabled: true, - }); - brushGroup.add(brushBorderOuter); - - // Create the rect preview - this is a rectangle drawn from the last mouse down position to the current cursor position - const rect = new Konva.Rect({ - id: PREVIEW_RECT_ID, - listening: false, - strokeEnabled: false, - }); - - group.add(rect); - group.add(brushGroup); - return { - group, - brush: { - group: brushGroup, - fill: brushFill, - innerBorder: brushBorderInner, - outerBorder: brushBorderOuter, - }, - rect: { - rect, - }, - }; -}; - -/** - * Gets the tool preview (brush, eraser, rect) render function. - * @param manager The konva node manager - * @returns The tool preview render function - */ -export const getRenderToolPreview = (manager: KonvaNodeManager) => { - const { - getToolState, - getCurrentFill, - getSelectedEntity, - getLastCursorPos, - getLastMouseDownPos, - getIsDrawing, - getIsMouseDown, - } = manager.stateApi; - - return (): void => { - const stage = manager.stage; - const layerCount = manager.adapters.size; - const toolState = getToolState(); - const currentFill = getCurrentFill(); - const selectedEntity = getSelectedEntity(); - const cursorPos = getLastCursorPos(); - const lastMouseDownPos = getLastMouseDownPos(); - const isDrawing = getIsDrawing(); - const isMouseDown = getIsMouseDown(); - const tool = toolState.selected; - const isDrawableEntity = - selectedEntity?.type === 'regional_guidance' || - selectedEntity?.type === 'layer' || - selectedEntity?.type === 'inpaint_mask'; - - // Update the stage's pointer style - if (tool === 'view') { - // View gets a hand - stage.container().style.cursor = isMouseDown ? 'grabbing' : 'grab'; - } else if (layerCount === 0) { - // We have no layers, so we should not render any tool - stage.container().style.cursor = 'default'; - } else if (!isDrawableEntity) { - // Non-drawable layers don't have tools - stage.container().style.cursor = 'not-allowed'; - } else if (tool === 'move') { - // Move tool gets a pointer - stage.container().style.cursor = 'default'; - } else if (tool === 'rect') { - // Rect gets a crosshair - stage.container().style.cursor = 'crosshair'; - } else if (tool === 'brush' || tool === 'eraser') { - // Hide the native cursor and use the konva-rendered brush preview - stage.container().style.cursor = 'none'; - } else if (tool === 'bbox') { - stage.container().style.cursor = 'default'; - } - - stage.draggable(tool === 'view'); - - if (!cursorPos || layerCount === 0 || !isDrawableEntity) { - // We can bail early if the mouse isn't over the stage or there are no layers - manager.preview.tool.group.visible(false); - } else { - manager.preview.tool.group.visible(true); - - // No need to render the brush preview if the cursor position or color is missing - if (cursorPos && (tool === 'brush' || tool === 'eraser')) { - const scale = stage.scaleX(); - // Update the fill circle - const radius = (tool === 'brush' ? toolState.brush.width : toolState.eraser.width) / 2; - manager.preview.tool.brush.fill.setAttrs({ - x: cursorPos.x, - y: cursorPos.y, - radius, - fill: isDrawing ? '' : rgbaColorToString(currentFill), - globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out', - }); - - // Update the inner border of the brush preview - manager.preview.tool.brush.innerBorder.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius }); - - // Update the outer border of the brush preview - manager.preview.tool.brush.outerBorder.setAttrs({ - x: cursorPos.x, - y: cursorPos.y, - radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, - }); - - scaleToolPreview(manager, toolState); - - manager.preview.tool.brush.group.visible(true); - } else { - manager.preview.tool.brush.group.visible(false); - } + this.group.add(this.outerRect); + this.group.add(this.innerRect); + } - if (cursorPos && lastMouseDownPos && tool === 'rect') { - manager.preview.tool.rect.rect.setAttrs({ - x: Math.min(cursorPos.x, lastMouseDownPos.x), - y: Math.min(cursorPos.y, lastMouseDownPos.y), - width: Math.abs(cursorPos.x - lastMouseDownPos.x), - height: Math.abs(cursorPos.y - lastMouseDownPos.y), - fill: rgbaColorToString(currentFill), - visible: true, - }); - } else { - manager.preview.tool.rect.rect.visible(false); - } - } - }; -}; - -/** - * Scales the tool preview nodes. Depending on the scale of the stage, the border width and radius of the brush preview - * need to be adjusted. - * @param manager The konva node manager - * @param toolState The tool state - */ -const scaleToolPreview = (manager: KonvaNodeManager, toolState: CanvasV2State['tool']): void => { - const scale = manager.stage.scaleX(); - const radius = (toolState.selected === 'brush' ? toolState.brush.width : toolState.eraser.width) / 2; - manager.preview.tool.brush.innerBorder.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale); - manager.preview.tool.brush.outerBorder.setAttrs({ - strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, - radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, - }); -}; - -/** - * Creates the document overlay konva nodes. - * @returns The document overlay konva nodes - */ -export const createDocumentOverlay = (): KonvaNodeManager['preview']['documentOverlay'] => { - const group = new Konva.Group({ id: 'document_overlay_group', listening: false }); - const outerRect = new Konva.Rect({ - id: 'document_overlay_outer_rect', - listening: false, - fill: getArbitraryBaseColor(10), - opacity: 0.7, - }); - const innerRect = new Konva.Rect({ - id: 'document_overlay_inner_rect', - listening: false, - fill: 'white', - globalCompositeOperation: 'destination-out', - }); - group.add(outerRect); - group.add(innerRect); - return { group, innerRect, outerRect }; -}; - -/** - * Gets the document overlay render function. - * @param manager The konva node manager - * @returns The document overlay render function - */ -export const getRenderDocumentOverlay = (manager: KonvaNodeManager) => { - const { getDocument } = manager.stateApi; - - function renderDocumentOverlay(): void { - const document = getDocument(); - const stage = manager.stage; - - manager.preview.documentOverlay.group.zIndex(0); + render(stage: Konva.Stage, document: CanvasV2State['document']) { + this.group.zIndex(0); const x = stage.x(); const y = stage.y(); @@ -497,14 +54,14 @@ export const getRenderDocumentOverlay = (manager: KonvaNodeManager) => { const height = stage.height(); const scale = stage.scaleX(); - manager.preview.documentOverlay.outerRect.setAttrs({ + this.outerRect.setAttrs({ offsetX: x / scale, offsetY: y / scale, width: width / scale, height: height / scale, }); - manager.preview.documentOverlay.innerRect.setAttrs({ + this.innerRect.setAttrs({ x: 0, y: 0, width: document.width, @@ -512,119 +69,172 @@ export const getRenderDocumentOverlay = (manager: KonvaNodeManager) => { }); } - return renderDocumentOverlay; -}; + fitToStage(stage: Konva.Stage, document: CanvasV2State['document'], setStageAttrs: (attrs: StageAttrs) => void) { + // Fit & center the document on the stage + const width = stage.width(); + const height = stage.height(); + const docWidthWithBuffer = document.width + this.padding * 2; + const docHeightWithBuffer = document.height + this.padding * 2; + const scale = Math.min(Math.min(width / docWidthWithBuffer, height / docHeightWithBuffer), 1); + const x = (width - docWidthWithBuffer * scale) / 2 + this.padding * scale; + const y = (height - docHeightWithBuffer * scale) / 2 + this.padding * scale; + stage.setAttrs({ x, y, width, height, scaleX: scale, scaleY: scale }); + setStageAttrs({ x, y, width, height, scale }); + } +} -export const createStagingArea = (): KonvaNodeManager['preview']['stagingArea'] => { - const group = new Konva.Group({ id: 'staging_area_group', listening: false }); - return { group, image: null }; -}; +export class CanvasStagingArea { + group: Konva.Group; + image: KonvaImage | null; -export const getRenderStagingArea = async (manager: KonvaNodeManager) => { - const { getStagingAreaState } = manager.stateApi; - const stagingArea = getStagingAreaState(); + constructor() { + this.group = new Konva.Group({ listening: false }); + this.image = null; + } - if (!stagingArea || stagingArea.selectedImageIndex === null) { - if (manager.preview.stagingArea.image) { - manager.preview.stagingArea.image.konvaImageGroup.visible(false); - manager.preview.stagingArea.image = null; + async render(stagingArea: CanvasV2State['stagingArea']) { + if (!stagingArea || stagingArea.selectedImageIndex === null) { + if (this.image) { + this.image.destroy(); + this.image = null; + } + return; } - return; - } - if (stagingArea.selectedImageIndex) { - const imageDTO = stagingArea.images[stagingArea.selectedImageIndex]; - assert(imageDTO, 'Image must exist'); - if (manager.preview.stagingArea.image) { - if (manager.preview.stagingArea.image.imageName !== imageDTO.image_name) { - await updateImageSource({ - objectRecord: manager.preview.stagingArea.image, - image: imageDTOToImageWithDims(imageDTO), + if (stagingArea.selectedImageIndex) { + const imageDTO = stagingArea.images[stagingArea.selectedImageIndex]; + assert(imageDTO, 'Image must exist'); + if (this.image) { + if (this.image.imageName !== imageDTO.image_name) { + await this.image.updateImageSource(imageDTO.image_name); + } + } else { + const { image_name, width, height } = imageDTO; + this.image = new KonvaImage({ + imageObject: { + id: 'staging-area-image', + type: 'image', + x: 0, + y: 0, + width, + height, + filters: [], + image: { + name: image_name, + width, + height, + }, + }, }); } - } else { - manager.preview.stagingArea.image = await createImageObjectGroup({ - obj: imageDTOToImageObject(imageDTO), - name: imageDTO.image_name, - }); } } -}; +} -export class KonvaPreview { - konvaLayer: Konva.Layer; - bbox: { +export class CanvasTool { + group: Konva.Group; + brush: { group: Konva.Group; - rect: Konva.Rect; - transformer: Konva.Transformer; - }; - tool: { - group: Konva.Group; - brush: { - group: Konva.Group; - fill: Konva.Circle; - innerBorder: Konva.Circle; - outerBorder: Konva.Circle; - }; - rect: { - rect: Konva.Rect; - }; + fillCircle: Konva.Circle; + innerBorderCircle: Konva.Circle; + outerBorderCircle: Konva.Circle; }; - documentOverlay: { + eraser: { group: Konva.Group; - innerRect: Konva.Rect; - outerRect: Konva.Rect; + fillCircle: Konva.Circle; + innerBorderCircle: Konva.Circle; + outerBorderCircle: Konva.Circle; }; - stagingArea: { + rect: { group: Konva.Group; - // image: KonvaImage | null; + fillRect: Konva.Rect; }; - constructor( - stage: Konva.Stage, - getBbox: () => IRect, - onBboxTransformed: (bbox: IRect) => void, - getShiftKey: () => boolean, - getCtrlKey: () => boolean, - getMetaKey: () => boolean, - getAltKey: () => boolean - ) { - this.konvaLayer = createPreviewLayer(); - this.bbox = createBboxNodes(stage, getBbox, onBboxTransformed, getShiftKey, getCtrlKey, getMetaKey, getAltKey); - this.tool = createToolPreviewNodes(); - this.documentOverlay = createDocumentOverlay(); - this.stagingArea = createStagingArea(); + constructor() { + this.group = new Konva.Group(); + + // Create the brush preview group & circles + this.brush = { + group: new Konva.Group(), + fillCircle: new Konva.Circle({ + listening: false, + strokeEnabled: false, + }), + innerBorderCircle: new Konva.Circle({ + listening: false, + stroke: BRUSH_BORDER_INNER_COLOR, + strokeWidth: BRUSH_ERASER_BORDER_WIDTH, + strokeEnabled: true, + }), + outerBorderCircle: new Konva.Circle({ + listening: false, + stroke: BRUSH_BORDER_OUTER_COLOR, + strokeWidth: BRUSH_ERASER_BORDER_WIDTH, + strokeEnabled: true, + }), + }; + this.brush.group.add(this.brush.fillCircle); + this.brush.group.add(this.brush.innerBorderCircle); + this.brush.group.add(this.brush.outerBorderCircle); + this.group.add(this.brush.group); + + this.eraser = { + group: new Konva.Group(), + fillCircle: new Konva.Circle({ + listening: false, + strokeEnabled: false, + fill: 'white', + globalCompositeOperation: 'destination-out', + }), + innerBorderCircle: new Konva.Circle({ + listening: false, + stroke: BRUSH_BORDER_INNER_COLOR, + strokeWidth: BRUSH_ERASER_BORDER_WIDTH, + strokeEnabled: true, + }), + outerBorderCircle: new Konva.Circle({ + listening: false, + stroke: BRUSH_BORDER_OUTER_COLOR, + strokeWidth: BRUSH_ERASER_BORDER_WIDTH, + strokeEnabled: true, + }), + }; + this.eraser.group.add(this.eraser.fillCircle); + this.eraser.group.add(this.eraser.innerBorderCircle); + this.eraser.group.add(this.eraser.outerBorderCircle); + this.group.add(this.eraser.group); + + // Create the rect preview - this is a rectangle drawn from the last mouse down position to the current cursor position + this.rect = { + group: new Konva.Group(), + fillRect: new Konva.Rect({ + id: PREVIEW_RECT_ID, + listening: false, + strokeEnabled: false, + }), + }; + this.group.add(this.rect.group); } - renderBbox(bbox: CanvasV2State['bbox'], toolState: CanvasV2State['tool']) { - this.bbox.group.listening(toolState.selected === 'bbox'); - // This updates the bbox during transformation - this.bbox.rect.setAttrs({ - x: bbox.x, - y: bbox.y, - width: bbox.width, - height: bbox.height, - scaleX: 1, - scaleY: 1, - listening: toolState.selected === 'bbox', - }); - this.bbox.transformer.setAttrs({ - listening: toolState.selected === 'bbox', - enabledAnchors: toolState.selected === 'bbox' ? ALL_ANCHORS : NO_ANCHORS, + scaleTool(stage: Konva.Stage, toolState: CanvasV2State['tool']) { + const scale = stage.scaleX(); + + const brushRadius = toolState.brush.width / 2; + this.brush.innerBorderCircle.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale); + this.brush.outerBorderCircle.setAttrs({ + strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, + radius: brushRadius + BRUSH_ERASER_BORDER_WIDTH / scale, }); - } - scaleToolPreview(stage: Konva.Stage, toolState: CanvasV2State['tool']) { - const scale = stage.scaleX(); - const radius = (toolState.selected === 'brush' ? toolState.brush.width : toolState.eraser.width) / 2; - this.tool.brush.innerBorder.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale); - this.tool.brush.outerBorder.setAttrs({ + const eraserRadius = toolState.eraser.width / 2; + this.eraser.innerBorderCircle.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale); + this.eraser.outerBorderCircle.setAttrs({ strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, - radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, + radius: eraserRadius + BRUSH_ERASER_BORDER_WIDTH / scale, }); } - renderToolPreview( + render( stage: Konva.Stage, renderedEntityCount: number, toolState: CanvasV2State['tool'], @@ -668,42 +278,65 @@ export class KonvaPreview { if (!cursorPos || renderedEntityCount === 0 || !isDrawableEntity) { // We can bail early if the mouse isn't over the stage or there are no layers - this.tool.group.visible(false); + this.group.visible(false); } else { - this.tool.group.visible(true); + this.group.visible(true); // No need to render the brush preview if the cursor position or color is missing - if (cursorPos && (tool === 'brush' || tool === 'eraser')) { + if (cursorPos && tool === 'brush') { const scale = stage.scaleX(); // Update the fill circle - const radius = (tool === 'brush' ? toolState.brush.width : toolState.eraser.width) / 2; - this.tool.brush.fill.setAttrs({ + const radius = toolState.brush.width / 2; + this.brush.fillCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius, fill: isDrawing ? '' : rgbaColorToString(currentFill), - globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out', }); // Update the inner border of the brush preview - this.tool.brush.innerBorder.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius }); + this.brush.innerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius }); // Update the outer border of the brush preview - this.tool.brush.outerBorder.setAttrs({ + this.brush.outerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, }); - this.scaleToolPreview(stage, toolState); + this.scaleTool(stage, toolState); - this.tool.brush.group.visible(true); - } else { - this.tool.brush.group.visible(false); - } + this.brush.group.visible(true); + this.eraser.group.visible(false); + this.rect.group.visible(false); + } else if (cursorPos && tool === 'eraser') { + const scale = stage.scaleX(); + // Update the fill circle + const radius = toolState.eraser.width / 2; + this.eraser.fillCircle.setAttrs({ + x: cursorPos.x, + y: cursorPos.y, + radius, + fill: 'white', + }); - if (cursorPos && lastMouseDownPos && tool === 'rect') { - this.tool.rect.rect.setAttrs({ + // Update the inner border of the eraser preview + this.eraser.innerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius }); + + // Update the outer border of the eraser preview + this.eraser.outerBorderCircle.setAttrs({ + x: cursorPos.x, + y: cursorPos.y, + radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, + }); + + this.scaleTool(stage, toolState); + + this.brush.group.visible(false); + this.eraser.group.visible(true); + this.rect.group.visible(false); + } else if (cursorPos && lastMouseDownPos && tool === 'rect') { + this.rect.fillRect.setAttrs({ x: Math.min(cursorPos.x, lastMouseDownPos.x), y: Math.min(cursorPos.y, lastMouseDownPos.y), width: Math.abs(cursorPos.x - lastMouseDownPos.x), @@ -711,33 +344,270 @@ export class KonvaPreview { fill: rgbaColorToString(currentFill), visible: true, }); + this.brush.group.visible(false); + this.eraser.group.visible(false); + this.rect.group.visible(true); } else { - this.tool.rect.rect.visible(false); + this.brush.group.visible(false); + this.eraser.group.visible(false); + this.rect.group.visible(false); } } } +} - renderDocumentOverlay(stage: Konva.Stage, document: CanvasV2State['document']) { - this.documentOverlay.group.zIndex(0); +export class CanvasBbox { + group: Konva.Group; + rect: Konva.Rect; + transformer: Konva.Transformer; + + ALL_ANCHORS: string[] = [ + 'top-left', + 'top-center', + 'top-right', + 'middle-right', + 'middle-left', + 'bottom-left', + 'bottom-center', + 'bottom-right', + ]; + CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right']; + NO_ANCHORS: string[] = []; - const x = stage.x(); - const y = stage.y(); - const width = stage.width(); - const height = stage.height(); - const scale = stage.scaleX(); + constructor( + stage: Konva.Stage, + getBbox: () => IRect, + onBboxTransformed: (bbox: IRect) => void, + getShiftKey: () => boolean, + getCtrlKey: () => boolean, + getMetaKey: () => boolean, + getAltKey: () => boolean + ) { + // Create a stash to hold onto the last aspect ratio of the bbox - this allows for locking the aspect ratio when + // transforming the bbox. + const bbox = getBbox(); + const $aspectRatioBuffer = atom(bbox.width / bbox.height); + + // Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully + // transparent rect for this purpose. + this.group = new Konva.Group({ id: PREVIEW_GENERATION_BBOX_GROUP, listening: false }); + this.rect = new Konva.Rect({ + id: PREVIEW_GENERATION_BBOX_DUMMY_RECT, + listening: false, + strokeEnabled: false, + draggable: true, + ...getBbox(), + }); + this.rect.on('dragmove', () => { + const gridSize = getCtrlKey() || getMetaKey() ? 8 : 64; + const oldBbox = getBbox(); + const newBbox: IRect = { + ...oldBbox, + x: roundToMultiple(this.rect.x(), gridSize), + y: roundToMultiple(this.rect.y(), gridSize), + }; + this.rect.setAttrs(newBbox); + if (oldBbox.x !== newBbox.x || oldBbox.y !== newBbox.y) { + onBboxTransformed(newBbox); + } + }); - this.documentOverlay.outerRect.setAttrs({ - offsetX: x / scale, - offsetY: y / scale, - width: width / scale, - height: height / scale, + this.transformer = new Konva.Transformer({ + id: PREVIEW_GENERATION_BBOX_TRANSFORMER, + borderDash: [5, 5], + borderStroke: 'rgba(212,216,234,1)', + borderEnabled: true, + rotateEnabled: false, + keepRatio: false, + ignoreStroke: true, + listening: false, + flipEnabled: false, + anchorFill: 'rgba(212,216,234,1)', + anchorStroke: 'rgb(42,42,42)', + anchorSize: 12, + anchorCornerRadius: 3, + shiftBehavior: 'none', // we will implement our own shift behavior + centeredScaling: false, + anchorStyleFunc: (anchor) => { + // Make the x/y resize anchors little bars + if (anchor.hasName('top-center') || anchor.hasName('bottom-center')) { + anchor.height(8); + anchor.offsetY(4); + anchor.width(30); + anchor.offsetX(15); + } + if (anchor.hasName('middle-left') || anchor.hasName('middle-right')) { + anchor.height(30); + anchor.offsetY(15); + anchor.width(8); + anchor.offsetX(4); + } + }, + anchorDragBoundFunc: (_oldAbsPos, newAbsPos) => { + // This function works with absolute position - that is, a position in "physical" pixels on the screen, as opposed + // to konva's internal coordinate system. + + // We need to snap the anchors to the grid. If the user is holding ctrl/meta, we use the finer 8px grid. + const gridSize = getCtrlKey() || getMetaKey() ? 8 : 64; + // Because we are working in absolute coordinates, we need to scale the grid size by the stage scale. + const scaledGridSize = gridSize * stage.scaleX(); + // To snap the anchor to the grid, we need to calculate an offset from the stage's absolute position. + const stageAbsPos = stage.getAbsolutePosition(); + // The offset is the remainder of the stage's absolute position divided by the scaled grid size. + const offsetX = stageAbsPos.x % scaledGridSize; + const offsetY = stageAbsPos.y % scaledGridSize; + // Finally, calculate the position by rounding to the grid and adding the offset. + return { + x: roundToMultiple(newAbsPos.x, scaledGridSize) + offsetX, + y: roundToMultiple(newAbsPos.y, scaledGridSize) + offsetY, + }; + }, }); - this.documentOverlay.innerRect.setAttrs({ - x: 0, - y: 0, - width: document.width, - height: document.height, + this.transformer.on('transform', () => { + // In the transform callback, we calculate the bbox's new dims and pos and update the konva object. + + // Some special handling is needed depending on the anchor being dragged. + const anchor = this.transformer.getActiveAnchor(); + if (!anchor) { + // Pretty sure we should always have an anchor here? + return; + } + + const alt = getAltKey(); + const ctrl = getCtrlKey(); + const meta = getMetaKey(); + const shift = getShiftKey(); + + // Grid size depends on the modifier keys + let gridSize = ctrl || meta ? 8 : 64; + + // Alt key indicates we are using centered scaling. We need to double the gride size used when calculating the + // new dimensions so that each size scales in the correct increments and doesn't mis-place the bbox. For example, if + // we snapped the width and height to 8px increments, the bbox would be mis-placed by 4px in the x and y axes. + // Doubling the grid size ensures the bbox's coords remain aligned to the 8px/64px grid. + if (getAltKey()) { + gridSize = gridSize * 2; + } + + // The coords should be correct per the anchorDragBoundFunc. + let x = this.rect.x(); + let y = this.rect.y(); + + // Konva transforms by scaling the dims, not directly changing width and height. At this point, the width and height + // *have not changed*, only the scale has changed. To get the final height, we need to scale the dims and then snap + // them to the grid. + let width = roundToMultipleMin(this.rect.width() * this.rect.scaleX(), gridSize); + let height = roundToMultipleMin(this.rect.height() * this.rect.scaleY(), gridSize); + + // If shift is held and we are resizing from a corner, retain aspect ratio - needs special handling. We skip this + // if alt/opt is held - this requires math too big for my brain. + if (shift && this.CORNER_ANCHORS.includes(anchor) && !alt) { + // Fit the bbox to the last aspect ratio + let fittedWidth = Math.sqrt(width * height * $aspectRatioBuffer.get()); + let fittedHeight = fittedWidth / $aspectRatioBuffer.get(); + fittedWidth = roundToMultipleMin(fittedWidth, gridSize); + fittedHeight = roundToMultipleMin(fittedHeight, gridSize); + + // We need to adjust the x and y coords to have the resize occur from the right origin. + if (anchor === 'top-left') { + // The transform origin is the bottom-right anchor. Both x and y need to be updated. + x = x - (fittedWidth - width); + y = y - (fittedHeight - height); + } + if (anchor === 'top-right') { + // The transform origin is the bottom-left anchor. Only y needs to be updated. + y = y - (fittedHeight - height); + } + if (anchor === 'bottom-left') { + // The transform origin is the top-right anchor. Only x needs to be updated. + x = x - (fittedWidth - width); + } + // Update the width and height to the fitted dims. + width = fittedWidth; + height = fittedHeight; + } + + const bbox = { + x: Math.round(x), + y: Math.round(y), + width, + height, + }; + + // Update the bboxRect's attrs directly with the new transform, and reset its scale to 1. + // TODO(psyche): In `renderBboxPreview()` we also call setAttrs, need to do it twice to ensure it renders correctly. + // Gotta be a way to avoid setting it twice... + this.rect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1 }); + + // Update the bbox in internal state. + onBboxTransformed(bbox); + + // Update the aspect ratio buffer whenever the shift key is not held - this allows for a nice UX where you can start + // a transform, get the right aspect ratio, then hold shift to lock it in. + if (!shift) { + $aspectRatioBuffer.set(bbox.width / bbox.height); + } + }); + + this.transformer.on('transformend', () => { + // Always update the aspect ratio buffer when the transform ends, so if the next transform starts with shift held, + // we have the correct aspect ratio to start from. + $aspectRatioBuffer.set(this.rect.width() / this.rect.height()); + }); + + // The transformer will always be transforming the dummy rect + this.transformer.nodes([this.rect]); + this.group.add(this.rect); + this.group.add(this.transformer); + } + + render(bbox: CanvasV2State['bbox'], toolState: CanvasV2State['tool']) { + this.group.listening(toolState.selected === 'bbox'); + this.rect.setAttrs({ + x: bbox.x, + y: bbox.y, + width: bbox.width, + height: bbox.height, + scaleX: 1, + scaleY: 1, + listening: toolState.selected === 'bbox', + }); + this.transformer.setAttrs({ + listening: toolState.selected === 'bbox', + enabledAnchors: toolState.selected === 'bbox' ? this.ALL_ANCHORS : this.NO_ANCHORS, }); } } + +export class CanvasPreview { + konvaLayer: Konva.Layer; + tool: CanvasTool; + bbox: CanvasBbox; + documentSizeOverlay: CanvasDocumentSizeOverlay; + stagingArea: CanvasStagingArea; + + constructor( + stage: Konva.Stage, + getBbox: () => IRect, + onBboxTransformed: (bbox: IRect) => void, + getShiftKey: () => boolean, + getCtrlKey: () => boolean, + getMetaKey: () => boolean, + getAltKey: () => boolean + ) { + this.konvaLayer = new Konva.Layer({ listening: true }); + + this.bbox = new CanvasBbox(stage, getBbox, onBboxTransformed, getShiftKey, getCtrlKey, getMetaKey, getAltKey); + this.konvaLayer.add(this.bbox.group); + + this.tool = new CanvasTool(); + this.konvaLayer.add(this.tool.group); + + this.documentSizeOverlay = new CanvasDocumentSizeOverlay(); + this.konvaLayer.add(this.documentSizeOverlay.group); + + this.stagingArea = new CanvasStagingArea(); + this.konvaLayer.add(this.stagingArea.group); + } +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts index a7263df9f40..c715ae37818 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts @@ -9,7 +9,7 @@ import Konva from 'konva'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; -export class KonvaRegion { +export class CanvasRegion { id: string; konvaLayer: Konva.Layer; konvaObjectGroup: Konva.Group; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts index dad2e4a51cb..01d3e93d994 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -5,9 +5,7 @@ import { $isDebugging } from 'app/store/nanostores/isDebugging'; import type { RootState } from 'app/store/store'; import { setStageEventHandlers } from 'features/controlLayers/konva/events'; import { KonvaNodeManager, setNodeManager } from 'features/controlLayers/konva/nodeManager'; -import { KonvaBackground } from 'features/controlLayers/konva/renderers/background'; import { updateBboxes } from 'features/controlLayers/konva/renderers/bbox'; -import { KonvaPreview } from 'features/controlLayers/konva/renderers/preview'; import { $stageAttrs, bboxChanged, @@ -74,7 +72,7 @@ export const initializeRenderer = ( */ const logIfDebugging = (message: string) => { if ($isDebugging.get()) { - _log.trace(message); + _log.debug(message); } }; @@ -338,22 +336,6 @@ export const initializeRenderer = ( setNodeManager(manager); console.log(manager); - manager.background = new KonvaBackground(); - manager.stage.add(manager.background.konvaLayer); - manager.preview = new KonvaPreview({ - stage, - getBbox, - onBboxTransformed, - getShiftKey: $shift.get, - getCtrlKey: $ctrl.get, - getMetaKey: $meta.get, - getAltKey: $alt.get, - }); - manager.preview.konvaLayer.add(manager.preview.bbox.group); - manager.preview.konvaLayer.add(manager.preview.tool.group); - manager.preview.konvaLayer.add(manager.preview.documentOverlay.group); - manager.stage.add(manager.preview.konvaLayer); - const cleanupListeners = setStageEventHandlers(manager); // Calculating bounding boxes is expensive, must be debounced to not block the UI thread during a user interaction. @@ -408,7 +390,7 @@ export const initializeRenderer = ( if (isFirstRender || canvasV2.document !== prevCanvasV2.document) { logIfDebugging('Rendering document bounds overlay'); - manager.renderDocumentOverlay(); + manager.renderDocumentSizeOverlay(); } if (isFirstRender || canvasV2.bbox !== prevCanvasV2.bbox || canvasV2.tool.selected !== prevCanvasV2.tool.selected) { @@ -447,7 +429,7 @@ export const initializeRenderer = ( // We can use a resize observer to ensure the stage always fits the container. We also need to re-render the bg and // document bounds overlay when the stage is resized. - const resizeObserver = new ResizeObserver(manager.fitStageToContainer); + const resizeObserver = new ResizeObserver(manager.fitStageToContainer.bind(manager)); resizeObserver.observe(container); manager.fitStageToContainer(); @@ -455,7 +437,8 @@ export const initializeRenderer = ( logIfDebugging('First render of konva stage'); // On first render, the document should be fit to the stage. - manager.fitDocumentToStage(); + manager.renderDocumentSizeOverlay(); + manager.fitDocument(); manager.renderToolPreview(); renderCanvas(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stage.ts deleted file mode 100644 index 70b05cf104a..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stage.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { DOCUMENT_FIT_PADDING_PX } from 'features/controlLayers/konva/constants'; -import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; - -/** - * Gets a function to fit the document to the stage, resetting the stage scale to 100%. - * If the document is smaller than the stage, the stage scale is increased to fit the document. - * @param manager The konva node manager - * @returns A function to fit the document to the stage - */ -export const getFitDocumentToStage = (manager: KonvaNodeManager) => { - function fitDocumentToStage(): void { - const { getDocument, setStageAttrs } = manager.stateApi; - const document = getDocument(); - // Fit & center the document on the stage - const width = manager.stage.width(); - const height = manager.stage.height(); - const docWidthWithBuffer = document.width + DOCUMENT_FIT_PADDING_PX * 2; - const docHeightWithBuffer = document.height + DOCUMENT_FIT_PADDING_PX * 2; - const scale = Math.min(Math.min(width / docWidthWithBuffer, height / docHeightWithBuffer), 1); - const x = (width - docWidthWithBuffer * scale) / 2 + DOCUMENT_FIT_PADDING_PX * scale; - const y = (height - docHeightWithBuffer * scale) / 2 + DOCUMENT_FIT_PADDING_PX * scale; - manager.stage.setAttrs({ x, y, width, height, scaleX: scale, scaleY: scale }); - setStageAttrs({ x, y, width, height, scale }); - } - - return fitDocumentToStage; -}; - -/** - * Gets a function to fit the stage to its container element. Called during resize events. - * @param manager The konva node manager - * @returns A function to fit the stage to its container - */ -export const getFitStageToContainer = (manager: KonvaNodeManager) => { - const { stage, container } = manager; - const { setStageAttrs } = manager.stateApi; - function fitStageToContainer(): void { - stage.width(container.offsetWidth); - stage.height(container.offsetHeight); - setStageAttrs({ - x: stage.x(), - y: stage.y(), - width: stage.width(), - height: stage.height(), - scale: stage.scaleX(), - }); - manager.konvaApi.renderBackground(); - manager.renderDocumentOverlay(); - } - - return fitStageToContainer; -}; From 471ded85f74c21e44896f567d9fd680865c82403 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 26 Jun 2024 18:27:18 +1000 Subject: [PATCH 127/678] feat(ui): staging area barely works --- .../listeners/enqueueRequestedLinear.ts | 18 +++++---- .../controlLayers/konva/nodeManager.ts | 40 ++++++++++++------- .../controlLayers/konva/renderers/objects.ts | 2 +- .../controlLayers/konva/renderers/preview.ts | 32 +++++++-------- .../controlLayers/konva/renderers/renderer.ts | 7 +++- 5 files changed, 60 insertions(+), 39 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index bbac10e2a14..277e6b2206f 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -1,7 +1,7 @@ import { enqueueRequested } from 'app/store/actions'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { getNodeManager } from 'features/controlLayers/konva/nodeManager'; -import { stagingAreaInitialized } from 'features/controlLayers/store/canvasV2Slice'; +import { stagingAreaBatchIdAdded, stagingAreaInitialized } from 'features/controlLayers/store/canvasV2Slice'; import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph'; @@ -48,12 +48,16 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) // TODO(psyche): update the backend schema, this is always provided const batchId = enqueueResult.batch.batch_id; assert(batchId, 'No batch ID found in enqueue result'); - dispatch( - stagingAreaInitialized({ - batchIds: [batchId], - bbox: getState().canvasV2.bbox, - }) - ); + if (!state.canvasV2.stagingArea) { + dispatch( + stagingAreaInitialized({ + batchIds: [batchId], + bbox: state.canvasV2.bbox, + }) + ); + } else { + dispatch(stagingAreaBatchIdAdded({ batchId })); + } } finally { req.reset(); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts index 4f0b043bb45..73d22024063 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts @@ -1,6 +1,12 @@ import { getImageDataTransparency } from 'common/util/arrayBuffer'; import { CanvasBackground } from 'features/controlLayers/konva/renderers/background'; -import { CanvasPreview } from 'features/controlLayers/konva/renderers/preview'; +import { + CanvasBbox, + CanvasDocumentSizeOverlay, + CanvasPreview, + CanvasStagingArea, + CanvasTool, +} from 'features/controlLayers/konva/renderers/preview'; import { konvaNodeToBlob, konvaNodeToImageData, previewBlob } from 'features/controlLayers/konva/util'; import type { BrushLineAddedArg, @@ -106,7 +112,7 @@ export class KonvaNodeManager { controlAdapters: Map; layers: Map; regions: Map; - inpaintMask: CanvasInpaintMask | null; + inpaintMask: CanvasInpaintMask; util: Util; stateApi: StateApi; preview: CanvasPreview; @@ -134,23 +140,29 @@ export class KonvaNodeManager { }; this.preview = new CanvasPreview( - this.stage, - this.stateApi.getBbox, - this.stateApi.onBboxTransformed, - this.stateApi.getShiftKey, - this.stateApi.getCtrlKey, - this.stateApi.getMetaKey, - this.stateApi.getAltKey + new CanvasBbox( + this.stateApi.getBbox, + this.stateApi.onBboxTransformed, + this.stateApi.getShiftKey, + this.stateApi.getCtrlKey, + this.stateApi.getMetaKey, + this.stateApi.getAltKey + ), + new CanvasTool(), + new CanvasDocumentSizeOverlay(), + new CanvasStagingArea() ); this.stage.add(this.preview.konvaLayer); this.background = new CanvasBackground(); this.stage.add(this.background.konvaLayer); + this.inpaintMask = new CanvasInpaintMask(this.stateApi.getInpaintMaskState(), this.stateApi.onPosChanged); + this.stage.add(this.inpaintMask.konvaLayer); + this.layers = new Map(); this.regions = new Map(); this.controlAdapters = new Map(); - this.inpaintMask = null; } renderLayers() { @@ -202,10 +214,6 @@ export class KonvaNodeManager { renderInpaintMask() { const inpaintMaskState = this.stateApi.getInpaintMaskState(); - if (!this.inpaintMask) { - this.inpaintMask = new CanvasInpaintMask(inpaintMaskState, this.stateApi.onPosChanged); - this.stage.add(this.inpaintMask.konvaLayer); - } const toolState = this.stateApi.getToolState(); const selectedEntity = this.stateApi.getSelectedEntity(); const maskOpacity = this.stateApi.getMaskOpacity(); @@ -280,6 +288,10 @@ export class KonvaNodeManager { this.background.renderBackground(this.stage); } + renderStagingArea() { + this.preview.stagingArea.render(this.stateApi.getStagingAreaState()); + } + fitDocument() { this.preview.documentSizeOverlay.fitToStage(this.stage, this.stateApi.getDocument(), this.stateApi.setStageAttrs); } 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 bd540b1f203..8a6d81a35f8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -189,8 +189,8 @@ export class KonvaImage { image: imageEl, }); this.konvaImageGroup.add(this.konvaImage); - this.imageName = imageName; } + this.imageName = imageName; this.isLoading = false; this.isError = false; this.konvaPlaceholderGroup.visible(false); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts index 798052c7a10..c2ada34fc48 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts @@ -101,11 +101,11 @@ export class CanvasStagingArea { return; } - if (stagingArea.selectedImageIndex) { + if (stagingArea.selectedImageIndex !== null) { const imageDTO = stagingArea.images[stagingArea.selectedImageIndex]; assert(imageDTO, 'Image must exist'); if (this.image) { - if (this.image.imageName !== imageDTO.image_name) { + if (!this.image.isLoading && !this.image.isError && this.image.imageName !== imageDTO.image_name) { await this.image.updateImageSource(imageDTO.image_name); } } else { @@ -114,8 +114,8 @@ export class CanvasStagingArea { imageObject: { id: 'staging-area-image', type: 'image', - x: 0, - y: 0, + x: stagingArea.bbox.x, + y: stagingArea.bbox.y, width, height, filters: [], @@ -126,6 +126,8 @@ export class CanvasStagingArea { }, }, }); + this.group.add(this.image.konvaImageGroup); + await this.image.updateImageSource(imageDTO.image_name); } } } @@ -375,7 +377,6 @@ export class CanvasBbox { NO_ANCHORS: string[] = []; constructor( - stage: Konva.Stage, getBbox: () => IRect, onBboxTransformed: (bbox: IRect) => void, getShiftKey: () => boolean, @@ -446,6 +447,8 @@ export class CanvasBbox { anchorDragBoundFunc: (_oldAbsPos, newAbsPos) => { // This function works with absolute position - that is, a position in "physical" pixels on the screen, as opposed // to konva's internal coordinate system. + const stage = this.transformer.getStage(); + assert(stage, 'Stage must exist'); // We need to snap the anchors to the grid. If the user is holding ctrl/meta, we use the finer 8px grid. const gridSize = getCtrlKey() || getMetaKey() ? 8 : 64; @@ -588,26 +591,23 @@ export class CanvasPreview { stagingArea: CanvasStagingArea; constructor( - stage: Konva.Stage, - getBbox: () => IRect, - onBboxTransformed: (bbox: IRect) => void, - getShiftKey: () => boolean, - getCtrlKey: () => boolean, - getMetaKey: () => boolean, - getAltKey: () => boolean + bbox: CanvasBbox, + tool: CanvasTool, + documentSizeOverlay: CanvasDocumentSizeOverlay, + stagingArea: CanvasStagingArea ) { this.konvaLayer = new Konva.Layer({ listening: true }); - this.bbox = new CanvasBbox(stage, getBbox, onBboxTransformed, getShiftKey, getCtrlKey, getMetaKey, getAltKey); + this.bbox = bbox; this.konvaLayer.add(this.bbox.group); - this.tool = new CanvasTool(); + this.tool = tool; this.konvaLayer.add(this.tool.group); - this.documentSizeOverlay = new CanvasDocumentSizeOverlay(); + this.documentSizeOverlay = documentSizeOverlay; this.konvaLayer.add(this.documentSizeOverlay.group); - this.stagingArea = new CanvasStagingArea(); + this.stagingArea = stagingArea; this.konvaLayer.add(this.stagingArea.group); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts index 01d3e93d994..1551752ef22 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -343,7 +343,7 @@ export const initializeRenderer = ( // the entire state over when needed. const debouncedUpdateBboxes = debounce(updateBboxes, 300); - const renderCanvas = () => { + const renderCanvas = async () => { canvasV2 = store.getState().canvasV2; if (prevCanvasV2 === canvasV2 && !isFirstRender) { @@ -408,6 +408,11 @@ export const initializeRenderer = ( // debouncedUpdateBboxes(stage, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions, onBboxChanged); } + if (isFirstRender || canvasV2.stagingArea !== prevCanvasV2.stagingArea) { + logIfDebugging('Rendering staging area'); + manager.renderStagingArea(); + } + if ( isFirstRender || canvasV2.layers.entities !== prevCanvasV2.layers.entities || From e37e885546c3e2c907df134bcbc85402a84788e3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 26 Jun 2024 19:53:15 +1000 Subject: [PATCH 128/678] feat(ui): staging area works more better --- .../addCommitStagingAreaImageListener.ts | 2 +- .../listeners/enqueueRequestedLinear.ts | 3 + .../components/StageComponent.tsx | 1 - .../controlLayers/konva/nodeManager.ts | 56 +++++---- .../konva/renderers/inpaintMask.ts | 115 +++++++++++------- .../controlLayers/konva/renderers/layers.ts | 9 +- .../controlLayers/konva/renderers/objects.ts | 4 +- .../controlLayers/konva/renderers/preview.ts | 1 + .../controlLayers/konva/renderers/regions.ts | 108 +++++++++------- .../controlLayers/konva/renderers/renderer.ts | 3 + .../controlLayers/store/canvasV2Slice.ts | 6 +- .../store/inpaintMaskReducers.ts | 10 +- .../controlLayers/store/regionsReducers.ts | 6 +- .../src/features/controlLayers/store/types.ts | 2 +- .../util/graph/generation/addImageToImage.ts | 2 +- .../nodes/util/graph/generation/addInpaint.ts | 4 +- .../util/graph/generation/addOutpaint.ts | 4 +- .../nodes/util/graph/generation/addRegions.ts | 2 +- .../util/graph/generation/buildSD1Graph.ts | 2 +- .../util/graph/generation/buildSDXLGraph.ts | 2 +- 20 files changed, 198 insertions(+), 144 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts index 48e52a46aa9..9e988b8bd45 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts @@ -69,7 +69,7 @@ export const addStagingListeners = (startAppListening: AppStartListening) => { if (!layer) { // We need to create a new layer to add the accepted image api.dispatch(layerAdded()); - layer = layers.entities[0]; + layer = api.getState().canvasV2.layers.entities[0]; } assert(layer, 'No layer found to stage image'); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index 277e6b2206f..8ded74a06d9 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -25,6 +25,9 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) assert(model, 'No model found in state'); const base = model.base; + manager.getInpaintMaskImage({ bbox: state.canvasV2.bbox, preview: true }); + manager.getImageSourceImage({ bbox: state.canvasV2.bbox, preview: true }); + if (base === 'sdxl') { graph = await buildSDXLGraph(state, manager); } else if (base === 'sd-1' || base === 'sd-2') { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index 0be12933a1e..cb2e1070e13 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -61,7 +61,6 @@ export const StageComponent = memo(({ asPreview = false }: Props) => { bottom={0} left={0} ref={containerRef} - tabIndex={-1} borderRadius="base" border={1} borderStyle="solid" diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts index 73d22024063..c61857fe984 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts @@ -88,12 +88,6 @@ type Util = { image_category: ImageCategory, is_intermediate: boolean ) => Promise; - getRegionMaskImage: (arg: { id: string; bbox?: Rect; preview?: boolean }) => Promise; - getInpaintMaskImage: (arg: { bbox?: Rect; preview?: boolean }) => Promise; - getImageSourceImage: (arg: { bbox?: Rect; preview?: boolean }) => Promise; - getMaskLayerClone: (arg: { id: string }) => Konva.Layer; - getCompositeLayerStageClone: () => Konva.Stage; - getGenerationMode: () => GenerationMode; }; const $nodeManager = atom(null); @@ -131,12 +125,6 @@ export class KonvaNodeManager { this.util = { getImageDTO, uploadImage, - getRegionMaskImage: this._getRegionMaskImage.bind(this), - getInpaintMaskImage: this._getInpaintMaskImage.bind(this), - getImageSourceImage: this._getImageSourceImage.bind(this), - getMaskLayerClone: this._getMaskLayerClone.bind(this), - getCompositeLayerStageClone: this._getCompositeLayerStageClone.bind(this), - getGenerationMode: this._getGenerationMode.bind(this), }; this.preview = new CanvasPreview( @@ -310,9 +298,7 @@ export class KonvaNodeManager { this.renderDocumentSizeOverlay(); } - _getMaskLayerClone(): Konva.Layer { - assert(this.inpaintMask, 'Inpaint mask layer has not been set'); - + getInpaintMaskLayerClone(): Konva.Layer { const layerClone = this.inpaintMask.konvaLayer.clone(); const objectGroupClone = this.inpaintMask.konvaObjectGroup.clone(); @@ -325,7 +311,25 @@ export class KonvaNodeManager { return layerClone; } - _getCompositeLayerStageClone(): Konva.Stage { + getRegionMaskLayerClone(arg: { id: string }): Konva.Layer { + const { id } = arg; + + const canvasRegion = this.regions.get(id); + assert(canvasRegion, `Canvas region with id ${id} not found`); + + const layerClone = canvasRegion.konvaLayer.clone(); + const objectGroupClone = canvasRegion.konvaObjectGroup.clone(); + + layerClone.destroyChildren(); + layerClone.add(objectGroupClone); + + objectGroupClone.opacity(1); + objectGroupClone.cache(); + + return layerClone; + } + + getCompositeLayerStageClone(): Konva.Stage { const layersState = this.stateApi.getLayersState(); const stageClone = this.stage.clone(); @@ -357,12 +361,12 @@ export class KonvaNodeManager { return stageClone; } - _getGenerationMode(): GenerationMode { + getGenerationMode(): GenerationMode { const { x, y, width, height } = this.stateApi.getBbox(); - const inpaintMaskLayer = this.util.getMaskLayerClone({ id: 'inpaint_mask' }); + const inpaintMaskLayer = this.getInpaintMaskLayerClone(); const inpaintMaskImageData = konvaNodeToImageData(inpaintMaskLayer, { x, y, width, height }); const inpaintMaskTransparency = getImageDataTransparency(inpaintMaskImageData); - const compositeLayer = this.util.getCompositeLayerStageClone(); + const compositeLayer = this.getCompositeLayerStageClone(); const compositeLayerImageData = konvaNodeToImageData(compositeLayer, { x, y, width, height }); const compositeLayerTransparency = getImageDataTransparency(compositeLayerImageData); if (compositeLayerTransparency.isPartiallyTransparent) { @@ -378,7 +382,7 @@ export class KonvaNodeManager { } } - async _getRegionMaskImage(arg: { id: string; bbox?: Rect; preview?: boolean }): Promise { + async getRegionMaskImage(arg: { id: string; bbox?: Rect; preview?: boolean }): Promise { const { id, bbox, preview = false } = arg; const region = this.stateApi.getRegionsState().entities.find((entity) => entity.id === id); assert(region, `Region entity state with id ${id} not found`); @@ -390,7 +394,7 @@ export class KonvaNodeManager { // } // } - const layerClone = this.util.getMaskLayerClone({ id }); + const layerClone = this.getRegionMaskLayerClone({ id }); const blob = await konvaNodeToBlob(layerClone, bbox); if (preview) { @@ -404,9 +408,9 @@ export class KonvaNodeManager { return imageDTO; } - async _getInpaintMaskImage(arg: { bbox?: Rect; preview?: boolean }): Promise { + async getInpaintMaskImage(arg: { bbox?: Rect; preview?: boolean }): Promise { const { bbox, preview = false } = arg; - const inpaintMask = this.stateApi.getInpaintMaskState(); + // const inpaintMask = this.stateApi.getInpaintMaskState(); // if (inpaintMask.imageCache) { // const imageDTO = await this.util.getImageDTO(inpaintMask.imageCache.name); @@ -415,7 +419,7 @@ export class KonvaNodeManager { // } // } - const layerClone = this.util.getMaskLayerClone({ id: inpaintMask.id }); + const layerClone = this.getInpaintMaskLayerClone(); const blob = await konvaNodeToBlob(layerClone, bbox); if (preview) { @@ -429,7 +433,7 @@ export class KonvaNodeManager { return imageDTO; } - async _getImageSourceImage(arg: { bbox?: Rect; preview?: boolean }): Promise { + async getImageSourceImage(arg: { bbox?: Rect; preview?: boolean }): Promise { const { bbox, preview = false } = arg; // const { imageCache } = this.stateApi.getLayersState(); @@ -440,7 +444,7 @@ export class KonvaNodeManager { // } // } - const stageClone = this.util.getCompositeLayerStageClone(); + const stageClone = this.getCompositeLayerStageClone(); const blob = await konvaNodeToBlob(stageClone, bbox); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts index 86639898043..93d56be4444 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts @@ -67,6 +67,7 @@ export class CanvasInpaintMask { // Destroy any objects that are no longer in state for (const object of this.objects.values()) { if (!objectIds.includes(object.id)) { + this.objects.delete(object.id); object.destroy(); groupNeedsCache = true; } @@ -80,7 +81,7 @@ export class CanvasInpaintMask { if (!brushLine) { brushLine = new KonvaBrushLine({ brushLine: obj }); this.objects.set(brushLine.id, brushLine); - this.konvaLayer.add(brushLine.konvaLineGroup); + this.konvaObjectGroup.add(brushLine.konvaLineGroup); groupNeedsCache = true; } @@ -95,7 +96,7 @@ export class CanvasInpaintMask { if (!eraserLine) { eraserLine = new KonvaEraserLine({ eraserLine: obj }); this.objects.set(eraserLine.id, eraserLine); - this.konvaLayer.add(eraserLine.konvaLineGroup); + this.konvaObjectGroup.add(eraserLine.konvaLineGroup); groupNeedsCache = true; } @@ -110,7 +111,7 @@ export class CanvasInpaintMask { if (!rect) { rect = new KonvaRect({ rectShape: obj }); this.objects.set(rect.id, rect); - this.konvaLayer.add(rect.konvaRect); + this.konvaObjectGroup.add(rect.konvaRect); groupNeedsCache = true; } } @@ -128,50 +129,72 @@ export class CanvasInpaintMask { return; } - const isSelected = selectedEntityIdentifier?.id === inpaintMaskState.id; - - /** - * When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows - * shapes to render as a "raster" layer with all pixels drawn at the same color and opacity. - * - * Without this special handling, each shape is drawn individually with the given opacity, atop the other shapes. The - * effect is like if you have a Photoshop Group consisting of many shapes, each of which has the given opacity. - * Overlapping shapes will have their colors blended together, and the final color is the result of all the shapes. - * - * 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. - */ - if (isSelected && selectedTool !== 'move') { - // We must clear the cache first so Konva will re-draw the group with the new compositing rect - if (this.konvaObjectGroup.isCached()) { - this.konvaObjectGroup.clearCache(); - } - // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work - this.konvaObjectGroup.opacity(1); - - this.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 - ...(!inpaintMaskState.bboxNeedsUpdate && inpaintMaskState.bbox - ? inpaintMaskState.bbox - : getLayerBboxFast(this.konvaLayer)), - fill: rgbColor, - opacity: maskOpacity, - // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) - globalCompositeOperation: 'source-in', - visible: true, - // This rect must always be on top of all other shapes - zIndex: this.objects.size + 1, - }); - } else { - // The compositing rect should only be shown when the layer is selected. - this.compositingRect.visible(false); - // Cache only if needed - or if we are on this code path and _don't_ have a cache - if (groupNeedsCache || !this.konvaObjectGroup.isCached()) { - this.konvaObjectGroup.cache(); - } - // Updating group opacity does not require re-caching - this.konvaObjectGroup.opacity(maskOpacity); + + // We must clear the cache first so Konva will re-draw the group with the new compositing rect + if (this.konvaObjectGroup.isCached()) { + this.konvaObjectGroup.clearCache(); } + // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work + this.konvaObjectGroup.opacity(1); + + this.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 + ...(!inpaintMaskState.bboxNeedsUpdate && inpaintMaskState.bbox + ? inpaintMaskState.bbox + : getLayerBboxFast(this.konvaLayer)), + fill: rgbColor, + opacity: maskOpacity, + // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) + globalCompositeOperation: 'source-in', + visible: true, + // This rect must always be on top of all other shapes + zIndex: this.objects.size + 1, + }); + + // const isSelected = selectedEntityIdentifier?.id === inpaintMaskState.id; + + // /** + // * When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows + // * shapes to render as a "raster" layer with all pixels drawn at the same color and opacity. + // * + // * Without this special handling, each shape is drawn individually with the given opacity, atop the other shapes. The + // * effect is like if you have a Photoshop Group consisting of many shapes, each of which has the given opacity. + // * Overlapping shapes will have their colors blended together, and the final color is the result of all the shapes. + // * + // * 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. + // */ + // if (isSelected && selectedTool !== 'move') { + // // We must clear the cache first so Konva will re-draw the group with the new compositing rect + // if (this.konvaObjectGroup.isCached()) { + // this.konvaObjectGroup.clearCache(); + // } + // // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work + // this.konvaObjectGroup.opacity(1); + + // this.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 + // ...(!inpaintMaskState.bboxNeedsUpdate && inpaintMaskState.bbox + // ? inpaintMaskState.bbox + // : getLayerBboxFast(this.konvaLayer)), + // fill: rgbColor, + // opacity: maskOpacity, + // // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) + // globalCompositeOperation: 'source-in', + // visible: true, + // // This rect must always be on top of all other shapes + // zIndex: this.objects.size + 1, + // }); + // } else { + // // The compositing rect should only be shown when the layer is selected. + // this.compositingRect.visible(false); + // // Cache only if needed - or if we are on this code path and _don't_ have a cache + // if (groupNeedsCache || !this.konvaObjectGroup.isCached()) { + // this.konvaObjectGroup.cache(); + // } + // // Updating group opacity does not require re-caching + // this.konvaObjectGroup.opacity(maskOpacity); + // } // const bboxRect = // regionMap.konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rg, regionMap.konvaLayer); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts index ad138a66ab4..c216f978b27 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -52,6 +52,7 @@ export class CanvasLayer { // Destroy any objects that are no longer in state for (const object of this.objects.values()) { if (!objectIds.includes(object.id)) { + this.objects.delete(object.id); object.destroy(); } } @@ -64,7 +65,7 @@ export class CanvasLayer { if (!brushLine) { brushLine = new KonvaBrushLine({ brushLine: obj }); this.objects.set(brushLine.id, brushLine); - this.konvaLayer.add(brushLine.konvaLineGroup); + this.konvaObjectGroup.add(brushLine.konvaLineGroup); } if (obj.points.length !== brushLine.konvaLine.points().length) { brushLine.konvaLine.points(obj.points); @@ -76,7 +77,7 @@ export class CanvasLayer { if (!eraserLine) { eraserLine = new KonvaEraserLine({ eraserLine: obj }); this.objects.set(eraserLine.id, eraserLine); - this.konvaLayer.add(eraserLine.konvaLineGroup); + this.konvaObjectGroup.add(eraserLine.konvaLineGroup); } if (obj.points.length !== eraserLine.konvaLine.points().length) { eraserLine.konvaLine.points(obj.points); @@ -88,7 +89,7 @@ export class CanvasLayer { if (!rect) { rect = new KonvaRect({ rectShape: obj }); this.objects.set(rect.id, rect); - this.konvaLayer.add(rect.konvaRect); + this.konvaObjectGroup.add(rect.konvaRect); } } else if (obj.type === 'image') { let image = this.objects.get(obj.id); @@ -97,7 +98,7 @@ export class CanvasLayer { if (!image) { image = await new KonvaImage({ imageObject: obj }); this.objects.set(image.id, image); - this.konvaLayer.add(image.konvaImageGroup); + this.konvaObjectGroup.add(image.konvaImageGroup); } if (image.imageName !== obj.image.name) { image.updateImageSource(obj.image.name); 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 8a6d81a35f8..5a0c90b8059 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -1,7 +1,7 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { getLayerBboxId, LAYER_BBOX_NAME } from 'features/controlLayers/konva/naming'; import type { BrushLine, CanvasEntity, EraserLine, ImageObject, RectShape } from 'features/controlLayers/store/types'; -import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types'; +import { RGBA_RED } from 'features/controlLayers/store/types'; import { t } from 'i18next'; import Konva from 'konva'; import { getImageDTO as defaultGetImageDTO } from 'services/api/endpoints/images'; @@ -77,7 +77,7 @@ export class KonvaEraserLine { lineCap: 'round', lineJoin: 'round', globalCompositeOperation: 'destination-out', - stroke: rgbaColorToString(DEFAULT_RGBA_COLOR), + stroke: rgbaColorToString(RGBA_RED), }); this.konvaLineGroup.add(this.konvaLine); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts index c2ada34fc48..5df04d4d6cb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts @@ -215,6 +215,7 @@ export class CanvasTool { strokeEnabled: false, }), }; + this.rect.group.add(this.rect.fillRect); this.group.add(this.rect.group); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts index c715ae37818..cc0862ecd2e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts @@ -67,6 +67,7 @@ export class CanvasRegion { // Destroy any objects that are no longer in state for (const object of this.objects.values()) { if (!objectIds.includes(object.id)) { + this.objects.delete(object.id); object.destroy(); groupNeedsCache = true; } @@ -80,7 +81,7 @@ export class CanvasRegion { if (!brushLine) { brushLine = new KonvaBrushLine({ brushLine: obj }); this.objects.set(brushLine.id, brushLine); - this.konvaLayer.add(brushLine.konvaLineGroup); + this.konvaObjectGroup.add(brushLine.konvaLineGroup); groupNeedsCache = true; } @@ -95,7 +96,7 @@ export class CanvasRegion { if (!eraserLine) { eraserLine = new KonvaEraserLine({ eraserLine: obj }); this.objects.set(eraserLine.id, eraserLine); - this.konvaLayer.add(eraserLine.konvaLineGroup); + this.konvaObjectGroup.add(eraserLine.konvaLineGroup); groupNeedsCache = true; } @@ -110,7 +111,7 @@ export class CanvasRegion { if (!rect) { rect = new KonvaRect({ rectShape: obj }); this.objects.set(rect.id, rect); - this.konvaLayer.add(rect.konvaRect); + this.konvaObjectGroup.add(rect.konvaRect); groupNeedsCache = true; } } @@ -128,48 +129,67 @@ export class CanvasRegion { return; } - const isSelected = selectedEntityIdentifier?.id === regionState.id; - - /** - * When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows - * shapes to render as a "raster" layer with all pixels drawn at the same color and opacity. - * - * Without this special handling, each shape is drawn individually with the given opacity, atop the other shapes. The - * effect is like if you have a Photoshop Group consisting of many shapes, each of which has the given opacity. - * Overlapping shapes will have their colors blended together, and the final color is the result of all the shapes. - * - * 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. - */ - if (isSelected && selectedTool !== 'move') { - // We must clear the cache first so Konva will re-draw the group with the new compositing rect - if (this.konvaObjectGroup.isCached()) { - this.konvaObjectGroup.clearCache(); - } - // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work - this.konvaObjectGroup.opacity(1); - - this.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 - ...(!regionState.bboxNeedsUpdate && regionState.bbox ? regionState.bbox : getLayerBboxFast(this.konvaLayer)), - fill: rgbColor, - opacity: maskOpacity, - // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) - globalCompositeOperation: 'source-in', - visible: true, - // This rect must always be on top of all other shapes - zIndex: this.objects.size + 1, - }); - } else { - // The compositing rect should only be shown when the layer is selected. - this.compositingRect.visible(false); - // Cache only if needed - or if we are on this code path and _don't_ have a cache - if (groupNeedsCache || !this.konvaObjectGroup.isCached()) { - this.konvaObjectGroup.cache(); - } - // Updating group opacity does not require re-caching - this.konvaObjectGroup.opacity(maskOpacity); + // We must clear the cache first so Konva will re-draw the group with the new compositing rect + if (this.konvaObjectGroup.isCached()) { + this.konvaObjectGroup.clearCache(); } + // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work + this.konvaObjectGroup.opacity(1); + + this.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 + ...(!regionState.bboxNeedsUpdate && regionState.bbox ? regionState.bbox : getLayerBboxFast(this.konvaLayer)), + fill: rgbColor, + opacity: maskOpacity, + // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) + globalCompositeOperation: 'source-in', + visible: true, + // This rect must always be on top of all other shapes + zIndex: this.objects.size + 1, + }); + + // const isSelected = selectedEntityIdentifier?.id === regionState.id; + + // /** + // * When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows + // * shapes to render as a "raster" layer with all pixels drawn at the same color and opacity. + // * + // * Without this special handling, each shape is drawn individually with the given opacity, atop the other shapes. The + // * effect is like if you have a Photoshop Group consisting of many shapes, each of which has the given opacity. + // * Overlapping shapes will have their colors blended together, and the final color is the result of all the shapes. + // * + // * 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. + // */ + // if (isSelected && selectedTool !== 'move') { + // // We must clear the cache first so Konva will re-draw the group with the new compositing rect + // if (this.konvaObjectGroup.isCached()) { + // this.konvaObjectGroup.clearCache(); + // } + // // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work + // this.konvaObjectGroup.opacity(1); + + // this.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 + // ...(!regionState.bboxNeedsUpdate && regionState.bbox ? regionState.bbox : getLayerBboxFast(this.konvaLayer)), + // fill: rgbColor, + // opacity: maskOpacity, + // // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) + // globalCompositeOperation: 'source-in', + // visible: true, + // // This rect must always be on top of all other shapes + // zIndex: this.objects.size + 1, + // }); + // } else { + // // The compositing rect should only be shown when the layer is selected. + // this.compositingRect.visible(false); + // // Cache only if needed - or if we are on this code path and _don't_ have a cache + // if (groupNeedsCache || !this.konvaObjectGroup.isCached()) { + // this.konvaObjectGroup.cache(); + // } + // // Updating group opacity does not require re-caching + // this.konvaObjectGroup.opacity(maskOpacity); + // } // const bboxRect = // regionMap.konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rg, regionMap.konvaLayer); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts index 1551752ef22..eb0f83fc736 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -18,6 +18,7 @@ import { imEraserLineAdded, imImageCacheChanged, imLinePointAdded, + imRectAdded, imTranslated, layerBboxChanged, layerBrushLineAdded, @@ -146,6 +147,8 @@ export const initializeRenderer = ( dispatch(layerRectAdded(arg)); } else if (entityType === 'regional_guidance') { dispatch(rgRectAdded(arg)); + } else if (entityType === 'inpaint_mask') { + dispatch(imRectAdded(arg)); } }; const onBboxTransformed = (bbox: IRect) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 8c579c1cb4f..f0ac34622fd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -20,7 +20,7 @@ import type { AspectRatioState } from 'features/parameters/components/ImageSize/ import { atom } from 'nanostores'; import type { CanvasEntityIdentifier, CanvasV2State, StageAttrs } from './types'; -import { DEFAULT_RGBA_COLOR } from './types'; +import { RGBA_RED } from './types'; const initialState: CanvasV2State = { _version: 3, @@ -35,7 +35,7 @@ const initialState: CanvasV2State = { type: 'inpaint_mask', bbox: null, bboxNeedsUpdate: false, - fill: DEFAULT_RGBA_COLOR, + fill: RGBA_RED, imageCache: null, isEnabled: true, objects: [], @@ -46,7 +46,7 @@ const initialState: CanvasV2State = { selected: 'bbox', selectedBuffer: null, invertScroll: false, - fill: DEFAULT_RGBA_COLOR, + fill: RGBA_RED, brush: { width: 50, }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts index 881028b0d62..1b7a9346ee9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts @@ -1,7 +1,7 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; import type { CanvasV2State, InpaintMaskEntity } from 'features/controlLayers/store/types'; -import { imageDTOToImageWithDims } from 'features/controlLayers/store/types'; +import { imageDTOToImageWithDims,RGBA_RED } from 'features/controlLayers/store/types'; import type { IRect } from 'konva/lib/types'; import type { ImageDTO } from 'services/api/types'; import { v4 as uuidv4 } from 'uuid'; @@ -44,13 +44,13 @@ export const inpaintMaskReducers = { }, imBrushLineAdded: { reducer: (state, action: PayloadAction & { lineId: string }>) => { - const { points, lineId, color, width, clip } = action.payload; + const { points, lineId, width, clip } = action.payload; state.inpaintMask.objects.push({ id: getBrushLineId(state.inpaintMask.id, lineId), type: 'brush_line', points, strokeWidth: width, - color, + color: RGBA_RED, clip, }); state.inpaintMask.bboxNeedsUpdate = true; @@ -89,7 +89,7 @@ export const inpaintMaskReducers = { }, imRectAdded: { reducer: (state, action: PayloadAction & { rectId: string }>) => { - const { rect, rectId, color } = action.payload; + const { rect, rectId } = action.payload; if (rect.height === 0 || rect.width === 0) { // Ignore zero-area rectangles return; @@ -98,7 +98,7 @@ export const inpaintMaskReducers = { type: 'rect_shape', id: getRectShapeId(state.inpaintMask.id, rectId), ...rect, - color, + color: RGBA_RED, }); state.inpaintMask.bboxNeedsUpdate = true; state.inpaintMask.imageCache = null; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index 54e73bc582d..59ad0d5314f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -2,7 +2,7 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; import type { CanvasV2State, CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types'; -import { DEFAULT_RGBA_COLOR, imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/types'; +import { imageDTOToImageObject, imageDTOToImageWithDims,RGBA_RED } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas'; import type { IRect } from 'konva/lib/types'; @@ -319,7 +319,7 @@ export const regionsReducers = { type: 'brush_line', points, strokeWidth: width, - color: DEFAULT_RGBA_COLOR, + color: RGBA_RED, clip, }); rg.bboxNeedsUpdate = true; @@ -379,7 +379,7 @@ export const regionsReducers = { type: 'rect_shape', id: getRectShapeId(id, rectId), ...rect, - color: DEFAULT_RGBA_COLOR, + color: RGBA_RED, }); rg.bboxNeedsUpdate = true; rg.imageCache = null; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 4f27d721862..ea55c6b5818 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -495,7 +495,7 @@ const zRgbaColor = zRgbColor.extend({ a: z.number().min(0).max(1), }); export type RgbaColor = z.infer; -export const DEFAULT_RGBA_COLOR: RgbaColor = { r: 255, g: 255, b: 255, a: 1 }; +export const RGBA_RED: RgbaColor = { r: 255, g: 0, b: 0, a: 1 }; const zOpacity = z.number().gte(0).lte(1); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts index 32e36a5c826..59f2cd9a6a3 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts @@ -18,7 +18,7 @@ export const addImageToImage = async ( denoise.denoising_start = denoising_start; const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']); - const initialImage = await manager.util.getImageSourceImage({ + const initialImage = await manager.getImageSourceImage({ bbox: cropBbox, preview: true, }); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts index 41e18071b5b..9936166ec69 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts @@ -22,11 +22,11 @@ export const addInpaint = async ( denoise.denoising_start = denoising_start; const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']); - const initialImage = await manager.util.getImageSourceImage({ + const initialImage = await manager.getImageSourceImage({ bbox: cropBbox, preview: true, }); - const maskImage = await manager.util.getInpaintMaskImage({ + const maskImage = await manager.getInpaintMaskImage({ bbox: cropBbox, preview: true, }); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index 29102cc2e3d..aede59db137 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts @@ -21,11 +21,11 @@ export const addOutpaint = async ( vaePrecision: ParameterPrecision ): Promise> => { const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']); - const initialImage = await manager.util.getImageSourceImage({ + const initialImage = await manager.getImageSourceImage({ bbox: cropBbox, preview: true, }); - const maskImage = await manager.util.getInpaintMaskImage({ + const maskImage = await manager.getInpaintMaskImage({ bbox: cropBbox, preview: true, }); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts index c7a8a3a002a..3b85f15bfd6 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts @@ -44,7 +44,7 @@ export const addRegions = async ( for (const region of validRegions) { // Upload the mask image, or get the cached image if it exists - const { image_name } = await manager.util.getRegionMaskImage({ id: region.id, bbox, preview: true }); + const { image_name } = await manager.getRegionMaskImage({ id: region.id, bbox, preview: true }); // The main mask-to-tensor node const maskToTensor = g.addNode({ diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index a954938c15d..f55c8ad6b4d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -36,7 +36,7 @@ import { assert } from 'tsafe'; import { addRegions } from './addRegions'; export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager): Promise => { - const generationMode = manager.util.getGenerationMode(); + const generationMode = manager.getGenerationMode(); const { bbox, params } = state.canvasV2; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index 3d780a2f3df..d75044c736c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -34,7 +34,7 @@ import { assert } from 'tsafe'; import { addRegions } from './addRegions'; export const buildSDXLGraph = async (state: RootState, manager: KonvaNodeManager): Promise => { - const generationMode = manager.util.getGenerationMode(); + const generationMode = manager.getGenerationMode(); const { bbox, params } = state.canvasV2; From d073fe467d1c47ad6c71e9e68f1365d61aeb5cc2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 26 Jun 2024 20:55:16 +1000 Subject: [PATCH 129/678] fix(ui): reset cursor pos when fitting document --- .../frontend/web/src/features/controlLayers/konva/events.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index c0901e4f0dd..a7b079f08e8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -517,7 +517,11 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) = setToolBuffer(getToolState().selected); setTool('view'); setSpaceKey(true); + setLastCursorPos(null); + setLastMouseDownPos(null); } else if (e.key === 'r') { + setLastCursorPos(null); + setLastMouseDownPos(null); manager.fitDocument(); manager.renderBackground(); manager.renderDocumentSizeOverlay(); From bb18a82a9cbee753d31eb41a9a1c3e93ed4e4b99 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 26 Jun 2024 21:02:04 +1000 Subject: [PATCH 130/678] tidy(ui): file organisation --- .../controlLayers/konva/nodeManager.ts | 36 +- .../controlLayers/konva/renderers/bbox.ts | 442 +++++++------ .../konva/renderers/documentSizeOverlay.ts | 67 ++ .../konva/renderers/entityBbox.ts | 244 ++++++++ .../konva/renderers/inpaintMask.ts | 2 +- .../controlLayers/konva/renderers/preview.ts | 587 +----------------- .../controlLayers/konva/renderers/regions.ts | 2 +- .../controlLayers/konva/renderers/renderer.ts | 2 +- .../konva/renderers/stagingArea.ts | 74 ++- .../controlLayers/konva/renderers/tool.ts | 235 +++++++ 10 files changed, 831 insertions(+), 860 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/renderers/documentSizeOverlay.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/renderers/entityBbox.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/renderers/tool.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts index c61857fe984..7612fbef385 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts @@ -1,12 +1,6 @@ import { getImageDataTransparency } from 'common/util/arrayBuffer'; import { CanvasBackground } from 'features/controlLayers/konva/renderers/background'; -import { - CanvasBbox, - CanvasDocumentSizeOverlay, - CanvasPreview, - CanvasStagingArea, - CanvasTool, -} from 'features/controlLayers/konva/renderers/preview'; +import { CanvasPreview } from 'features/controlLayers/konva/renderers/preview'; import { konvaNodeToBlob, konvaNodeToImageData, previewBlob } from 'features/controlLayers/konva/util'; import type { BrushLineAddedArg, @@ -30,10 +24,14 @@ import { getImageDTO as defaultGetImageDTO, uploadImage as defaultUploadImage } import type { ImageCategory, ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; +import { CanvasBbox } from './renderers/bbox'; import { CanvasControlAdapter } from './renderers/controlAdapters'; +import { CanvasDocumentSizeOverlay } from './renderers/documentSizeOverlay'; import { CanvasInpaintMask } from './renderers/inpaintMask'; import { CanvasLayer } from './renderers/layers'; import { CanvasRegion } from './renderers/regions'; +import { CanvasStagingArea } from './renderers/stagingArea'; +import { CanvasTool } from './renderers/tool'; export type StateApi = { getToolState: () => CanvasV2State['tool']; @@ -157,10 +155,10 @@ export class KonvaNodeManager { const { entities } = this.stateApi.getLayersState(); const toolState = this.stateApi.getToolState(); - for (const adapter of this.layers.values()) { - if (!entities.find((l) => l.id === adapter.id)) { - adapter.destroy(); - this.layers.delete(adapter.id); + for (const canvasLayer of this.layers.values()) { + if (!entities.find((l) => l.id === canvasLayer.id)) { + canvasLayer.destroy(); + this.layers.delete(canvasLayer.id); } } @@ -182,10 +180,10 @@ export class KonvaNodeManager { const selectedEntity = this.stateApi.getSelectedEntity(); // Destroy the konva nodes for nonexistent entities - for (const adapter of this.regions.values()) { - if (!entities.find((rg) => rg.id === adapter.id)) { - adapter.destroy(); - this.regions.delete(adapter.id); + for (const canvasRegion of this.regions.values()) { + if (!entities.find((rg) => rg.id === canvasRegion.id)) { + canvasRegion.destroy(); + this.regions.delete(canvasRegion.id); } } @@ -212,10 +210,10 @@ export class KonvaNodeManager { renderControlAdapters() { const { entities } = this.stateApi.getControlAdaptersState(); - for (const adapter of this.controlAdapters.values()) { - if (!entities.find((ca) => ca.id === adapter.id)) { - adapter.destroy(); - this.controlAdapters.delete(adapter.id); + for (const canvasControlAdapter of this.controlAdapters.values()) { + if (!entities.find((ca) => ca.id === canvasControlAdapter.id)) { + canvasControlAdapter.destroy(); + this.controlAdapters.delete(canvasControlAdapter.id); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts index 5c226d017a1..f6e20708e35 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts @@ -1,244 +1,236 @@ -import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; +import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMultiple'; import { - CA_LAYER_IMAGE_NAME, - LAYER_BBOX_NAME, - RASTER_LAYER_OBJECT_GROUP_NAME, - RG_LAYER_OBJECT_GROUP_NAME, + PREVIEW_GENERATION_BBOX_DUMMY_RECT, + PREVIEW_GENERATION_BBOX_GROUP, + PREVIEW_GENERATION_BBOX_TRANSFORMER } from 'features/controlLayers/konva/naming'; -import { createBboxRect } from 'features/controlLayers/konva/renderers/objects'; -import { imageDataToDataURL } from 'features/controlLayers/konva/util'; -import type { - BboxChangedArg, - CanvasEntity, - ControlAdapterEntity, - LayerEntity, - RegionEntity, -} from 'features/controlLayers/store/types'; +import type { CanvasV2State } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; +import { atom } from 'nanostores'; import { assert } from 'tsafe'; -/** - * Logic to create and render bounding boxes for layers. - * Some utils are included for calculating bounding boxes. - */ - -type Extents = { - minX: number; - minY: number; - maxX: number; - maxY: number; -}; - -const GET_CLIENT_RECT_CONFIG = { skipTransform: true }; - -/** - * Get the bounding box of an image. - * @param imageData The ImageData object to get the bounding box of. - * @returns The minimum and maximum x and y values of the image's bounding box, or null if the image has no pixels. - */ -const getImageDataBbox = (imageData: ImageData): Extents | null => { - const { data, width, height } = imageData; - let minX = width; - let minY = height; - let maxX = -1; - let maxY = -1; - let alpha = 0; - let isEmpty = true; - - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - alpha = data[(y * width + x) * 4 + 3] ?? 0; - if (alpha > 0) { - isEmpty = false; - if (x < minX) { - minX = x; - } - if (x > maxX) { - maxX = x; - } - if (y < minY) { - minY = y; + +export class CanvasBbox { + group: Konva.Group; + rect: Konva.Rect; + transformer: Konva.Transformer; + + ALL_ANCHORS: string[] = [ + 'top-left', + 'top-center', + 'top-right', + 'middle-right', + 'middle-left', + 'bottom-left', + 'bottom-center', + 'bottom-right', + ]; + CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right']; + NO_ANCHORS: string[] = []; + + constructor( + getBbox: () => IRect, + onBboxTransformed: (bbox: IRect) => void, + getShiftKey: () => boolean, + getCtrlKey: () => boolean, + getMetaKey: () => boolean, + getAltKey: () => boolean + ) { + // Create a stash to hold onto the last aspect ratio of the bbox - this allows for locking the aspect ratio when + // transforming the bbox. + const bbox = getBbox(); + const $aspectRatioBuffer = atom(bbox.width / bbox.height); + + // Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully + // transparent rect for this purpose. + this.group = new Konva.Group({ id: PREVIEW_GENERATION_BBOX_GROUP, listening: false }); + this.rect = new Konva.Rect({ + id: PREVIEW_GENERATION_BBOX_DUMMY_RECT, + listening: false, + strokeEnabled: false, + draggable: true, + ...getBbox(), + }); + this.rect.on('dragmove', () => { + const gridSize = getCtrlKey() || getMetaKey() ? 8 : 64; + const oldBbox = getBbox(); + const newBbox: IRect = { + ...oldBbox, + x: roundToMultiple(this.rect.x(), gridSize), + y: roundToMultiple(this.rect.y(), gridSize), + }; + this.rect.setAttrs(newBbox); + if (oldBbox.x !== newBbox.x || oldBbox.y !== newBbox.y) { + onBboxTransformed(newBbox); + } + }); + + this.transformer = new Konva.Transformer({ + id: PREVIEW_GENERATION_BBOX_TRANSFORMER, + borderDash: [5, 5], + borderStroke: 'rgba(212,216,234,1)', + borderEnabled: true, + rotateEnabled: false, + keepRatio: false, + ignoreStroke: true, + listening: false, + flipEnabled: false, + anchorFill: 'rgba(212,216,234,1)', + anchorStroke: 'rgb(42,42,42)', + anchorSize: 12, + anchorCornerRadius: 3, + shiftBehavior: 'none', // we will implement our own shift behavior + centeredScaling: false, + anchorStyleFunc: (anchor) => { + // Make the x/y resize anchors little bars + if (anchor.hasName('top-center') || anchor.hasName('bottom-center')) { + anchor.height(8); + anchor.offsetY(4); + anchor.width(30); + anchor.offsetX(15); } - if (y > maxY) { - maxY = y; + if (anchor.hasName('middle-left') || anchor.hasName('middle-right')) { + anchor.height(30); + anchor.offsetY(15); + anchor.width(8); + anchor.offsetX(4); } + }, + anchorDragBoundFunc: (_oldAbsPos, newAbsPos) => { + // This function works with absolute position - that is, a position in "physical" pixels on the screen, as opposed + // to konva's internal coordinate system. + const stage = this.transformer.getStage(); + assert(stage, 'Stage must exist'); + + // We need to snap the anchors to the grid. If the user is holding ctrl/meta, we use the finer 8px grid. + const gridSize = getCtrlKey() || getMetaKey() ? 8 : 64; + // Because we are working in absolute coordinates, we need to scale the grid size by the stage scale. + const scaledGridSize = gridSize * stage.scaleX(); + // To snap the anchor to the grid, we need to calculate an offset from the stage's absolute position. + const stageAbsPos = stage.getAbsolutePosition(); + // The offset is the remainder of the stage's absolute position divided by the scaled grid size. + const offsetX = stageAbsPos.x % scaledGridSize; + const offsetY = stageAbsPos.y % scaledGridSize; + // Finally, calculate the position by rounding to the grid and adding the offset. + return { + x: roundToMultiple(newAbsPos.x, scaledGridSize) + offsetX, + y: roundToMultiple(newAbsPos.y, scaledGridSize) + offsetY, + }; + }, + }); + + this.transformer.on('transform', () => { + // In the transform callback, we calculate the bbox's new dims and pos and update the konva object. + // Some special handling is needed depending on the anchor being dragged. + const anchor = this.transformer.getActiveAnchor(); + if (!anchor) { + // Pretty sure we should always have an anchor here? + return; } - } - } - - return isEmpty ? null : { minX, minY, maxX, maxY }; -}; - -/** - * Clones a regional guidance konva layer onto an offscreen stage/canvas. This allows the pixel data for a given layer - * to be captured, manipulated or analyzed without interference from other layers. - * @param layer The konva layer to clone. - * @param filterChildren A callback to filter out unwanted children - * @returns The cloned stage and layer. - */ -const getIsolatedLayerClone = ( - layer: Konva.Layer, - filterChildren: (node: Konva.Node) => boolean -): { stageClone: Konva.Stage; layerClone: Konva.Layer } => { - const stage = layer.getStage(); - - // Construct an offscreen canvas with the same dimensions as the layer's stage. - const offscreenStageContainer = document.createElement('div'); - const stageClone = new Konva.Stage({ - container: offscreenStageContainer, - x: stage.x(), - y: stage.y(), - width: stage.width(), - height: stage.height(), - }); - - // Clone the layer and filter out unwanted children. - const layerClone = layer.clone(); - stageClone.add(layerClone); - - for (const child of layerClone.getChildren()) { - if (filterChildren(child) && child.hasChildren()) { - // We need to cache the group to ensure it composites out eraser strokes correctly - child.opacity(1); - child.cache(); - } else { - // Filter out unwanted children. - child.destroy(); - } - } - return { stageClone, layerClone }; -}; - -/** - * Get the bounding box of a regional prompt konva layer. This function has special handling for regional prompt layers. - * @param layer The konva layer to get the bounding box of. - * @param preview Whether to open a new tab displaying the rendered layer, which is used to calculate the bbox. - */ -const getLayerBboxPixels = ( - layer: Konva.Layer, - filterChildren: (node: Konva.Node) => boolean, - preview: boolean = false -): IRect | null => { - // To calculate the layer's bounding box, we must first export it to a pixel array, then do some math. - // - // Though it is relatively fast, we can't use Konva's `getClientRect`. It programmatically determines the rect - // by calculating the extents of individual shapes from their "vector" shape data. - // - // This doesn't work when some shapes are drawn with composite operations that "erase" pixels, like eraser lines. - // These shapes' extents are still calculated as if they were solid, leading to a bounding box that is too large. - const { stageClone, layerClone } = getIsolatedLayerClone(layer, filterChildren); - - // Get a worst-case rect using the relatively fast `getClientRect`. - const layerRect = layerClone.getClientRect(); - if (layerRect.width === 0 || layerRect.height === 0) { - return null; - } - // Capture the image data with the above rect. - const layerImageData = stageClone - .toCanvas(layerRect) - .getContext('2d') - ?.getImageData(0, 0, layerRect.width, layerRect.height); - assert(layerImageData, "Unable to get layer's image data"); - - if (preview) { - openBase64ImageInTab([{ base64: imageDataToDataURL(layerImageData), caption: layer.id() }]); - } + const alt = getAltKey(); + const ctrl = getCtrlKey(); + const meta = getMetaKey(); + const shift = getShiftKey(); - // Calculate the layer's bounding box. - const layerBbox = getImageDataBbox(layerImageData); + // Grid size depends on the modifier keys + let gridSize = ctrl || meta ? 8 : 64; - if (!layerBbox) { - return null; - } + // Alt key indicates we are using centered scaling. We need to double the gride size used when calculating the + // new dimensions so that each size scales in the correct increments and doesn't mis-place the bbox. For example, if + // we snapped the width and height to 8px increments, the bbox would be mis-placed by 4px in the x and y axes. + // Doubling the grid size ensures the bbox's coords remain aligned to the 8px/64px grid. + if (getAltKey()) { + gridSize = gridSize * 2; + } - // Correct the bounding box to be relative to the layer's position. - const correctedLayerBbox = { - x: layerBbox.minX - Math.floor(stageClone.x()) + layerRect.x - Math.floor(layer.x()), - y: layerBbox.minY - Math.floor(stageClone.y()) + layerRect.y - Math.floor(layer.y()), - width: layerBbox.maxX - layerBbox.minX, - height: layerBbox.maxY - layerBbox.minY, - }; - - return correctedLayerBbox; -}; - -/** - * Get the bounding box of a konva layer. This function is faster than `getLayerBboxPixels` but less accurate. It - * should only be used when there are no eraser strokes or shapes in the layer. - * @param layer The konva layer to get the bounding box of. - * @returns The bounding box of the layer. - */ -export const getLayerBboxFast = (layer: Konva.Layer): IRect => { - const bbox = layer.getClientRect(GET_CLIENT_RECT_CONFIG); - return { - x: Math.floor(bbox.x), - y: Math.floor(bbox.y), - width: Math.floor(bbox.width), - height: Math.floor(bbox.height), - }; -}; - -const filterRGChildren = (node: Konva.Node): boolean => node.name() === RG_LAYER_OBJECT_GROUP_NAME; -const filterLayerChildren = (node: Konva.Node): boolean => node.name() === RASTER_LAYER_OBJECT_GROUP_NAME; -const filterCAChildren = (node: Konva.Node): boolean => node.name() === CA_LAYER_IMAGE_NAME; - -/** - * Calculates the bbox of each regional guidance layer. Only calculates if the mask has changed. - * @param stage The konva stage - * @param entityStates An array of layers to calculate bboxes for - * @param onBboxChanged Callback for when the bounding box changes - */ -export const updateBboxes = ( - stage: Konva.Stage, - layers: LayerEntity[], - controlAdapters: ControlAdapterEntity[], - regions: RegionEntity[], - onBboxChanged: (arg: BboxChangedArg, entityType: CanvasEntity['type']) => void -): void => { - for (const entityState of [...layers, ...controlAdapters, ...regions]) { - const konvaLayer = stage.findOne(`#${entityState.id}`); - assert(konvaLayer, `Layer ${entityState.id} not found in stage`); - // We only need to recalculate the bbox if the layer has changed - if (entityState.bboxNeedsUpdate) { - const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(entityState, konvaLayer); - - // Hide the bbox while we calculate the new bbox, else the bbox will be included in the calculation - const visible = bboxRect.visible(); - bboxRect.visible(false); - - if (entityState.type === 'layer') { - if (entityState.objects.length === 0) { - // No objects - no bbox to calculate - onBboxChanged({ id: entityState.id, bbox: null }, 'layer'); - } else { - onBboxChanged({ id: entityState.id, bbox: getLayerBboxPixels(konvaLayer, filterLayerChildren) }, 'layer'); + // The coords should be correct per the anchorDragBoundFunc. + let x = this.rect.x(); + let y = this.rect.y(); + + // Konva transforms by scaling the dims, not directly changing width and height. At this point, the width and height + // *have not changed*, only the scale has changed. To get the final height, we need to scale the dims and then snap + // them to the grid. + let width = roundToMultipleMin(this.rect.width() * this.rect.scaleX(), gridSize); + let height = roundToMultipleMin(this.rect.height() * this.rect.scaleY(), gridSize); + + // If shift is held and we are resizing from a corner, retain aspect ratio - needs special handling. We skip this + // if alt/opt is held - this requires math too big for my brain. + if (shift && this.CORNER_ANCHORS.includes(anchor) && !alt) { + // Fit the bbox to the last aspect ratio + let fittedWidth = Math.sqrt(width * height * $aspectRatioBuffer.get()); + let fittedHeight = fittedWidth / $aspectRatioBuffer.get(); + fittedWidth = roundToMultipleMin(fittedWidth, gridSize); + fittedHeight = roundToMultipleMin(fittedHeight, gridSize); + + // We need to adjust the x and y coords to have the resize occur from the right origin. + if (anchor === 'top-left') { + // The transform origin is the bottom-right anchor. Both x and y need to be updated. + x = x - (fittedWidth - width); + y = y - (fittedHeight - height); } - } else if (entityState.type === 'control_adapter') { - if (!entityState.imageObject && !entityState.processedImageObject) { - // No objects - no bbox to calculate - onBboxChanged({ id: entityState.id, bbox: null }, 'control_adapter'); - } else { - onBboxChanged( - { id: entityState.id, bbox: getLayerBboxPixels(konvaLayer, filterCAChildren) }, - 'control_adapter' - ); + if (anchor === 'top-right') { + // The transform origin is the bottom-left anchor. Only y needs to be updated. + y = y - (fittedHeight - height); } - } else if (entityState.type === 'regional_guidance') { - if (entityState.objects.length === 0) { - // No objects - no bbox to calculate - onBboxChanged({ id: entityState.id, bbox: null }, 'regional_guidance'); - } else { - onBboxChanged( - { id: entityState.id, bbox: getLayerBboxPixels(konvaLayer, filterRGChildren) }, - 'regional_guidance' - ); + if (anchor === 'bottom-left') { + // The transform origin is the top-right anchor. Only x needs to be updated. + x = x - (fittedWidth - width); } + // Update the width and height to the fitted dims. + width = fittedWidth; + height = fittedHeight; } - // Restore the visibility of the bbox - bboxRect.visible(visible); - } + const bbox = { + x: Math.round(x), + y: Math.round(y), + width, + height, + }; + + // Update the bboxRect's attrs directly with the new transform, and reset its scale to 1. + // TODO(psyche): In `renderBboxPreview()` we also call setAttrs, need to do it twice to ensure it renders correctly. + // Gotta be a way to avoid setting it twice... + this.rect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1 }); + + // Update the bbox in internal state. + onBboxTransformed(bbox); + + // Update the aspect ratio buffer whenever the shift key is not held - this allows for a nice UX where you can start + // a transform, get the right aspect ratio, then hold shift to lock it in. + if (!shift) { + $aspectRatioBuffer.set(bbox.width / bbox.height); + } + }); + + this.transformer.on('transformend', () => { + // Always update the aspect ratio buffer when the transform ends, so if the next transform starts with shift held, + // we have the correct aspect ratio to start from. + $aspectRatioBuffer.set(this.rect.width() / this.rect.height()); + }); + + // The transformer will always be transforming the dummy rect + this.transformer.nodes([this.rect]); + this.group.add(this.rect); + this.group.add(this.transformer); + } + + render(bbox: CanvasV2State['bbox'], toolState: CanvasV2State['tool']) { + this.group.listening(toolState.selected === 'bbox'); + this.rect.setAttrs({ + x: bbox.x, + y: bbox.y, + width: bbox.width, + height: bbox.height, + scaleX: 1, + scaleY: 1, + listening: toolState.selected === 'bbox', + }); + this.transformer.setAttrs({ + listening: toolState.selected === 'bbox', + enabledAnchors: toolState.selected === 'bbox' ? this.ALL_ANCHORS : this.NO_ANCHORS, + }); } -}; +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/documentSizeOverlay.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/documentSizeOverlay.ts new file mode 100644 index 00000000000..abf87c485b7 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/documentSizeOverlay.ts @@ -0,0 +1,67 @@ +import { getArbitraryBaseColor } from '@invoke-ai/ui-library'; +import { DOCUMENT_FIT_PADDING_PX } from 'features/controlLayers/konva/constants'; +import type { CanvasV2State, StageAttrs } from 'features/controlLayers/store/types'; +import Konva from 'konva'; + +export class CanvasDocumentSizeOverlay { + group: Konva.Group; + outerRect: Konva.Rect; + innerRect: Konva.Rect; + padding: number; + + constructor(padding?: number) { + this.padding = padding ?? DOCUMENT_FIT_PADDING_PX; + this.group = new Konva.Group({ id: 'document_overlay_group', listening: false }); + this.outerRect = new Konva.Rect({ + id: 'document_overlay_outer_rect', + listening: false, + fill: getArbitraryBaseColor(10), + opacity: 0.7, + }); + this.innerRect = new Konva.Rect({ + id: 'document_overlay_inner_rect', + listening: false, + fill: 'white', + globalCompositeOperation: 'destination-out', + }); + this.group.add(this.outerRect); + this.group.add(this.innerRect); + } + + render(stage: Konva.Stage, document: CanvasV2State['document']) { + this.group.zIndex(0); + + const x = stage.x(); + const y = stage.y(); + const width = stage.width(); + const height = stage.height(); + const scale = stage.scaleX(); + + this.outerRect.setAttrs({ + offsetX: x / scale, + offsetY: y / scale, + width: width / scale, + height: height / scale, + }); + + this.innerRect.setAttrs({ + x: 0, + y: 0, + width: document.width, + height: document.height, + }); + } + + fitToStage(stage: Konva.Stage, document: CanvasV2State['document'], setStageAttrs: (attrs: StageAttrs) => void) { + // Fit & center the document on the stage + const width = stage.width(); + const height = stage.height(); + const docWidthWithBuffer = document.width + this.padding * 2; + const docHeightWithBuffer = document.height + this.padding * 2; + const scale = Math.min(Math.min(width / docWidthWithBuffer, height / docHeightWithBuffer), 1); + const x = (width - docWidthWithBuffer * scale) / 2 + this.padding * scale; + const y = (height - docHeightWithBuffer * scale) / 2 + this.padding * scale; + stage.setAttrs({ x, y, width, height, scaleX: scale, scaleY: scale }); + setStageAttrs({ x, y, width, height, scale }); + } +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/entityBbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/entityBbox.ts new file mode 100644 index 00000000000..5c226d017a1 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/entityBbox.ts @@ -0,0 +1,244 @@ +import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; +import { + CA_LAYER_IMAGE_NAME, + LAYER_BBOX_NAME, + RASTER_LAYER_OBJECT_GROUP_NAME, + RG_LAYER_OBJECT_GROUP_NAME, +} from 'features/controlLayers/konva/naming'; +import { createBboxRect } from 'features/controlLayers/konva/renderers/objects'; +import { imageDataToDataURL } from 'features/controlLayers/konva/util'; +import type { + BboxChangedArg, + CanvasEntity, + ControlAdapterEntity, + LayerEntity, + RegionEntity, +} from 'features/controlLayers/store/types'; +import Konva from 'konva'; +import type { IRect } from 'konva/lib/types'; +import { assert } from 'tsafe'; + +/** + * Logic to create and render bounding boxes for layers. + * Some utils are included for calculating bounding boxes. + */ + +type Extents = { + minX: number; + minY: number; + maxX: number; + maxY: number; +}; + +const GET_CLIENT_RECT_CONFIG = { skipTransform: true }; + +/** + * Get the bounding box of an image. + * @param imageData The ImageData object to get the bounding box of. + * @returns The minimum and maximum x and y values of the image's bounding box, or null if the image has no pixels. + */ +const getImageDataBbox = (imageData: ImageData): Extents | null => { + const { data, width, height } = imageData; + let minX = width; + let minY = height; + let maxX = -1; + let maxY = -1; + let alpha = 0; + let isEmpty = true; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + alpha = data[(y * width + x) * 4 + 3] ?? 0; + if (alpha > 0) { + isEmpty = false; + if (x < minX) { + minX = x; + } + if (x > maxX) { + maxX = x; + } + if (y < minY) { + minY = y; + } + if (y > maxY) { + maxY = y; + } + } + } + } + + return isEmpty ? null : { minX, minY, maxX, maxY }; +}; + +/** + * Clones a regional guidance konva layer onto an offscreen stage/canvas. This allows the pixel data for a given layer + * to be captured, manipulated or analyzed without interference from other layers. + * @param layer The konva layer to clone. + * @param filterChildren A callback to filter out unwanted children + * @returns The cloned stage and layer. + */ +const getIsolatedLayerClone = ( + layer: Konva.Layer, + filterChildren: (node: Konva.Node) => boolean +): { stageClone: Konva.Stage; layerClone: Konva.Layer } => { + const stage = layer.getStage(); + + // Construct an offscreen canvas with the same dimensions as the layer's stage. + const offscreenStageContainer = document.createElement('div'); + const stageClone = new Konva.Stage({ + container: offscreenStageContainer, + x: stage.x(), + y: stage.y(), + width: stage.width(), + height: stage.height(), + }); + + // Clone the layer and filter out unwanted children. + const layerClone = layer.clone(); + stageClone.add(layerClone); + + for (const child of layerClone.getChildren()) { + if (filterChildren(child) && child.hasChildren()) { + // We need to cache the group to ensure it composites out eraser strokes correctly + child.opacity(1); + child.cache(); + } else { + // Filter out unwanted children. + child.destroy(); + } + } + + return { stageClone, layerClone }; +}; + +/** + * Get the bounding box of a regional prompt konva layer. This function has special handling for regional prompt layers. + * @param layer The konva layer to get the bounding box of. + * @param preview Whether to open a new tab displaying the rendered layer, which is used to calculate the bbox. + */ +const getLayerBboxPixels = ( + layer: Konva.Layer, + filterChildren: (node: Konva.Node) => boolean, + preview: boolean = false +): IRect | null => { + // To calculate the layer's bounding box, we must first export it to a pixel array, then do some math. + // + // Though it is relatively fast, we can't use Konva's `getClientRect`. It programmatically determines the rect + // by calculating the extents of individual shapes from their "vector" shape data. + // + // This doesn't work when some shapes are drawn with composite operations that "erase" pixels, like eraser lines. + // These shapes' extents are still calculated as if they were solid, leading to a bounding box that is too large. + const { stageClone, layerClone } = getIsolatedLayerClone(layer, filterChildren); + + // Get a worst-case rect using the relatively fast `getClientRect`. + const layerRect = layerClone.getClientRect(); + if (layerRect.width === 0 || layerRect.height === 0) { + return null; + } + // Capture the image data with the above rect. + const layerImageData = stageClone + .toCanvas(layerRect) + .getContext('2d') + ?.getImageData(0, 0, layerRect.width, layerRect.height); + assert(layerImageData, "Unable to get layer's image data"); + + if (preview) { + openBase64ImageInTab([{ base64: imageDataToDataURL(layerImageData), caption: layer.id() }]); + } + + // Calculate the layer's bounding box. + const layerBbox = getImageDataBbox(layerImageData); + + if (!layerBbox) { + return null; + } + + // Correct the bounding box to be relative to the layer's position. + const correctedLayerBbox = { + x: layerBbox.minX - Math.floor(stageClone.x()) + layerRect.x - Math.floor(layer.x()), + y: layerBbox.minY - Math.floor(stageClone.y()) + layerRect.y - Math.floor(layer.y()), + width: layerBbox.maxX - layerBbox.minX, + height: layerBbox.maxY - layerBbox.minY, + }; + + return correctedLayerBbox; +}; + +/** + * Get the bounding box of a konva layer. This function is faster than `getLayerBboxPixels` but less accurate. It + * should only be used when there are no eraser strokes or shapes in the layer. + * @param layer The konva layer to get the bounding box of. + * @returns The bounding box of the layer. + */ +export const getLayerBboxFast = (layer: Konva.Layer): IRect => { + const bbox = layer.getClientRect(GET_CLIENT_RECT_CONFIG); + return { + x: Math.floor(bbox.x), + y: Math.floor(bbox.y), + width: Math.floor(bbox.width), + height: Math.floor(bbox.height), + }; +}; + +const filterRGChildren = (node: Konva.Node): boolean => node.name() === RG_LAYER_OBJECT_GROUP_NAME; +const filterLayerChildren = (node: Konva.Node): boolean => node.name() === RASTER_LAYER_OBJECT_GROUP_NAME; +const filterCAChildren = (node: Konva.Node): boolean => node.name() === CA_LAYER_IMAGE_NAME; + +/** + * Calculates the bbox of each regional guidance layer. Only calculates if the mask has changed. + * @param stage The konva stage + * @param entityStates An array of layers to calculate bboxes for + * @param onBboxChanged Callback for when the bounding box changes + */ +export const updateBboxes = ( + stage: Konva.Stage, + layers: LayerEntity[], + controlAdapters: ControlAdapterEntity[], + regions: RegionEntity[], + onBboxChanged: (arg: BboxChangedArg, entityType: CanvasEntity['type']) => void +): void => { + for (const entityState of [...layers, ...controlAdapters, ...regions]) { + const konvaLayer = stage.findOne(`#${entityState.id}`); + assert(konvaLayer, `Layer ${entityState.id} not found in stage`); + // We only need to recalculate the bbox if the layer has changed + if (entityState.bboxNeedsUpdate) { + const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(entityState, konvaLayer); + + // Hide the bbox while we calculate the new bbox, else the bbox will be included in the calculation + const visible = bboxRect.visible(); + bboxRect.visible(false); + + if (entityState.type === 'layer') { + if (entityState.objects.length === 0) { + // No objects - no bbox to calculate + onBboxChanged({ id: entityState.id, bbox: null }, 'layer'); + } else { + onBboxChanged({ id: entityState.id, bbox: getLayerBboxPixels(konvaLayer, filterLayerChildren) }, 'layer'); + } + } else if (entityState.type === 'control_adapter') { + if (!entityState.imageObject && !entityState.processedImageObject) { + // No objects - no bbox to calculate + onBboxChanged({ id: entityState.id, bbox: null }, 'control_adapter'); + } else { + onBboxChanged( + { id: entityState.id, bbox: getLayerBboxPixels(konvaLayer, filterCAChildren) }, + 'control_adapter' + ); + } + } else if (entityState.type === 'regional_guidance') { + if (entityState.objects.length === 0) { + // No objects - no bbox to calculate + onBboxChanged({ id: entityState.id, bbox: null }, 'regional_guidance'); + } else { + onBboxChanged( + { id: entityState.id, bbox: getLayerBboxPixels(konvaLayer, filterRGChildren) }, + 'regional_guidance' + ); + } + } + + // Restore the visibility of the bbox + bboxRect.visible(visible); + } + } +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts index 93d56be4444..db4f7b6cbfc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts @@ -1,7 +1,7 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { getObjectGroupId } from 'features/controlLayers/konva/naming'; import type { StateApi } from 'features/controlLayers/konva/nodeManager'; -import { getLayerBboxFast } from 'features/controlLayers/konva/renderers/bbox'; +import { getLayerBboxFast } from 'features/controlLayers/konva/renderers/entityBbox'; import { KonvaBrushLine, KonvaEraserLine, KonvaRect } from 'features/controlLayers/konva/renderers/objects'; import { mapId } from 'features/controlLayers/konva/util'; import type { CanvasEntityIdentifier, InpaintMaskEntity, Tool } from 'features/controlLayers/store/types'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts index 5df04d4d6cb..871e21353e4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts @@ -1,588 +1,9 @@ -import { getArbitraryBaseColor } from '@invoke-ai/ui-library'; -import { rgbaColorToString } from 'common/util/colorCodeTransformers'; -import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMultiple'; -import { - BRUSH_BORDER_INNER_COLOR, - BRUSH_BORDER_OUTER_COLOR, - BRUSH_ERASER_BORDER_WIDTH, - DOCUMENT_FIT_PADDING_PX, -} from 'features/controlLayers/konva/constants'; -import { - PREVIEW_GENERATION_BBOX_DUMMY_RECT, - PREVIEW_GENERATION_BBOX_GROUP, - PREVIEW_GENERATION_BBOX_TRANSFORMER, - PREVIEW_RECT_ID, -} from 'features/controlLayers/konva/naming'; -import { KonvaImage } from 'features/controlLayers/konva/renderers/objects'; -import type { CanvasEntity, CanvasV2State, Position, RgbaColor, StageAttrs } from 'features/controlLayers/store/types'; import Konva from 'konva'; -import type { IRect } from 'konva/lib/types'; -import { atom } from 'nanostores'; -import { assert } from 'tsafe'; -export class CanvasDocumentSizeOverlay { - group: Konva.Group; - outerRect: Konva.Rect; - innerRect: Konva.Rect; - padding: number; - - constructor(padding?: number) { - this.padding = padding ?? DOCUMENT_FIT_PADDING_PX; - this.group = new Konva.Group({ id: 'document_overlay_group', listening: false }); - this.outerRect = new Konva.Rect({ - id: 'document_overlay_outer_rect', - listening: false, - fill: getArbitraryBaseColor(10), - opacity: 0.7, - }); - this.innerRect = new Konva.Rect({ - id: 'document_overlay_inner_rect', - listening: false, - fill: 'white', - globalCompositeOperation: 'destination-out', - }); - this.group.add(this.outerRect); - this.group.add(this.innerRect); - } - - render(stage: Konva.Stage, document: CanvasV2State['document']) { - this.group.zIndex(0); - - const x = stage.x(); - const y = stage.y(); - const width = stage.width(); - const height = stage.height(); - const scale = stage.scaleX(); - - this.outerRect.setAttrs({ - offsetX: x / scale, - offsetY: y / scale, - width: width / scale, - height: height / scale, - }); - - this.innerRect.setAttrs({ - x: 0, - y: 0, - width: document.width, - height: document.height, - }); - } - - fitToStage(stage: Konva.Stage, document: CanvasV2State['document'], setStageAttrs: (attrs: StageAttrs) => void) { - // Fit & center the document on the stage - const width = stage.width(); - const height = stage.height(); - const docWidthWithBuffer = document.width + this.padding * 2; - const docHeightWithBuffer = document.height + this.padding * 2; - const scale = Math.min(Math.min(width / docWidthWithBuffer, height / docHeightWithBuffer), 1); - const x = (width - docWidthWithBuffer * scale) / 2 + this.padding * scale; - const y = (height - docHeightWithBuffer * scale) / 2 + this.padding * scale; - stage.setAttrs({ x, y, width, height, scaleX: scale, scaleY: scale }); - setStageAttrs({ x, y, width, height, scale }); - } -} - -export class CanvasStagingArea { - group: Konva.Group; - image: KonvaImage | null; - - constructor() { - this.group = new Konva.Group({ listening: false }); - this.image = null; - } - - async render(stagingArea: CanvasV2State['stagingArea']) { - if (!stagingArea || stagingArea.selectedImageIndex === null) { - if (this.image) { - this.image.destroy(); - this.image = null; - } - return; - } - - if (stagingArea.selectedImageIndex !== null) { - const imageDTO = stagingArea.images[stagingArea.selectedImageIndex]; - assert(imageDTO, 'Image must exist'); - if (this.image) { - if (!this.image.isLoading && !this.image.isError && this.image.imageName !== imageDTO.image_name) { - await this.image.updateImageSource(imageDTO.image_name); - } - } else { - const { image_name, width, height } = imageDTO; - this.image = new KonvaImage({ - imageObject: { - id: 'staging-area-image', - type: 'image', - x: stagingArea.bbox.x, - y: stagingArea.bbox.y, - width, - height, - filters: [], - image: { - name: image_name, - width, - height, - }, - }, - }); - this.group.add(this.image.konvaImageGroup); - await this.image.updateImageSource(imageDTO.image_name); - } - } - } -} - -export class CanvasTool { - group: Konva.Group; - brush: { - group: Konva.Group; - fillCircle: Konva.Circle; - innerBorderCircle: Konva.Circle; - outerBorderCircle: Konva.Circle; - }; - eraser: { - group: Konva.Group; - fillCircle: Konva.Circle; - innerBorderCircle: Konva.Circle; - outerBorderCircle: Konva.Circle; - }; - rect: { - group: Konva.Group; - fillRect: Konva.Rect; - }; - - constructor() { - this.group = new Konva.Group(); - - // Create the brush preview group & circles - this.brush = { - group: new Konva.Group(), - fillCircle: new Konva.Circle({ - listening: false, - strokeEnabled: false, - }), - innerBorderCircle: new Konva.Circle({ - listening: false, - stroke: BRUSH_BORDER_INNER_COLOR, - strokeWidth: BRUSH_ERASER_BORDER_WIDTH, - strokeEnabled: true, - }), - outerBorderCircle: new Konva.Circle({ - listening: false, - stroke: BRUSH_BORDER_OUTER_COLOR, - strokeWidth: BRUSH_ERASER_BORDER_WIDTH, - strokeEnabled: true, - }), - }; - this.brush.group.add(this.brush.fillCircle); - this.brush.group.add(this.brush.innerBorderCircle); - this.brush.group.add(this.brush.outerBorderCircle); - this.group.add(this.brush.group); - - this.eraser = { - group: new Konva.Group(), - fillCircle: new Konva.Circle({ - listening: false, - strokeEnabled: false, - fill: 'white', - globalCompositeOperation: 'destination-out', - }), - innerBorderCircle: new Konva.Circle({ - listening: false, - stroke: BRUSH_BORDER_INNER_COLOR, - strokeWidth: BRUSH_ERASER_BORDER_WIDTH, - strokeEnabled: true, - }), - outerBorderCircle: new Konva.Circle({ - listening: false, - stroke: BRUSH_BORDER_OUTER_COLOR, - strokeWidth: BRUSH_ERASER_BORDER_WIDTH, - strokeEnabled: true, - }), - }; - this.eraser.group.add(this.eraser.fillCircle); - this.eraser.group.add(this.eraser.innerBorderCircle); - this.eraser.group.add(this.eraser.outerBorderCircle); - this.group.add(this.eraser.group); - - // Create the rect preview - this is a rectangle drawn from the last mouse down position to the current cursor position - this.rect = { - group: new Konva.Group(), - fillRect: new Konva.Rect({ - id: PREVIEW_RECT_ID, - listening: false, - strokeEnabled: false, - }), - }; - this.rect.group.add(this.rect.fillRect); - this.group.add(this.rect.group); - } - - scaleTool(stage: Konva.Stage, toolState: CanvasV2State['tool']) { - const scale = stage.scaleX(); - - const brushRadius = toolState.brush.width / 2; - this.brush.innerBorderCircle.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale); - this.brush.outerBorderCircle.setAttrs({ - strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, - radius: brushRadius + BRUSH_ERASER_BORDER_WIDTH / scale, - }); - - const eraserRadius = toolState.eraser.width / 2; - this.eraser.innerBorderCircle.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale); - this.eraser.outerBorderCircle.setAttrs({ - strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, - radius: eraserRadius + BRUSH_ERASER_BORDER_WIDTH / scale, - }); - } - - render( - stage: Konva.Stage, - renderedEntityCount: number, - toolState: CanvasV2State['tool'], - currentFill: RgbaColor, - selectedEntity: CanvasEntity | null, - cursorPos: Position | null, - lastMouseDownPos: Position | null, - isDrawing: boolean, - isMouseDown: boolean - ) { - const tool = toolState.selected; - const isDrawableEntity = - selectedEntity?.type === 'regional_guidance' || - selectedEntity?.type === 'layer' || - selectedEntity?.type === 'inpaint_mask'; - - // Update the stage's pointer style - if (tool === 'view') { - // View gets a hand - stage.container().style.cursor = isMouseDown ? 'grabbing' : 'grab'; - } else if (renderedEntityCount === 0) { - // We have no layers, so we should not render any tool - stage.container().style.cursor = 'default'; - } else if (!isDrawableEntity) { - // Non-drawable layers don't have tools - stage.container().style.cursor = 'not-allowed'; - } else if (tool === 'move') { - // Move tool gets a pointer - stage.container().style.cursor = 'default'; - } else if (tool === 'rect') { - // Rect gets a crosshair - stage.container().style.cursor = 'crosshair'; - } else if (tool === 'brush' || tool === 'eraser') { - // Hide the native cursor and use the konva-rendered brush preview - stage.container().style.cursor = 'none'; - } else if (tool === 'bbox') { - stage.container().style.cursor = 'default'; - } - - stage.draggable(tool === 'view'); - - if (!cursorPos || renderedEntityCount === 0 || !isDrawableEntity) { - // We can bail early if the mouse isn't over the stage or there are no layers - this.group.visible(false); - } else { - this.group.visible(true); - - // No need to render the brush preview if the cursor position or color is missing - if (cursorPos && tool === 'brush') { - const scale = stage.scaleX(); - // Update the fill circle - const radius = toolState.brush.width / 2; - this.brush.fillCircle.setAttrs({ - x: cursorPos.x, - y: cursorPos.y, - radius, - fill: isDrawing ? '' : rgbaColorToString(currentFill), - }); - - // Update the inner border of the brush preview - this.brush.innerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius }); - - // Update the outer border of the brush preview - this.brush.outerBorderCircle.setAttrs({ - x: cursorPos.x, - y: cursorPos.y, - radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, - }); - - this.scaleTool(stage, toolState); - - this.brush.group.visible(true); - this.eraser.group.visible(false); - this.rect.group.visible(false); - } else if (cursorPos && tool === 'eraser') { - const scale = stage.scaleX(); - // Update the fill circle - const radius = toolState.eraser.width / 2; - this.eraser.fillCircle.setAttrs({ - x: cursorPos.x, - y: cursorPos.y, - radius, - fill: 'white', - }); - - // Update the inner border of the eraser preview - this.eraser.innerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius }); - - // Update the outer border of the eraser preview - this.eraser.outerBorderCircle.setAttrs({ - x: cursorPos.x, - y: cursorPos.y, - radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, - }); - - this.scaleTool(stage, toolState); - - this.brush.group.visible(false); - this.eraser.group.visible(true); - this.rect.group.visible(false); - } else if (cursorPos && lastMouseDownPos && tool === 'rect') { - this.rect.fillRect.setAttrs({ - x: Math.min(cursorPos.x, lastMouseDownPos.x), - y: Math.min(cursorPos.y, lastMouseDownPos.y), - width: Math.abs(cursorPos.x - lastMouseDownPos.x), - height: Math.abs(cursorPos.y - lastMouseDownPos.y), - fill: rgbaColorToString(currentFill), - visible: true, - }); - this.brush.group.visible(false); - this.eraser.group.visible(false); - this.rect.group.visible(true); - } else { - this.brush.group.visible(false); - this.eraser.group.visible(false); - this.rect.group.visible(false); - } - } - } -} - -export class CanvasBbox { - group: Konva.Group; - rect: Konva.Rect; - transformer: Konva.Transformer; - - ALL_ANCHORS: string[] = [ - 'top-left', - 'top-center', - 'top-right', - 'middle-right', - 'middle-left', - 'bottom-left', - 'bottom-center', - 'bottom-right', - ]; - CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right']; - NO_ANCHORS: string[] = []; - - constructor( - getBbox: () => IRect, - onBboxTransformed: (bbox: IRect) => void, - getShiftKey: () => boolean, - getCtrlKey: () => boolean, - getMetaKey: () => boolean, - getAltKey: () => boolean - ) { - // Create a stash to hold onto the last aspect ratio of the bbox - this allows for locking the aspect ratio when - // transforming the bbox. - const bbox = getBbox(); - const $aspectRatioBuffer = atom(bbox.width / bbox.height); - - // Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully - // transparent rect for this purpose. - this.group = new Konva.Group({ id: PREVIEW_GENERATION_BBOX_GROUP, listening: false }); - this.rect = new Konva.Rect({ - id: PREVIEW_GENERATION_BBOX_DUMMY_RECT, - listening: false, - strokeEnabled: false, - draggable: true, - ...getBbox(), - }); - this.rect.on('dragmove', () => { - const gridSize = getCtrlKey() || getMetaKey() ? 8 : 64; - const oldBbox = getBbox(); - const newBbox: IRect = { - ...oldBbox, - x: roundToMultiple(this.rect.x(), gridSize), - y: roundToMultiple(this.rect.y(), gridSize), - }; - this.rect.setAttrs(newBbox); - if (oldBbox.x !== newBbox.x || oldBbox.y !== newBbox.y) { - onBboxTransformed(newBbox); - } - }); - - this.transformer = new Konva.Transformer({ - id: PREVIEW_GENERATION_BBOX_TRANSFORMER, - borderDash: [5, 5], - borderStroke: 'rgba(212,216,234,1)', - borderEnabled: true, - rotateEnabled: false, - keepRatio: false, - ignoreStroke: true, - listening: false, - flipEnabled: false, - anchorFill: 'rgba(212,216,234,1)', - anchorStroke: 'rgb(42,42,42)', - anchorSize: 12, - anchorCornerRadius: 3, - shiftBehavior: 'none', // we will implement our own shift behavior - centeredScaling: false, - anchorStyleFunc: (anchor) => { - // Make the x/y resize anchors little bars - if (anchor.hasName('top-center') || anchor.hasName('bottom-center')) { - anchor.height(8); - anchor.offsetY(4); - anchor.width(30); - anchor.offsetX(15); - } - if (anchor.hasName('middle-left') || anchor.hasName('middle-right')) { - anchor.height(30); - anchor.offsetY(15); - anchor.width(8); - anchor.offsetX(4); - } - }, - anchorDragBoundFunc: (_oldAbsPos, newAbsPos) => { - // This function works with absolute position - that is, a position in "physical" pixels on the screen, as opposed - // to konva's internal coordinate system. - const stage = this.transformer.getStage(); - assert(stage, 'Stage must exist'); - - // We need to snap the anchors to the grid. If the user is holding ctrl/meta, we use the finer 8px grid. - const gridSize = getCtrlKey() || getMetaKey() ? 8 : 64; - // Because we are working in absolute coordinates, we need to scale the grid size by the stage scale. - const scaledGridSize = gridSize * stage.scaleX(); - // To snap the anchor to the grid, we need to calculate an offset from the stage's absolute position. - const stageAbsPos = stage.getAbsolutePosition(); - // The offset is the remainder of the stage's absolute position divided by the scaled grid size. - const offsetX = stageAbsPos.x % scaledGridSize; - const offsetY = stageAbsPos.y % scaledGridSize; - // Finally, calculate the position by rounding to the grid and adding the offset. - return { - x: roundToMultiple(newAbsPos.x, scaledGridSize) + offsetX, - y: roundToMultiple(newAbsPos.y, scaledGridSize) + offsetY, - }; - }, - }); - - this.transformer.on('transform', () => { - // In the transform callback, we calculate the bbox's new dims and pos and update the konva object. - - // Some special handling is needed depending on the anchor being dragged. - const anchor = this.transformer.getActiveAnchor(); - if (!anchor) { - // Pretty sure we should always have an anchor here? - return; - } - - const alt = getAltKey(); - const ctrl = getCtrlKey(); - const meta = getMetaKey(); - const shift = getShiftKey(); - - // Grid size depends on the modifier keys - let gridSize = ctrl || meta ? 8 : 64; - - // Alt key indicates we are using centered scaling. We need to double the gride size used when calculating the - // new dimensions so that each size scales in the correct increments and doesn't mis-place the bbox. For example, if - // we snapped the width and height to 8px increments, the bbox would be mis-placed by 4px in the x and y axes. - // Doubling the grid size ensures the bbox's coords remain aligned to the 8px/64px grid. - if (getAltKey()) { - gridSize = gridSize * 2; - } - - // The coords should be correct per the anchorDragBoundFunc. - let x = this.rect.x(); - let y = this.rect.y(); - - // Konva transforms by scaling the dims, not directly changing width and height. At this point, the width and height - // *have not changed*, only the scale has changed. To get the final height, we need to scale the dims and then snap - // them to the grid. - let width = roundToMultipleMin(this.rect.width() * this.rect.scaleX(), gridSize); - let height = roundToMultipleMin(this.rect.height() * this.rect.scaleY(), gridSize); - - // If shift is held and we are resizing from a corner, retain aspect ratio - needs special handling. We skip this - // if alt/opt is held - this requires math too big for my brain. - if (shift && this.CORNER_ANCHORS.includes(anchor) && !alt) { - // Fit the bbox to the last aspect ratio - let fittedWidth = Math.sqrt(width * height * $aspectRatioBuffer.get()); - let fittedHeight = fittedWidth / $aspectRatioBuffer.get(); - fittedWidth = roundToMultipleMin(fittedWidth, gridSize); - fittedHeight = roundToMultipleMin(fittedHeight, gridSize); - - // We need to adjust the x and y coords to have the resize occur from the right origin. - if (anchor === 'top-left') { - // The transform origin is the bottom-right anchor. Both x and y need to be updated. - x = x - (fittedWidth - width); - y = y - (fittedHeight - height); - } - if (anchor === 'top-right') { - // The transform origin is the bottom-left anchor. Only y needs to be updated. - y = y - (fittedHeight - height); - } - if (anchor === 'bottom-left') { - // The transform origin is the top-right anchor. Only x needs to be updated. - x = x - (fittedWidth - width); - } - // Update the width and height to the fitted dims. - width = fittedWidth; - height = fittedHeight; - } - - const bbox = { - x: Math.round(x), - y: Math.round(y), - width, - height, - }; - - // Update the bboxRect's attrs directly with the new transform, and reset its scale to 1. - // TODO(psyche): In `renderBboxPreview()` we also call setAttrs, need to do it twice to ensure it renders correctly. - // Gotta be a way to avoid setting it twice... - this.rect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1 }); - - // Update the bbox in internal state. - onBboxTransformed(bbox); - - // Update the aspect ratio buffer whenever the shift key is not held - this allows for a nice UX where you can start - // a transform, get the right aspect ratio, then hold shift to lock it in. - if (!shift) { - $aspectRatioBuffer.set(bbox.width / bbox.height); - } - }); - - this.transformer.on('transformend', () => { - // Always update the aspect ratio buffer when the transform ends, so if the next transform starts with shift held, - // we have the correct aspect ratio to start from. - $aspectRatioBuffer.set(this.rect.width() / this.rect.height()); - }); - - // The transformer will always be transforming the dummy rect - this.transformer.nodes([this.rect]); - this.group.add(this.rect); - this.group.add(this.transformer); - } - - render(bbox: CanvasV2State['bbox'], toolState: CanvasV2State['tool']) { - this.group.listening(toolState.selected === 'bbox'); - this.rect.setAttrs({ - x: bbox.x, - y: bbox.y, - width: bbox.width, - height: bbox.height, - scaleX: 1, - scaleY: 1, - listening: toolState.selected === 'bbox', - }); - this.transformer.setAttrs({ - listening: toolState.selected === 'bbox', - enabledAnchors: toolState.selected === 'bbox' ? this.ALL_ANCHORS : this.NO_ANCHORS, - }); - } -} +import type { CanvasBbox } from './bbox'; +import type { CanvasDocumentSizeOverlay } from './documentSizeOverlay'; +import type { CanvasStagingArea } from './stagingArea'; +import type { CanvasTool } from './tool'; export class CanvasPreview { konvaLayer: Konva.Layer; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts index cc0862ecd2e..7f6ebd5ddb3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts @@ -1,7 +1,7 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { getObjectGroupId } from 'features/controlLayers/konva/naming'; import type { StateApi } from 'features/controlLayers/konva/nodeManager'; -import { getLayerBboxFast } from 'features/controlLayers/konva/renderers/bbox'; +import { getLayerBboxFast } from 'features/controlLayers/konva/renderers/entityBbox'; import { KonvaBrushLine, KonvaEraserLine, KonvaRect } from 'features/controlLayers/konva/renderers/objects'; import { mapId } from 'features/controlLayers/konva/util'; import type { CanvasEntityIdentifier, RegionEntity, Tool } from 'features/controlLayers/store/types'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts index eb0f83fc736..7b23fff52dd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -5,7 +5,7 @@ import { $isDebugging } from 'app/store/nanostores/isDebugging'; import type { RootState } from 'app/store/store'; import { setStageEventHandlers } from 'features/controlLayers/konva/events'; import { KonvaNodeManager, setNodeManager } from 'features/controlLayers/konva/nodeManager'; -import { updateBboxes } from 'features/controlLayers/konva/renderers/bbox'; +import { updateBboxes } from 'features/controlLayers/konva/renderers/entityBbox'; import { $stageAttrs, bboxChanged, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stagingArea.ts index d4178e67396..2b2deed4895 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stagingArea.ts @@ -1,41 +1,55 @@ -import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; -import { createImageObjectGroup, updateImageSource } from 'features/controlLayers/konva/renderers/objects'; -import { imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/types'; +import { KonvaImage } from 'features/controlLayers/konva/renderers/objects'; +import type { CanvasV2State } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { assert } from 'tsafe'; -export const createStagingArea = (): KonvaNodeManager['preview']['stagingArea'] => { - const group = new Konva.Group({ id: 'staging_area_group', listening: false }); - return { group, image: null }; -}; -export const getRenderStagingArea = async (manager: KonvaNodeManager) => { - const { getStagingAreaState } = manager.stateApi; - const stagingArea = getStagingAreaState(); +export class CanvasStagingArea { + group: Konva.Group; + image: KonvaImage | null; - if (!stagingArea || stagingArea.selectedImageIndex === null) { - if (manager.preview.stagingArea.image) { - manager.preview.stagingArea.image.konvaImageGroup.visible(false); - manager.preview.stagingArea.image = null; - } - return; + constructor() { + this.group = new Konva.Group({ listening: false }); + this.image = null; } - if (stagingArea.selectedImageIndex) { - const imageDTO = stagingArea.images[stagingArea.selectedImageIndex]; - assert(imageDTO, 'Image must exist'); - if (manager.preview.stagingArea.image) { - if (manager.preview.stagingArea.image.imageName !== imageDTO.image_name) { - await updateImageSource({ - objectRecord: manager.preview.stagingArea.image, - image: imageDTOToImageWithDims(imageDTO), + async render(stagingArea: CanvasV2State['stagingArea']) { + if (!stagingArea || stagingArea.selectedImageIndex === null) { + if (this.image) { + this.image.destroy(); + this.image = null; + } + return; + } + + if (stagingArea.selectedImageIndex !== null) { + const imageDTO = stagingArea.images[stagingArea.selectedImageIndex]; + assert(imageDTO, 'Image must exist'); + if (this.image) { + if (!this.image.isLoading && !this.image.isError && this.image.imageName !== imageDTO.image_name) { + await this.image.updateImageSource(imageDTO.image_name); + } + } else { + const { image_name, width, height } = imageDTO; + this.image = new KonvaImage({ + imageObject: { + id: 'staging-area-image', + type: 'image', + x: stagingArea.bbox.x, + y: stagingArea.bbox.y, + width, + height, + filters: [], + image: { + name: image_name, + width, + height, + }, + }, }); + this.group.add(this.image.konvaImageGroup); + await this.image.updateImageSource(imageDTO.image_name); } - } else { - manager.preview.stagingArea.image = await createImageObjectGroup({ - obj: imageDTOToImageObject(imageDTO), - name: imageDTO.image_name, - }); } } -}; +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/tool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/tool.ts new file mode 100644 index 00000000000..d7b05d0cb35 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/tool.ts @@ -0,0 +1,235 @@ +import { rgbaColorToString } from 'common/util/colorCodeTransformers'; +import { + BRUSH_BORDER_INNER_COLOR, + BRUSH_BORDER_OUTER_COLOR, + BRUSH_ERASER_BORDER_WIDTH +} from 'features/controlLayers/konva/constants'; +import { PREVIEW_RECT_ID } from 'features/controlLayers/konva/naming'; +import type { CanvasEntity, CanvasV2State, Position, RgbaColor } from 'features/controlLayers/store/types'; +import Konva from 'konva'; + + +export class CanvasTool { + group: Konva.Group; + brush: { + group: Konva.Group; + fillCircle: Konva.Circle; + innerBorderCircle: Konva.Circle; + outerBorderCircle: Konva.Circle; + }; + eraser: { + group: Konva.Group; + fillCircle: Konva.Circle; + innerBorderCircle: Konva.Circle; + outerBorderCircle: Konva.Circle; + }; + rect: { + group: Konva.Group; + fillRect: Konva.Rect; + }; + + constructor() { + this.group = new Konva.Group(); + + // Create the brush preview group & circles + this.brush = { + group: new Konva.Group(), + fillCircle: new Konva.Circle({ + listening: false, + strokeEnabled: false, + }), + innerBorderCircle: new Konva.Circle({ + listening: false, + stroke: BRUSH_BORDER_INNER_COLOR, + strokeWidth: BRUSH_ERASER_BORDER_WIDTH, + strokeEnabled: true, + }), + outerBorderCircle: new Konva.Circle({ + listening: false, + stroke: BRUSH_BORDER_OUTER_COLOR, + strokeWidth: BRUSH_ERASER_BORDER_WIDTH, + strokeEnabled: true, + }), + }; + this.brush.group.add(this.brush.fillCircle); + this.brush.group.add(this.brush.innerBorderCircle); + this.brush.group.add(this.brush.outerBorderCircle); + this.group.add(this.brush.group); + + this.eraser = { + group: new Konva.Group(), + fillCircle: new Konva.Circle({ + listening: false, + strokeEnabled: false, + fill: 'white', + globalCompositeOperation: 'destination-out', + }), + innerBorderCircle: new Konva.Circle({ + listening: false, + stroke: BRUSH_BORDER_INNER_COLOR, + strokeWidth: BRUSH_ERASER_BORDER_WIDTH, + strokeEnabled: true, + }), + outerBorderCircle: new Konva.Circle({ + listening: false, + stroke: BRUSH_BORDER_OUTER_COLOR, + strokeWidth: BRUSH_ERASER_BORDER_WIDTH, + strokeEnabled: true, + }), + }; + this.eraser.group.add(this.eraser.fillCircle); + this.eraser.group.add(this.eraser.innerBorderCircle); + this.eraser.group.add(this.eraser.outerBorderCircle); + this.group.add(this.eraser.group); + + // Create the rect preview - this is a rectangle drawn from the last mouse down position to the current cursor position + this.rect = { + group: new Konva.Group(), + fillRect: new Konva.Rect({ + id: PREVIEW_RECT_ID, + listening: false, + strokeEnabled: false, + }), + }; + this.rect.group.add(this.rect.fillRect); + this.group.add(this.rect.group); + } + + scaleTool(stage: Konva.Stage, toolState: CanvasV2State['tool']) { + const scale = stage.scaleX(); + + const brushRadius = toolState.brush.width / 2; + this.brush.innerBorderCircle.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale); + this.brush.outerBorderCircle.setAttrs({ + strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, + radius: brushRadius + BRUSH_ERASER_BORDER_WIDTH / scale, + }); + + const eraserRadius = toolState.eraser.width / 2; + this.eraser.innerBorderCircle.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale); + this.eraser.outerBorderCircle.setAttrs({ + strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, + radius: eraserRadius + BRUSH_ERASER_BORDER_WIDTH / scale, + }); + } + + render( + stage: Konva.Stage, + renderedEntityCount: number, + toolState: CanvasV2State['tool'], + currentFill: RgbaColor, + selectedEntity: CanvasEntity | null, + cursorPos: Position | null, + lastMouseDownPos: Position | null, + isDrawing: boolean, + isMouseDown: boolean + ) { + const tool = toolState.selected; + const isDrawableEntity = selectedEntity?.type === 'regional_guidance' || + selectedEntity?.type === 'layer' || + selectedEntity?.type === 'inpaint_mask'; + + // Update the stage's pointer style + if (tool === 'view') { + // View gets a hand + stage.container().style.cursor = isMouseDown ? 'grabbing' : 'grab'; + } else if (renderedEntityCount === 0) { + // We have no layers, so we should not render any tool + stage.container().style.cursor = 'default'; + } else if (!isDrawableEntity) { + // Non-drawable layers don't have tools + stage.container().style.cursor = 'not-allowed'; + } else if (tool === 'move') { + // Move tool gets a pointer + stage.container().style.cursor = 'default'; + } else if (tool === 'rect') { + // Rect gets a crosshair + stage.container().style.cursor = 'crosshair'; + } else if (tool === 'brush' || tool === 'eraser') { + // Hide the native cursor and use the konva-rendered brush preview + stage.container().style.cursor = 'none'; + } else if (tool === 'bbox') { + stage.container().style.cursor = 'default'; + } + + stage.draggable(tool === 'view'); + + if (!cursorPos || renderedEntityCount === 0 || !isDrawableEntity) { + // We can bail early if the mouse isn't over the stage or there are no layers + this.group.visible(false); + } else { + this.group.visible(true); + + // No need to render the brush preview if the cursor position or color is missing + if (cursorPos && tool === 'brush') { + const scale = stage.scaleX(); + // Update the fill circle + const radius = toolState.brush.width / 2; + this.brush.fillCircle.setAttrs({ + x: cursorPos.x, + y: cursorPos.y, + radius, + fill: isDrawing ? '' : rgbaColorToString(currentFill), + }); + + // Update the inner border of the brush preview + this.brush.innerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius }); + + // Update the outer border of the brush preview + this.brush.outerBorderCircle.setAttrs({ + x: cursorPos.x, + y: cursorPos.y, + radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, + }); + + this.scaleTool(stage, toolState); + + this.brush.group.visible(true); + this.eraser.group.visible(false); + this.rect.group.visible(false); + } else if (cursorPos && tool === 'eraser') { + const scale = stage.scaleX(); + // Update the fill circle + const radius = toolState.eraser.width / 2; + this.eraser.fillCircle.setAttrs({ + x: cursorPos.x, + y: cursorPos.y, + radius, + fill: 'white', + }); + + // Update the inner border of the eraser preview + this.eraser.innerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius }); + + // Update the outer border of the eraser preview + this.eraser.outerBorderCircle.setAttrs({ + x: cursorPos.x, + y: cursorPos.y, + radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, + }); + + this.scaleTool(stage, toolState); + + this.brush.group.visible(false); + this.eraser.group.visible(true); + this.rect.group.visible(false); + } else if (cursorPos && lastMouseDownPos && tool === 'rect') { + this.rect.fillRect.setAttrs({ + x: Math.min(cursorPos.x, lastMouseDownPos.x), + y: Math.min(cursorPos.y, lastMouseDownPos.y), + width: Math.abs(cursorPos.x - lastMouseDownPos.x), + height: Math.abs(cursorPos.y - lastMouseDownPos.y), + fill: rgbaColorToString(currentFill), + visible: true, + }); + this.brush.group.visible(false); + this.eraser.group.visible(false); + this.rect.group.visible(true); + } else { + this.brush.group.visible(false); + this.eraser.group.visible(false); + this.rect.group.visible(false); + } + } + } +} From 4329dfd12813ec3cfdd7d7e2be184c4f150b6ca3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 26 Jun 2024 21:05:50 +1000 Subject: [PATCH 131/678] tidy(ui): naming things --- .../controlLayers/konva/nodeManager.ts | 32 ++++++------- .../konva/renderers/background.ts | 12 ++--- .../konva/renderers/controlAdapters.ts | 46 +++++++++---------- .../konva/renderers/inpaintMask.ts | 40 ++++++++-------- .../controlLayers/konva/renderers/layers.ts | 34 +++++++------- .../controlLayers/konva/renderers/preview.ts | 12 ++--- .../controlLayers/konva/renderers/regions.ts | 40 ++++++++-------- 7 files changed, 108 insertions(+), 108 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts index 7612fbef385..e82f3144acb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts @@ -138,13 +138,13 @@ export class KonvaNodeManager { new CanvasDocumentSizeOverlay(), new CanvasStagingArea() ); - this.stage.add(this.preview.konvaLayer); + this.stage.add(this.preview.layer); this.background = new CanvasBackground(); - this.stage.add(this.background.konvaLayer); + this.stage.add(this.background.layer); this.inpaintMask = new CanvasInpaintMask(this.stateApi.getInpaintMaskState(), this.stateApi.onPosChanged); - this.stage.add(this.inpaintMask.konvaLayer); + this.stage.add(this.inpaintMask.layer); this.layers = new Map(); this.regions = new Map(); @@ -167,7 +167,7 @@ export class KonvaNodeManager { if (!adapter) { adapter = new CanvasLayer(entity, this.stateApi.onPosChanged); this.layers.set(adapter.id, adapter); - this.stage.add(adapter.konvaLayer); + this.stage.add(adapter.layer); } adapter.render(entity, toolState.selected); } @@ -192,7 +192,7 @@ export class KonvaNodeManager { if (!adapter) { adapter = new CanvasRegion(entity, this.stateApi.onPosChanged); this.regions.set(adapter.id, adapter); - this.stage.add(adapter.konvaLayer); + this.stage.add(adapter.layer); } adapter.render(entity, toolState.selected, selectedEntity, maskOpacity); } @@ -222,7 +222,7 @@ export class KonvaNodeManager { if (!adapter) { adapter = new CanvasControlAdapter(entity); this.controlAdapters.set(adapter.id, adapter); - this.stage.add(adapter.konvaLayer); + this.stage.add(adapter.layer); } adapter.render(entity); } @@ -234,18 +234,18 @@ export class KonvaNodeManager { const controlAdapters = getControlAdaptersState().entities; const regions = getRegionsState().entities; let zIndex = 0; - this.background.konvaLayer.zIndex(++zIndex); + this.background.layer.zIndex(++zIndex); for (const layer of layers) { - this.layers.get(layer.id)?.konvaLayer.zIndex(++zIndex); + this.layers.get(layer.id)?.layer.zIndex(++zIndex); } for (const ca of controlAdapters) { - this.controlAdapters.get(ca.id)?.konvaLayer.zIndex(++zIndex); + this.controlAdapters.get(ca.id)?.layer.zIndex(++zIndex); } for (const rg of regions) { - this.regions.get(rg.id)?.konvaLayer.zIndex(++zIndex); + this.regions.get(rg.id)?.layer.zIndex(++zIndex); } - this.inpaintMask?.konvaLayer.zIndex(++zIndex); - this.preview.konvaLayer.zIndex(++zIndex); + this.inpaintMask?.layer.zIndex(++zIndex); + this.preview.layer.zIndex(++zIndex); } renderDocumentSizeOverlay() { @@ -297,8 +297,8 @@ export class KonvaNodeManager { } getInpaintMaskLayerClone(): Konva.Layer { - const layerClone = this.inpaintMask.konvaLayer.clone(); - const objectGroupClone = this.inpaintMask.konvaObjectGroup.clone(); + const layerClone = this.inpaintMask.layer.clone(); + const objectGroupClone = this.inpaintMask.group.clone(); layerClone.destroyChildren(); layerClone.add(objectGroupClone); @@ -315,8 +315,8 @@ export class KonvaNodeManager { const canvasRegion = this.regions.get(id); assert(canvasRegion, `Canvas region with id ${id} not found`); - const layerClone = canvasRegion.konvaLayer.clone(); - const objectGroupClone = canvasRegion.konvaObjectGroup.clone(); + const layerClone = canvasRegion.layer.clone(); + const objectGroupClone = canvasRegion.group.clone(); layerClone.destroyChildren(); layerClone.add(objectGroupClone); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts index a4d75214636..77dad03b27a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts @@ -29,14 +29,14 @@ const getGridSpacing = (scale: number): number => { }; export class CanvasBackground { - konvaLayer: Konva.Layer; + layer: Konva.Layer; constructor() { - this.konvaLayer = new Konva.Layer({ listening: false }); + this.layer = new Konva.Layer({ listening: false }); } renderBackground(stage: Konva.Stage): void { - this.konvaLayer.zIndex(0); + this.layer.zIndex(0); const scale = stage.scaleX(); const gridSpacing = getGridSpacing(scale); const x = stage.x(); @@ -80,11 +80,11 @@ export class CanvasBackground { let _x = 0; let _y = 0; - this.konvaLayer.destroyChildren(); + this.layer.destroyChildren(); for (let i = 0; i < xSteps; i++) { _x = gridFullRect.x1 + i * gridSpacing; - this.konvaLayer.add( + this.layer.add( new Konva.Line({ x: _x, y: gridFullRect.y1, @@ -97,7 +97,7 @@ export class CanvasBackground { } for (let i = 0; i < ySteps; i++) { _y = gridFullRect.y1 + i * gridSpacing; - this.konvaLayer.add( + this.layer.add( new Konva.Line({ x: gridFullRect.x1, y: _y, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts index a63ff802697..13f2b3a899c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts @@ -9,31 +9,31 @@ import { KonvaImage } from './objects'; export class CanvasControlAdapter { id: string; - konvaLayer: Konva.Layer; - konvaObjectGroup: Konva.Group; - konvaImageObject: KonvaImage | null; + layer: Konva.Layer; + group: Konva.Group; + image: KonvaImage | null; constructor(entity: ControlAdapterEntity) { const { id } = entity; this.id = id; - this.konvaLayer = new Konva.Layer({ + this.layer = new Konva.Layer({ id, imageSmoothingEnabled: false, listening: false, }); - this.konvaObjectGroup = new Konva.Group({ - id: getObjectGroupId(this.konvaLayer.id(), uuidv4()), + this.group = new Konva.Group({ + id: getObjectGroupId(this.layer.id(), uuidv4()), listening: false, }); - this.konvaLayer.add(this.konvaObjectGroup); - this.konvaImageObject = null; + this.layer.add(this.group); + this.image = null; } async render(entity: ControlAdapterEntity) { const imageObject = entity.processedImageObject ?? entity.imageObject; if (!imageObject) { - if (this.konvaImageObject) { - this.konvaImageObject.destroy(); + if (this.image) { + this.image.destroy(); } return; } @@ -42,8 +42,8 @@ export class CanvasControlAdapter { const visible = entity.isEnabled; const filters = entity.filter === 'LightnessToAlphaFilter' ? [LightnessToAlphaFilter] : []; - if (!this.konvaImageObject) { - this.konvaImageObject = await new KonvaImage({ + if (!this.image) { + this.image = await new KonvaImage({ imageObject, onLoad: (konvaImage) => { konvaImage.filters(filters); @@ -52,25 +52,25 @@ export class CanvasControlAdapter { konvaImage.visible(visible); }, }); - this.konvaObjectGroup.add(this.konvaImageObject.konvaImageGroup); + this.group.add(this.image.konvaImageGroup); } - if (this.konvaImageObject.isLoading || this.konvaImageObject.isError) { + if (this.image.isLoading || this.image.isError) { return; } - if (this.konvaImageObject.imageName !== imageObject.image.name) { - this.konvaImageObject.updateImageSource(imageObject.image.name); + if (this.image.imageName !== imageObject.image.name) { + this.image.updateImageSource(imageObject.image.name); } - if (this.konvaImageObject.konvaImage) { - if (!isEqual(this.konvaImageObject.konvaImage.filters(), filters)) { - this.konvaImageObject.konvaImage.filters(filters); - this.konvaImageObject.konvaImage.cache(); + if (this.image.konvaImage) { + if (!isEqual(this.image.konvaImage.filters(), filters)) { + this.image.konvaImage.filters(filters); + this.image.konvaImage.cache(); } - this.konvaImageObject.konvaImage.opacity(opacity); - this.konvaImageObject.konvaImage.visible(visible); + this.image.konvaImage.opacity(opacity); + this.image.konvaImage.visible(visible); } } destroy(): void { - this.konvaLayer.destroy(); + this.layer.destroy(); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts index db4f7b6cbfc..eb7be067426 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts @@ -11,15 +11,15 @@ import { v4 as uuidv4 } from 'uuid'; export class CanvasInpaintMask { id: string; - konvaLayer: Konva.Layer; - konvaObjectGroup: Konva.Group; + layer: Konva.Layer; + group: Konva.Group; compositingRect: Konva.Rect; objects: Map; constructor(entity: InpaintMaskEntity, onPosChanged: StateApi['onPosChanged']) { this.id = entity.id; - this.konvaLayer = new Konva.Layer({ + this.layer = new Konva.Layer({ id: entity.id, draggable: true, dragDistance: 0, @@ -27,21 +27,21 @@ export class CanvasInpaintMask { // 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. - this.konvaLayer.on('dragend', function (e) { + this.layer.on('dragend', function (e) { onPosChanged({ id: entity.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'inpaint_mask'); }); - this.konvaObjectGroup = new Konva.Group({ - id: getObjectGroupId(this.konvaLayer.id(), uuidv4()), + this.group = new Konva.Group({ + id: getObjectGroupId(this.layer.id(), uuidv4()), listening: false, }); - this.konvaLayer.add(this.konvaObjectGroup); + this.layer.add(this.group); this.compositingRect = new Konva.Rect({ listening: false }); - this.konvaLayer.add(this.compositingRect); + this.layer.add(this.compositingRect); this.objects = new Map(); } destroy(): void { - this.konvaLayer.destroy(); + this.layer.destroy(); } async render( @@ -51,7 +51,7 @@ export class CanvasInpaintMask { maskOpacity: number ) { // Update the layer's position and listening state - this.konvaLayer.setAttrs({ + this.layer.setAttrs({ listening: selectedTool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events x: Math.floor(inpaintMaskState.x), y: Math.floor(inpaintMaskState.y), @@ -81,7 +81,7 @@ export class CanvasInpaintMask { if (!brushLine) { brushLine = new KonvaBrushLine({ brushLine: obj }); this.objects.set(brushLine.id, brushLine); - this.konvaObjectGroup.add(brushLine.konvaLineGroup); + this.group.add(brushLine.konvaLineGroup); groupNeedsCache = true; } @@ -96,7 +96,7 @@ export class CanvasInpaintMask { if (!eraserLine) { eraserLine = new KonvaEraserLine({ eraserLine: obj }); this.objects.set(eraserLine.id, eraserLine); - this.konvaObjectGroup.add(eraserLine.konvaLineGroup); + this.group.add(eraserLine.konvaLineGroup); groupNeedsCache = true; } @@ -111,37 +111,37 @@ export class CanvasInpaintMask { if (!rect) { rect = new KonvaRect({ rectShape: obj }); this.objects.set(rect.id, rect); - this.konvaObjectGroup.add(rect.konvaRect); + this.group.add(rect.konvaRect); groupNeedsCache = true; } } } // Only update layer visibility if it has changed. - if (this.konvaLayer.visible() !== inpaintMaskState.isEnabled) { - this.konvaLayer.visible(inpaintMaskState.isEnabled); + if (this.layer.visible() !== inpaintMaskState.isEnabled) { + this.layer.visible(inpaintMaskState.isEnabled); groupNeedsCache = true; } if (this.objects.size === 0) { // No objects - clear the cache to reset the previous pixel data - this.konvaObjectGroup.clearCache(); + this.group.clearCache(); return; } // We must clear the cache first so Konva will re-draw the group with the new compositing rect - if (this.konvaObjectGroup.isCached()) { - this.konvaObjectGroup.clearCache(); + if (this.group.isCached()) { + this.group.clearCache(); } // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work - this.konvaObjectGroup.opacity(1); + this.group.opacity(1); this.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 ...(!inpaintMaskState.bboxNeedsUpdate && inpaintMaskState.bbox ? inpaintMaskState.bbox - : getLayerBboxFast(this.konvaLayer)), + : getLayerBboxFast(this.layer)), fill: rgbColor, opacity: maskOpacity, // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts index c216f978b27..c34155648e6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -9,14 +9,14 @@ import { v4 as uuidv4 } from 'uuid'; export class CanvasLayer { id: string; - konvaLayer: Konva.Layer; - konvaObjectGroup: Konva.Group; + layer: Konva.Layer; + group: Konva.Group; objects: Map; constructor(entity: LayerEntity, onPosChanged: StateApi['onPosChanged']) { this.id = entity.id; - this.konvaLayer = new Konva.Layer({ + this.layer = new Konva.Layer({ id: entity.id, draggable: true, dragDistance: 0, @@ -24,25 +24,25 @@ export class CanvasLayer { // 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. - this.konvaLayer.on('dragend', function (e) { + this.layer.on('dragend', function (e) { onPosChanged({ id: entity.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'layer'); }); - const konvaObjectGroup = new Konva.Group({ - id: getObjectGroupId(this.konvaLayer.id(), uuidv4()), + const group = new Konva.Group({ + id: getObjectGroupId(this.layer.id(), uuidv4()), listening: false, }); - this.konvaObjectGroup = konvaObjectGroup; - this.konvaLayer.add(this.konvaObjectGroup); + this.group = group; + this.layer.add(this.group); this.objects = new Map(); } destroy(): void { - this.konvaLayer.destroy(); + this.layer.destroy(); } async render(layerState: LayerEntity, selectedTool: Tool) { // Update the layer's position and listening state - this.konvaLayer.setAttrs({ + this.layer.setAttrs({ listening: selectedTool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events x: Math.floor(layerState.x), y: Math.floor(layerState.y), @@ -65,7 +65,7 @@ export class CanvasLayer { if (!brushLine) { brushLine = new KonvaBrushLine({ brushLine: obj }); this.objects.set(brushLine.id, brushLine); - this.konvaObjectGroup.add(brushLine.konvaLineGroup); + this.group.add(brushLine.konvaLineGroup); } if (obj.points.length !== brushLine.konvaLine.points().length) { brushLine.konvaLine.points(obj.points); @@ -77,7 +77,7 @@ export class CanvasLayer { if (!eraserLine) { eraserLine = new KonvaEraserLine({ eraserLine: obj }); this.objects.set(eraserLine.id, eraserLine); - this.konvaObjectGroup.add(eraserLine.konvaLineGroup); + this.group.add(eraserLine.konvaLineGroup); } if (obj.points.length !== eraserLine.konvaLine.points().length) { eraserLine.konvaLine.points(obj.points); @@ -89,7 +89,7 @@ export class CanvasLayer { if (!rect) { rect = new KonvaRect({ rectShape: obj }); this.objects.set(rect.id, rect); - this.konvaObjectGroup.add(rect.konvaRect); + this.group.add(rect.konvaRect); } } else if (obj.type === 'image') { let image = this.objects.get(obj.id); @@ -98,7 +98,7 @@ export class CanvasLayer { if (!image) { image = await new KonvaImage({ imageObject: obj }); this.objects.set(image.id, image); - this.konvaObjectGroup.add(image.konvaImageGroup); + this.group.add(image.konvaImageGroup); } if (image.imageName !== obj.image.name) { image.updateImageSource(obj.image.name); @@ -107,8 +107,8 @@ export class CanvasLayer { } // Only update layer visibility if it has changed. - if (this.konvaLayer.visible() !== layerState.isEnabled) { - this.konvaLayer.visible(layerState.isEnabled); + if (this.layer.visible() !== layerState.isEnabled) { + this.layer.visible(layerState.isEnabled); } // const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer); @@ -127,6 +127,6 @@ export class CanvasLayer { // } else { // bboxRect.visible(false); // } - this.konvaObjectGroup.opacity(layerState.opacity); + this.group.opacity(layerState.opacity); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts index 871e21353e4..17051e79cf1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts @@ -6,7 +6,7 @@ import type { CanvasStagingArea } from './stagingArea'; import type { CanvasTool } from './tool'; export class CanvasPreview { - konvaLayer: Konva.Layer; + layer: Konva.Layer; tool: CanvasTool; bbox: CanvasBbox; documentSizeOverlay: CanvasDocumentSizeOverlay; @@ -18,18 +18,18 @@ export class CanvasPreview { documentSizeOverlay: CanvasDocumentSizeOverlay, stagingArea: CanvasStagingArea ) { - this.konvaLayer = new Konva.Layer({ listening: true }); + this.layer = new Konva.Layer({ listening: true }); this.bbox = bbox; - this.konvaLayer.add(this.bbox.group); + this.layer.add(this.bbox.group); this.tool = tool; - this.konvaLayer.add(this.tool.group); + this.layer.add(this.tool.group); this.documentSizeOverlay = documentSizeOverlay; - this.konvaLayer.add(this.documentSizeOverlay.group); + this.layer.add(this.documentSizeOverlay.group); this.stagingArea = stagingArea; - this.konvaLayer.add(this.stagingArea.group); + this.layer.add(this.stagingArea.group); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts index 7f6ebd5ddb3..be6be27699b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts @@ -11,15 +11,15 @@ import { v4 as uuidv4 } from 'uuid'; export class CanvasRegion { id: string; - konvaLayer: Konva.Layer; - konvaObjectGroup: Konva.Group; + layer: Konva.Layer; + group: Konva.Group; compositingRect: Konva.Rect; objects: Map; constructor(entity: RegionEntity, onPosChanged: StateApi['onPosChanged']) { this.id = entity.id; - this.konvaLayer = new Konva.Layer({ + this.layer = new Konva.Layer({ id: entity.id, draggable: true, dragDistance: 0, @@ -27,21 +27,21 @@ export class CanvasRegion { // 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. - this.konvaLayer.on('dragend', function (e) { + this.layer.on('dragend', function (e) { onPosChanged({ id: entity.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'regional_guidance'); }); - this.konvaObjectGroup = new Konva.Group({ - id: getObjectGroupId(this.konvaLayer.id(), uuidv4()), + this.group = new Konva.Group({ + id: getObjectGroupId(this.layer.id(), uuidv4()), listening: false, }); - this.konvaLayer.add(this.konvaObjectGroup); + this.layer.add(this.group); this.compositingRect = new Konva.Rect({ listening: false }); - this.konvaLayer.add(this.compositingRect); + this.layer.add(this.compositingRect); this.objects = new Map(); } destroy(): void { - this.konvaLayer.destroy(); + this.layer.destroy(); } async render( @@ -51,7 +51,7 @@ export class CanvasRegion { maskOpacity: number ) { // Update the layer's position and listening state - this.konvaLayer.setAttrs({ + this.layer.setAttrs({ listening: selectedTool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events x: Math.floor(regionState.x), y: Math.floor(regionState.y), @@ -81,7 +81,7 @@ export class CanvasRegion { if (!brushLine) { brushLine = new KonvaBrushLine({ brushLine: obj }); this.objects.set(brushLine.id, brushLine); - this.konvaObjectGroup.add(brushLine.konvaLineGroup); + this.group.add(brushLine.konvaLineGroup); groupNeedsCache = true; } @@ -96,7 +96,7 @@ export class CanvasRegion { if (!eraserLine) { eraserLine = new KonvaEraserLine({ eraserLine: obj }); this.objects.set(eraserLine.id, eraserLine); - this.konvaObjectGroup.add(eraserLine.konvaLineGroup); + this.group.add(eraserLine.konvaLineGroup); groupNeedsCache = true; } @@ -111,34 +111,34 @@ export class CanvasRegion { if (!rect) { rect = new KonvaRect({ rectShape: obj }); this.objects.set(rect.id, rect); - this.konvaObjectGroup.add(rect.konvaRect); + this.group.add(rect.konvaRect); groupNeedsCache = true; } } } // Only update layer visibility if it has changed. - if (this.konvaLayer.visible() !== regionState.isEnabled) { - this.konvaLayer.visible(regionState.isEnabled); + if (this.layer.visible() !== regionState.isEnabled) { + this.layer.visible(regionState.isEnabled); groupNeedsCache = true; } if (this.objects.size === 0) { // No objects - clear the cache to reset the previous pixel data - this.konvaObjectGroup.clearCache(); + this.group.clearCache(); return; } // We must clear the cache first so Konva will re-draw the group with the new compositing rect - if (this.konvaObjectGroup.isCached()) { - this.konvaObjectGroup.clearCache(); + if (this.group.isCached()) { + this.group.clearCache(); } // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work - this.konvaObjectGroup.opacity(1); + this.group.opacity(1); this.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 - ...(!regionState.bboxNeedsUpdate && regionState.bbox ? regionState.bbox : getLayerBboxFast(this.konvaLayer)), + ...(!regionState.bboxNeedsUpdate && regionState.bbox ? regionState.bbox : getLayerBboxFast(this.layer)), fill: rgbColor, opacity: maskOpacity, // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) From 89740af2ab843760fdef6e60a07fbf0fc3f952eb Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 26 Jun 2024 21:25:54 +1000 Subject: [PATCH 132/678] fix(ui): do not select already-selected entity --- .../components/common/CanvasEntityContainer.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx index 700c1669bf6..1cc72218310 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx @@ -1,7 +1,7 @@ import type { ChakraProps } from '@invoke-ai/ui-library'; import { Flex } from '@invoke-ai/ui-library'; import type { PropsWithChildren } from 'react'; -import { memo, useMemo } from 'react'; +import { memo, useCallback, useMemo } from 'react'; type Props = PropsWithChildren<{ isSelected: boolean; @@ -16,11 +16,18 @@ export const CanvasEntityContainer = memo(({ isSelected, onSelect, selectedBorde } return 'base.800'; }, [isSelected, selectedBorderColor]); + const _onSelect = useCallback(() => { + if (isSelected) { + return; + } + onSelect(); + }, [isSelected, onSelect]); + return ( Date: Fri, 28 Jun 2024 18:11:26 +1000 Subject: [PATCH 133/678] tidy(ui): remove old canvas graphs --- .../canvas/addControlNetToLinearGraph.ts | 150 ---- .../graph/canvas/addIPAdapterToLinearGraph.ts | 109 --- .../util/graph/canvas/addLoRAsToGraph.ts | 158 ----- .../graph/canvas/addNSFWCheckerToGraph.ts | 39 -- .../util/graph/canvas/addSDXLLoRAstoGraph.ts | 208 ------ .../graph/canvas/addSDXLRefinerToGraph.ts | 239 ------- .../graph/canvas/addSeamlessToLinearGraph.ts | 102 --- .../canvas/addT2IAdapterToLinearGraph.ts | 140 ---- .../nodes/util/graph/canvas/addVAEToGraph.ts | 175 ----- .../graph/canvas/addWatermarkerToGraph.ts | 60 -- .../util/graph/canvas/buildCanvasGraph.ts | 57 -- .../canvas/buildCanvasImageToImageGraph.ts | 374 ---------- .../graph/canvas/buildCanvasInpaintGraph.ts | 469 ------------- .../graph/canvas/buildCanvasOutpaintGraph.ts | 629 ----------------- .../buildCanvasSDXLImageToImageGraph.ts | 382 ----------- .../canvas/buildCanvasSDXLInpaintGraph.ts | 487 ------------- .../canvas/buildCanvasSDXLOutpaintGraph.ts | 644 ------------------ .../canvas/buildCanvasSDXLTextToImageGraph.ts | 341 ---------- .../canvas/buildCanvasTextToImageGraph.ts | 324 --------- .../nodes/util/graph/canvas/metadata.ts | 66 -- 20 files changed, 5153 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/canvas/addControlNetToLinearGraph.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/canvas/addIPAdapterToLinearGraph.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/canvas/addLoRAsToGraph.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/canvas/addNSFWCheckerToGraph.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSDXLLoRAstoGraph.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSDXLRefinerToGraph.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSeamlessToLinearGraph.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/canvas/addT2IAdapterToLinearGraph.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/canvas/addVAEToGraph.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/canvas/addWatermarkerToGraph.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasGraph.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasImageToImageGraph.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasInpaintGraph.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasOutpaintGraph.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLImageToImageGraph.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLInpaintGraph.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLOutpaintGraph.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLTextToImageGraph.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasTextToImageGraph.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/canvas/metadata.ts diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addControlNetToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addControlNetToLinearGraph.ts deleted file mode 100644 index 4558b5a0d82..00000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addControlNetToLinearGraph.ts +++ /dev/null @@ -1,150 +0,0 @@ -import type { RootState } from 'app/store/store'; -import { selectValidControlNets } from 'features/controlAdapters/store/controlAdaptersSlice'; -import type { ControlAdapterProcessorType, ControlNetConfig } from 'features/controlAdapters/store/types'; -import type { ImageField } from 'features/nodes/types/common'; -import { upsertMetadata } from 'features/nodes/util/graph/canvas/metadata'; -import { CONTROL_NET_COLLECT } from 'features/nodes/util/graph/constants'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import type { Invocation, NonNullableGraph, S } from 'services/api/types'; -import { assert } from 'tsafe'; - -export const addControlNetToLinearGraph = async ( - state: RootState, - graph: NonNullableGraph, - baseNodeId: string -): Promise => { - const controlNetMetadata: S['CoreMetadataInvocation']['controlnets'] = []; - const controlNets = selectValidControlNets(state.controlAdapters).filter( - ({ model, processedControlImage, processorType, controlImage, isEnabled }) => { - const hasModel = Boolean(model); - const doesBaseMatch = model?.base === state.canvasV2.params.model?.base; - const hasControlImage = (processedControlImage && processorType !== 'none') || controlImage; - - return isEnabled && hasModel && doesBaseMatch && hasControlImage; - } - ); - - // The generation tab has special handling - its control adapters are set up in the Control Layers graph helper. - const activeTabName = activeTabNameSelector(state); - assert(activeTabName !== 'generation', 'Tried to use addControlNetToLinearGraph on generation tab'); - - if (controlNets.length) { - // Even though denoise_latents' control input is SINGLE_OR_COLLECTION, keep it simple and always use a collect - const controlNetIterateNode: Invocation<'collect'> = { - id: CONTROL_NET_COLLECT, - type: 'collect', - is_intermediate: true, - }; - graph.nodes[CONTROL_NET_COLLECT] = controlNetIterateNode; - graph.edges.push({ - source: { node_id: CONTROL_NET_COLLECT, field: 'collection' }, - destination: { - node_id: baseNodeId, - field: 'control', - }, - }); - - for (const controlNet of controlNets) { - if (!controlNet.model) { - return; - } - const { - id, - controlImage, - processedControlImage, - beginStepPct, - endStepPct, - controlMode, - resizeMode, - model, - processorType, - weight, - } = controlNet; - - const controlNetNode: Invocation<'controlnet'> = { - id: `control_net_${id}`, - type: 'controlnet', - is_intermediate: true, - begin_step_percent: beginStepPct, - end_step_percent: endStepPct, - control_mode: controlMode, - resize_mode: resizeMode, - control_model: model, - control_weight: weight, - image: buildControlImage(controlImage, processedControlImage, processorType), - }; - - graph.nodes[controlNetNode.id] = controlNetNode; - - controlNetMetadata.push(buildControlNetMetadata(controlNet)); - - graph.edges.push({ - source: { node_id: controlNetNode.id, field: 'control' }, - destination: { - node_id: CONTROL_NET_COLLECT, - field: 'item', - }, - }); - } - upsertMetadata(graph, { controlnets: controlNetMetadata }); - } -}; - -const buildControlImage = ( - controlImage: string | null, - processedControlImage: string | null, - processorType: ControlAdapterProcessorType -): ImageField => { - let image: ImageField | null = null; - if (processedControlImage && processorType !== 'none') { - // We've already processed the image in the app, so we can just use the processed image - image = { - image_name: processedControlImage, - }; - } else if (controlImage) { - // The control image is preprocessed - image = { - image_name: controlImage, - }; - } - assert(image, 'ControlNet image is required'); - return image; -}; - -const buildControlNetMetadata = (controlNet: ControlNetConfig): S['ControlNetMetadataField'] => { - const { - controlImage, - processedControlImage, - beginStepPct, - endStepPct, - controlMode, - resizeMode, - model, - processorType, - weight, - } = controlNet; - - assert(model, 'ControlNet model is required'); - - const processed_image = - processedControlImage && processorType !== 'none' - ? { - image_name: processedControlImage, - } - : null; - - assert(controlImage, 'ControlNet image is required'); - - return { - control_model: model, - control_weight: weight, - control_mode: controlMode, - begin_step_percent: beginStepPct, - end_step_percent: endStepPct, - resize_mode: resizeMode, - image: { - image_name: controlImage, - }, - processed_image, - }; -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addIPAdapterToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addIPAdapterToLinearGraph.ts deleted file mode 100644 index fc279c2c874..00000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addIPAdapterToLinearGraph.ts +++ /dev/null @@ -1,109 +0,0 @@ -import type { RootState } from 'app/store/store'; -import { selectValidIPAdapters } from 'features/controlAdapters/store/controlAdaptersSlice'; -import type { IPAdapterConfig } from 'features/controlAdapters/store/types'; -import type { ImageField } from 'features/nodes/types/common'; -import { upsertMetadata } from 'features/nodes/util/graph/canvas/metadata'; -import { IP_ADAPTER_COLLECT } from 'features/nodes/util/graph/constants'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import type { Invocation, NonNullableGraph, S } from 'services/api/types'; -import { assert } from 'tsafe'; - -export const addIPAdapterToLinearGraph = async ( - state: RootState, - graph: NonNullableGraph, - baseNodeId: string -): Promise => { - // The generation tab has special handling - its control adapters are set up in the Control Layers graph helper. - const activeTabName = activeTabNameSelector(state); - assert(activeTabName !== 'generation', 'Tried to use addT2IAdaptersToLinearGraph on generation tab'); - - const ipAdapters = selectValidIPAdapters(state.controlAdapters).filter(({ model, controlImage, isEnabled }) => { - const hasModel = Boolean(model); - const doesBaseMatch = model?.base === state.canvasV2.params.model?.base; - const hasControlImage = controlImage; - return isEnabled && hasModel && doesBaseMatch && hasControlImage; - }); - - if (ipAdapters.length) { - // Even though denoise_latents' ip adapter input is SINGLE_OR_COLLECTION, keep it simple and always use a collect - const ipAdapterCollectNode: Invocation<'collect'> = { - id: IP_ADAPTER_COLLECT, - type: 'collect', - is_intermediate: true, - }; - graph.nodes[IP_ADAPTER_COLLECT] = ipAdapterCollectNode; - graph.edges.push({ - source: { node_id: IP_ADAPTER_COLLECT, field: 'collection' }, - destination: { - node_id: baseNodeId, - field: 'ip_adapter', - }, - }); - - const ipAdapterMetdata: S['CoreMetadataInvocation']['ipAdapters'] = []; - - for (const ipAdapter of ipAdapters) { - if (!ipAdapter.model) { - return; - } - const { id, weight, model, clipVisionModel, method, beginStepPct, endStepPct, controlImage } = ipAdapter; - - assert(controlImage, 'IP Adapter image is required'); - - const ipAdapterNode: Invocation<'ip_adapter'> = { - id: `ip_adapter_${id}`, - type: 'ip_adapter', - is_intermediate: true, - weight: weight, - method: method, - ip_adapter_model: model, - clip_vision_model: clipVisionModel, - begin_step_percent: beginStepPct, - end_step_percent: endStepPct, - image: { - image_name: controlImage, - }, - }; - - graph.nodes[ipAdapterNode.id] = ipAdapterNode; - - ipAdapterMetdata.push(buildIPAdapterMetadata(ipAdapter)); - - graph.edges.push({ - source: { node_id: ipAdapterNode.id, field: 'ip_adapter' }, - destination: { - node_id: ipAdapterCollectNode.id, - field: 'item', - }, - }); - } - - upsertMetadata(graph, { ipAdapters: ipAdapterMetdata }); - } -}; - -const buildIPAdapterMetadata = (ipAdapter: IPAdapterConfig): S['IPAdapterMetadataField'] => { - const { controlImage, beginStepPct, endStepPct, model, clipVisionModel, method, weight } = ipAdapter; - - assert(model, 'IP Adapter model is required'); - - let image: ImageField | null = null; - - if (controlImage) { - image = { - image_name: controlImage, - }; - } - - assert(image, 'IP Adapter image is required'); - - return { - ip_adapter_model: model, - clip_vision_model: clipVisionModel, - weight, - method, - begin_step_percent: beginStepPct, - end_step_percent: endStepPct, - image, - }; -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addLoRAsToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addLoRAsToGraph.ts deleted file mode 100644 index 6c4ac9fc695..00000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addLoRAsToGraph.ts +++ /dev/null @@ -1,158 +0,0 @@ -import type { RootState } from 'app/store/store'; -import { zModelIdentifierField } from 'features/nodes/types/common'; -import { upsertMetadata } from 'features/nodes/util/graph/canvas/metadata'; -import { - CLIP_SKIP, - LORA_LOADER, - MAIN_MODEL_LOADER, - NEGATIVE_CONDITIONING, - POSITIVE_CONDITIONING, -} from 'features/nodes/util/graph/constants'; -import { filter, size } from 'lodash-es'; -import type { Invocation, NonNullableGraph, S } from 'services/api/types'; - -export const addLoRAsToGraph = async ( - state: RootState, - graph: NonNullableGraph, - baseNodeId: string, - modelLoaderNodeId: string = MAIN_MODEL_LOADER -): Promise => { - /** - * LoRA nodes get the UNet and CLIP models from the main model loader and apply the LoRA to them. - * They then output the UNet and CLIP models references on to either the next LoRA in the chain, - * or to the inference/conditioning nodes. - * - * So we need to inject a LoRA chain into the graph. - */ - - // TODO(MM2): check base model - const enabledLoRAs = filter(state.lora.loras, (l) => l.isEnabled ?? false); - const loraCount = size(enabledLoRAs); - - if (loraCount === 0) { - return; - } - - // Remove modelLoaderNodeId unet connection to feed it to LoRAs - graph.edges = graph.edges.filter( - (e) => !(e.source.node_id === modelLoaderNodeId && ['unet'].includes(e.source.field)) - ); - // Remove CLIP_SKIP connections to conditionings to feed it through LoRAs - graph.edges = graph.edges.filter((e) => !(e.source.node_id === CLIP_SKIP && ['clip'].includes(e.source.field))); - - // we need to remember the last lora so we can chain from it - let lastLoraNodeId = ''; - let currentLoraIndex = 0; - const loraMetadata: S['CoreMetadataInvocation']['loras'] = []; - - enabledLoRAs.forEach(async (lora) => { - const { weight } = lora; - const { key } = lora.model; - const currentLoraNodeId = `${LORA_LOADER}_${key}`; - const parsedModel = zModelIdentifierField.parse(lora.model); - - const loraLoaderNode: Invocation<'lora_loader'> = { - type: 'lora_loader', - id: currentLoraNodeId, - is_intermediate: true, - lora: parsedModel, - weight, - }; - - loraMetadata.push({ - model: parsedModel, - weight, - }); - - // add to graph - graph.nodes[currentLoraNodeId] = loraLoaderNode; - if (currentLoraIndex === 0) { - // first lora = start the lora chain, attach directly to model loader - graph.edges.push({ - source: { - node_id: modelLoaderNodeId, - field: 'unet', - }, - destination: { - node_id: currentLoraNodeId, - field: 'unet', - }, - }); - - graph.edges.push({ - source: { - node_id: CLIP_SKIP, - field: 'clip', - }, - destination: { - node_id: currentLoraNodeId, - field: 'clip', - }, - }); - } else { - // we are in the middle of the lora chain, instead connect to the previous lora - graph.edges.push({ - source: { - node_id: lastLoraNodeId, - field: 'unet', - }, - destination: { - node_id: currentLoraNodeId, - field: 'unet', - }, - }); - graph.edges.push({ - source: { - node_id: lastLoraNodeId, - field: 'clip', - }, - destination: { - node_id: currentLoraNodeId, - field: 'clip', - }, - }); - } - - if (currentLoraIndex === loraCount - 1) { - // final lora, end the lora chain - we need to connect up to inference and conditioning nodes - graph.edges.push({ - source: { - node_id: currentLoraNodeId, - field: 'unet', - }, - destination: { - node_id: baseNodeId, - field: 'unet', - }, - }); - - graph.edges.push({ - source: { - node_id: currentLoraNodeId, - field: 'clip', - }, - destination: { - node_id: POSITIVE_CONDITIONING, - field: 'clip', - }, - }); - - graph.edges.push({ - source: { - node_id: currentLoraNodeId, - field: 'clip', - }, - destination: { - node_id: NEGATIVE_CONDITIONING, - field: 'clip', - }, - }); - } - - // increment the lora for the next one in the chain - lastLoraNodeId = currentLoraNodeId; - currentLoraIndex += 1; - }); - - upsertMetadata(graph, { loras: loraMetadata }); -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addNSFWCheckerToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addNSFWCheckerToGraph.ts deleted file mode 100644 index 56bc3cc2adc..00000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addNSFWCheckerToGraph.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { RootState } from 'app/store/store'; -import { LATENTS_TO_IMAGE, NSFW_CHECKER } from 'features/nodes/util/graph/constants'; -import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; -import type { Invocation, NonNullableGraph } from 'services/api/types'; - -export const addNSFWCheckerToGraph = ( - state: RootState, - graph: NonNullableGraph, - nodeIdToAddTo = LATENTS_TO_IMAGE -): void => { - const nodeToAddTo = graph.nodes[nodeIdToAddTo] as Invocation<'l2i'> | undefined; - - if (!nodeToAddTo) { - // something has gone terribly awry - return; - } - - nodeToAddTo.is_intermediate = true; - nodeToAddTo.use_cache = true; - - const nsfwCheckerNode: Invocation<'img_nsfw'> = { - id: NSFW_CHECKER, - type: 'img_nsfw', - is_intermediate: getIsIntermediate(state), - board: getBoardField(state), - }; - - graph.nodes[NSFW_CHECKER] = nsfwCheckerNode; - graph.edges.push({ - source: { - node_id: nodeIdToAddTo, - field: 'image', - }, - destination: { - node_id: NSFW_CHECKER, - field: 'image', - }, - }); -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSDXLLoRAstoGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSDXLLoRAstoGraph.ts deleted file mode 100644 index 695b7a73edd..00000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSDXLLoRAstoGraph.ts +++ /dev/null @@ -1,208 +0,0 @@ -import type { RootState } from 'app/store/store'; -import { zModelIdentifierField } from 'features/nodes/types/common'; -import { upsertMetadata } from 'features/nodes/util/graph/canvas/metadata'; -import { - LORA_LOADER, - NEGATIVE_CONDITIONING, - POSITIVE_CONDITIONING, - SDXL_MODEL_LOADER, - SDXL_REFINER_INPAINT_CREATE_MASK, - SEAMLESS, -} from 'features/nodes/util/graph/constants'; -import { filter, size } from 'lodash-es'; -import type { Invocation, NonNullableGraph, S } from 'services/api/types'; - -export const addSDXLLoRAsToGraph = async ( - state: RootState, - graph: NonNullableGraph, - baseNodeId: string, - modelLoaderNodeId: string = SDXL_MODEL_LOADER -): Promise => { - /** - * LoRA nodes get the UNet and CLIP models from the main model loader and apply the LoRA to them. - * They then output the UNet and CLIP models references on to either the next LoRA in the chain, - * or to the inference/conditioning nodes. - * - * So we need to inject a LoRA chain into the graph. - */ - - // TODO(MM2): check base model - const enabledLoRAs = filter(state.lora.loras, (l) => l.isEnabled ?? false); - const loraCount = size(enabledLoRAs); - - if (loraCount === 0) { - return; - } - - const loraMetadata: S['CoreMetadataInvocation']['loras'] = []; - - // Handle Seamless Plugs - const unetLoaderId = modelLoaderNodeId; - let clipLoaderId = modelLoaderNodeId; - if ([SEAMLESS, SDXL_REFINER_INPAINT_CREATE_MASK].includes(modelLoaderNodeId)) { - clipLoaderId = SDXL_MODEL_LOADER; - } - - // Remove modelLoaderNodeId unet/clip/clip2 connections to feed it to LoRAs - graph.edges = graph.edges.filter( - (e) => - !(e.source.node_id === unetLoaderId && ['unet'].includes(e.source.field)) && - !(e.source.node_id === clipLoaderId && ['clip'].includes(e.source.field)) && - !(e.source.node_id === clipLoaderId && ['clip2'].includes(e.source.field)) - ); - - // we need to remember the last lora so we can chain from it - let lastLoraNodeId = ''; - let currentLoraIndex = 0; - - enabledLoRAs.forEach(async (lora) => { - const { weight } = lora; - const currentLoraNodeId = `${LORA_LOADER}_${lora.model.key}`; - const parsedModel = zModelIdentifierField.parse(lora.model); - - const loraLoaderNode: Invocation<'sdxl_lora_loader'> = { - type: 'sdxl_lora_loader', - id: currentLoraNodeId, - is_intermediate: true, - lora: parsedModel, - weight, - }; - - loraMetadata.push({ model: parsedModel, weight }); - - // add to graph - graph.nodes[currentLoraNodeId] = loraLoaderNode; - if (currentLoraIndex === 0) { - // first lora = start the lora chain, attach directly to model loader - graph.edges.push({ - source: { - node_id: unetLoaderId, - field: 'unet', - }, - destination: { - node_id: currentLoraNodeId, - field: 'unet', - }, - }); - - graph.edges.push({ - source: { - node_id: clipLoaderId, - field: 'clip', - }, - destination: { - node_id: currentLoraNodeId, - field: 'clip', - }, - }); - - graph.edges.push({ - source: { - node_id: clipLoaderId, - field: 'clip2', - }, - destination: { - node_id: currentLoraNodeId, - field: 'clip2', - }, - }); - } else { - // we are in the middle of the lora chain, instead connect to the previous lora - graph.edges.push({ - source: { - node_id: lastLoraNodeId, - field: 'unet', - }, - destination: { - node_id: currentLoraNodeId, - field: 'unet', - }, - }); - graph.edges.push({ - source: { - node_id: lastLoraNodeId, - field: 'clip', - }, - destination: { - node_id: currentLoraNodeId, - field: 'clip', - }, - }); - - graph.edges.push({ - source: { - node_id: lastLoraNodeId, - field: 'clip2', - }, - destination: { - node_id: currentLoraNodeId, - field: 'clip2', - }, - }); - } - - if (currentLoraIndex === loraCount - 1) { - // final lora, end the lora chain - we need to connect up to inference and conditioning nodes - graph.edges.push({ - source: { - node_id: currentLoraNodeId, - field: 'unet', - }, - destination: { - node_id: baseNodeId, - field: 'unet', - }, - }); - - graph.edges.push({ - source: { - node_id: currentLoraNodeId, - field: 'clip', - }, - destination: { - node_id: POSITIVE_CONDITIONING, - field: 'clip', - }, - }); - - graph.edges.push({ - source: { - node_id: currentLoraNodeId, - field: 'clip', - }, - destination: { - node_id: NEGATIVE_CONDITIONING, - field: 'clip', - }, - }); - - graph.edges.push({ - source: { - node_id: currentLoraNodeId, - field: 'clip2', - }, - destination: { - node_id: POSITIVE_CONDITIONING, - field: 'clip2', - }, - }); - - graph.edges.push({ - source: { - node_id: currentLoraNodeId, - field: 'clip2', - }, - destination: { - node_id: NEGATIVE_CONDITIONING, - field: 'clip2', - }, - }); - } - - // increment the lora for the next one in the chain - lastLoraNodeId = currentLoraNodeId; - currentLoraIndex += 1; - }); - - upsertMetadata(graph, { loras: loraMetadata }); -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSDXLRefinerToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSDXLRefinerToGraph.ts deleted file mode 100644 index 4a8572a69ff..00000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSDXLRefinerToGraph.ts +++ /dev/null @@ -1,239 +0,0 @@ -import type { RootState } from 'app/store/store'; -import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; -import { getModelMetadataField, upsertMetadata } from 'features/nodes/util/graph/canvas/metadata'; -import { - CANVAS_OUTPUT, - INPAINT_CREATE_MASK, - LATENTS_TO_IMAGE, - SDXL_CANVAS_IMAGE_TO_IMAGE_GRAPH, - SDXL_CANVAS_INPAINT_GRAPH, - SDXL_CANVAS_OUTPAINT_GRAPH, - SDXL_CANVAS_TEXT_TO_IMAGE_GRAPH, - SDXL_MODEL_LOADER, - SDXL_REFINER_DENOISE_LATENTS, - SDXL_REFINER_MODEL_LOADER, - SDXL_REFINER_NEGATIVE_CONDITIONING, - SDXL_REFINER_POSITIVE_CONDITIONING, - SDXL_REFINER_SEAMLESS, -} from 'features/nodes/util/graph/constants'; -import { getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils'; -import type { NonNullableGraph } from 'services/api/types'; -import { isRefinerMainModelModelConfig } from 'services/api/types'; - -export const addSDXLRefinerToGraph = async ( - state: RootState, - graph: NonNullableGraph, - baseNodeId: string, - modelLoaderNodeId?: string -): Promise => { - const { - refinerModel, - refinerPositiveAestheticScore, - refinerNegativeAestheticScore, - refinerSteps, - refinerScheduler, - refinerCFGScale, - refinerStart, - } = state.canvasV2.params; - - if (!refinerModel) { - return; - } - - const { seamlessXAxis, seamlessYAxis } = state.canvasV2.params; - const { boundingBoxScaleMethod } = state.canvas; - - const isUsingScaledDimensions = ['auto', 'manual'].includes(boundingBoxScaleMethod); - const modelConfig = await fetchModelConfigWithTypeGuard(refinerModel.key, isRefinerMainModelModelConfig); - - upsertMetadata(graph, { - refiner_model: getModelMetadataField(modelConfig), - refiner_positive_aesthetic_score: refinerPositiveAestheticScore, - refiner_negative_aesthetic_score: refinerNegativeAestheticScore, - refiner_cfg_scale: refinerCFGScale, - refiner_scheduler: refinerScheduler, - refiner_start: refinerStart, - refiner_steps: refinerSteps, - }); - - const modelLoaderId = modelLoaderNodeId ? modelLoaderNodeId : SDXL_MODEL_LOADER; - - // Construct Style Prompt - const { positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state); - - // Unplug SDXL Latents Generation To Latents To Image - graph.edges = graph.edges.filter((e) => !(e.source.node_id === baseNodeId && ['latents'].includes(e.source.field))); - - graph.edges = graph.edges.filter((e) => !(e.source.node_id === modelLoaderId && ['vae'].includes(e.source.field))); - - graph.nodes[SDXL_REFINER_MODEL_LOADER] = { - type: 'sdxl_refiner_model_loader', - id: SDXL_REFINER_MODEL_LOADER, - model: refinerModel, - }; - graph.nodes[SDXL_REFINER_POSITIVE_CONDITIONING] = { - type: 'sdxl_refiner_compel_prompt', - id: SDXL_REFINER_POSITIVE_CONDITIONING, - style: positiveStylePrompt, - aesthetic_score: refinerPositiveAestheticScore, - }; - graph.nodes[SDXL_REFINER_NEGATIVE_CONDITIONING] = { - type: 'sdxl_refiner_compel_prompt', - id: SDXL_REFINER_NEGATIVE_CONDITIONING, - style: negativeStylePrompt, - aesthetic_score: refinerNegativeAestheticScore, - }; - graph.nodes[SDXL_REFINER_DENOISE_LATENTS] = { - type: 'denoise_latents', - id: SDXL_REFINER_DENOISE_LATENTS, - cfg_scale: refinerCFGScale, - steps: refinerSteps, - scheduler: refinerScheduler, - denoising_start: refinerStart, - denoising_end: 1, - }; - - // Add Seamless To Refiner - if (seamlessXAxis || seamlessYAxis) { - graph.nodes[SDXL_REFINER_SEAMLESS] = { - id: SDXL_REFINER_SEAMLESS, - type: 'seamless', - seamless_x: seamlessXAxis, - seamless_y: seamlessYAxis, - }; - - graph.edges.push( - { - source: { - node_id: SDXL_REFINER_MODEL_LOADER, - field: 'unet', - }, - destination: { - node_id: SDXL_REFINER_SEAMLESS, - field: 'unet', - }, - }, - { - source: { - node_id: SDXL_REFINER_MODEL_LOADER, - field: 'vae', - }, - destination: { - node_id: SDXL_REFINER_SEAMLESS, - field: 'vae', - }, - }, - { - source: { - node_id: SDXL_REFINER_SEAMLESS, - field: 'unet', - }, - destination: { - node_id: SDXL_REFINER_DENOISE_LATENTS, - field: 'unet', - }, - } - ); - } else { - graph.edges.push({ - source: { - node_id: SDXL_REFINER_MODEL_LOADER, - field: 'unet', - }, - destination: { - node_id: SDXL_REFINER_DENOISE_LATENTS, - field: 'unet', - }, - }); - } - - graph.edges.push( - { - source: { - node_id: SDXL_REFINER_MODEL_LOADER, - field: 'clip2', - }, - destination: { - node_id: SDXL_REFINER_POSITIVE_CONDITIONING, - field: 'clip2', - }, - }, - { - source: { - node_id: SDXL_REFINER_MODEL_LOADER, - field: 'clip2', - }, - destination: { - node_id: SDXL_REFINER_NEGATIVE_CONDITIONING, - field: 'clip2', - }, - }, - { - source: { - node_id: SDXL_REFINER_POSITIVE_CONDITIONING, - field: 'conditioning', - }, - destination: { - node_id: SDXL_REFINER_DENOISE_LATENTS, - field: 'positive_conditioning', - }, - }, - { - source: { - node_id: SDXL_REFINER_NEGATIVE_CONDITIONING, - field: 'conditioning', - }, - destination: { - node_id: SDXL_REFINER_DENOISE_LATENTS, - field: 'negative_conditioning', - }, - }, - { - source: { - node_id: baseNodeId, - field: 'latents', - }, - destination: { - node_id: SDXL_REFINER_DENOISE_LATENTS, - field: 'latents', - }, - } - ); - - if (graph.id === SDXL_CANVAS_INPAINT_GRAPH || graph.id === SDXL_CANVAS_OUTPAINT_GRAPH) { - graph.edges.push({ - source: { - node_id: INPAINT_CREATE_MASK, - field: 'denoise_mask', - }, - destination: { - node_id: SDXL_REFINER_DENOISE_LATENTS, - field: 'denoise_mask', - }, - }); - } - - if (graph.id === SDXL_CANVAS_TEXT_TO_IMAGE_GRAPH || graph.id === SDXL_CANVAS_IMAGE_TO_IMAGE_GRAPH) { - graph.edges.push({ - source: { - node_id: SDXL_REFINER_DENOISE_LATENTS, - field: 'latents', - }, - destination: { - node_id: isUsingScaledDimensions ? LATENTS_TO_IMAGE : CANVAS_OUTPUT, - field: 'latents', - }, - }); - } else { - graph.edges.push({ - source: { - node_id: SDXL_REFINER_DENOISE_LATENTS, - field: 'latents', - }, - destination: { - node_id: LATENTS_TO_IMAGE, - field: 'latents', - }, - }); - } -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSeamlessToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSeamlessToLinearGraph.ts deleted file mode 100644 index d4fb8647262..00000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addSeamlessToLinearGraph.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type { RootState } from 'app/store/store'; -import { upsertMetadata } from 'features/nodes/util/graph/canvas/metadata'; -import { - DENOISE_LATENTS, - SDXL_CANVAS_IMAGE_TO_IMAGE_GRAPH, - SDXL_CANVAS_INPAINT_GRAPH, - SDXL_CANVAS_OUTPAINT_GRAPH, - SDXL_CANVAS_TEXT_TO_IMAGE_GRAPH, - SDXL_CONTROL_LAYERS_GRAPH, - SDXL_DENOISE_LATENTS, - SEAMLESS, - VAE_LOADER, -} from 'features/nodes/util/graph/constants'; -import type { NonNullableGraph } from 'services/api/types'; - -export const addSeamlessToLinearGraph = ( - state: RootState, - graph: NonNullableGraph, - modelLoaderNodeId: string -): void => { - // Remove Existing UNet Connections - const { seamlessXAxis, seamlessYAxis, vae } = state.canvasV2.params; - const isAutoVae = !vae; - - graph.nodes[SEAMLESS] = { - id: SEAMLESS, - type: 'seamless', - seamless_x: seamlessXAxis, - seamless_y: seamlessYAxis, - }; - - if (!isAutoVae) { - graph.nodes[VAE_LOADER] = { - type: 'vae_loader', - id: VAE_LOADER, - is_intermediate: true, - vae_model: vae, - }; - } - - if (seamlessXAxis) { - upsertMetadata(graph, { - seamless_x: seamlessXAxis, - }); - } - if (seamlessYAxis) { - upsertMetadata(graph, { - seamless_y: seamlessYAxis, - }); - } - - let denoisingNodeId = DENOISE_LATENTS; - - if ( - graph.id === SDXL_CONTROL_LAYERS_GRAPH || - graph.id === SDXL_CANVAS_TEXT_TO_IMAGE_GRAPH || - graph.id === SDXL_CANVAS_IMAGE_TO_IMAGE_GRAPH || - graph.id === SDXL_CANVAS_INPAINT_GRAPH || - graph.id === SDXL_CANVAS_OUTPAINT_GRAPH - ) { - denoisingNodeId = SDXL_DENOISE_LATENTS; - } - - graph.edges = graph.edges.filter( - (e) => - !(e.source.node_id === modelLoaderNodeId && ['unet'].includes(e.source.field)) && - !(e.source.node_id === modelLoaderNodeId && ['vae'].includes(e.source.field)) - ); - - graph.edges.push( - { - source: { - node_id: modelLoaderNodeId, - field: 'unet', - }, - destination: { - node_id: SEAMLESS, - field: 'unet', - }, - }, - { - source: { - node_id: isAutoVae ? modelLoaderNodeId : VAE_LOADER, - field: 'vae', - }, - destination: { - node_id: SEAMLESS, - field: 'vae', - }, - }, - { - source: { - node_id: SEAMLESS, - field: 'unet', - }, - destination: { - node_id: denoisingNodeId, - field: 'unet', - }, - } - ); -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addT2IAdapterToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addT2IAdapterToLinearGraph.ts deleted file mode 100644 index cd142580b32..00000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addT2IAdapterToLinearGraph.ts +++ /dev/null @@ -1,140 +0,0 @@ -import type { RootState } from 'app/store/store'; -import { selectValidT2IAdapters } from 'features/controlAdapters/store/controlAdaptersSlice'; -import type { ControlAdapterProcessorType, T2IAdapterConfig } from 'features/controlAdapters/store/types'; -import type { ImageField } from 'features/nodes/types/common'; -import { upsertMetadata } from 'features/nodes/util/graph/canvas/metadata'; -import { T2I_ADAPTER_COLLECT } from 'features/nodes/util/graph/constants'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import type { Invocation, NonNullableGraph, S } from 'services/api/types'; -import { assert } from 'tsafe'; - -export const addT2IAdaptersToLinearGraph = async ( - state: RootState, - graph: NonNullableGraph, - baseNodeId: string -): Promise => { - // The generation tab has special handling - its control adapters are set up in the Control Layers graph helper. - const activeTabName = activeTabNameSelector(state); - assert(activeTabName !== 'generation', 'Tried to use addT2IAdaptersToLinearGraph on generation tab'); - - const t2iAdapters = selectValidT2IAdapters(state.controlAdapters).filter( - ({ model, processedControlImage, processorType, controlImage, isEnabled }) => { - const hasModel = Boolean(model); - const doesBaseMatch = model?.base === state.canvasV2.params.model?.base; - const hasControlImage = (processedControlImage && processorType !== 'none') || controlImage; - - return isEnabled && hasModel && doesBaseMatch && hasControlImage; - } - ); - - if (t2iAdapters.length) { - // Even though denoise_latents' t2i adapter input is SINGLE_OR_COLLECTION, keep it simple and always use a collect - const t2iAdapterCollectNode: Invocation<'collect'> = { - id: T2I_ADAPTER_COLLECT, - type: 'collect', - is_intermediate: true, - }; - graph.nodes[T2I_ADAPTER_COLLECT] = t2iAdapterCollectNode; - graph.edges.push({ - source: { node_id: T2I_ADAPTER_COLLECT, field: 'collection' }, - destination: { - node_id: baseNodeId, - field: 't2i_adapter', - }, - }); - - const t2iAdapterMetadata: S['CoreMetadataInvocation']['t2iAdapters'] = []; - - for (const t2iAdapter of t2iAdapters) { - if (!t2iAdapter.model) { - return; - } - const { - id, - controlImage, - processedControlImage, - beginStepPct, - endStepPct, - resizeMode, - model, - processorType, - weight, - } = t2iAdapter; - - const t2iAdapterNode: Invocation<'t2i_adapter'> = { - id: `t2i_adapter_${id}`, - type: 't2i_adapter', - is_intermediate: true, - begin_step_percent: beginStepPct, - end_step_percent: endStepPct, - resize_mode: resizeMode, - t2i_adapter_model: model, - weight: weight, - image: buildControlImage(controlImage, processedControlImage, processorType), - }; - - graph.nodes[t2iAdapterNode.id] = t2iAdapterNode; - - t2iAdapterMetadata.push(buildT2IAdapterMetadata(t2iAdapter)); - - graph.edges.push({ - source: { node_id: t2iAdapterNode.id, field: 't2i_adapter' }, - destination: { - node_id: T2I_ADAPTER_COLLECT, - field: 'item', - }, - }); - } - - upsertMetadata(graph, { t2iAdapters: t2iAdapterMetadata }); - } -}; - -const buildControlImage = ( - controlImage: string | null, - processedControlImage: string | null, - processorType: ControlAdapterProcessorType -): ImageField => { - let image: ImageField | null = null; - if (processedControlImage && processorType !== 'none') { - // We've already processed the image in the app, so we can just use the processed image - image = { - image_name: processedControlImage, - }; - } else if (controlImage) { - // The control image is preprocessed - image = { - image_name: controlImage, - }; - } - assert(image, 'T2I Adapter image is required'); - return image; -}; - -const buildT2IAdapterMetadata = (t2iAdapter: T2IAdapterConfig): S['T2IAdapterMetadataField'] => { - const { controlImage, processedControlImage, beginStepPct, endStepPct, resizeMode, model, processorType, weight } = - t2iAdapter; - - assert(model, 'T2I Adapter model is required'); - - const processed_image = - processedControlImage && processorType !== 'none' - ? { - image_name: processedControlImage, - } - : null; - - assert(controlImage, 'T2I Adapter image is required'); - - return { - t2i_adapter_model: model, - weight, - begin_step_percent: beginStepPct, - end_step_percent: endStepPct, - resize_mode: resizeMode, - image: { - image_name: controlImage, - }, - processed_image, - }; -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addVAEToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addVAEToGraph.ts deleted file mode 100644 index 00129ac4b33..00000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addVAEToGraph.ts +++ /dev/null @@ -1,175 +0,0 @@ -import type { RootState } from 'app/store/store'; -import { upsertMetadata } from 'features/nodes/util/graph/canvas/metadata'; -import { - CANVAS_IMAGE_TO_IMAGE_GRAPH, - CANVAS_INPAINT_GRAPH, - CANVAS_OUTPAINT_GRAPH, - CANVAS_OUTPUT, - CANVAS_TEXT_TO_IMAGE_GRAPH, - CONTROL_LAYERS_GRAPH, - IMAGE_TO_LATENTS, - INPAINT_CREATE_MASK, - INPAINT_IMAGE, - LATENTS_TO_IMAGE, - MAIN_MODEL_LOADER, - SDXL_CANVAS_IMAGE_TO_IMAGE_GRAPH, - SDXL_CANVAS_INPAINT_GRAPH, - SDXL_CANVAS_OUTPAINT_GRAPH, - SDXL_CANVAS_TEXT_TO_IMAGE_GRAPH, - SDXL_CONTROL_LAYERS_GRAPH, - SDXL_REFINER_SEAMLESS, - SEAMLESS, - VAE_LOADER, -} from 'features/nodes/util/graph/constants'; -import type { NonNullableGraph } from 'services/api/types'; - -export const addVAEToGraph = async ( - state: RootState, - graph: NonNullableGraph, - modelLoaderNodeId: string = MAIN_MODEL_LOADER -): Promise => { - const { vae, seamlessXAxis, seamlessYAxis } = state.canvasV2.params; - const { boundingBoxScaleMethod } = state.canvas; - const { refinerModel } = state.canvasV2.params; - - const isUsingScaledDimensions = ['auto', 'manual'].includes(boundingBoxScaleMethod); - - const isAutoVae = !vae; - const isSeamlessEnabled = seamlessXAxis || seamlessYAxis; - const isSDXL = Boolean(graph.id?.includes('sdxl')); - const isUsingRefiner = isSDXL && Boolean(refinerModel); - - if (!isAutoVae && !isSeamlessEnabled) { - graph.nodes[VAE_LOADER] = { - type: 'vae_loader', - id: VAE_LOADER, - is_intermediate: true, - vae_model: vae, - }; - } - - if (graph.id === CONTROL_LAYERS_GRAPH || graph.id === SDXL_CONTROL_LAYERS_GRAPH) { - graph.edges.push({ - source: { - node_id: isSeamlessEnabled - ? isUsingRefiner - ? SDXL_REFINER_SEAMLESS - : SEAMLESS - : isAutoVae - ? modelLoaderNodeId - : VAE_LOADER, - field: 'vae', - }, - destination: { - node_id: LATENTS_TO_IMAGE, - field: 'vae', - }, - }); - } - - if ( - graph.id === CANVAS_TEXT_TO_IMAGE_GRAPH || - graph.id === CANVAS_IMAGE_TO_IMAGE_GRAPH || - graph.id === SDXL_CANVAS_TEXT_TO_IMAGE_GRAPH || - graph.id === SDXL_CANVAS_IMAGE_TO_IMAGE_GRAPH - ) { - graph.edges.push({ - source: { - node_id: isSeamlessEnabled - ? isUsingRefiner - ? SDXL_REFINER_SEAMLESS - : SEAMLESS - : isAutoVae - ? modelLoaderNodeId - : VAE_LOADER, - field: 'vae', - }, - destination: { - node_id: isUsingScaledDimensions ? LATENTS_TO_IMAGE : CANVAS_OUTPUT, - field: 'vae', - }, - }); - } - - if ( - (graph.id === CONTROL_LAYERS_GRAPH || - graph.id === SDXL_CONTROL_LAYERS_GRAPH || - graph.id === CANVAS_IMAGE_TO_IMAGE_GRAPH || - graph.id === SDXL_CANVAS_IMAGE_TO_IMAGE_GRAPH) && - Boolean(graph.nodes[IMAGE_TO_LATENTS]) - ) { - graph.edges.push({ - source: { - node_id: isSeamlessEnabled - ? isUsingRefiner - ? SDXL_REFINER_SEAMLESS - : SEAMLESS - : isAutoVae - ? modelLoaderNodeId - : VAE_LOADER, - field: 'vae', - }, - destination: { - node_id: IMAGE_TO_LATENTS, - field: 'vae', - }, - }); - } - - if ( - graph.id === CANVAS_INPAINT_GRAPH || - graph.id === CANVAS_OUTPAINT_GRAPH || - graph.id === SDXL_CANVAS_INPAINT_GRAPH || - graph.id === SDXL_CANVAS_OUTPAINT_GRAPH - ) { - graph.edges.push( - { - source: { - node_id: isSeamlessEnabled - ? isUsingRefiner - ? SDXL_REFINER_SEAMLESS - : SEAMLESS - : isAutoVae - ? modelLoaderNodeId - : VAE_LOADER, - field: 'vae', - }, - destination: { - node_id: INPAINT_IMAGE, - field: 'vae', - }, - }, - { - source: { - node_id: isSeamlessEnabled ? SEAMLESS : isAutoVae ? modelLoaderNodeId : VAE_LOADER, - field: 'vae', - }, - destination: { - node_id: INPAINT_CREATE_MASK, - field: 'vae', - }, - }, - - { - source: { - node_id: isSeamlessEnabled - ? isUsingRefiner - ? SDXL_REFINER_SEAMLESS - : SEAMLESS - : isAutoVae - ? modelLoaderNodeId - : VAE_LOADER, - field: 'vae', - }, - destination: { - node_id: LATENTS_TO_IMAGE, - field: 'vae', - }, - } - ); - } - - if (vae) { - upsertMetadata(graph, { vae }); - } -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addWatermarkerToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addWatermarkerToGraph.ts deleted file mode 100644 index f87517b5d14..00000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/addWatermarkerToGraph.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { RootState } from 'app/store/store'; -import { LATENTS_TO_IMAGE, NSFW_CHECKER, WATERMARKER } from 'features/nodes/util/graph/constants'; -import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; -import type { Invocation, NonNullableGraph } from 'services/api/types'; - -export const addWatermarkerToGraph = ( - state: RootState, - graph: NonNullableGraph, - nodeIdToAddTo = LATENTS_TO_IMAGE -): void => { - const nodeToAddTo = graph.nodes[nodeIdToAddTo] as Invocation<'l2i'> | undefined; - - const nsfwCheckerNode = graph.nodes[NSFW_CHECKER] as Invocation<'img_nsfw'> | undefined; - - if (!nodeToAddTo) { - // something has gone terribly awry - return; - } - - const watermarkerNode: Invocation<'img_watermark'> = { - id: WATERMARKER, - type: 'img_watermark', - is_intermediate: getIsIntermediate(state), - board: getBoardField(state), - }; - - graph.nodes[WATERMARKER] = watermarkerNode; - - // no matter the situation, we want the l2i node to be intermediate - nodeToAddTo.is_intermediate = true; - nodeToAddTo.use_cache = true; - - if (nsfwCheckerNode) { - // if we are using NSFW checker, we need to "disable" it output by marking it intermediate, - // then connect it to the watermark node - nsfwCheckerNode.is_intermediate = true; - graph.edges.push({ - source: { - node_id: NSFW_CHECKER, - field: 'image', - }, - destination: { - node_id: WATERMARKER, - field: 'image', - }, - }); - } else { - // otherwise we just connect to the watermark node - graph.edges.push({ - source: { - node_id: nodeIdToAddTo, - field: 'image', - }, - destination: { - node_id: WATERMARKER, - field: 'image', - }, - }); - } -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasGraph.ts deleted file mode 100644 index dca39772a55..00000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasGraph.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { RootState } from 'app/store/store'; -import type { ImageDTO, NonNullableGraph } from 'services/api/types'; - -import { buildCanvasImageToImageGraph } from './buildCanvasImageToImageGraph'; -import { buildCanvasInpaintGraph } from './buildCanvasInpaintGraph'; -import { buildCanvasOutpaintGraph } from './buildCanvasOutpaintGraph'; -import { buildCanvasSDXLImageToImageGraph } from './buildCanvasSDXLImageToImageGraph'; -import { buildCanvasSDXLInpaintGraph } from './buildCanvasSDXLInpaintGraph'; -import { buildCanvasSDXLOutpaintGraph } from './buildCanvasSDXLOutpaintGraph'; -import { buildCanvasSDXLTextToImageGraph } from './buildCanvasSDXLTextToImageGraph'; -import { buildCanvasTextToImageGraph } from './buildCanvasTextToImageGraph'; - -export const buildCanvasGraph = async ( - state: RootState, - generationMode: 'txt2img' | 'img2img' | 'inpaint' | 'outpaint', - canvasInitImage: ImageDTO | undefined, - canvasMaskImage: ImageDTO | undefined -): Promise => { - let graph: NonNullableGraph; - - if (generationMode === 'txt2img') { - if (state.canvasV2.params.model && state.canvasV2.params.model.base === 'sdxl') { - graph = await buildCanvasSDXLTextToImageGraph(state); - } else { - graph = await buildCanvasTextToImageGraph(state); - } - } else if (generationMode === 'img2img') { - if (!canvasInitImage) { - throw new Error('Missing canvas init image'); - } - if (state.canvasV2.params.model && state.canvasV2.params.model.base === 'sdxl') { - graph = await buildCanvasSDXLImageToImageGraph(state, canvasInitImage); - } else { - graph = await buildCanvasImageToImageGraph(state, canvasInitImage); - } - } else if (generationMode === 'inpaint') { - if (!canvasInitImage || !canvasMaskImage) { - throw new Error('Missing canvas init and mask images'); - } - if (state.canvasV2.params.model && state.canvasV2.params.model.base === 'sdxl') { - graph = await buildCanvasSDXLInpaintGraph(state, canvasInitImage, canvasMaskImage); - } else { - graph = await buildCanvasInpaintGraph(state, canvasInitImage, canvasMaskImage); - } - } else { - if (!canvasInitImage) { - throw new Error('Missing canvas init image'); - } - if (state.canvasV2.params.model && state.canvasV2.params.model.base === 'sdxl') { - graph = await buildCanvasSDXLOutpaintGraph(state, canvasInitImage, canvasMaskImage); - } else { - graph = await buildCanvasOutpaintGraph(state, canvasInitImage, canvasMaskImage); - } - } - - return graph; -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasImageToImageGraph.ts deleted file mode 100644 index 5d674dba910..00000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasImageToImageGraph.ts +++ /dev/null @@ -1,374 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { RootState } from 'app/store/store'; -import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; -import { addCoreMetadataNode, getModelMetadataField } from 'features/nodes/util/graph/canvas/metadata'; -import { - CANVAS_IMAGE_TO_IMAGE_GRAPH, - CANVAS_OUTPUT, - CLIP_SKIP, - DENOISE_LATENTS, - IMAGE_TO_LATENTS, - IMG2IMG_RESIZE, - LATENTS_TO_IMAGE, - MAIN_MODEL_LOADER, - NEGATIVE_CONDITIONING, - NOISE, - POSITIVE_CONDITIONING, - SEAMLESS, -} from 'features/nodes/util/graph/constants'; -import { - getBoardField, - getIsIntermediate, - getPresetModifiedPrompts, -} from 'features/nodes/util/graph/graphBuilderUtils'; -import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types'; -import { isNonRefinerMainModelConfig } from 'services/api/types'; - -import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; -import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; -import { addLoRAsToGraph } from './addLoRAsToGraph'; -import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; -import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; -import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph'; -import { addVAEToGraph } from './addVAEToGraph'; -import { addWatermarkerToGraph } from './addWatermarkerToGraph'; - -/** - * Builds the Canvas tab's Image to Image graph. - */ -export const buildCanvasImageToImageGraph = async ( - state: RootState, - initialImage: ImageDTO -): Promise => { - const log = logger('nodes'); - const { - model, - cfgScale: cfg_scale, - cfgRescaleMultiplier: cfg_rescale_multiplier, - scheduler, - seed, - steps, - img2imgStrength: strength, - vaePrecision, - clipSkip, - shouldUseCpuNoise, - seamlessXAxis, - seamlessYAxis, - } = state.canvasV2.params; - - // The bounding box determines width and height, not the width and height params - const { width, height } = state.canvas.boundingBoxDimensions; - - const { scaledBoundingBoxDimensions, boundingBoxScaleMethod } = state.canvas; - - const fp32 = vaePrecision === 'fp32'; - const is_intermediate = true; - const isUsingScaledDimensions = ['auto', 'manual'].includes(boundingBoxScaleMethod); - - if (!model) { - log.error('No model found in state'); - throw new Error('No model found in state'); - } - - let modelLoaderNodeId = MAIN_MODEL_LOADER; - - const use_cpu = shouldUseCpuNoise; - - const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state); - - /** - * The easiest way to build linear graphs is to do it in the node editor, then copy and paste the - * full graph here as a template. Then use the parameters from app state and set friendlier node - * ids. - * - * The only thing we need extra logic for is handling randomized seed, control net, and for img2img, - * the `fit` param. These are added to the graph at the end. - */ - - // copy-pasted graph from node editor, filled in with state values & friendly node ids - const graph: NonNullableGraph = { - id: CANVAS_IMAGE_TO_IMAGE_GRAPH, - nodes: { - [modelLoaderNodeId]: { - type: 'main_model_loader', - id: modelLoaderNodeId, - is_intermediate, - model, - }, - [CLIP_SKIP]: { - type: 'clip_skip', - id: CLIP_SKIP, - is_intermediate, - skipped_layers: clipSkip, - }, - [POSITIVE_CONDITIONING]: { - type: 'compel', - id: POSITIVE_CONDITIONING, - is_intermediate, - prompt: positivePrompt, - }, - [NEGATIVE_CONDITIONING]: { - type: 'compel', - id: NEGATIVE_CONDITIONING, - is_intermediate, - prompt: negativePrompt, - }, - [NOISE]: { - type: 'noise', - id: NOISE, - is_intermediate, - use_cpu, - seed, - width: !isUsingScaledDimensions ? width : scaledBoundingBoxDimensions.width, - height: !isUsingScaledDimensions ? height : scaledBoundingBoxDimensions.height, - }, - [IMAGE_TO_LATENTS]: { - type: 'i2l', - id: IMAGE_TO_LATENTS, - is_intermediate, - }, - [DENOISE_LATENTS]: { - type: 'denoise_latents', - id: DENOISE_LATENTS, - is_intermediate, - cfg_scale, - cfg_rescale_multiplier, - scheduler, - steps, - denoising_start: 1 - strength, - denoising_end: 1, - }, - [CANVAS_OUTPUT]: { - type: 'l2i', - id: CANVAS_OUTPUT, - is_intermediate: getIsIntermediate(state), - board: getBoardField(state), - use_cache: false, - }, - }, - edges: [ - // Connect Model Loader to CLIP Skip and UNet - { - source: { - node_id: modelLoaderNodeId, - field: 'unet', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'unet', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'clip', - }, - destination: { - node_id: CLIP_SKIP, - field: 'clip', - }, - }, - // Connect CLIP Skip To Conditioning - { - source: { - node_id: CLIP_SKIP, - field: 'clip', - }, - destination: { - node_id: POSITIVE_CONDITIONING, - field: 'clip', - }, - }, - { - source: { - node_id: CLIP_SKIP, - field: 'clip', - }, - destination: { - node_id: NEGATIVE_CONDITIONING, - field: 'clip', - }, - }, - // Connect Everything To Denoise Latents - { - source: { - node_id: POSITIVE_CONDITIONING, - field: 'conditioning', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'positive_conditioning', - }, - }, - { - source: { - node_id: NEGATIVE_CONDITIONING, - field: 'conditioning', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'negative_conditioning', - }, - }, - { - source: { - node_id: NOISE, - field: 'noise', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'noise', - }, - }, - { - source: { - node_id: IMAGE_TO_LATENTS, - field: 'latents', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'latents', - }, - }, - ], - }; - - // Decode Latents To Image & Handle Scaled Before Processing - if (isUsingScaledDimensions) { - graph.nodes[IMG2IMG_RESIZE] = { - id: IMG2IMG_RESIZE, - type: 'img_resize', - is_intermediate, - image: initialImage, - width: scaledBoundingBoxDimensions.width, - height: scaledBoundingBoxDimensions.height, - }; - graph.nodes[LATENTS_TO_IMAGE] = { - id: LATENTS_TO_IMAGE, - type: 'l2i', - is_intermediate, - fp32, - }; - graph.nodes[CANVAS_OUTPUT] = { - id: CANVAS_OUTPUT, - type: 'img_resize', - is_intermediate: getIsIntermediate(state), - board: getBoardField(state), - width: width, - height: height, - use_cache: false, - }; - - graph.edges.push( - { - source: { - node_id: IMG2IMG_RESIZE, - field: 'image', - }, - destination: { - node_id: IMAGE_TO_LATENTS, - field: 'image', - }, - }, - { - source: { - node_id: DENOISE_LATENTS, - field: 'latents', - }, - destination: { - node_id: LATENTS_TO_IMAGE, - field: 'latents', - }, - }, - { - source: { - node_id: LATENTS_TO_IMAGE, - field: 'image', - }, - destination: { - node_id: CANVAS_OUTPUT, - field: 'image', - }, - } - ); - } else { - graph.nodes[CANVAS_OUTPUT] = { - type: 'l2i', - id: CANVAS_OUTPUT, - is_intermediate: getIsIntermediate(state), - board: getBoardField(state), - fp32, - use_cache: false, - }; - - (graph.nodes[IMAGE_TO_LATENTS] as Invocation<'i2l'>).image = initialImage; - - graph.edges.push({ - source: { - node_id: DENOISE_LATENTS, - field: 'latents', - }, - destination: { - node_id: CANVAS_OUTPUT, - field: 'latents', - }, - }); - } - - const modelConfig = await fetchModelConfigWithTypeGuard(model.key, isNonRefinerMainModelConfig); - - addCoreMetadataNode( - graph, - { - generation_mode: 'img2img', - cfg_scale, - cfg_rescale_multiplier, - width: !isUsingScaledDimensions ? width : scaledBoundingBoxDimensions.width, - height: !isUsingScaledDimensions ? height : scaledBoundingBoxDimensions.height, - positive_prompt: positivePrompt, - negative_prompt: negativePrompt, - model: getModelMetadataField(modelConfig), - seed, - steps, - rand_device: use_cpu ? 'cpu' : 'cuda', - scheduler, - clip_skip: clipSkip, - strength, - init_image: initialImage.image_name, - _canvas_objects: state.canvas.layerState.objects, - }, - CANVAS_OUTPUT - ); - - // Add Seamless To Graph - if (seamlessXAxis || seamlessYAxis) { - addSeamlessToLinearGraph(state, graph, modelLoaderNodeId); - modelLoaderNodeId = SEAMLESS; - } - - // add LoRA support - await addLoRAsToGraph(state, graph, DENOISE_LATENTS); - - // optionally add custom VAE - await addVAEToGraph(state, graph, modelLoaderNodeId); - - // add controlnet, mutating `graph` - await addControlNetToLinearGraph(state, graph, DENOISE_LATENTS); - - // Add IP Adapter - await addIPAdapterToLinearGraph(state, graph, DENOISE_LATENTS); - await addT2IAdaptersToLinearGraph(state, graph, DENOISE_LATENTS); - - // NSFW & watermark - must be last thing added to graph - if (state.system.shouldUseNSFWChecker) { - // must add before watermarker! - addNSFWCheckerToGraph(state, graph, CANVAS_OUTPUT); - } - - if (state.system.shouldUseWatermarker) { - // must add after nsfw checker! - addWatermarkerToGraph(state, graph, CANVAS_OUTPUT); - } - - return graph; -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasInpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasInpaintGraph.ts deleted file mode 100644 index dfd69e202d4..00000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasInpaintGraph.ts +++ /dev/null @@ -1,469 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { RootState } from 'app/store/store'; -import { addCoreMetadataNode } from 'features/nodes/util/graph/canvas/metadata'; -import { - CANVAS_INPAINT_GRAPH, - CANVAS_OUTPUT, - CLIP_SKIP, - DENOISE_LATENTS, - INPAINT_CREATE_MASK, - INPAINT_IMAGE, - INPAINT_IMAGE_RESIZE_DOWN, - INPAINT_IMAGE_RESIZE_UP, - LATENTS_TO_IMAGE, - MAIN_MODEL_LOADER, - MASK_RESIZE_DOWN, - MASK_RESIZE_UP, - NEGATIVE_CONDITIONING, - NOISE, - POSITIVE_CONDITIONING, - SEAMLESS, -} from 'features/nodes/util/graph/constants'; -import { - getBoardField, - getIsIntermediate, - getPresetModifiedPrompts, -} from 'features/nodes/util/graph/graphBuilderUtils'; -import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types'; - -import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; -import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; -import { addLoRAsToGraph } from './addLoRAsToGraph'; -import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; -import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; -import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph'; -import { addVAEToGraph } from './addVAEToGraph'; -import { addWatermarkerToGraph } from './addWatermarkerToGraph'; - -/** - * Builds the Canvas tab's Inpaint graph. - */ -export const buildCanvasInpaintGraph = async ( - state: RootState, - canvasInitImage: ImageDTO, - canvasMaskImage: ImageDTO -): Promise => { - const log = logger('nodes'); - const { - model, - cfgScale: cfg_scale, - cfgRescaleMultiplier: cfg_rescale_multiplier, - scheduler, - steps, - img2imgStrength: strength, - seed, - vaePrecision, - shouldUseCpuNoise, - clipSkip, - seamlessXAxis, - seamlessYAxis, - canvasCoherenceMode, - canvasCoherenceMinDenoise, - canvasCoherenceEdgeSize, - maskBlur, - } = state.canvasV2.params; - - if (!model) { - log.error('No model found in state'); - throw new Error('No model found in state'); - } - - // The bounding box determines width and height, not the width and height params - const { width, height } = state.canvas.boundingBoxDimensions; - - // We may need to set the inpaint width and height to scale the image - const { scaledBoundingBoxDimensions, boundingBoxScaleMethod } = state.canvas; - const is_intermediate = true; - const fp32 = vaePrecision === 'fp32'; - - const isUsingScaledDimensions = ['auto', 'manual'].includes(boundingBoxScaleMethod); - - let modelLoaderNodeId = MAIN_MODEL_LOADER; - - const use_cpu = shouldUseCpuNoise; - - const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state); - - const graph: NonNullableGraph = { - id: CANVAS_INPAINT_GRAPH, - nodes: { - [modelLoaderNodeId]: { - type: 'main_model_loader', - id: modelLoaderNodeId, - is_intermediate, - model, - }, - [CLIP_SKIP]: { - type: 'clip_skip', - id: CLIP_SKIP, - is_intermediate, - skipped_layers: clipSkip, - }, - [POSITIVE_CONDITIONING]: { - type: 'compel', - id: POSITIVE_CONDITIONING, - is_intermediate, - prompt: positivePrompt, - }, - [NEGATIVE_CONDITIONING]: { - type: 'compel', - id: NEGATIVE_CONDITIONING, - is_intermediate, - prompt: negativePrompt, - }, - [INPAINT_IMAGE]: { - type: 'i2l', - id: INPAINT_IMAGE, - is_intermediate, - fp32, - }, - [NOISE]: { - type: 'noise', - id: NOISE, - use_cpu, - seed, - is_intermediate, - }, - [INPAINT_CREATE_MASK]: { - type: 'create_gradient_mask', - id: INPAINT_CREATE_MASK, - is_intermediate, - coherence_mode: canvasCoherenceMode, - minimum_denoise: canvasCoherenceMinDenoise, - edge_radius: canvasCoherenceEdgeSize, - tiled: false, - fp32: fp32, - }, - [DENOISE_LATENTS]: { - type: 'denoise_latents', - id: DENOISE_LATENTS, - is_intermediate, - steps: steps, - cfg_scale: cfg_scale, - cfg_rescale_multiplier, - scheduler: scheduler, - denoising_start: 1 - strength, - denoising_end: 1, - }, - [LATENTS_TO_IMAGE]: { - type: 'l2i', - id: LATENTS_TO_IMAGE, - is_intermediate, - fp32, - }, - [CANVAS_OUTPUT]: { - type: 'canvas_paste_back', - id: CANVAS_OUTPUT, - is_intermediate: getIsIntermediate(state), - board: getBoardField(state), - mask_blur: maskBlur, - source_image: canvasInitImage, - }, - }, - edges: [ - // Connect Model Loader to CLIP Skip and UNet - { - source: { - node_id: modelLoaderNodeId, - field: 'unet', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'unet', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'clip', - }, - destination: { - node_id: CLIP_SKIP, - field: 'clip', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'unet', - }, - destination: { - node_id: INPAINT_CREATE_MASK, - field: 'unet', - }, - }, - // Connect CLIP Skip to Conditioning - { - source: { - node_id: CLIP_SKIP, - field: 'clip', - }, - destination: { - node_id: POSITIVE_CONDITIONING, - field: 'clip', - }, - }, - { - source: { - node_id: CLIP_SKIP, - field: 'clip', - }, - destination: { - node_id: NEGATIVE_CONDITIONING, - field: 'clip', - }, - }, - // Connect Everything To Inpaint Node - { - source: { - node_id: POSITIVE_CONDITIONING, - field: 'conditioning', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'positive_conditioning', - }, - }, - { - source: { - node_id: NEGATIVE_CONDITIONING, - field: 'conditioning', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'negative_conditioning', - }, - }, - { - source: { - node_id: NOISE, - field: 'noise', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'noise', - }, - }, - { - source: { - node_id: INPAINT_IMAGE, - field: 'latents', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'latents', - }, - }, - { - source: { - node_id: INPAINT_CREATE_MASK, - field: 'denoise_mask', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'denoise_mask', - }, - }, - // Decode Inpainted Latents To Image - { - source: { - node_id: DENOISE_LATENTS, - field: 'latents', - }, - destination: { - node_id: LATENTS_TO_IMAGE, - field: 'latents', - }, - }, - ], - }; - - // Handle Scale Before Processing - if (isUsingScaledDimensions) { - const scaledWidth: number = scaledBoundingBoxDimensions.width; - const scaledHeight: number = scaledBoundingBoxDimensions.height; - - // Add Scaling Nodes - graph.nodes[INPAINT_IMAGE_RESIZE_UP] = { - type: 'img_resize', - id: INPAINT_IMAGE_RESIZE_UP, - is_intermediate, - width: scaledWidth, - height: scaledHeight, - image: canvasInitImage, - }; - graph.nodes[MASK_RESIZE_UP] = { - type: 'img_resize', - id: MASK_RESIZE_UP, - is_intermediate, - width: scaledWidth, - height: scaledHeight, - image: canvasMaskImage, - }; - graph.nodes[INPAINT_IMAGE_RESIZE_DOWN] = { - type: 'img_resize', - id: INPAINT_IMAGE_RESIZE_DOWN, - is_intermediate, - width: width, - height: height, - }; - graph.nodes[MASK_RESIZE_DOWN] = { - type: 'img_resize', - id: MASK_RESIZE_DOWN, - is_intermediate, - width: width, - height: height, - }; - - (graph.nodes[NOISE] as Invocation<'noise'>).width = scaledWidth; - (graph.nodes[NOISE] as Invocation<'noise'>).height = scaledHeight; - - // Connect Nodes - graph.edges.push( - // Scale Inpaint Image and Mask - { - source: { - node_id: INPAINT_IMAGE_RESIZE_UP, - field: 'image', - }, - destination: { - node_id: INPAINT_IMAGE, - field: 'image', - }, - }, - { - source: { - node_id: MASK_RESIZE_UP, - field: 'image', - }, - destination: { - node_id: INPAINT_CREATE_MASK, - field: 'mask', - }, - }, - { - source: { - node_id: INPAINT_IMAGE_RESIZE_UP, - field: 'image', - }, - destination: { - node_id: INPAINT_CREATE_MASK, - field: 'image', - }, - }, - // Resize Down - { - source: { - node_id: LATENTS_TO_IMAGE, - field: 'image', - }, - destination: { - node_id: INPAINT_IMAGE_RESIZE_DOWN, - field: 'image', - }, - }, - { - source: { - node_id: INPAINT_CREATE_MASK, - field: 'expanded_mask_area', - }, - destination: { - node_id: MASK_RESIZE_DOWN, - field: 'image', - }, - }, - // Paste Back - { - source: { - node_id: INPAINT_IMAGE_RESIZE_DOWN, - field: 'image', - }, - destination: { - node_id: CANVAS_OUTPUT, - field: 'target_image', - }, - }, - { - source: { - node_id: MASK_RESIZE_DOWN, - field: 'image', - }, - destination: { - node_id: CANVAS_OUTPUT, - field: 'mask', - }, - } - ); - } else { - // Add Images To Nodes - (graph.nodes[NOISE] as Invocation<'noise'>).width = width; - (graph.nodes[NOISE] as Invocation<'noise'>).height = height; - - graph.nodes[INPAINT_IMAGE] = { - ...(graph.nodes[INPAINT_IMAGE] as Invocation<'i2l'>), - image: canvasInitImage, - }; - - graph.nodes[INPAINT_CREATE_MASK] = { - ...(graph.nodes[INPAINT_CREATE_MASK] as Invocation<'create_gradient_mask'>), - mask: canvasMaskImage, - }; - - // Paste Back - graph.nodes[CANVAS_OUTPUT] = { - ...(graph.nodes[CANVAS_OUTPUT] as Invocation<'canvas_paste_back'>), - mask: canvasMaskImage, - }; - - graph.edges.push({ - source: { - node_id: LATENTS_TO_IMAGE, - field: 'image', - }, - destination: { - node_id: CANVAS_OUTPUT, - field: 'target_image', - }, - }); - } - - addCoreMetadataNode( - graph, - { - generation_mode: 'inpaint', - _canvas_objects: state.canvas.layerState.objects, - }, - CANVAS_OUTPUT - ); - - // Add Seamless To Graph - if (seamlessXAxis || seamlessYAxis) { - addSeamlessToLinearGraph(state, graph, modelLoaderNodeId); - modelLoaderNodeId = SEAMLESS; - } - - // Add VAE - await addVAEToGraph(state, graph, modelLoaderNodeId); - - // add LoRA support - await addLoRAsToGraph(state, graph, DENOISE_LATENTS, modelLoaderNodeId); - - // add controlnet, mutating `graph` - await addControlNetToLinearGraph(state, graph, DENOISE_LATENTS); - - // Add IP Adapter - await addIPAdapterToLinearGraph(state, graph, DENOISE_LATENTS); - await addT2IAdaptersToLinearGraph(state, graph, DENOISE_LATENTS); - // NSFW & watermark - must be last thing added to graph - if (state.system.shouldUseNSFWChecker) { - // must add before watermarker! - addNSFWCheckerToGraph(state, graph, CANVAS_OUTPUT); - } - - if (state.system.shouldUseWatermarker) { - // must add after nsfw checker! - addWatermarkerToGraph(state, graph, CANVAS_OUTPUT); - } - - return graph; -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasOutpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasOutpaintGraph.ts deleted file mode 100644 index 1fa6b83e088..00000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasOutpaintGraph.ts +++ /dev/null @@ -1,629 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { RootState } from 'app/store/store'; -import { addCoreMetadataNode } from 'features/nodes/util/graph/canvas/metadata'; -import { - CANVAS_OUTPAINT_GRAPH, - CANVAS_OUTPUT, - CLIP_SKIP, - DENOISE_LATENTS, - INPAINT_CREATE_MASK, - INPAINT_IMAGE, - INPAINT_IMAGE_RESIZE_DOWN, - INPAINT_IMAGE_RESIZE_UP, - INPAINT_INFILL, - INPAINT_INFILL_RESIZE_DOWN, - LATENTS_TO_IMAGE, - MAIN_MODEL_LOADER, - MASK_COMBINE, - MASK_FROM_ALPHA, - MASK_RESIZE_DOWN, - MASK_RESIZE_UP, - NEGATIVE_CONDITIONING, - NOISE, - POSITIVE_CONDITIONING, - SEAMLESS, -} from 'features/nodes/util/graph/constants'; -import { - getBoardField, - getIsIntermediate, - getPresetModifiedPrompts, -} from 'features/nodes/util/graph/graphBuilderUtils'; -import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types'; - -import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; -import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; -import { addLoRAsToGraph } from './addLoRAsToGraph'; -import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; -import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; -import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph'; -import { addVAEToGraph } from './addVAEToGraph'; -import { addWatermarkerToGraph } from './addWatermarkerToGraph'; - -/** - * Builds the Canvas tab's Outpaint graph. - */ -export const buildCanvasOutpaintGraph = async ( - state: RootState, - canvasInitImage: ImageDTO, - canvasMaskImage?: ImageDTO -): Promise => { - const log = logger('nodes'); - const { - model, - cfgScale: cfg_scale, - cfgRescaleMultiplier: cfg_rescale_multiplier, - scheduler, - steps, - img2imgStrength: strength, - seed, - vaePrecision, - shouldUseCpuNoise, - infillTileSize, - infillPatchmatchDownscaleSize, - infillMethod, - // infillMosaicTileWidth, - // infillMosaicTileHeight, - // infillMosaicMinColor, - // infillMosaicMaxColor, - infillColorValue, - clipSkip, - seamlessXAxis, - seamlessYAxis, - canvasCoherenceMode, - canvasCoherenceMinDenoise, - canvasCoherenceEdgeSize, - maskBlur, - } = state.canvasV2.params; - - if (!model) { - log.error('No model found in state'); - throw new Error('No model found in state'); - } - - // The bounding box determines width and height, not the width and height params - const { width, height } = state.canvas.boundingBoxDimensions; - - // We may need to set the inpaint width and height to scale the image - const { scaledBoundingBoxDimensions, boundingBoxScaleMethod } = state.canvas; - - const fp32 = vaePrecision === 'fp32'; - const is_intermediate = true; - const isUsingScaledDimensions = ['auto', 'manual'].includes(boundingBoxScaleMethod); - - let modelLoaderNodeId = MAIN_MODEL_LOADER; - - const use_cpu = shouldUseCpuNoise; - - const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state); - - const graph: NonNullableGraph = { - id: CANVAS_OUTPAINT_GRAPH, - nodes: { - [modelLoaderNodeId]: { - type: 'main_model_loader', - id: modelLoaderNodeId, - is_intermediate, - model, - }, - [CLIP_SKIP]: { - type: 'clip_skip', - id: CLIP_SKIP, - is_intermediate, - skipped_layers: clipSkip, - }, - [POSITIVE_CONDITIONING]: { - type: 'compel', - id: POSITIVE_CONDITIONING, - is_intermediate, - prompt: positivePrompt, - }, - [NEGATIVE_CONDITIONING]: { - type: 'compel', - id: NEGATIVE_CONDITIONING, - is_intermediate, - prompt: negativePrompt, - }, - [MASK_FROM_ALPHA]: { - type: 'tomask', - id: MASK_FROM_ALPHA, - is_intermediate, - image: canvasInitImage, - }, - [MASK_COMBINE]: { - type: 'mask_combine', - id: MASK_COMBINE, - is_intermediate, - mask2: canvasMaskImage, - }, - [INPAINT_IMAGE]: { - type: 'i2l', - id: INPAINT_IMAGE, - is_intermediate, - fp32, - }, - [NOISE]: { - type: 'noise', - id: NOISE, - use_cpu, - seed, - is_intermediate, - }, - [INPAINT_CREATE_MASK]: { - type: 'create_gradient_mask', - id: INPAINT_CREATE_MASK, - is_intermediate, - coherence_mode: canvasCoherenceMode, - edge_radius: canvasCoherenceEdgeSize, - minimum_denoise: canvasCoherenceMinDenoise, - tiled: false, - fp32: fp32, - }, - [DENOISE_LATENTS]: { - type: 'denoise_latents', - id: DENOISE_LATENTS, - is_intermediate, - steps: steps, - cfg_scale: cfg_scale, - cfg_rescale_multiplier, - scheduler: scheduler, - denoising_start: 1 - strength, - denoising_end: 1, - }, - - [LATENTS_TO_IMAGE]: { - type: 'l2i', - id: LATENTS_TO_IMAGE, - is_intermediate, - fp32, - }, - [CANVAS_OUTPUT]: { - type: 'canvas_paste_back', - id: CANVAS_OUTPUT, - is_intermediate: getIsIntermediate(state), - board: getBoardField(state), - use_cache: false, - mask_blur: maskBlur, - }, - }, - edges: [ - // Connect Model Loader To UNet & Clip Skip - { - source: { - node_id: modelLoaderNodeId, - field: 'unet', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'unet', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'clip', - }, - destination: { - node_id: CLIP_SKIP, - field: 'clip', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'unet', - }, - destination: { - node_id: INPAINT_CREATE_MASK, - field: 'unet', - }, - }, - // Connect CLIP Skip to Conditioning - { - source: { - node_id: CLIP_SKIP, - field: 'clip', - }, - destination: { - node_id: POSITIVE_CONDITIONING, - field: 'clip', - }, - }, - { - source: { - node_id: CLIP_SKIP, - field: 'clip', - }, - destination: { - node_id: NEGATIVE_CONDITIONING, - field: 'clip', - }, - }, - // Connect Infill Result To Inpaint Image - { - source: { - node_id: INPAINT_INFILL, - field: 'image', - }, - destination: { - node_id: INPAINT_IMAGE, - field: 'image', - }, - }, - // Combine Mask from Init Image with User Painted Mask - { - source: { - node_id: MASK_FROM_ALPHA, - field: 'image', - }, - destination: { - node_id: MASK_COMBINE, - field: 'mask1', - }, - }, - // Plug Everything Into Inpaint Node - { - source: { - node_id: POSITIVE_CONDITIONING, - field: 'conditioning', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'positive_conditioning', - }, - }, - { - source: { - node_id: NEGATIVE_CONDITIONING, - field: 'conditioning', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'negative_conditioning', - }, - }, - { - source: { - node_id: NOISE, - field: 'noise', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'noise', - }, - }, - { - source: { - node_id: INPAINT_IMAGE, - field: 'latents', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'latents', - }, - }, - // Create Inpaint Mask - { - source: { - node_id: isUsingScaledDimensions ? MASK_RESIZE_UP : MASK_COMBINE, - field: 'image', - }, - destination: { - node_id: INPAINT_CREATE_MASK, - field: 'mask', - }, - }, - { - source: { - node_id: INPAINT_CREATE_MASK, - field: 'denoise_mask', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'denoise_mask', - }, - }, - - { - source: { - node_id: DENOISE_LATENTS, - field: 'latents', - }, - destination: { - node_id: LATENTS_TO_IMAGE, - field: 'latents', - }, - }, - ], - }; - - // Add Infill Nodes - if (infillMethod === 'patchmatch') { - graph.nodes[INPAINT_INFILL] = { - type: 'infill_patchmatch', - id: INPAINT_INFILL, - is_intermediate, - downscale: infillPatchmatchDownscaleSize, - }; - } - - if (infillMethod === 'lama') { - graph.nodes[INPAINT_INFILL] = { - type: 'infill_lama', - id: INPAINT_INFILL, - is_intermediate, - }; - } - - if (infillMethod === 'cv2') { - graph.nodes[INPAINT_INFILL] = { - type: 'infill_cv2', - id: INPAINT_INFILL, - is_intermediate, - }; - } - - if (infillMethod === 'tile') { - graph.nodes[INPAINT_INFILL] = { - type: 'infill_tile', - id: INPAINT_INFILL, - is_intermediate, - tile_size: infillTileSize, - }; - } - - // TODO: add mosaic back - // if (infillMethod === 'mosaic') { - // graph.nodes[INPAINT_INFILL] = { - // type: 'infill_mosaic', - // id: INPAINT_INFILL, - // is_intermediate, - // tile_width: infillMosaicTileWidth, - // tile_height: infillMosaicTileHeight, - // min_color: infillMosaicMinColor, - // max_color: infillMosaicMaxColor, - // }; - // } - - if (infillMethod === 'color') { - graph.nodes[INPAINT_INFILL] = { - type: 'infill_rgba', - id: INPAINT_INFILL, - color: infillColorValue, - is_intermediate, - }; - } - - // Handle Scale Before Processing - if (isUsingScaledDimensions) { - const scaledWidth: number = scaledBoundingBoxDimensions.width; - const scaledHeight: number = scaledBoundingBoxDimensions.height; - - // Add Scaling Nodes - graph.nodes[INPAINT_IMAGE_RESIZE_UP] = { - type: 'img_resize', - id: INPAINT_IMAGE_RESIZE_UP, - is_intermediate, - width: scaledWidth, - height: scaledHeight, - image: canvasInitImage, - }; - graph.nodes[MASK_RESIZE_UP] = { - type: 'img_resize', - id: MASK_RESIZE_UP, - is_intermediate, - width: scaledWidth, - height: scaledHeight, - }; - graph.nodes[INPAINT_IMAGE_RESIZE_DOWN] = { - type: 'img_resize', - id: INPAINT_IMAGE_RESIZE_DOWN, - is_intermediate, - width: width, - height: height, - }; - graph.nodes[INPAINT_INFILL_RESIZE_DOWN] = { - type: 'img_resize', - id: INPAINT_INFILL_RESIZE_DOWN, - is_intermediate, - width: width, - height: height, - }; - graph.nodes[MASK_RESIZE_DOWN] = { - type: 'img_resize', - id: MASK_RESIZE_DOWN, - is_intermediate, - width: width, - height: height, - }; - - (graph.nodes[NOISE] as Invocation<'noise'>).width = scaledWidth; - (graph.nodes[NOISE] as Invocation<'noise'>).height = scaledHeight; - - // Connect Nodes - graph.edges.push( - // Scale Inpaint Image - { - source: { - node_id: INPAINT_IMAGE_RESIZE_UP, - field: 'image', - }, - destination: { - node_id: INPAINT_INFILL, - field: 'image', - }, - }, - // Take combined mask and resize - { - source: { - node_id: MASK_COMBINE, - field: 'image', - }, - destination: { - node_id: MASK_RESIZE_UP, - field: 'image', - }, - }, - { - source: { - node_id: INPAINT_IMAGE_RESIZE_UP, - field: 'image', - }, - destination: { - node_id: INPAINT_CREATE_MASK, - field: 'image', - }, - }, - // Resize Results Down - { - source: { - node_id: LATENTS_TO_IMAGE, - field: 'image', - }, - destination: { - node_id: INPAINT_IMAGE_RESIZE_DOWN, - field: 'image', - }, - }, - { - source: { - node_id: INPAINT_CREATE_MASK, - field: 'expanded_mask_area', - }, - destination: { - node_id: MASK_RESIZE_DOWN, - field: 'image', - }, - }, - { - source: { - node_id: INPAINT_INFILL, - field: 'image', - }, - destination: { - node_id: INPAINT_INFILL_RESIZE_DOWN, - field: 'image', - }, - }, - // Paste Back - { - source: { - node_id: INPAINT_INFILL_RESIZE_DOWN, - field: 'image', - }, - destination: { - node_id: CANVAS_OUTPUT, - field: 'source_image', - }, - }, - { - source: { - node_id: INPAINT_IMAGE_RESIZE_DOWN, - field: 'image', - }, - destination: { - node_id: CANVAS_OUTPUT, - field: 'target_image', - }, - }, - { - source: { - node_id: INPAINT_CREATE_MASK, - field: 'expanded_mask_area', - }, - destination: { - node_id: CANVAS_OUTPUT, - field: 'mask', - }, - } - ); - } else { - // Add Images To Nodes - graph.nodes[INPAINT_INFILL] = { - ...(graph.nodes[INPAINT_INFILL] as Invocation<'infill_tile'> | Invocation<'infill_patchmatch'>), - image: canvasInitImage, - }; - - (graph.nodes[NOISE] as Invocation<'noise'>).width = width; - (graph.nodes[NOISE] as Invocation<'noise'>).height = height; - - graph.nodes[INPAINT_IMAGE] = { - ...(graph.nodes[INPAINT_IMAGE] as Invocation<'i2l'>), - image: canvasInitImage, - }; - - graph.edges.push( - { - source: { - node_id: INPAINT_INFILL, - field: 'image', - }, - destination: { - node_id: CANVAS_OUTPUT, - field: 'source_image', - }, - }, - { - source: { - node_id: LATENTS_TO_IMAGE, - field: 'image', - }, - destination: { - node_id: CANVAS_OUTPUT, - field: 'target_image', - }, - }, - { - source: { - node_id: MASK_COMBINE, - field: 'image', - }, - destination: { - node_id: CANVAS_OUTPUT, - field: 'mask', - }, - } - ); - } - - addCoreMetadataNode( - graph, - { - generation_mode: 'outpaint', - _canvas_objects: state.canvas.layerState.objects, - }, - CANVAS_OUTPUT - ); - - // Add Seamless To Graph - if (seamlessXAxis || seamlessYAxis) { - addSeamlessToLinearGraph(state, graph, modelLoaderNodeId); - modelLoaderNodeId = SEAMLESS; - } - - // Add VAE - await addVAEToGraph(state, graph, modelLoaderNodeId); - - // add LoRA support - await addLoRAsToGraph(state, graph, DENOISE_LATENTS, modelLoaderNodeId); - - // add controlnet, mutating `graph` - await addControlNetToLinearGraph(state, graph, DENOISE_LATENTS); - - // Add IP Adapter - await addIPAdapterToLinearGraph(state, graph, DENOISE_LATENTS); - - await addT2IAdaptersToLinearGraph(state, graph, DENOISE_LATENTS); - - // NSFW & watermark - must be last thing added to graph - if (state.system.shouldUseNSFWChecker) { - // must add before watermarker! - addNSFWCheckerToGraph(state, graph, CANVAS_OUTPUT); - } - - if (state.system.shouldUseWatermarker) { - // must add after nsfw checker! - addWatermarkerToGraph(state, graph, CANVAS_OUTPUT); - } - - return graph; -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLImageToImageGraph.ts deleted file mode 100644 index ddc2b11855b..00000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLImageToImageGraph.ts +++ /dev/null @@ -1,382 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { RootState } from 'app/store/store'; -import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; -import { addCoreMetadataNode, getModelMetadataField } from 'features/nodes/util/graph/canvas/metadata'; -import { - CANVAS_OUTPUT, - IMAGE_TO_LATENTS, - IMG2IMG_RESIZE, - LATENTS_TO_IMAGE, - NEGATIVE_CONDITIONING, - NOISE, - POSITIVE_CONDITIONING, - SDXL_CANVAS_IMAGE_TO_IMAGE_GRAPH, - SDXL_DENOISE_LATENTS, - SDXL_MODEL_LOADER, - SDXL_REFINER_SEAMLESS, - SEAMLESS, -} from 'features/nodes/util/graph/constants'; -import { - getBoardField, - getIsIntermediate, - getPresetModifiedPrompts, -} from 'features/nodes/util/graph/graphBuilderUtils'; -import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types'; -import { isNonRefinerMainModelConfig } from 'services/api/types'; - -import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; -import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; -import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; -import { addSDXLLoRAsToGraph } from './addSDXLLoRAstoGraph'; -import { addSDXLRefinerToGraph } from './addSDXLRefinerToGraph'; -import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; -import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph'; -import { addVAEToGraph } from './addVAEToGraph'; -import { addWatermarkerToGraph } from './addWatermarkerToGraph'; - -/** - * Builds the Canvas tab's Image to Image graph. - */ -export const buildCanvasSDXLImageToImageGraph = async ( - state: RootState, - initialImage: ImageDTO -): Promise => { - const log = logger('nodes'); - const { - model, - cfgScale: cfg_scale, - cfgRescaleMultiplier: cfg_rescale_multiplier, - scheduler, - seed, - steps, - vaePrecision, - shouldUseCpuNoise, - seamlessXAxis, - seamlessYAxis, - img2imgStrength: strength, - } = state.canvasV2.params; - - const { refinerModel, refinerStart } = state.canvasV2.params; - - // The bounding box determines width and height, not the width and height params - const { width, height } = state.canvas.boundingBoxDimensions; - - const { scaledBoundingBoxDimensions, boundingBoxScaleMethod } = state.canvas; - - const fp32 = vaePrecision === 'fp32'; - const is_intermediate = true; - const isUsingScaledDimensions = ['auto', 'manual'].includes(boundingBoxScaleMethod); - - if (!model) { - log.error('No model found in state'); - throw new Error('No model found in state'); - } - - // Model Loader ID - let modelLoaderNodeId = SDXL_MODEL_LOADER; - - const use_cpu = shouldUseCpuNoise; - - // Construct Style Prompt - const { positivePrompt, negativePrompt, positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state); - - /** - * The easiest way to build linear graphs is to do it in the node editor, then copy and paste the - * full graph here as a template. Then use the parameters from app state and set friendlier node - * ids. - * - * The only thing we need extra logic for is handling randomized seed, control net, and for img2img, - * the `fit` param. These are added to the graph at the end. - */ - - // copy-pasted graph from node editor, filled in with state values & friendly node ids - const graph: NonNullableGraph = { - id: SDXL_CANVAS_IMAGE_TO_IMAGE_GRAPH, - nodes: { - [modelLoaderNodeId]: { - type: 'sdxl_model_loader', - id: modelLoaderNodeId, - model, - }, - [POSITIVE_CONDITIONING]: { - type: 'sdxl_compel_prompt', - id: POSITIVE_CONDITIONING, - prompt: positivePrompt, - style: positiveStylePrompt, - }, - [NEGATIVE_CONDITIONING]: { - type: 'sdxl_compel_prompt', - id: NEGATIVE_CONDITIONING, - prompt: negativePrompt, - style: negativeStylePrompt, - }, - [NOISE]: { - type: 'noise', - id: NOISE, - is_intermediate, - use_cpu, - seed, - width: !isUsingScaledDimensions ? width : scaledBoundingBoxDimensions.width, - height: !isUsingScaledDimensions ? height : scaledBoundingBoxDimensions.height, - }, - [IMAGE_TO_LATENTS]: { - type: 'i2l', - id: IMAGE_TO_LATENTS, - is_intermediate, - fp32, - }, - [SDXL_DENOISE_LATENTS]: { - type: 'denoise_latents', - id: SDXL_DENOISE_LATENTS, - is_intermediate, - cfg_scale, - cfg_rescale_multiplier, - scheduler, - steps, - denoising_start: refinerModel ? Math.min(refinerStart, 1 - strength) : 1 - strength, - denoising_end: refinerModel ? refinerStart : 1, - }, - }, - edges: [ - // Connect Model Loader To UNet & CLIP - { - source: { - node_id: modelLoaderNodeId, - field: 'unet', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'unet', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'clip', - }, - destination: { - node_id: POSITIVE_CONDITIONING, - field: 'clip', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'clip2', - }, - destination: { - node_id: POSITIVE_CONDITIONING, - field: 'clip2', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'clip', - }, - destination: { - node_id: NEGATIVE_CONDITIONING, - field: 'clip', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'clip2', - }, - destination: { - node_id: NEGATIVE_CONDITIONING, - field: 'clip2', - }, - }, - // Connect Everything to Denoise Latents - { - source: { - node_id: POSITIVE_CONDITIONING, - field: 'conditioning', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'positive_conditioning', - }, - }, - { - source: { - node_id: NEGATIVE_CONDITIONING, - field: 'conditioning', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'negative_conditioning', - }, - }, - { - source: { - node_id: NOISE, - field: 'noise', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'noise', - }, - }, - { - source: { - node_id: IMAGE_TO_LATENTS, - field: 'latents', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'latents', - }, - }, - ], - }; - - // Decode Latents To Image & Handle Scaled Before Processing - if (isUsingScaledDimensions) { - graph.nodes[IMG2IMG_RESIZE] = { - id: IMG2IMG_RESIZE, - type: 'img_resize', - is_intermediate, - image: initialImage, - width: scaledBoundingBoxDimensions.width, - height: scaledBoundingBoxDimensions.height, - }; - graph.nodes[LATENTS_TO_IMAGE] = { - id: LATENTS_TO_IMAGE, - type: 'l2i', - is_intermediate, - fp32, - }; - graph.nodes[CANVAS_OUTPUT] = { - id: CANVAS_OUTPUT, - type: 'img_resize', - is_intermediate: getIsIntermediate(state), - board: getBoardField(state), - width: width, - height: height, - use_cache: false, - }; - - graph.edges.push( - { - source: { - node_id: IMG2IMG_RESIZE, - field: 'image', - }, - destination: { - node_id: IMAGE_TO_LATENTS, - field: 'image', - }, - }, - { - source: { - node_id: SDXL_DENOISE_LATENTS, - field: 'latents', - }, - destination: { - node_id: LATENTS_TO_IMAGE, - field: 'latents', - }, - }, - { - source: { - node_id: LATENTS_TO_IMAGE, - field: 'image', - }, - destination: { - node_id: CANVAS_OUTPUT, - field: 'image', - }, - } - ); - } else { - graph.nodes[CANVAS_OUTPUT] = { - type: 'l2i', - id: CANVAS_OUTPUT, - is_intermediate, - fp32, - use_cache: false, - }; - - (graph.nodes[IMAGE_TO_LATENTS] as Invocation<'i2l'>).image = initialImage; - - graph.edges.push({ - source: { - node_id: SDXL_DENOISE_LATENTS, - field: 'latents', - }, - destination: { - node_id: CANVAS_OUTPUT, - field: 'latents', - }, - }); - } - - const modelConfig = await fetchModelConfigWithTypeGuard(model.key, isNonRefinerMainModelConfig); - - addCoreMetadataNode( - graph, - { - generation_mode: 'img2img', - cfg_scale, - cfg_rescale_multiplier, - width: !isUsingScaledDimensions ? width : scaledBoundingBoxDimensions.width, - height: !isUsingScaledDimensions ? height : scaledBoundingBoxDimensions.height, - positive_prompt: positivePrompt, - negative_prompt: negativePrompt, - model: getModelMetadataField(modelConfig), - seed, - steps, - rand_device: use_cpu ? 'cpu' : 'cuda', - scheduler, - strength, - init_image: initialImage.image_name, - positive_style_prompt: positiveStylePrompt, - negative_style_prompt: negativeStylePrompt, - _canvas_objects: state.canvas.layerState.objects, - }, - CANVAS_OUTPUT - ); - - // Add Seamless To Graph - if (seamlessXAxis || seamlessYAxis) { - addSeamlessToLinearGraph(state, graph, modelLoaderNodeId); - modelLoaderNodeId = SEAMLESS; - } - - // Add Refiner if enabled - if (refinerModel) { - await addSDXLRefinerToGraph(state, graph, SDXL_DENOISE_LATENTS, modelLoaderNodeId); - if (seamlessXAxis || seamlessYAxis) { - modelLoaderNodeId = SDXL_REFINER_SEAMLESS; - } - } - - // optionally add custom VAE - await addVAEToGraph(state, graph, modelLoaderNodeId); - - // add LoRA support - await addSDXLLoRAsToGraph(state, graph, SDXL_DENOISE_LATENTS, modelLoaderNodeId); - - // add controlnet, mutating `graph` - await addControlNetToLinearGraph(state, graph, SDXL_DENOISE_LATENTS); - - // Add IP Adapter - await addIPAdapterToLinearGraph(state, graph, SDXL_DENOISE_LATENTS); - await addT2IAdaptersToLinearGraph(state, graph, SDXL_DENOISE_LATENTS); - - // NSFW & watermark - must be last thing added to graph - if (state.system.shouldUseNSFWChecker) { - // must add before watermarker! - addNSFWCheckerToGraph(state, graph, CANVAS_OUTPUT); - } - - if (state.system.shouldUseWatermarker) { - // must add after nsfw checker! - addWatermarkerToGraph(state, graph, CANVAS_OUTPUT); - } - - return graph; -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLInpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLInpaintGraph.ts deleted file mode 100644 index 3ecf2a02c06..00000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLInpaintGraph.ts +++ /dev/null @@ -1,487 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { RootState } from 'app/store/store'; -import { addCoreMetadataNode } from 'features/nodes/util/graph/canvas/metadata'; -import { - CANVAS_OUTPUT, - INPAINT_CREATE_MASK, - INPAINT_IMAGE, - INPAINT_IMAGE_RESIZE_DOWN, - INPAINT_IMAGE_RESIZE_UP, - LATENTS_TO_IMAGE, - MASK_RESIZE_DOWN, - MASK_RESIZE_UP, - NEGATIVE_CONDITIONING, - NOISE, - POSITIVE_CONDITIONING, - SDXL_CANVAS_INPAINT_GRAPH, - SDXL_DENOISE_LATENTS, - SDXL_MODEL_LOADER, - SDXL_REFINER_SEAMLESS, - SEAMLESS, -} from 'features/nodes/util/graph/constants'; -import { - getBoardField, - getIsIntermediate, - getPresetModifiedPrompts, -} from 'features/nodes/util/graph/graphBuilderUtils'; -import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types'; - -import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; -import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; -import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; -import { addSDXLLoRAsToGraph } from './addSDXLLoRAstoGraph'; -import { addSDXLRefinerToGraph } from './addSDXLRefinerToGraph'; -import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; -import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph'; -import { addVAEToGraph } from './addVAEToGraph'; -import { addWatermarkerToGraph } from './addWatermarkerToGraph'; - -/** - * Builds the Canvas tab's Inpaint graph. - */ -export const buildCanvasSDXLInpaintGraph = async ( - state: RootState, - canvasInitImage: ImageDTO, - canvasMaskImage: ImageDTO -): Promise => { - const log = logger('nodes'); - const { - model, - cfgScale: cfg_scale, - cfgRescaleMultiplier: cfg_rescale_multiplier, - scheduler, - steps, - img2imgStrength: strength, - seed, - vaePrecision, - shouldUseCpuNoise, - seamlessXAxis, - seamlessYAxis, - canvasCoherenceMode, - canvasCoherenceMinDenoise, - canvasCoherenceEdgeSize, - maskBlur, - } = state.canvasV2.params; - - const { refinerModel, refinerStart } = state.canvasV2.params; - - if (!model) { - log.error('No model found in state'); - throw new Error('No model found in state'); - } - - // The bounding box determines width and height, not the width and height params - const { width, height } = state.canvas.boundingBoxDimensions; - - // We may need to set the inpaint width and height to scale the image - const { scaledBoundingBoxDimensions, boundingBoxScaleMethod } = state.canvas; - - const is_intermediate = true; - const fp32 = vaePrecision === 'fp32'; - - const isUsingScaledDimensions = ['auto', 'manual'].includes(boundingBoxScaleMethod); - - let modelLoaderNodeId = SDXL_MODEL_LOADER; - - const use_cpu = shouldUseCpuNoise; - - // Construct Style Prompt - const { positivePrompt, negativePrompt, positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state); - - const graph: NonNullableGraph = { - id: SDXL_CANVAS_INPAINT_GRAPH, - nodes: { - [modelLoaderNodeId]: { - type: 'sdxl_model_loader', - id: modelLoaderNodeId, - is_intermediate, - model, - }, - [POSITIVE_CONDITIONING]: { - type: 'sdxl_compel_prompt', - id: POSITIVE_CONDITIONING, - is_intermediate, - prompt: positivePrompt, - style: positiveStylePrompt, - }, - [NEGATIVE_CONDITIONING]: { - type: 'sdxl_compel_prompt', - id: NEGATIVE_CONDITIONING, - is_intermediate, - prompt: negativePrompt, - style: negativeStylePrompt, - }, - [INPAINT_IMAGE]: { - type: 'i2l', - id: INPAINT_IMAGE, - is_intermediate, - fp32, - }, - [NOISE]: { - type: 'noise', - id: NOISE, - use_cpu, - seed, - is_intermediate, - }, - [INPAINT_CREATE_MASK]: { - type: 'create_gradient_mask', - id: INPAINT_CREATE_MASK, - is_intermediate, - coherence_mode: canvasCoherenceMode, - minimum_denoise: refinerModel ? Math.max(0.2, canvasCoherenceMinDenoise) : canvasCoherenceMinDenoise, - edge_radius: canvasCoherenceEdgeSize, - tiled: false, - fp32: fp32, - }, - [SDXL_DENOISE_LATENTS]: { - type: 'denoise_latents', - id: SDXL_DENOISE_LATENTS, - is_intermediate, - steps: steps, - cfg_scale: cfg_scale, - cfg_rescale_multiplier, - scheduler: scheduler, - denoising_start: refinerModel ? Math.min(refinerStart, 1 - strength) : 1 - strength, - denoising_end: refinerModel ? refinerStart : 1, - }, - [LATENTS_TO_IMAGE]: { - type: 'l2i', - id: LATENTS_TO_IMAGE, - is_intermediate, - fp32, - }, - [CANVAS_OUTPUT]: { - type: 'canvas_paste_back', - id: CANVAS_OUTPUT, - is_intermediate: getIsIntermediate(state), - board: getBoardField(state), - mask_blur: maskBlur, - source_image: canvasInitImage, - }, - }, - edges: [ - // Connect Model Loader to UNet and CLIP - { - source: { - node_id: modelLoaderNodeId, - field: 'unet', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'unet', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'clip', - }, - destination: { - node_id: POSITIVE_CONDITIONING, - field: 'clip', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'clip2', - }, - destination: { - node_id: POSITIVE_CONDITIONING, - field: 'clip2', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'clip', - }, - destination: { - node_id: NEGATIVE_CONDITIONING, - field: 'clip', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'clip2', - }, - destination: { - node_id: NEGATIVE_CONDITIONING, - field: 'clip2', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'unet', - }, - destination: { - node_id: INPAINT_CREATE_MASK, - field: 'unet', - }, - }, - // Connect Everything To Inpaint Node - { - source: { - node_id: POSITIVE_CONDITIONING, - field: 'conditioning', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'positive_conditioning', - }, - }, - { - source: { - node_id: NEGATIVE_CONDITIONING, - field: 'conditioning', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'negative_conditioning', - }, - }, - { - source: { - node_id: NOISE, - field: 'noise', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'noise', - }, - }, - { - source: { - node_id: INPAINT_IMAGE, - field: 'latents', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'latents', - }, - }, - { - source: { - node_id: INPAINT_CREATE_MASK, - field: 'denoise_mask', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'denoise_mask', - }, - }, - // Decode Inpainted Latents To Image - { - source: { - node_id: SDXL_DENOISE_LATENTS, - field: 'latents', - }, - destination: { - node_id: LATENTS_TO_IMAGE, - field: 'latents', - }, - }, - ], - }; - - // Handle Scale Before Processing - if (isUsingScaledDimensions) { - const scaledWidth: number = scaledBoundingBoxDimensions.width; - const scaledHeight: number = scaledBoundingBoxDimensions.height; - - // Add Scaling Nodes - graph.nodes[INPAINT_IMAGE_RESIZE_UP] = { - type: 'img_resize', - id: INPAINT_IMAGE_RESIZE_UP, - is_intermediate, - width: scaledWidth, - height: scaledHeight, - image: canvasInitImage, - }; - graph.nodes[MASK_RESIZE_UP] = { - type: 'img_resize', - id: MASK_RESIZE_UP, - is_intermediate, - width: scaledWidth, - height: scaledHeight, - image: canvasMaskImage, - }; - graph.nodes[INPAINT_IMAGE_RESIZE_DOWN] = { - type: 'img_resize', - id: INPAINT_IMAGE_RESIZE_DOWN, - is_intermediate, - width: width, - height: height, - }; - graph.nodes[MASK_RESIZE_DOWN] = { - type: 'img_resize', - id: MASK_RESIZE_DOWN, - is_intermediate, - width: width, - height: height, - }; - - (graph.nodes[NOISE] as Invocation<'noise'>).width = scaledWidth; - (graph.nodes[NOISE] as Invocation<'noise'>).height = scaledHeight; - - // Connect Nodes - graph.edges.push( - // Scale Inpaint Image and Mask - { - source: { - node_id: INPAINT_IMAGE_RESIZE_UP, - field: 'image', - }, - destination: { - node_id: INPAINT_IMAGE, - field: 'image', - }, - }, - { - source: { - node_id: MASK_RESIZE_UP, - field: 'image', - }, - destination: { - node_id: INPAINT_CREATE_MASK, - field: 'mask', - }, - }, - { - source: { - node_id: INPAINT_IMAGE_RESIZE_UP, - field: 'image', - }, - destination: { - node_id: INPAINT_CREATE_MASK, - field: 'image', - }, - }, - // Resize Down - { - source: { - node_id: LATENTS_TO_IMAGE, - field: 'image', - }, - destination: { - node_id: INPAINT_IMAGE_RESIZE_DOWN, - field: 'image', - }, - }, - { - source: { - node_id: INPAINT_CREATE_MASK, - field: 'expanded_mask_area', - }, - destination: { - node_id: MASK_RESIZE_DOWN, - field: 'image', - }, - }, - // Paste Back - { - source: { - node_id: INPAINT_IMAGE_RESIZE_DOWN, - field: 'image', - }, - destination: { - node_id: CANVAS_OUTPUT, - field: 'target_image', - }, - }, - { - source: { - node_id: MASK_RESIZE_DOWN, - field: 'image', - }, - destination: { - node_id: CANVAS_OUTPUT, - field: 'mask', - }, - } - ); - } else { - // Add Images To Nodes - (graph.nodes[NOISE] as Invocation<'noise'>).width = width; - (graph.nodes[NOISE] as Invocation<'noise'>).height = height; - - graph.nodes[INPAINT_IMAGE] = { - ...(graph.nodes[INPAINT_IMAGE] as Invocation<'i2l'>), - image: canvasInitImage, - }; - - graph.nodes[INPAINT_CREATE_MASK] = { - ...(graph.nodes[INPAINT_CREATE_MASK] as Invocation<'create_gradient_mask'>), - mask: canvasMaskImage, - }; - - // Paste Back - graph.nodes[CANVAS_OUTPUT] = { - ...(graph.nodes[CANVAS_OUTPUT] as Invocation<'canvas_paste_back'>), - mask: canvasMaskImage, - }; - - graph.edges.push({ - source: { - node_id: LATENTS_TO_IMAGE, - field: 'image', - }, - destination: { - node_id: CANVAS_OUTPUT, - field: 'target_image', - }, - }); - } - - addCoreMetadataNode( - graph, - { - generation_mode: 'sdxl_inpaint', - _canvas_objects: state.canvas.layerState.objects, - }, - CANVAS_OUTPUT - ); - - // Add Seamless To Graph - if (seamlessXAxis || seamlessYAxis) { - addSeamlessToLinearGraph(state, graph, modelLoaderNodeId); - modelLoaderNodeId = SEAMLESS; - } - - // Add Refiner if enabled - if (refinerModel) { - await addSDXLRefinerToGraph(state, graph, SDXL_DENOISE_LATENTS, modelLoaderNodeId); - if (seamlessXAxis || seamlessYAxis) { - modelLoaderNodeId = SDXL_REFINER_SEAMLESS; - } - } - - // Add VAE - await addVAEToGraph(state, graph, modelLoaderNodeId); - - // add LoRA support - await addSDXLLoRAsToGraph(state, graph, SDXL_DENOISE_LATENTS, modelLoaderNodeId); - - // add controlnet, mutating `graph` - await addControlNetToLinearGraph(state, graph, SDXL_DENOISE_LATENTS); - - // Add IP Adapter - await addIPAdapterToLinearGraph(state, graph, SDXL_DENOISE_LATENTS); - await addT2IAdaptersToLinearGraph(state, graph, SDXL_DENOISE_LATENTS); - - // NSFW & watermark - must be last thing added to graph - if (state.system.shouldUseNSFWChecker) { - // must add before watermarker! - addNSFWCheckerToGraph(state, graph, CANVAS_OUTPUT); - } - - if (state.system.shouldUseWatermarker) { - // must add after nsfw checker! - addWatermarkerToGraph(state, graph, CANVAS_OUTPUT); - } - - return graph; -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLOutpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLOutpaintGraph.ts deleted file mode 100644 index 1beadc5a96f..00000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLOutpaintGraph.ts +++ /dev/null @@ -1,644 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { RootState } from 'app/store/store'; -import { addCoreMetadataNode } from 'features/nodes/util/graph/canvas/metadata'; -import { - CANVAS_OUTPUT, - INPAINT_CREATE_MASK, - INPAINT_IMAGE, - INPAINT_IMAGE_RESIZE_DOWN, - INPAINT_IMAGE_RESIZE_UP, - INPAINT_INFILL, - INPAINT_INFILL_RESIZE_DOWN, - LATENTS_TO_IMAGE, - MASK_COMBINE, - MASK_FROM_ALPHA, - MASK_RESIZE_DOWN, - MASK_RESIZE_UP, - NEGATIVE_CONDITIONING, - NOISE, - POSITIVE_CONDITIONING, - SDXL_CANVAS_OUTPAINT_GRAPH, - SDXL_DENOISE_LATENTS, - SDXL_MODEL_LOADER, - SDXL_REFINER_SEAMLESS, - SEAMLESS, -} from 'features/nodes/util/graph/constants'; -import { - getBoardField, - getIsIntermediate, - getPresetModifiedPrompts, -} from 'features/nodes/util/graph/graphBuilderUtils'; -import type { ImageDTO, Invocation, NonNullableGraph } from 'services/api/types'; - -import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; -import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; -import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; -import { addSDXLLoRAsToGraph } from './addSDXLLoRAstoGraph'; -import { addSDXLRefinerToGraph } from './addSDXLRefinerToGraph'; -import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; -import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph'; -import { addVAEToGraph } from './addVAEToGraph'; -import { addWatermarkerToGraph } from './addWatermarkerToGraph'; - -/** - * Builds the Canvas tab's Outpaint graph. - */ -export const buildCanvasSDXLOutpaintGraph = async ( - state: RootState, - canvasInitImage: ImageDTO, - canvasMaskImage?: ImageDTO -): Promise => { - const log = logger('nodes'); - const { - model, - cfgScale: cfg_scale, - cfgRescaleMultiplier: cfg_rescale_multiplier, - scheduler, - steps, - img2imgStrength: strength, - seed, - vaePrecision, - shouldUseCpuNoise, - infillTileSize, - infillPatchmatchDownscaleSize, - infillMethod, - // infillMosaicTileWidth, - // infillMosaicTileHeight, - // infillMosaicMinColor, - // infillMosaicMaxColor, - infillColorValue, - seamlessXAxis, - seamlessYAxis, - canvasCoherenceMode, - canvasCoherenceMinDenoise, - canvasCoherenceEdgeSize, - maskBlur, - } = state.canvasV2.params; - - const { refinerModel, refinerStart } = state.canvasV2.params; - - if (!model) { - log.error('No model found in state'); - throw new Error('No model found in state'); - } - - // The bounding box determines width and height, not the width and height params - const { width, height } = state.canvas.boundingBoxDimensions; - - // We may need to set the inpaint width and height to scale the image - const { scaledBoundingBoxDimensions, boundingBoxScaleMethod } = state.canvas; - - const fp32 = vaePrecision === 'fp32'; - const is_intermediate = true; - const isUsingScaledDimensions = ['auto', 'manual'].includes(boundingBoxScaleMethod); - - let modelLoaderNodeId = SDXL_MODEL_LOADER; - - const use_cpu = shouldUseCpuNoise; - - // Construct Style Prompt - const { positivePrompt, negativePrompt, positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state); - - const graph: NonNullableGraph = { - id: SDXL_CANVAS_OUTPAINT_GRAPH, - nodes: { - [SDXL_MODEL_LOADER]: { - type: 'sdxl_model_loader', - id: SDXL_MODEL_LOADER, - model, - }, - [POSITIVE_CONDITIONING]: { - type: 'sdxl_compel_prompt', - id: POSITIVE_CONDITIONING, - is_intermediate, - prompt: positivePrompt, - style: positiveStylePrompt, - }, - [NEGATIVE_CONDITIONING]: { - type: 'sdxl_compel_prompt', - id: NEGATIVE_CONDITIONING, - is_intermediate, - prompt: negativePrompt, - style: negativeStylePrompt, - }, - [MASK_FROM_ALPHA]: { - type: 'tomask', - id: MASK_FROM_ALPHA, - is_intermediate, - image: canvasInitImage, - }, - [MASK_COMBINE]: { - type: 'mask_combine', - id: MASK_COMBINE, - is_intermediate, - mask2: canvasMaskImage, - }, - [INPAINT_IMAGE]: { - type: 'i2l', - id: INPAINT_IMAGE, - is_intermediate, - fp32, - }, - [NOISE]: { - type: 'noise', - id: NOISE, - use_cpu, - seed, - is_intermediate, - }, - [INPAINT_CREATE_MASK]: { - type: 'create_gradient_mask', - id: INPAINT_CREATE_MASK, - is_intermediate, - coherence_mode: canvasCoherenceMode, - edge_radius: canvasCoherenceEdgeSize, - minimum_denoise: refinerModel ? Math.max(0.2, canvasCoherenceMinDenoise) : canvasCoherenceMinDenoise, - tiled: false, - fp32: fp32, - }, - [SDXL_DENOISE_LATENTS]: { - type: 'denoise_latents', - id: SDXL_DENOISE_LATENTS, - is_intermediate, - steps: steps, - cfg_scale: cfg_scale, - cfg_rescale_multiplier, - scheduler: scheduler, - denoising_start: refinerModel ? Math.min(refinerStart, 1 - strength) : 1 - strength, - denoising_end: refinerModel ? refinerStart : 1, - }, - - [LATENTS_TO_IMAGE]: { - type: 'l2i', - id: LATENTS_TO_IMAGE, - is_intermediate, - fp32, - }, - [CANVAS_OUTPUT]: { - type: 'canvas_paste_back', - id: CANVAS_OUTPUT, - is_intermediate: getIsIntermediate(state), - board: getBoardField(state), - use_cache: false, - mask_blur: maskBlur, - }, - }, - edges: [ - // Connect Model Loader To UNet and CLIP - { - source: { - node_id: SDXL_MODEL_LOADER, - field: 'unet', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'unet', - }, - }, - { - source: { - node_id: SDXL_MODEL_LOADER, - field: 'clip', - }, - destination: { - node_id: POSITIVE_CONDITIONING, - field: 'clip', - }, - }, - { - source: { - node_id: SDXL_MODEL_LOADER, - field: 'clip2', - }, - destination: { - node_id: POSITIVE_CONDITIONING, - field: 'clip2', - }, - }, - { - source: { - node_id: SDXL_MODEL_LOADER, - field: 'clip', - }, - destination: { - node_id: NEGATIVE_CONDITIONING, - field: 'clip', - }, - }, - { - source: { - node_id: SDXL_MODEL_LOADER, - field: 'clip2', - }, - destination: { - node_id: NEGATIVE_CONDITIONING, - field: 'clip2', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'unet', - }, - destination: { - node_id: INPAINT_CREATE_MASK, - field: 'unet', - }, - }, - // Connect Infill Result To Inpaint Image - { - source: { - node_id: INPAINT_INFILL, - field: 'image', - }, - destination: { - node_id: INPAINT_IMAGE, - field: 'image', - }, - }, - // Combine Mask from Init Image with User Painted Mask - { - source: { - node_id: MASK_FROM_ALPHA, - field: 'image', - }, - destination: { - node_id: MASK_COMBINE, - field: 'mask1', - }, - }, - // Plug Everything Into Inpaint Node - { - source: { - node_id: POSITIVE_CONDITIONING, - field: 'conditioning', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'positive_conditioning', - }, - }, - { - source: { - node_id: NEGATIVE_CONDITIONING, - field: 'conditioning', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'negative_conditioning', - }, - }, - { - source: { - node_id: NOISE, - field: 'noise', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'noise', - }, - }, - { - source: { - node_id: INPAINT_IMAGE, - field: 'latents', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'latents', - }, - }, - // Create Inpaint Mask - { - source: { - node_id: isUsingScaledDimensions ? MASK_RESIZE_UP : MASK_COMBINE, - field: 'image', - }, - destination: { - node_id: INPAINT_CREATE_MASK, - field: 'mask', - }, - }, - { - source: { - node_id: INPAINT_CREATE_MASK, - field: 'denoise_mask', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'denoise_mask', - }, - }, - - { - source: { - node_id: SDXL_DENOISE_LATENTS, - field: 'latents', - }, - destination: { - node_id: LATENTS_TO_IMAGE, - field: 'latents', - }, - }, - ], - }; - - // Add Infill Nodes - if (infillMethod === 'patchmatch') { - graph.nodes[INPAINT_INFILL] = { - type: 'infill_patchmatch', - id: INPAINT_INFILL, - is_intermediate, - downscale: infillPatchmatchDownscaleSize, - }; - } - - if (infillMethod === 'lama') { - graph.nodes[INPAINT_INFILL] = { - type: 'infill_lama', - id: INPAINT_INFILL, - is_intermediate, - }; - } - - if (infillMethod === 'cv2') { - graph.nodes[INPAINT_INFILL] = { - type: 'infill_cv2', - id: INPAINT_INFILL, - is_intermediate, - }; - } - - if (infillMethod === 'tile') { - graph.nodes[INPAINT_INFILL] = { - type: 'infill_tile', - id: INPAINT_INFILL, - is_intermediate, - tile_size: infillTileSize, - }; - } - - // TODO: add mosaic back - // if (infillMethod === 'mosaic') { - // graph.nodes[INPAINT_INFILL] = { - // type: 'infill_mosaic', - // id: INPAINT_INFILL, - // is_intermediate, - // tile_width: infillMosaicTileWidth, - // tile_height: infillMosaicTileHeight, - // min_color: infillMosaicMinColor, - // max_color: infillMosaicMaxColor, - // }; - // } - - if (infillMethod === 'color') { - graph.nodes[INPAINT_INFILL] = { - type: 'infill_rgba', - id: INPAINT_INFILL, - is_intermediate, - color: infillColorValue, - }; - } - - // Handle Scale Before Processing - if (isUsingScaledDimensions) { - const scaledWidth: number = scaledBoundingBoxDimensions.width; - const scaledHeight: number = scaledBoundingBoxDimensions.height; - - // Add Scaling Nodes - graph.nodes[INPAINT_IMAGE_RESIZE_UP] = { - type: 'img_resize', - id: INPAINT_IMAGE_RESIZE_UP, - is_intermediate, - width: scaledWidth, - height: scaledHeight, - image: canvasInitImage, - }; - graph.nodes[MASK_RESIZE_UP] = { - type: 'img_resize', - id: MASK_RESIZE_UP, - is_intermediate, - width: scaledWidth, - height: scaledHeight, - }; - graph.nodes[INPAINT_IMAGE_RESIZE_DOWN] = { - type: 'img_resize', - id: INPAINT_IMAGE_RESIZE_DOWN, - is_intermediate, - width: width, - height: height, - }; - graph.nodes[INPAINT_INFILL_RESIZE_DOWN] = { - type: 'img_resize', - id: INPAINT_INFILL_RESIZE_DOWN, - is_intermediate, - width: width, - height: height, - }; - graph.nodes[MASK_RESIZE_DOWN] = { - type: 'img_resize', - id: MASK_RESIZE_DOWN, - is_intermediate, - width: width, - height: height, - }; - - (graph.nodes[NOISE] as Invocation<'noise'>).width = scaledWidth; - (graph.nodes[NOISE] as Invocation<'noise'>).height = scaledHeight; - - // Connect Nodes - graph.edges.push( - // Scale Inpaint Image - { - source: { - node_id: INPAINT_IMAGE_RESIZE_UP, - field: 'image', - }, - destination: { - node_id: INPAINT_INFILL, - field: 'image', - }, - }, - { - source: { - node_id: INPAINT_IMAGE_RESIZE_UP, - field: 'image', - }, - destination: { - node_id: INPAINT_CREATE_MASK, - field: 'image', - }, - }, - // Take combined mask and resize - { - source: { - node_id: MASK_COMBINE, - field: 'image', - }, - destination: { - node_id: MASK_RESIZE_UP, - field: 'image', - }, - }, - // Resize Results Down - { - source: { - node_id: LATENTS_TO_IMAGE, - field: 'image', - }, - destination: { - node_id: INPAINT_IMAGE_RESIZE_DOWN, - field: 'image', - }, - }, - { - source: { - node_id: INPAINT_CREATE_MASK, - field: 'expanded_mask_area', - }, - destination: { - node_id: MASK_RESIZE_DOWN, - field: 'image', - }, - }, - { - source: { - node_id: INPAINT_INFILL, - field: 'image', - }, - destination: { - node_id: INPAINT_INFILL_RESIZE_DOWN, - field: 'image', - }, - }, - // Paste Back - { - source: { - node_id: INPAINT_INFILL_RESIZE_DOWN, - field: 'image', - }, - destination: { - node_id: CANVAS_OUTPUT, - field: 'source_image', - }, - }, - { - source: { - node_id: INPAINT_IMAGE_RESIZE_DOWN, - field: 'image', - }, - destination: { - node_id: CANVAS_OUTPUT, - field: 'target_image', - }, - }, - { - source: { - node_id: MASK_RESIZE_DOWN, - field: 'image', - }, - destination: { - node_id: CANVAS_OUTPUT, - field: 'mask', - }, - } - ); - } else { - // Add Images To Nodes - graph.nodes[INPAINT_INFILL] = { - ...(graph.nodes[INPAINT_INFILL] as Invocation<'infill_tile'> | Invocation<'infill_patchmatch'>), - image: canvasInitImage, - }; - - (graph.nodes[NOISE] as Invocation<'noise'>).width = width; - (graph.nodes[NOISE] as Invocation<'noise'>).height = height; - - graph.nodes[INPAINT_IMAGE] = { - ...(graph.nodes[INPAINT_IMAGE] as Invocation<'i2l'>), - image: canvasInitImage, - }; - - graph.edges.push( - { - source: { - node_id: INPAINT_INFILL, - field: 'image', - }, - destination: { - node_id: CANVAS_OUTPUT, - field: 'source_image', - }, - }, - { - source: { - node_id: LATENTS_TO_IMAGE, - field: 'image', - }, - destination: { - node_id: CANVAS_OUTPUT, - field: 'target_image', - }, - }, - { - source: { - node_id: INPAINT_CREATE_MASK, - field: 'expanded_mask_area', - }, - destination: { - node_id: CANVAS_OUTPUT, - field: 'mask', - }, - } - ); - } - - addCoreMetadataNode( - graph, - { - generation_mode: 'sdxl_outpaint', - _canvas_objects: state.canvas.layerState.objects, - }, - CANVAS_OUTPUT - ); - - // Add Seamless To Graph - if (seamlessXAxis || seamlessYAxis) { - addSeamlessToLinearGraph(state, graph, modelLoaderNodeId); - modelLoaderNodeId = SEAMLESS; - } - - // Add Refiner if enabled - if (refinerModel) { - await addSDXLRefinerToGraph(state, graph, SDXL_DENOISE_LATENTS, modelLoaderNodeId); - if (seamlessXAxis || seamlessYAxis) { - modelLoaderNodeId = SDXL_REFINER_SEAMLESS; - } - } - - // Add VAE - await addVAEToGraph(state, graph, modelLoaderNodeId); - - // add LoRA support - await addSDXLLoRAsToGraph(state, graph, SDXL_DENOISE_LATENTS, modelLoaderNodeId); - - // add controlnet, mutating `graph` - await addControlNetToLinearGraph(state, graph, SDXL_DENOISE_LATENTS); - - // Add IP Adapter - await addIPAdapterToLinearGraph(state, graph, SDXL_DENOISE_LATENTS); - - await addT2IAdaptersToLinearGraph(state, graph, SDXL_DENOISE_LATENTS); - - // NSFW & watermark - must be last thing added to graph - if (state.system.shouldUseNSFWChecker) { - // must add before watermarker! - addNSFWCheckerToGraph(state, graph, CANVAS_OUTPUT); - } - - if (state.system.shouldUseWatermarker) { - // must add after nsfw checker! - addWatermarkerToGraph(state, graph, CANVAS_OUTPUT); - } - - return graph; -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLTextToImageGraph.ts deleted file mode 100644 index b75dd210d63..00000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasSDXLTextToImageGraph.ts +++ /dev/null @@ -1,341 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { RootState } from 'app/store/store'; -import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; -import { addCoreMetadataNode, getModelMetadataField } from 'features/nodes/util/graph/canvas/metadata'; -import { - CANVAS_OUTPUT, - LATENTS_TO_IMAGE, - NEGATIVE_CONDITIONING, - NOISE, - POSITIVE_CONDITIONING, - SDXL_CANVAS_TEXT_TO_IMAGE_GRAPH, - SDXL_DENOISE_LATENTS, - SDXL_MODEL_LOADER, - SDXL_REFINER_SEAMLESS, - SEAMLESS, -} from 'features/nodes/util/graph/constants'; -import { - getBoardField, - getIsIntermediate, - getPresetModifiedPrompts, -} from 'features/nodes/util/graph/graphBuilderUtils'; -import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types'; - -import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; -import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; -import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; -import { addSDXLLoRAsToGraph } from './addSDXLLoRAstoGraph'; -import { addSDXLRefinerToGraph } from './addSDXLRefinerToGraph'; -import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; -import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph'; -import { addVAEToGraph } from './addVAEToGraph'; -import { addWatermarkerToGraph } from './addWatermarkerToGraph'; - -/** - * Builds the Canvas tab's Text to Image graph. - */ -export const buildCanvasSDXLTextToImageGraph = async (state: RootState): Promise => { - const log = logger('nodes'); - const { - model, - cfgScale: cfg_scale, - cfgRescaleMultiplier: cfg_rescale_multiplier, - scheduler, - seed, - steps, - vaePrecision, - shouldUseCpuNoise, - seamlessXAxis, - seamlessYAxis, - } = state.canvasV2.params; - - // The bounding box determines width and height, not the width and height params - const { width, height } = state.canvas.boundingBoxDimensions; - - const { scaledBoundingBoxDimensions, boundingBoxScaleMethod } = state.canvas; - - const fp32 = vaePrecision === 'fp32'; - const is_intermediate = true; - const isUsingScaledDimensions = ['auto', 'manual'].includes(boundingBoxScaleMethod); - - const { refinerModel, refinerStart } = state.canvasV2.params; - - if (!model) { - log.error('No model found in state'); - throw new Error('No model found in state'); - } - - const use_cpu = shouldUseCpuNoise; - - let modelLoaderNodeId = SDXL_MODEL_LOADER; - - // Construct Style Prompt - const { positivePrompt, negativePrompt, positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state); - - /** - * The easiest way to build linear graphs is to do it in the node editor, then copy and paste the - * full graph here as a template. Then use the parameters from app state and set friendlier node - * ids. - * - * The only thing we need extra logic for is handling randomized seed, control net, and for img2img, - * the `fit` param. These are added to the graph at the end. - */ - - // copy-pasted graph from node editor, filled in with state values & friendly node ids - const graph: NonNullableGraph = { - id: SDXL_CANVAS_TEXT_TO_IMAGE_GRAPH, - nodes: { - [modelLoaderNodeId]: { - type: 'sdxl_model_loader', - id: modelLoaderNodeId, - is_intermediate, - model, - }, - [POSITIVE_CONDITIONING]: { - type: 'sdxl_compel_prompt', - id: POSITIVE_CONDITIONING, - is_intermediate, - prompt: positivePrompt, - style: positiveStylePrompt, - }, - [NEGATIVE_CONDITIONING]: { - type: 'sdxl_compel_prompt', - id: NEGATIVE_CONDITIONING, - is_intermediate, - prompt: negativePrompt, - style: negativeStylePrompt, - }, - [NOISE]: { - type: 'noise', - id: NOISE, - is_intermediate, - seed, - width: !isUsingScaledDimensions ? width : scaledBoundingBoxDimensions.width, - height: !isUsingScaledDimensions ? height : scaledBoundingBoxDimensions.height, - use_cpu, - }, - [SDXL_DENOISE_LATENTS]: { - type: 'denoise_latents', - id: SDXL_DENOISE_LATENTS, - is_intermediate, - cfg_scale, - cfg_rescale_multiplier, - scheduler, - steps, - denoising_start: 0, - denoising_end: refinerModel ? refinerStart : 1, - }, - }, - edges: [ - // Connect Model Loader to UNet and CLIP - { - source: { - node_id: modelLoaderNodeId, - field: 'unet', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'unet', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'clip', - }, - destination: { - node_id: POSITIVE_CONDITIONING, - field: 'clip', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'clip2', - }, - destination: { - node_id: POSITIVE_CONDITIONING, - field: 'clip2', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'clip', - }, - destination: { - node_id: NEGATIVE_CONDITIONING, - field: 'clip', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'clip2', - }, - destination: { - node_id: NEGATIVE_CONDITIONING, - field: 'clip2', - }, - }, - // Connect everything to Denoise Latents - { - source: { - node_id: POSITIVE_CONDITIONING, - field: 'conditioning', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'positive_conditioning', - }, - }, - { - source: { - node_id: NEGATIVE_CONDITIONING, - field: 'conditioning', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'negative_conditioning', - }, - }, - { - source: { - node_id: NOISE, - field: 'noise', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'noise', - }, - }, - ], - }; - - // Decode Latents To Image & Handle Scaled Before Processing - if (isUsingScaledDimensions) { - graph.nodes[LATENTS_TO_IMAGE] = { - id: LATENTS_TO_IMAGE, - type: 'l2i', - is_intermediate, - fp32, - }; - - graph.nodes[CANVAS_OUTPUT] = { - id: CANVAS_OUTPUT, - type: 'img_resize', - is_intermediate: getIsIntermediate(state), - board: getBoardField(state), - width: width, - height: height, - use_cache: false, - }; - - graph.edges.push( - { - source: { - node_id: SDXL_DENOISE_LATENTS, - field: 'latents', - }, - destination: { - node_id: LATENTS_TO_IMAGE, - field: 'latents', - }, - }, - { - source: { - node_id: LATENTS_TO_IMAGE, - field: 'image', - }, - destination: { - node_id: CANVAS_OUTPUT, - field: 'image', - }, - } - ); - } else { - graph.nodes[CANVAS_OUTPUT] = { - type: 'l2i', - id: CANVAS_OUTPUT, - is_intermediate: getIsIntermediate(state), - board: getBoardField(state), - fp32, - use_cache: false, - }; - - graph.edges.push({ - source: { - node_id: SDXL_DENOISE_LATENTS, - field: 'latents', - }, - destination: { - node_id: CANVAS_OUTPUT, - field: 'latents', - }, - }); - } - - const modelConfig = await fetchModelConfigWithTypeGuard(model.key, isNonRefinerMainModelConfig); - - addCoreMetadataNode( - graph, - { - generation_mode: 'txt2img', - cfg_scale, - cfg_rescale_multiplier, - width: !isUsingScaledDimensions ? width : scaledBoundingBoxDimensions.width, - height: !isUsingScaledDimensions ? height : scaledBoundingBoxDimensions.height, - positive_prompt: positivePrompt, - negative_prompt: negativePrompt, - positive_style_prompt: positiveStylePrompt, - negative_style_prompt: negativeStylePrompt, - model: getModelMetadataField(modelConfig), - seed, - steps, - rand_device: use_cpu ? 'cpu' : 'cuda', - scheduler, - _canvas_objects: state.canvas.layerState.objects, - }, - CANVAS_OUTPUT - ); - - // Add Seamless To Graph - if (seamlessXAxis || seamlessYAxis) { - addSeamlessToLinearGraph(state, graph, modelLoaderNodeId); - modelLoaderNodeId = SEAMLESS; - } - - // Add Refiner if enabled - if (refinerModel) { - await addSDXLRefinerToGraph(state, graph, SDXL_DENOISE_LATENTS, modelLoaderNodeId); - if (seamlessXAxis || seamlessYAxis) { - modelLoaderNodeId = SDXL_REFINER_SEAMLESS; - } - } - - // add LoRA support - await addSDXLLoRAsToGraph(state, graph, SDXL_DENOISE_LATENTS, modelLoaderNodeId); - - // optionally add custom VAE - await addVAEToGraph(state, graph, modelLoaderNodeId); - - // add controlnet, mutating `graph` - await addControlNetToLinearGraph(state, graph, SDXL_DENOISE_LATENTS); - - // Add IP Adapter - await addIPAdapterToLinearGraph(state, graph, SDXL_DENOISE_LATENTS); - await addT2IAdaptersToLinearGraph(state, graph, SDXL_DENOISE_LATENTS); - - // NSFW & watermark - must be last thing added to graph - if (state.system.shouldUseNSFWChecker) { - // must add before watermarker! - addNSFWCheckerToGraph(state, graph, CANVAS_OUTPUT); - } - - if (state.system.shouldUseWatermarker) { - // must add after nsfw checker! - addWatermarkerToGraph(state, graph, CANVAS_OUTPUT); - } - - return graph; -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasTextToImageGraph.ts deleted file mode 100644 index 045a83f6ea9..00000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/buildCanvasTextToImageGraph.ts +++ /dev/null @@ -1,324 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { RootState } from 'app/store/store'; -import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; -import { addCoreMetadataNode, getModelMetadataField } from 'features/nodes/util/graph/canvas/metadata'; -import { - CANVAS_OUTPUT, - CANVAS_TEXT_TO_IMAGE_GRAPH, - CLIP_SKIP, - DENOISE_LATENTS, - LATENTS_TO_IMAGE, - MAIN_MODEL_LOADER, - NEGATIVE_CONDITIONING, - NOISE, - POSITIVE_CONDITIONING, - SEAMLESS, -} from 'features/nodes/util/graph/constants'; -import { - getBoardField, - getIsIntermediate, - getPresetModifiedPrompts, -} from 'features/nodes/util/graph/graphBuilderUtils'; -import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types'; - -import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; -import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; -import { addLoRAsToGraph } from './addLoRAsToGraph'; -import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; -import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; -import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph'; -import { addVAEToGraph } from './addVAEToGraph'; -import { addWatermarkerToGraph } from './addWatermarkerToGraph'; - -/** - * Builds the Canvas tab's Text to Image graph. - */ -export const buildCanvasTextToImageGraph = async (state: RootState): Promise => { - const log = logger('nodes'); - const { - model, - cfgScale: cfg_scale, - cfgRescaleMultiplier: cfg_rescale_multiplier, - scheduler, - seed, - steps, - vaePrecision, - clipSkip, - shouldUseCpuNoise, - seamlessXAxis, - seamlessYAxis, - } = state.canvasV2.params; - - // The bounding box determines width and height, not the width and height params - const { width, height } = state.canvas.boundingBoxDimensions; - - const { scaledBoundingBoxDimensions, boundingBoxScaleMethod } = state.canvas; - - const fp32 = vaePrecision === 'fp32'; - const is_intermediate = true; - const isUsingScaledDimensions = ['auto', 'manual'].includes(boundingBoxScaleMethod); - - if (!model) { - log.error('No model found in state'); - throw new Error('No model found in state'); - } - - const use_cpu = shouldUseCpuNoise; - - let modelLoaderNodeId = MAIN_MODEL_LOADER; - - const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state); - - /** - * The easiest way to build linear graphs is to do it in the node editor, then copy and paste the - * full graph here as a template. Then use the parameters from app state and set friendlier node - * ids. - * - * The only thing we need extra logic for is handling randomized seed, control net, and for img2img, - * the `fit` param. These are added to the graph at the end. - */ - - // copy-pasted graph from node editor, filled in with state values & friendly node ids - const graph: NonNullableGraph = { - id: CANVAS_TEXT_TO_IMAGE_GRAPH, - nodes: { - [modelLoaderNodeId]: { - type: 'main_model_loader', - id: modelLoaderNodeId, - is_intermediate, - model, - }, - [CLIP_SKIP]: { - type: 'clip_skip', - id: CLIP_SKIP, - is_intermediate, - skipped_layers: clipSkip, - }, - [POSITIVE_CONDITIONING]: { - type: 'compel', - id: POSITIVE_CONDITIONING, - is_intermediate, - prompt: positivePrompt, - }, - [NEGATIVE_CONDITIONING]: { - type: 'compel', - id: NEGATIVE_CONDITIONING, - is_intermediate, - prompt: negativePrompt, - }, - [NOISE]: { - type: 'noise', - id: NOISE, - is_intermediate, - seed, - width: !isUsingScaledDimensions ? width : scaledBoundingBoxDimensions.width, - height: !isUsingScaledDimensions ? height : scaledBoundingBoxDimensions.height, - use_cpu, - }, - [DENOISE_LATENTS]: { - type: 'denoise_latents', - id: DENOISE_LATENTS, - is_intermediate, - cfg_scale, - cfg_rescale_multiplier, - scheduler, - steps, - denoising_start: 0, - denoising_end: 1, - }, - }, - edges: [ - // Connect Model Loader to UNet & CLIP Skip - { - source: { - node_id: modelLoaderNodeId, - field: 'unet', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'unet', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'clip', - }, - destination: { - node_id: CLIP_SKIP, - field: 'clip', - }, - }, - // Connect CLIP Skip to Conditioning - { - source: { - node_id: CLIP_SKIP, - field: 'clip', - }, - destination: { - node_id: POSITIVE_CONDITIONING, - field: 'clip', - }, - }, - { - source: { - node_id: CLIP_SKIP, - field: 'clip', - }, - destination: { - node_id: NEGATIVE_CONDITIONING, - field: 'clip', - }, - }, - // Connect everything to Denoise Latents - { - source: { - node_id: POSITIVE_CONDITIONING, - field: 'conditioning', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'positive_conditioning', - }, - }, - { - source: { - node_id: NEGATIVE_CONDITIONING, - field: 'conditioning', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'negative_conditioning', - }, - }, - { - source: { - node_id: NOISE, - field: 'noise', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'noise', - }, - }, - ], - }; - - // Decode Latents To Image & Handle Scaled Before Processing - if (isUsingScaledDimensions) { - graph.nodes[LATENTS_TO_IMAGE] = { - id: LATENTS_TO_IMAGE, - type: 'l2i', - is_intermediate, - fp32, - }; - - graph.nodes[CANVAS_OUTPUT] = { - id: CANVAS_OUTPUT, - type: 'img_resize', - is_intermediate: getIsIntermediate(state), - board: getBoardField(state), - width: width, - height: height, - use_cache: false, - }; - - graph.edges.push( - { - source: { - node_id: DENOISE_LATENTS, - field: 'latents', - }, - destination: { - node_id: LATENTS_TO_IMAGE, - field: 'latents', - }, - }, - { - source: { - node_id: LATENTS_TO_IMAGE, - field: 'image', - }, - destination: { - node_id: CANVAS_OUTPUT, - field: 'image', - }, - } - ); - } else { - graph.nodes[CANVAS_OUTPUT] = { - type: 'l2i', - id: CANVAS_OUTPUT, - is_intermediate: getIsIntermediate(state), - board: getBoardField(state), - fp32, - use_cache: false, - }; - - graph.edges.push({ - source: { - node_id: DENOISE_LATENTS, - field: 'latents', - }, - destination: { - node_id: CANVAS_OUTPUT, - field: 'latents', - }, - }); - } - - const modelConfig = await fetchModelConfigWithTypeGuard(model.key, isNonRefinerMainModelConfig); - - addCoreMetadataNode( - graph, - { - generation_mode: 'txt2img', - cfg_scale, - cfg_rescale_multiplier, - width: !isUsingScaledDimensions ? width : scaledBoundingBoxDimensions.width, - height: !isUsingScaledDimensions ? height : scaledBoundingBoxDimensions.height, - positive_prompt: positivePrompt, - negative_prompt: negativePrompt, - model: getModelMetadataField(modelConfig), - seed, - steps, - rand_device: use_cpu ? 'cpu' : 'cuda', - scheduler, - clip_skip: clipSkip, - _canvas_objects: state.canvas.layerState.objects, - }, - CANVAS_OUTPUT - ); - - // Add Seamless To Graph - if (seamlessXAxis || seamlessYAxis) { - addSeamlessToLinearGraph(state, graph, modelLoaderNodeId); - modelLoaderNodeId = SEAMLESS; - } - - // optionally add custom VAE - await addVAEToGraph(state, graph, modelLoaderNodeId); - - // add LoRA support - await addLoRAsToGraph(state, graph, DENOISE_LATENTS, modelLoaderNodeId); - - // add controlnet, mutating `graph` - await addControlNetToLinearGraph(state, graph, DENOISE_LATENTS); - - // Add IP Adapter - await addIPAdapterToLinearGraph(state, graph, DENOISE_LATENTS); - await addT2IAdaptersToLinearGraph(state, graph, DENOISE_LATENTS); - - // NSFW & watermark - must be last thing added to graph - if (state.system.shouldUseNSFWChecker) { - // must add before watermarker! - addNSFWCheckerToGraph(state, graph, CANVAS_OUTPUT); - } - - if (state.system.shouldUseWatermarker) { - // must add after nsfw checker! - addWatermarkerToGraph(state, graph, CANVAS_OUTPUT); - } - - return graph; -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/metadata.ts b/invokeai/frontend/web/src/features/nodes/util/graph/canvas/metadata.ts deleted file mode 100644 index 97f77f58d9b..00000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/canvas/metadata.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { JSONObject } from 'common/types'; -import type { ModelIdentifierField } from 'features/nodes/types/common'; -import { METADATA } from 'features/nodes/util/graph/constants'; -import type { AnyModelConfig, NonNullableGraph, S } from 'services/api/types'; - -export const addCoreMetadataNode = ( - graph: NonNullableGraph, - metadata: Partial, - nodeId: string -): void => { - graph.nodes[METADATA] = { - id: METADATA, - type: 'core_metadata', - ...metadata, - }; - - graph.edges.push({ - source: { - node_id: METADATA, - field: 'metadata', - }, - destination: { - node_id: nodeId, - field: 'metadata', - }, - }); - - return; -}; - -export const upsertMetadata = ( - graph: NonNullableGraph, - metadata: Partial | JSONObject -): void => { - const metadataNode = graph.nodes[METADATA] as S['CoreMetadataInvocation'] | undefined; - - if (!metadataNode) { - return; - } - - Object.assign(metadataNode, metadata); -}; - -export const removeMetadata = (graph: NonNullableGraph, key: keyof S['CoreMetadataInvocation']): void => { - const metadataNode = graph.nodes[METADATA] as S['CoreMetadataInvocation'] | undefined; - - if (!metadataNode) { - return; - } - - delete metadataNode[key]; -}; - -export const getHasMetadata = (graph: NonNullableGraph): boolean => { - const metadataNode = graph.nodes[METADATA] as S['CoreMetadataInvocation'] | undefined; - - return Boolean(metadataNode); -}; - -export const getModelMetadataField = ({ key, hash, name, base, type }: AnyModelConfig): ModelIdentifierField => ({ - key, - hash, - name, - base, - type, -}); From 5621075cb70d0191350bc6b9f814b575837b66b1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 28 Jun 2024 18:25:09 +1000 Subject: [PATCH 134/678] feat(ui): make Graph class's getMetadataNode public --- .../nodes/util/graph/generation/Graph.test.ts | 16 ++++++++-------- .../nodes/util/graph/generation/Graph.ts | 12 ++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.test.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.test.ts index c3c3ca23488..f2e09adfa1c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.test.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.test.ts @@ -545,14 +545,14 @@ describe('Graph', () => { }); describe('metadata utils', () => { - describe('_getMetadataNode', () => { + describe('getMetadataNode', () => { it("should get the metadata node, creating it if it doesn't exist", () => { const g = new Graph(); - const metadata = g._getMetadataNode(); + const metadata = g.getMetadataNode(); expect(metadata.id).toBe('core_metadata'); expect(metadata.type).toBe('core_metadata'); g.upsertMetadata({ test: 'test' }); - const metadata2 = g._getMetadataNode(); + const metadata2 = g.getMetadataNode(); expect(metadata2).toHaveProperty('test'); }); }); @@ -561,14 +561,14 @@ describe('Graph', () => { it('should add metadata to the metadata node', () => { const g = new Graph(); g.upsertMetadata({ test: 'test' }); - const metadata = g._getMetadataNode(); + const metadata = g.getMetadataNode(); expect(metadata).toHaveProperty('test'); }); it('should update metadata on the metadata node', () => { const g = new Graph(); g.upsertMetadata({ test: 'test' }); g.upsertMetadata({ test: 'test2' }); - const metadata = g._getMetadataNode(); + const metadata = g.getMetadataNode(); expect(metadata.test).toBe('test2'); }); }); @@ -578,14 +578,14 @@ describe('Graph', () => { const g = new Graph(); g.upsertMetadata({ test: 'test', test2: 'test2' }); g.removeMetadata(['test']); - const metadata = g._getMetadataNode(); + const metadata = g.getMetadataNode(); expect(metadata).not.toHaveProperty('test'); }); it('should remove multiple metadata from the metadata node', () => { const g = new Graph(); g.upsertMetadata({ test: 'test', test2: 'test2' }); g.removeMetadata(['test', 'test2']); - const metadata = g._getMetadataNode(); + const metadata = g.getMetadataNode(); expect(metadata).not.toHaveProperty('test'); expect(metadata).not.toHaveProperty('test2'); }); @@ -615,7 +615,7 @@ describe('Graph', () => { }); g.upsertMetadata({ test: 'test' }); g.setMetadataReceivingNode(n1); - const metadata = g._getMetadataNode(); + const metadata = g.getMetadataNode(); expect(g.getEdgesFrom(metadata as unknown as AnyInvocation).length).toBe(1); expect(g.getEdgesTo(n1).length).toBe(1); }); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.ts index db96a5f7d0e..d223d1f500f 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.ts @@ -356,10 +356,10 @@ export class Graph { //#region Metadata /** - * INTERNAL: Get the metadata node. If it does not exist, it is created. + * Get the metadata node. If it does not exist, it is created. * @returns The metadata node. */ - _getMetadataNode(): S['CoreMetadataInvocation'] { + getMetadataNode(): S['CoreMetadataInvocation'] { try { const node = this.getNode(METADATA) as AnyInvocationIncMetadata; assert(node.type === 'core_metadata'); @@ -378,7 +378,7 @@ export class Graph { * @returns The metadata node. */ upsertMetadata(metadata: Partial): S['CoreMetadataInvocation'] { - const node = this._getMetadataNode(); + const node = this.getMetadataNode(); Object.assign(node, metadata); return node; } @@ -389,7 +389,7 @@ export class Graph { * @returns The metadata node */ removeMetadata(keys: string[]): S['CoreMetadataInvocation'] { - const metadataNode = this._getMetadataNode(); + const metadataNode = this.getMetadataNode(); for (const k of keys) { unset(metadataNode, k); } @@ -417,9 +417,9 @@ export class Graph { */ setMetadataReceivingNode(node: AnyInvocation): void { // @ts-expect-error `Graph` excludes `core_metadata` nodes due to its excessively wide typing - this.deleteEdgesFrom(this._getMetadataNode()); + this.deleteEdgesFrom(this.getMetadataNode()); // @ts-expect-error `Graph` excludes `core_metadata` nodes due to its excessively wide typing - this.addEdge(this._getMetadataNode(), 'metadata', node, 'metadata'); + this.addEdge(this.getMetadataNode(), 'metadata', node, 'metadata'); } //#endregion From 183c9dd73670f5e4c037bb322065d616bf9a4ddd Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 28 Jun 2024 18:28:27 +1000 Subject: [PATCH 135/678] fix(ui): batch building after removing canvas files --- .../listeners/enqueueRequestedLinear.ts | 8 +- .../util/graph/buildLinearBatchConfig.ts | 91 +++++++------------ .../util/graph/generation/addSDXLRefiner.ts | 5 +- .../util/graph/generation/buildSD1Graph.ts | 5 +- .../util/graph/generation/buildSDXLGraph.ts | 6 +- 5 files changed, 44 insertions(+), 71 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index 8ded74a06d9..1a476f889f5 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -19,7 +19,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) const model = state.canvasV2.params.model; const { prepend } = action.payload; - let graph; + let g; const manager = getNodeManager(); assert(model, 'No model found in state'); @@ -29,14 +29,14 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) manager.getImageSourceImage({ bbox: state.canvasV2.bbox, preview: true }); if (base === 'sdxl') { - graph = await buildSDXLGraph(state, manager); + g = await buildSDXLGraph(state, manager); } else if (base === 'sd-1' || base === 'sd-2') { - graph = await buildSD1Graph(state, manager); + g = await buildSD1Graph(state, manager); } else { assert(false, `No graph builders for base ${base}`); } - const batchConfig = prepareLinearUIBatch(state, graph, prepend); + const batchConfig = prepareLinearUIBatch(state, g, prepend); const req = dispatch( queueApi.endpoints.enqueueBatch.initiate(batchConfig, { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts index 7fd8ab10654..bb282863b98 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts @@ -1,14 +1,13 @@ -import { NUMPY_RAND_MAX } from 'app/constants'; import type { RootState } from 'app/store/store'; import { generateSeeds } from 'common/util/generateSeeds'; +import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { range } from 'lodash-es'; import type { components } from 'services/api/schema'; -import type { Batch, BatchConfig, NonNullableGraph } from 'services/api/types'; +import type { Batch, BatchConfig } from 'services/api/types'; -import { getHasMetadata, removeMetadata } from './canvas/metadata'; -import { CANVAS_COHERENCE_NOISE, METADATA, NOISE, POSITIVE_CONDITIONING } from './constants'; +import { NOISE, POSITIVE_CONDITIONING } from './constants'; -export const prepareLinearUIBatch = (state: RootState, graph: NonNullableGraph, prepend: boolean): BatchConfig => { +export const prepareLinearUIBatch = (state: RootState, g: Graph, prepend: boolean): BatchConfig => { const { iterations, model, shouldRandomizeSeed, seed, shouldConcatPrompts } = state.canvasV2.params; const { prompts, seedBehaviour } = state.dynamicPrompts; @@ -23,7 +22,7 @@ export const prepareLinearUIBatch = (state: RootState, graph: NonNullableGraph, start: shouldRandomizeSeed ? undefined : seed, }); - if (graph.nodes[NOISE]) { + if (g.hasNode(NOISE)) { firstBatchDatumList.push({ node_path: NOISE, field_name: 'seed', @@ -32,22 +31,12 @@ export const prepareLinearUIBatch = (state: RootState, graph: NonNullableGraph, } // add to metadata - if (getHasMetadata(graph)) { - removeMetadata(graph, 'seed'); - firstBatchDatumList.push({ - node_path: METADATA, - field_name: 'seed', - items: seeds, - }); - } - - if (graph.nodes[CANVAS_COHERENCE_NOISE]) { - firstBatchDatumList.push({ - node_path: CANVAS_COHERENCE_NOISE, - field_name: 'seed', - items: seeds.map((seed) => (seed + 1) % NUMPY_RAND_MAX), - }); - } + g.removeMetadata(['seed']); + firstBatchDatumList.push({ + node_path: g.getMetadataNode().id, + field_name: 'seed', + items: seeds, + }); } else { // seedBehaviour = SeedBehaviour.PerRun const seeds = generateSeeds({ @@ -55,7 +44,7 @@ export const prepareLinearUIBatch = (state: RootState, graph: NonNullableGraph, start: shouldRandomizeSeed ? undefined : seed, }); - if (graph.nodes[NOISE]) { + if (g.hasNode(NOISE)) { secondBatchDatumList.push({ node_path: NOISE, field_name: 'seed', @@ -64,29 +53,19 @@ export const prepareLinearUIBatch = (state: RootState, graph: NonNullableGraph, } // add to metadata - if (getHasMetadata(graph)) { - removeMetadata(graph, 'seed'); - secondBatchDatumList.push({ - node_path: METADATA, - field_name: 'seed', - items: seeds, - }); - } - - if (graph.nodes[CANVAS_COHERENCE_NOISE]) { - secondBatchDatumList.push({ - node_path: CANVAS_COHERENCE_NOISE, - field_name: 'seed', - items: seeds.map((seed) => (seed + 1) % NUMPY_RAND_MAX), - }); - } + g.removeMetadata(['seed']); + secondBatchDatumList.push({ + node_path: g.getMetadataNode().id, + field_name: 'seed', + items: seeds, + }); data.push(secondBatchDatumList); } const extendedPrompts = seedBehaviour === 'PER_PROMPT' ? range(iterations).flatMap(() => prompts) : prompts; // zipped batch of prompts - if (graph.nodes[POSITIVE_CONDITIONING]) { + if (g.hasNode(POSITIVE_CONDITIONING)) { firstBatchDatumList.push({ node_path: POSITIVE_CONDITIONING, field_name: 'prompt', @@ -95,17 +74,15 @@ export const prepareLinearUIBatch = (state: RootState, graph: NonNullableGraph, } // add to metadata - if (getHasMetadata(graph)) { - removeMetadata(graph, 'positive_prompt'); - firstBatchDatumList.push({ - node_path: METADATA, - field_name: 'positive_prompt', - items: extendedPrompts, - }); - } + g.removeMetadata(['positive_prompt']); + firstBatchDatumList.push({ + node_path: g.getMetadataNode().id, + field_name: 'positive_prompt', + items: extendedPrompts, + }); if (shouldConcatPrompts && model?.base === 'sdxl') { - if (graph.nodes[POSITIVE_CONDITIONING]) { + if (g.hasNode(POSITIVE_CONDITIONING)) { firstBatchDatumList.push({ node_path: POSITIVE_CONDITIONING, field_name: 'style', @@ -114,14 +91,12 @@ export const prepareLinearUIBatch = (state: RootState, graph: NonNullableGraph, } // add to metadata - if (getHasMetadata(graph)) { - removeMetadata(graph, 'positive_style_prompt'); - firstBatchDatumList.push({ - node_path: METADATA, - field_name: 'positive_style_prompt', - items: extendedPrompts, - }); - } + g.removeMetadata(['positive_style_prompt']); + firstBatchDatumList.push({ + node_path: g.getMetadataNode().id, + field_name: 'positive_style_prompt', + items: extendedPrompts, + }); } data.push(firstBatchDatumList); @@ -129,7 +104,7 @@ export const prepareLinearUIBatch = (state: RootState, graph: NonNullableGraph, const enqueueBatchArg: BatchConfig = { prepend, batch: { - graph, + graph: g.getGraph(), runs: 1, data, }, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLRefiner.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLRefiner.ts index f92e3cf7f89..7e79ffe4ff2 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLRefiner.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLRefiner.ts @@ -1,6 +1,5 @@ import type { RootState } from 'app/store/store'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; -import { getModelMetadataField } from 'features/nodes/util/graph/canvas/metadata'; import { SDXL_REFINER_DENOISE_LATENTS, SDXL_REFINER_MODEL_LOADER, @@ -8,7 +7,7 @@ import { SDXL_REFINER_POSITIVE_CONDITIONING, SDXL_REFINER_SEAMLESS, } from 'features/nodes/util/graph/constants'; -import type { Graph } from 'features/nodes/util/graph/generation/Graph'; +import { Graph } from 'features/nodes/util/graph/generation/Graph'; import type { Invocation } from 'services/api/types'; import { isRefinerMainModelModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; @@ -89,7 +88,7 @@ export const addSDXLRefiner = async ( g.addEdge(refinerDenoise, 'latents', l2i, 'latents'); g.upsertMetadata({ - refiner_model: getModelMetadataField(modelConfig), + refiner_model: Graph.getModelMetadataField(modelConfig), refiner_positive_aesthetic_score: refinerPositiveAestheticScore, refiner_negative_aesthetic_score: refinerNegativeAestheticScore, refiner_cfg_scale: refinerCFGScale, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index f55c8ad6b4d..9b929cc9ceb 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -26,7 +26,6 @@ import { addOutpaint } from 'features/nodes/util/graph/generation/addOutpaint'; import { addSeamless } from 'features/nodes/util/graph/generation/addSeamless'; import { addTextToImage } from 'features/nodes/util/graph/generation/addTextToImage'; import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermarker'; -import type { GraphType } from 'features/nodes/util/graph/generation/Graph'; import { Graph } from 'features/nodes/util/graph/generation/Graph'; import { getBoardField, getSizes } from 'features/nodes/util/graph/graphBuilderUtils'; import type { Invocation } from 'services/api/types'; @@ -35,7 +34,7 @@ import { assert } from 'tsafe'; import { addRegions } from './addRegions'; -export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager): Promise => { +export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager): Promise => { const generationMode = manager.getGenerationMode(); const { bbox, params } = state.canvasV2; @@ -248,5 +247,5 @@ export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager) }); g.setMetadataReceivingNode(canvasOutput); - return g.getGraph(); + return g; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index d75044c736c..04c0f0cf2f8 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -27,13 +27,13 @@ import { addTextToImage } from 'features/nodes/util/graph/generation/addTextToIm import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermarker'; import { Graph } from 'features/nodes/util/graph/generation/Graph'; import { getBoardField, getSDXLStylePrompts, getSizes } from 'features/nodes/util/graph/graphBuilderUtils'; -import type { Invocation, NonNullableGraph } from 'services/api/types'; +import type { Invocation } from 'services/api/types'; import { isNonRefinerMainModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; import { addRegions } from './addRegions'; -export const buildSDXLGraph = async (state: RootState, manager: KonvaNodeManager): Promise => { +export const buildSDXLGraph = async (state: RootState, manager: KonvaNodeManager): Promise => { const generationMode = manager.getGenerationMode(); const { bbox, params } = state.canvasV2; @@ -246,5 +246,5 @@ export const buildSDXLGraph = async (state: RootState, manager: KonvaNodeManager }); g.setMetadataReceivingNode(canvasOutput); - return g.getGraph(); + return g; }; From 7aa918cd463ee50c5679d6cb62233d19836a86e8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 28 Jun 2024 18:43:28 +1000 Subject: [PATCH 136/678] feat(ui): staging area image visibility toggle --- .../StagingArea/StagingAreaToolbar.tsx | 36 ++++++++++++++++--- .../controlLayers/konva/nodeManager.ts | 3 +- .../controlLayers/konva/renderers/renderer.ts | 6 ++++ .../konva/renderers/stagingArea.ts | 5 +-- .../controlLayers/store/canvasV2Slice.ts | 1 + 5 files changed, 44 insertions(+), 7 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx index e7faac86e8a..a64a6576ed5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx @@ -1,6 +1,8 @@ import { Button, ButtonGroup, IconButton } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { + $shouldShowStagedImage, stagingAreaImageAccepted, stagingAreaImageDiscarded, stagingAreaNextImageSelected, @@ -11,7 +13,16 @@ import type { CanvasV2State } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; -import { PiArrowLeftBold, PiArrowRightBold, PiCheckBold, PiTrashSimpleBold, PiXBold } from 'react-icons/pi'; +import { + PiArrowLeftBold, + PiArrowRightBold, + PiCheckBold, + PiEyeBold, + PiEyeSlashBold, + PiFloppyDiskBold, + PiTrashSimpleBold, + PiXBold, +} from 'react-icons/pi'; export const StagingAreaToolbar = memo(() => { const stagingArea = useAppSelector((s) => s.canvasV2.stagingArea); @@ -31,6 +42,7 @@ type Props = { export const StagingAreaToolbarContent = memo(({ stagingArea }: Props) => { const dispatch = useAppDispatch(); + const shouldShowStagedImage = useStore($shouldShowStagedImage); const images = useMemo(() => stagingArea.images, [stagingArea]); const imageDTO = useMemo(() => { if (stagingArea.selectedImageIndex === null) { @@ -74,6 +86,12 @@ export const StagingAreaToolbarContent = memo(({ stagingArea }: Props) => { dispatch(stagingAreaReset()); }, [dispatch, stagingArea]); + const onToggleShouldShowStagedImage = useCallback(() => { + $shouldShowStagedImage.set(!shouldShowStagedImage); + }, [shouldShowStagedImage]); + + const onSaveStagingImage = useCallback(() => {}, []); + useHotkeys(['left'], onPrev, { preventDefault: true, }); @@ -95,6 +113,7 @@ export const StagingAreaToolbarContent = memo(({ stagingArea }: Props) => { icon={} onClick={onPrev} colorScheme="invokeBlue" + isDisabled={images.length <= 1 || !shouldShowStagedImage} /> + { icon={} onClick={onAccept} colorScheme="invokeBlue" + isDisabled={!selectedImageDTO} /> { } onClick={onSaveStagingImage} colorScheme="invokeBlue" + isDisabled={!selectedImageDTO || !selectedImageDTO.is_intermediate} /> { onClick={onDiscardOne} colorScheme="invokeBlue" fontSize={16} - isDisabled={images.length <= 1} + isDisabled={!selectedImageDTO} /> { onClick={onDiscardAll} colorScheme="error" fontSize={16} - isDisabled={images.length === 0} /> diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx index d6b14e95493..6ec06412794 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx @@ -43,7 +43,7 @@ export const ToolChooser: React.FC = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); - const isStaging = useAppSelector((s) => s.canvasV2.stagingArea !== null); + const isStaging = useAppSelector((s) => s.canvasV2.stagingArea.isStaging); const isDrawingToolDisabled = useMemo( () => !getIsDrawingToolEnabled(selectedEntityIdentifier), [selectedEntityIdentifier] diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts index 941d34a106e..e216fc5a33a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts @@ -76,6 +76,7 @@ export type StateApi = { getInpaintMaskState: () => CanvasV2State['inpaintMask']; getStagingAreaState: () => CanvasV2State['stagingArea']; getLastProgressEvent: () => InvocationDenoiseProgressEvent | null; + resetLastProgressEvent: () => void; onInpaintMaskImageCached: (imageDTO: ImageDTO) => void; onRegionMaskImageCached: (id: string, imageDTO: ImageDTO) => void; onLayerImageCached: (imageDTO: ImageDTO) => void; @@ -280,8 +281,10 @@ export class KonvaNodeManager { renderStagingArea() { this.preview.stagingArea.render( this.stateApi.getStagingAreaState(), + this.stateApi.getBbox(), this.stateApi.getShouldShowStagedImage(), - this.stateApi.getLastProgressEvent() + this.stateApi.getLastProgressEvent(), + this.stateApi.resetLastProgressEvent ); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts index 17051e79cf1..beddbcc10bb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts @@ -18,18 +18,18 @@ export class CanvasPreview { documentSizeOverlay: CanvasDocumentSizeOverlay, stagingArea: CanvasStagingArea ) { - this.layer = new Konva.Layer({ listening: true }); - - this.bbox = bbox; - this.layer.add(this.bbox.group); - - this.tool = tool; - this.layer.add(this.tool.group); + this.layer = new Konva.Layer({ listening: true, imageSmoothingEnabled: false }); this.documentSizeOverlay = documentSizeOverlay; this.layer.add(this.documentSizeOverlay.group); this.stagingArea = stagingArea; this.layer.add(this.stagingArea.group); + + this.bbox = bbox; + this.layer.add(this.bbox.group); + + this.tool = tool; + this.layer.add(this.tool.group); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts index fa692558d80..24763e238e7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -307,6 +307,9 @@ export const initializeRenderer = ( getStagingAreaState, getShouldShowStagedImage: $shouldShowStagedImage.get, getLastProgressEvent: $lastProgressEvent.get, + resetLastProgressEvent: () => { + $lastProgressEvent.set(null); + }, // Read-write state setTool, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stagingArea.ts index 28b64f898b2..6bd5617aa17 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stagingArea.ts @@ -2,7 +2,6 @@ import { KonvaImage, KonvaProgressImage } from 'features/controlLayers/konva/ren import type { CanvasV2State } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { InvocationDenoiseProgressEvent } from 'services/events/types'; -import { assert } from 'tsafe'; export class CanvasStagingArea { group: Konva.Group; @@ -17,40 +16,20 @@ export class CanvasStagingArea { async render( stagingArea: CanvasV2State['stagingArea'], + bbox: CanvasV2State['bbox'], shouldShowStagedImage: boolean, - lastProgressEvent: InvocationDenoiseProgressEvent | null + lastProgressEvent: InvocationDenoiseProgressEvent | null, + resetLastProgressEvent: () => void ) { - if (stagingArea && lastProgressEvent) { - const { invocation, step, progress_image } = lastProgressEvent; - const { dataURL } = progress_image; - const { x, y, width, height } = stagingArea.bbox; - const progressImageId = `${invocation.id}_${step}`; - if (this.progressImage) { - if ( - !this.progressImage.isLoading && - !this.progressImage.isError && - this.progressImage.progressImageId !== progressImageId - ) { - await this.progressImage.updateImageSource(progressImageId, dataURL, x, y, width, height); - this.image?.konvaImageGroup.visible(false); - this.progressImage.konvaImageGroup.visible(true); - } - } else { - this.progressImage = new KonvaProgressImage({ id: 'progress-image' }); - this.group.add(this.progressImage.konvaImageGroup); - await this.progressImage.updateImageSource(progressImageId, dataURL, x, y, width, height); - this.image?.konvaImageGroup.visible(false); - this.progressImage.konvaImageGroup.visible(true); - } - } else if (stagingArea && stagingArea.selectedImageIndex !== null) { - const imageDTO = stagingArea.images[stagingArea.selectedImageIndex]; - assert(imageDTO, 'Image must exist'); + const imageDTO = stagingArea.images[stagingArea.selectedImageIndex]; + + if (imageDTO) { if (this.image) { if (!this.image.isLoading && !this.image.isError && this.image.imageName !== imageDTO.image_name) { await this.image.updateImageSource(imageDTO.image_name); } - this.image.konvaImageGroup.x(stagingArea.bbox.x); - this.image.konvaImageGroup.y(stagingArea.bbox.y); + this.image.konvaImageGroup.x(bbox.x); + this.image.konvaImageGroup.y(bbox.y); this.image.konvaImageGroup.visible(shouldShowStagedImage); this.progressImage?.konvaImageGroup.visible(false); } else { @@ -59,8 +38,8 @@ export class CanvasStagingArea { imageObject: { id: 'staging-area-image', type: 'image', - x: stagingArea.bbox.x, - y: stagingArea.bbox.y, + x: bbox.x, + y: bbox.y, width, height, filters: [], @@ -70,19 +49,49 @@ export class CanvasStagingArea { height, }, }, + onLoad: () => { + resetLastProgressEvent(); + }, }); this.group.add(this.image.konvaImageGroup); await this.image.updateImageSource(imageDTO.image_name); this.image.konvaImageGroup.visible(shouldShowStagedImage); this.progressImage?.konvaImageGroup.visible(false); } - } else { + } + + if (stagingArea.isStaging && lastProgressEvent) { + const { invocation, step, progress_image } = lastProgressEvent; + const { dataURL } = progress_image; + const { x, y, width, height } = bbox; + const progressImageId = `${invocation.id}_${step}`; + if (this.progressImage) { + if ( + !this.progressImage.isLoading && + !this.progressImage.isError && + this.progressImage.progressImageId !== progressImageId + ) { + await this.progressImage.updateImageSource(progressImageId, dataURL, x, y, width, height); + this.image?.konvaImageGroup.visible(false); + this.progressImage.konvaImageGroup.visible(true); + } + } else { + this.progressImage = new KonvaProgressImage({ id: 'progress-image' }); + this.group.add(this.progressImage.konvaImageGroup); + await this.progressImage.updateImageSource(progressImageId, dataURL, x, y, width, height); + this.image?.konvaImageGroup.visible(false); + this.progressImage.konvaImageGroup.visible(true); + } + } + + if (!imageDTO && !lastProgressEvent) { if (this.image) { this.image.konvaImageGroup.visible(false); } if (this.progressImage) { this.progressImage.konvaImageGroup.visible(false); } + resetLastProgressEvent(); } } } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 02804d7bb41..41df1dc7fbc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -121,7 +121,11 @@ const initialState: CanvasV2State = { refinerNegativeAestheticScore: 2.5, refinerStart: 0.8, }, - stagingArea: null, + stagingArea: { + isStaging: false, + images: [], + selectedImageIndex: 0, + }, }; export const canvasV2Slice = createSlice({ @@ -332,12 +336,11 @@ export const { imLinePointAdded, imRectAdded, // Staging - stagingAreaInitialized, + stagingAreaStartedStaging, stagingAreaImageAdded, - stagingAreaBatchIdAdded, stagingAreaImageDiscarded, stagingAreaImageAccepted, - stagingAreaReset, + stagingAreaCanceledStaging, stagingAreaNextImageSelected, stagingAreaPreviousImageSelected, } = canvasV2Slice.actions; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/stagingAreaReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/stagingAreaReducers.ts index 78d07b0fb80..9e6168d9668 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/stagingAreaReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/stagingAreaReducers.ts @@ -1,16 +1,11 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import type { CanvasV2State, Rect } from 'features/controlLayers/store/types'; +import type { CanvasV2State } from 'features/controlLayers/store/types'; import type { ImageDTO } from 'services/api/types'; export const stagingAreaReducers = { - stagingAreaInitialized: (state, action: PayloadAction<{ bbox: Rect; batchIds: string[] }>) => { - const { bbox, batchIds } = action.payload; - state.stagingArea = { - bbox, - batchIds, - selectedImageIndex: null, - images: [], - }; + stagingAreaStartedStaging: (state) => { + state.stagingArea.isStaging = true; + state.stagingArea.selectedImageIndex = 0; // When we start staging, the user should not be interacting with the stage except to move it around. Set the tool // to view. state.tool.selectedBuffer = state.tool.selected; @@ -18,67 +13,41 @@ export const stagingAreaReducers = { }, stagingAreaImageAdded: (state, action: PayloadAction<{ imageDTO: ImageDTO }>) => { const { imageDTO } = action.payload; - if (!state.stagingArea) { - // Should not happen - return; - } state.stagingArea.images.push(imageDTO); - if (!state.stagingArea.selectedImageIndex) { - state.stagingArea.selectedImageIndex = state.stagingArea.images.length - 1; - } + state.stagingArea.selectedImageIndex = state.stagingArea.images.length - 1; }, stagingAreaNextImageSelected: (state) => { - if (!state.stagingArea) { - // Should not happen - return; - } - if (state.stagingArea.selectedImageIndex === null) { - if (state.stagingArea.images.length > 0) { - state.stagingArea.selectedImageIndex = 0; - } - return; - } state.stagingArea.selectedImageIndex = (state.stagingArea.selectedImageIndex + 1) % state.stagingArea.images.length; }, stagingAreaPreviousImageSelected: (state) => { - if (!state.stagingArea) { - // Should not happen - return; - } - if (state.stagingArea.selectedImageIndex === null) { - if (state.stagingArea.images.length > 0) { - state.stagingArea.selectedImageIndex = 0; - } - return; - } state.stagingArea.selectedImageIndex = (state.stagingArea.selectedImageIndex - 1 + state.stagingArea.images.length) % state.stagingArea.images.length; }, - stagingAreaBatchIdAdded: (state, action: PayloadAction<{ batchId: string }>) => { - const { batchId } = action.payload; - if (!state.stagingArea) { - // Should not happen - return; - } - state.stagingArea.batchIds.push(batchId); - }, stagingAreaImageDiscarded: (state, action: PayloadAction<{ imageDTO: ImageDTO }>) => { const { imageDTO } = action.payload; - if (!state.stagingArea) { - // Should not happen - return; - } state.stagingArea.images = state.stagingArea.images.filter((image) => image.image_name !== imageDTO.image_name); + state.stagingArea.selectedImageIndex = Math.min( + state.stagingArea.selectedImageIndex, + state.stagingArea.images.length - 1 + ); + if (state.stagingArea.images.length === 0) { + state.stagingArea.isStaging = false; + } }, stagingAreaImageAccepted: (state, _: PayloadAction<{ imageDTO: ImageDTO }>) => { // When we finish staging, reset the tool back to the previous selection. + state.stagingArea.isStaging = false; + state.stagingArea.images = []; + state.stagingArea.selectedImageIndex = 0; if (state.tool.selectedBuffer) { state.tool.selected = state.tool.selectedBuffer; state.tool.selectedBuffer = null; } }, - stagingAreaReset: (state) => { - state.stagingArea = null; + stagingAreaCanceledStaging: (state) => { + state.stagingArea.isStaging = false; + state.stagingArea.images = []; + state.stagingArea.selectedImageIndex = 0; // When we finish staging, reset the tool back to the previous selection. if (state.tool.selectedBuffer) { state.tool.selected = state.tool.selectedBuffer; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index ea55c6b5818..a5beb70ead6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -883,11 +883,10 @@ export type CanvasV2State = { refinerStart: number; }; stagingArea: { - bbox: Rect; + isStaging: boolean; images: ImageDTO[]; - selectedImageIndex: number | null; - batchIds: string[]; - } | null; + selectedImageIndex: number; + }; }; export type StageAttrs = { x: number; y: number; width: number; height: number; scale: number }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts index bb282863b98..3cd80862ab0 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts @@ -107,6 +107,7 @@ export const prepareLinearUIBatch = (state: RootState, g: Graph, prepend: boolea graph: g.getGraph(), runs: 1, data, + origin: 'canvas', }, }; diff --git a/invokeai/frontend/web/src/services/api/endpoints/queue.ts b/invokeai/frontend/web/src/services/api/endpoints/queue.ts index e7edf9811f3..f64b2a30a3a 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/queue.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/queue.ts @@ -276,6 +276,26 @@ export const queueApi = api.injectEndpoints({ }, invalidatesTags: ['SessionQueueStatus', 'BatchStatus'], }), + cancelByBatchOrigin: build.mutation< + paths['/api/v1/queue/{queue_id}/cancel_by_origin']['put']['responses']['200']['content']['application/json'], + paths['/api/v1/queue/{queue_id}/cancel_by_origin']['put']['parameters']['query'] + >({ + query: (params) => ({ + url: buildQueueUrl('cancel_by_origin'), + method: 'PUT', + params, + }), + onQueryStarted: async (arg, api) => { + const { dispatch, queryFulfilled } = api; + try { + await queryFulfilled; + resetListQueryData(dispatch); + } catch { + // no-op + } + }, + invalidatesTags: ['SessionQueueStatus', 'BatchStatus'], + }), listQueueItems: build.query< EntityState & { has_more: boolean; From 65e1951f5d443295cffc2c310320263ca2d3cdd1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 2 Jul 2024 16:26:46 +1000 Subject: [PATCH 147/678] feat(ui): disable gallery hotkeys while staging --- .../features/gallery/hooks/useGalleryHotkeys.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts index a499b45f648..e6b68e7b772 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts @@ -9,7 +9,7 @@ import { useListImagesQuery } from 'services/api/endpoints/images'; * Registers gallery hotkeys. This hook is a singleton. */ export const useGalleryHotkeys = () => { - // TODO(psyche): Hotkeys when staging - cannot navigate gallery with arrow keys when staging! + const isStaging = useAppSelector((s) => s.canvasV2.stagingArea.isStaging); const { goNext, goPrev, isNextEnabled, isPrevEnabled } = useGalleryPagination(); const queryArgs = useAppSelector(selectListImagesQueryArgs); @@ -41,6 +41,9 @@ export const useGalleryHotkeys = () => { useHotkeys( ['right', 'alt+right'], (e) => { + if (isStaging) { + return; + } if (isOnLastImageOfView && isNextEnabled && !queryResult.isFetching) { goNext(e.altKey ? 'alt+arrow' : 'arrow'); return; @@ -49,12 +52,15 @@ export const useGalleryHotkeys = () => { handleRightImage(e.altKey); } }, - [isOnLastImageOfView, goNext, isNextEnabled, queryResult.isFetching, handleRightImage] + [isStaging, isOnLastImageOfView, goNext, isNextEnabled, queryResult.isFetching, handleRightImage] ); useHotkeys( ['up', 'alt+up'], (e) => { + if (isStaging) { + return; + } if (isOnFirstRow && isPrevEnabled && !queryResult.isFetching) { goPrev(e.altKey ? 'alt+arrow' : 'arrow'); return; @@ -62,12 +68,15 @@ export const useGalleryHotkeys = () => { handleUpImage(e.altKey); }, { preventDefault: true }, - [handleUpImage, canNavigateGallery, isOnFirstRow, goPrev, isPrevEnabled, queryResult.isFetching] + [isStaging, handleUpImage, isOnFirstRow, goPrev, isPrevEnabled, queryResult.isFetching] ); useHotkeys( ['down', 'alt+down'], (e) => { + if (isStaging) { + return; + } if (isOnLastRow && isNextEnabled && !queryResult.isFetching) { goNext(e.altKey ? 'alt+arrow' : 'arrow'); return; @@ -75,6 +84,6 @@ export const useGalleryHotkeys = () => { handleDownImage(e.altKey); }, { preventDefault: true }, - [isOnLastRow, goNext, isNextEnabled, queryResult.isFetching, handleDownImage] + [isStaging, isOnLastRow, goNext, isNextEnabled, queryResult.isFetching, handleDownImage] ); }; From 0d3dfb8d0f45e2f17edcd9ff662c38f56567d743 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 2 Jul 2024 16:33:04 +1000 Subject: [PATCH 148/678] feat(ui): do not floor cursor position --- .../web/src/features/controlLayers/konva/events.ts | 4 ++-- .../web/src/features/controlLayers/konva/util.ts | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index d7f4a045811..938743a7354 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -1,5 +1,5 @@ import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; -import { getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util'; +import { getScaledCursorPosition } from 'features/controlLayers/konva/util'; import type { CanvasEntity } from 'features/controlLayers/store/types'; import type Konva from 'konva'; import type { Vector2d } from 'konva/lib/types'; @@ -25,7 +25,7 @@ const updateLastCursorPos = ( stage: Konva.Stage, setLastCursorPos: KonvaNodeManager['stateApi']['setLastCursorPos'] ) => { - const pos = getScaledFlooredCursorPosition(stage); + const pos = getScaledCursorPosition(stage); if (!pos) { return null; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index 233c6f6dc80..1b4d603e0fc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -34,6 +34,19 @@ export const getScaledFlooredCursorPosition = (stage: Konva.Stage): Vector2d | n }; }; +/** + * Gets the scaled cursor position on the stage. If the cursor is not currently over the stage, returns null. + * @param stage The konva stage + */ +export const getScaledCursorPosition = (stage: Konva.Stage): Vector2d | null => { + const pointerPosition = stage.getPointerPosition(); + const stageTransform = stage.getAbsoluteTransform().copy(); + if (!pointerPosition) { + return null; + } + return stageTransform.invert().point(pointerPosition); +}; + /** * Snaps a position to the edge of the stage if within a threshold of the edge * @param pos The position to snap From f024fe4488e704f923ef067dbbb9a1599909c946 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 3 Jul 2024 15:56:18 +1000 Subject: [PATCH 149/678] feat(ui): move tool icon is pointer like in other apps --- .../web/src/features/controlLayers/components/ToolChooser.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx index 6ec06412794..89901bde2d4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx @@ -17,8 +17,8 @@ import { useCallback, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { - PiArrowsOutCardinalBold, PiBoundingBoxBold, + PiCursorBold, PiEraserBold, PiHandBold, PiPaintBrushBold, @@ -173,7 +173,7 @@ export const ToolChooser: React.FC = () => { } + icon={} variant={tool === 'move' ? 'solid' : 'outline'} onClick={setToolToMove} isDisabled={isMoveToolDisabled || isStaging} From 2676ff8ee3d7d08e261977818648454f91cd37fe Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 3 Jul 2024 15:56:29 +1000 Subject: [PATCH 150/678] feat(ui): transformable layers --- .../controlLayers/konva/nodeManager.ts | 10 +- .../konva/renderers/controlAdapters.ts | 3 +- .../konva/renderers/inpaintMask.ts | 7 +- .../controlLayers/konva/renderers/layers.ts | 177 +++++++++++++----- .../controlLayers/konva/renderers/objects.ts | 107 +++++++++-- .../controlLayers/konva/renderers/regions.ts | 6 +- .../controlLayers/konva/renderers/renderer.ts | 39 +++- .../controlLayers/store/canvasV2Slice.ts | 1 + .../controlLayers/store/layersReducers.ts | 30 +++ .../src/features/controlLayers/store/types.ts | 4 + 10 files changed, 299 insertions(+), 85 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts index e216fc5a33a..bea047c4c86 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts @@ -13,6 +13,7 @@ import type { Rect, RectShapeAddedArg, RgbaColor, + ScaleChangedArg, StageAttrs, Tool, } from 'features/controlLayers/store/types'; @@ -63,6 +64,8 @@ export type StateApi = { onBrushWidthChanged: (size: number) => void; onEraserWidthChanged: (size: number) => void; getMaskOpacity: () => number; + getIsSelected: (id: string) => boolean; + onScaleChanged: (arg: ScaleChangedArg, entityType: CanvasEntity['type']) => void; onPosChanged: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void; onBboxTransformed: (bbox: Rect) => void; getShiftKey: () => boolean; @@ -155,9 +158,8 @@ export class KonvaNodeManager { this.controlAdapters = new Map(); } - renderLayers() { + async renderLayers() { const { entities } = this.stateApi.getLayersState(); - const toolState = this.stateApi.getToolState(); for (const canvasLayer of this.layers.values()) { if (!entities.find((l) => l.id === canvasLayer.id)) { @@ -169,11 +171,11 @@ export class KonvaNodeManager { for (const entity of entities) { let adapter = this.layers.get(entity.id); if (!adapter) { - adapter = new CanvasLayer(entity, this.stateApi.onPosChanged); + adapter = new CanvasLayer(entity, this); this.layers.set(adapter.id, adapter); this.stage.add(adapter.layer); } - adapter.render(entity, toolState.selected); + await adapter.render(entity); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts index ea696cf2dcf..421ec4ad9aa 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts @@ -43,8 +43,7 @@ export class CanvasControlAdapter { const filters = entity.filter === 'LightnessToAlphaFilter' ? [LightnessToAlphaFilter] : []; if (!this.image) { - this.image = await new KonvaImage({ - imageObject, + this.image = await new KonvaImage(imageObject, { onLoad: (konvaImage) => { konvaImage.filters(filters); konvaImage.cache(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts index eb7be067426..85b6efff907 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts @@ -79,7 +79,7 @@ export class CanvasInpaintMask { assert(brushLine instanceof KonvaBrushLine || brushLine === undefined); if (!brushLine) { - brushLine = new KonvaBrushLine({ brushLine: obj }); + brushLine = new KonvaBrushLine(obj); this.objects.set(brushLine.id, brushLine); this.group.add(brushLine.konvaLineGroup); groupNeedsCache = true; @@ -94,7 +94,7 @@ export class CanvasInpaintMask { assert(eraserLine instanceof KonvaEraserLine || eraserLine === undefined); if (!eraserLine) { - eraserLine = new KonvaEraserLine({ eraserLine: obj }); + eraserLine = new KonvaEraserLine(obj); this.objects.set(eraserLine.id, eraserLine); this.group.add(eraserLine.konvaLineGroup); groupNeedsCache = true; @@ -109,7 +109,7 @@ export class CanvasInpaintMask { assert(rect instanceof KonvaRect || rect === undefined); if (!rect) { - rect = new KonvaRect({ rectShape: obj }); + rect = new KonvaRect(obj); this.objects.set(rect.id, rect); this.group.add(rect.konvaRect); groupNeedsCache = true; @@ -129,7 +129,6 @@ export class CanvasInpaintMask { return; } - // We must clear the cache first so Konva will re-draw the group with the new compositing rect if (this.group.isCached()) { this.group.clearCache(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts index c34155648e6..1935be13710 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -1,38 +1,53 @@ import { getObjectGroupId } from 'features/controlLayers/konva/naming'; -import type { StateApi } from 'features/controlLayers/konva/nodeManager'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import { KonvaBrushLine, KonvaEraserLine, KonvaImage, KonvaRect } from 'features/controlLayers/konva/renderers/objects'; import { mapId } from 'features/controlLayers/konva/util'; -import type { LayerEntity, Tool } from 'features/controlLayers/store/types'; +import { isDrawingTool, type LayerEntity } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; export class CanvasLayer { id: string; + manager: KonvaNodeManager; layer: Konva.Layer; group: Konva.Group; + transformer: Konva.Transformer; objects: Map; - constructor(entity: LayerEntity, onPosChanged: StateApi['onPosChanged']) { + constructor(entity: LayerEntity, manager: KonvaNodeManager) { this.id = entity.id; - + this.manager = manager; this.layer = new Konva.Layer({ id: entity.id, - draggable: true, - dragDistance: 0, + listening: false, }); - // 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. - this.layer.on('dragend', function (e) { - onPosChanged({ id: entity.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'layer'); - }); - const group = new Konva.Group({ + this.group = new Konva.Group({ id: getObjectGroupId(this.layer.id(), uuidv4()), listening: false, }); - this.group = group; this.layer.add(this.group); + + this.transformer = new Konva.Transformer({ + shouldOverdrawWholeArea: true, + draggable: true, + dragDistance: 0, + enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], + rotateEnabled: false, + flipEnabled: false, + }); + this.transformer.on('transformend', () => { + this.manager.stateApi.onScaleChanged( + { id: this.id, scale: this.group.scaleX(), x: this.group.x(), y: this.group.y() }, + 'layer' + ); + }); + this.transformer.on('dragend', () => { + this.manager.stateApi.onPosChanged({ id: this.id, x: this.group.x(), y: this.group.y() }, 'layer'); + }); + this.layer.add(this.transformer); + this.objects = new Map(); } @@ -40,20 +55,24 @@ export class CanvasLayer { this.layer.destroy(); } - async render(layerState: LayerEntity, selectedTool: Tool) { + async render(layerState: LayerEntity) { // Update the layer's position and listening state - this.layer.setAttrs({ - listening: selectedTool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events - x: Math.floor(layerState.x), - y: Math.floor(layerState.y), + this.group.setAttrs({ + x: layerState.x, + y: layerState.y, + scaleX: 1, + scaleY: 1, }); + let didDraw = false; + const objectIds = layerState.objects.map(mapId); // Destroy any objects that are no longer in state for (const object of this.objects.values()) { if (!objectIds.includes(object.id)) { this.objects.delete(object.id); object.destroy(); + didDraw = true; } } @@ -63,45 +82,60 @@ export class CanvasLayer { assert(brushLine instanceof KonvaBrushLine || brushLine === undefined); if (!brushLine) { - brushLine = new KonvaBrushLine({ brushLine: obj }); + brushLine = new KonvaBrushLine(obj); this.objects.set(brushLine.id, brushLine); this.group.add(brushLine.konvaLineGroup); - } - if (obj.points.length !== brushLine.konvaLine.points().length) { - brushLine.konvaLine.points(obj.points); + didDraw = true; + } else { + if (brushLine.update(obj)) { + didDraw = true; + } } } else if (obj.type === 'eraser_line') { let eraserLine = this.objects.get(obj.id); assert(eraserLine instanceof KonvaEraserLine || eraserLine === undefined); if (!eraserLine) { - eraserLine = new KonvaEraserLine({ eraserLine: obj }); + eraserLine = new KonvaEraserLine(obj); this.objects.set(eraserLine.id, eraserLine); this.group.add(eraserLine.konvaLineGroup); - } - if (obj.points.length !== eraserLine.konvaLine.points().length) { - eraserLine.konvaLine.points(obj.points); + didDraw = true; + } else { + if (eraserLine.update(obj)) { + didDraw = true; + } } } else if (obj.type === 'rect_shape') { let rect = this.objects.get(obj.id); assert(rect instanceof KonvaRect || rect === undefined); if (!rect) { - rect = new KonvaRect({ rectShape: obj }); + rect = new KonvaRect(obj); this.objects.set(rect.id, rect); this.group.add(rect.konvaRect); + didDraw = true; + } else { + if (rect.update(obj)) { + didDraw = true; + } } } else if (obj.type === 'image') { let image = this.objects.get(obj.id); assert(image instanceof KonvaImage || image === undefined); if (!image) { - image = await new KonvaImage({ imageObject: obj }); + image = await new KonvaImage(obj, { + onLoad: () => { + this.updateGroup(true); + }, + }); this.objects.set(image.id, image); this.group.add(image.konvaImageGroup); - } - if (image.imageName !== obj.image.name) { - image.updateImageSource(obj.image.name); + await image.updateImageSource(obj.image.name); + } else { + if (await image.update(obj)) { + didDraw = true; + } } } } @@ -111,22 +145,71 @@ export class CanvasLayer { this.layer.visible(layerState.isEnabled); } - // const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer); - // if (layerState.bbox) { - // const active = !layerState.bboxNeedsUpdate && layerState.isSelected && tool === 'move'; - // bboxRect.setAttrs({ - // visible: active, - // listening: active, - // x: layerState.bbox.x, - // y: layerState.bbox.y, - // width: layerState.bbox.width, - // height: layerState.bbox.height, - // stroke: layerState.isSelected ? BBOX_SELECTED_STROKE : '', - // strokeWidth: 1 / stage.scaleX(), - // }); - // } else { - // bboxRect.visible(false); - // } this.group.opacity(layerState.opacity); + + // The layer only listens when using the move tool - otherwise the stage is handling mouse events + this.updateGroup(didDraw); + } + + updateGroup(didDraw: boolean) { + const isSelected = this.manager.stateApi.getIsSelected(this.id); + const selectedTool = this.manager.stateApi.getToolState().selected; + + if (this.objects.size === 0) { + // If the layer is totally empty, reset the cache and bail out. + this.layer.listening(false); + this.transformer.nodes([]); + if (this.group.isCached()) { + this.group.clearCache(); + } + return; + } + + if (isSelected && selectedTool === 'move') { + // When the layer is selected and being moved, we should always cache it. + // We should update the cache if we drew to the layer. + if (!this.group.isCached() || didDraw) { + this.group.cache(); + } + // Activate the transformer + this.layer.listening(true); + this.transformer.nodes([this.group]); + this.transformer.forceUpdate(); + return; + } + + if (isSelected && selectedTool !== 'move') { + // If the layer is selected but not using the move tool, we don't want the layer to be listening. + this.layer.listening(false); + // The transformer also does not need to be active. + this.transformer.nodes([]); + if (isDrawingTool(selectedTool)) { + // We are using a drawing tool (brush, eraser, rect). These tools change the layer's rendered appearance, so we + // should never be cached. + if (this.group.isCached()) { + this.group.clearCache(); + } + } else { + // We are using a non-drawing tool (move, view, bbox), so we should cache the layer. + // We should update the cache if we drew to the layer. + if (!this.group.isCached() || didDraw) { + this.group.cache(); + } + } + return; + } + + if (!isSelected) { + // Unselected layers should not be listening + this.layer.listening(false); + // The transformer also does not need to be active. + this.transformer.nodes([]); + // Update the layer's cache if it's not already cached or we drew to it. + if (!this.group.isCached() || didDraw) { + this.group.cache(); + } + + return; + } } } 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 1c1230e0c5c..3a6ecbd0604 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -27,10 +27,10 @@ export class KonvaBrushLine { id: string; konvaLineGroup: Konva.Group; konvaLine: Konva.Line; + lastBrushLine: BrushLine; - constructor(arg: { brushLine: BrushLine }) { - const { brushLine } = arg; - const { id, strokeWidth, clip, color } = brushLine; + constructor(brushLine: BrushLine) { + const { id, strokeWidth, clip, color, points } = brushLine; this.id = id; this.konvaLineGroup = new Konva.Group({ clip, @@ -46,8 +46,26 @@ export class KonvaBrushLine { lineJoin: 'round', globalCompositeOperation: 'source-over', stroke: rgbaColorToString(color), + points, }); this.konvaLineGroup.add(this.konvaLine); + this.lastBrushLine = brushLine; + } + + update(brushLine: BrushLine, force?: boolean): boolean { + if (this.lastBrushLine !== brushLine || force) { + const { points, color, clip, strokeWidth } = brushLine; + this.konvaLine.setAttrs({ + points, + stroke: rgbaColorToString(color), + clip, + strokeWidth, + }); + this.lastBrushLine = brushLine; + return true; + } else { + return false; + } } destroy() { @@ -59,10 +77,10 @@ export class KonvaEraserLine { id: string; konvaLineGroup: Konva.Group; konvaLine: Konva.Line; + lastEraserLine: EraserLine; - constructor(arg: { eraserLine: EraserLine }) { - const { eraserLine } = arg; - const { id, strokeWidth, clip } = eraserLine; + constructor(eraserLine: EraserLine) { + const { id, strokeWidth, clip, points } = eraserLine; this.id = id; this.konvaLineGroup = new Konva.Group({ clip, @@ -78,8 +96,25 @@ export class KonvaEraserLine { lineJoin: 'round', globalCompositeOperation: 'destination-out', stroke: rgbaColorToString(RGBA_RED), + points, }); this.konvaLineGroup.add(this.konvaLine); + this.lastEraserLine = eraserLine; + } + + update(eraserLine: EraserLine, force?: boolean): boolean { + if (this.lastEraserLine !== eraserLine || force) { + const { points, clip, strokeWidth } = eraserLine; + this.konvaLine.setAttrs({ + points, + clip, + strokeWidth, + }); + this.lastEraserLine = eraserLine; + return true; + } else { + return false; + } } destroy() { @@ -90,9 +125,9 @@ export class KonvaEraserLine { export class KonvaRect { id: string; konvaRect: Konva.Rect; + lastRectShape: RectShape; - constructor(arg: { rectShape: RectShape }) { - const { rectShape } = arg; + constructor(rectShape: RectShape) { const { id, x, y, width, height } = rectShape; this.id = id; const konvaRect = new Konva.Rect({ @@ -105,6 +140,24 @@ export class KonvaRect { fill: rgbaColorToString(rectShape.color), }); this.konvaRect = konvaRect; + this.lastRectShape = rectShape; + } + + update(rectShape: RectShape, force?: boolean): boolean { + if (this.lastRectShape !== rectShape || force) { + const { x, y, width, height, color } = rectShape; + this.konvaRect.setAttrs({ + x, + y, + width, + height, + fill: rgbaColorToString(color), + }); + this.lastRectShape = rectShape; + return true; + } else { + return false; + } } destroy() { @@ -126,15 +179,18 @@ export class KonvaImage { onLoading: () => void; onLoad: (imageName: string, imageEl: HTMLImageElement) => void; onError: () => void; + lastImageObject: ImageObject; - constructor(arg: { - imageObject: ImageObject; - getImageDTO?: (imageName: string) => Promise; - onLoading?: () => void; - onLoad?: (konvaImage: Konva.Image) => void; - onError?: () => void; - }) { - const { imageObject, getImageDTO, onLoading, onLoad, onError } = arg; + constructor( + imageObject: ImageObject, + options: { + getImageDTO?: (imageName: string) => Promise; + onLoading?: () => void; + onLoad?: (konvaImage: Konva.Image) => void; + onError?: () => void; + } + ) { + const { getImageDTO, onLoading, onLoad, onError } = options; const { id, width, height, x, y } = imageObject; this.konvaImageGroup = new Konva.Group({ id, listening: false, x, y }); this.konvaPlaceholderGroup = new Konva.Group({ listening: false }); @@ -188,6 +244,8 @@ export class KonvaImage { id: this.id, listening: false, image: imageEl, + width, + height, }); this.konvaImageGroup.add(this.konvaImage); } @@ -213,6 +271,7 @@ export class KonvaImage { onError(); } }; + this.lastImageObject = imageObject; } async updateImageSource(imageName: string) { @@ -238,6 +297,22 @@ export class KonvaImage { } } + async update(imageObject: ImageObject, force?: boolean): Promise { + if (this.lastImageObject !== imageObject || force) { + const { width, height, x, y, image } = imageObject; + if (this.lastImageObject.image.name !== image.name || force) { + await this.updateImageSource(image.name); + } + this.konvaImage?.setAttrs({ x, y, width, height }); + this.konvaPlaceholderRect.setAttrs({ width, height }); + this.konvaPlaceholderText.setAttrs({ width, height, fontSize: width / 16 }); + this.lastImageObject = imageObject; + return true; + } else { + return false; + } + } + destroy() { this.konvaImageGroup.destroy(); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts index be6be27699b..0a2c5353d4c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts @@ -79,7 +79,7 @@ export class CanvasRegion { assert(brushLine instanceof KonvaBrushLine || brushLine === undefined); if (!brushLine) { - brushLine = new KonvaBrushLine({ brushLine: obj }); + brushLine = new KonvaBrushLine(obj); this.objects.set(brushLine.id, brushLine); this.group.add(brushLine.konvaLineGroup); groupNeedsCache = true; @@ -94,7 +94,7 @@ export class CanvasRegion { assert(eraserLine instanceof KonvaEraserLine || eraserLine === undefined); if (!eraserLine) { - eraserLine = new KonvaEraserLine({ eraserLine: obj }); + eraserLine = new KonvaEraserLine(obj); this.objects.set(eraserLine.id, eraserLine); this.group.add(eraserLine.konvaLineGroup); groupNeedsCache = true; @@ -109,7 +109,7 @@ export class CanvasRegion { assert(rect instanceof KonvaRect || rect === undefined); if (!rect) { - rect = new KonvaRect({ rectShape: obj }); + rect = new KonvaRect(obj); this.objects.set(rect.id, rect); this.group.add(rect.konvaRect); groupNeedsCache = true; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts index 24763e238e7..e5c67cbde74 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -28,6 +28,7 @@ import { layerImageCacheChanged, layerLinePointAdded, layerRectAdded, + layerScaled, layerTranslated, rgBboxChanged, rgBrushLineAdded, @@ -48,6 +49,7 @@ import type { PointAddedToLineArg, PosChangedArg, RectShapeAddedArg, + ScaleChangedArg, Tool, } from 'features/controlLayers/store/types'; import type Konva from 'konva'; @@ -90,7 +92,7 @@ export const initializeRenderer = ( // Set up callbacks for various events const onPosChanged = (arg: PosChangedArg, entityType: CanvasEntity['type']) => { - logIfDebugging('Position changed'); + logIfDebugging('onPosChanged'); if (entityType === 'layer') { dispatch(layerTranslated(arg)); } else if (entityType === 'control_adapter') { @@ -101,6 +103,12 @@ export const initializeRenderer = ( dispatch(imTranslated(arg)); } }; + const onScaleChanged = (arg: ScaleChangedArg, entityType: CanvasEntity['type']) => { + logIfDebugging('onScaleChanged'); + if (entityType === 'layer') { + dispatch(layerScaled(arg)); + } + }; const onBboxChanged = (arg: BboxChangedArg, entityType: CanvasEntity['type']) => { logIfDebugging('Entity bbox changed'); if (entityType === 'layer') { @@ -249,7 +257,12 @@ export const initializeRenderer = ( const getInpaintMaskState = () => canvasV2.inpaintMask; const getMaskOpacity = () => canvasV2.settings.maskOpacity; const getStagingAreaState = () => canvasV2.stagingArea; + const getIsSelected = (id: string) => getSelectedEntity()?.id === id; + // Read-only state, derived from nanostores + const resetLastProgressEvent = () => { + $lastProgressEvent.set(null); + }; // Read-write state, ephemeral interaction state let isDrawing = false; const getIsDrawing = () => isDrawing; @@ -307,9 +320,8 @@ export const initializeRenderer = ( getStagingAreaState, getShouldShowStagedImage: $shouldShowStagedImage.get, getLastProgressEvent: $lastProgressEvent.get, - resetLastProgressEvent: () => { - $lastProgressEvent.set(null); - }, + resetLastProgressEvent, + getIsSelected, // Read-write state setTool, @@ -340,6 +352,7 @@ export const initializeRenderer = ( onRegionMaskImageCached, onInpaintMaskImageCached, onLayerImageCached, + onScaleChanged, }; const manager = new KonvaNodeManager(stage, container, stateApi); @@ -367,7 +380,8 @@ export const initializeRenderer = ( if ( isFirstRender || canvasV2.layers.entities !== prevCanvasV2.layers.entities || - canvasV2.tool.selected !== prevCanvasV2.tool.selected + canvasV2.tool.selected !== prevCanvasV2.tool.selected || + prevSelectedEntity?.id !== selectedEntity?.id ) { logIfDebugging('Rendering layers'); manager.renderLayers(); @@ -377,7 +391,8 @@ export const initializeRenderer = ( isFirstRender || canvasV2.regions.entities !== prevCanvasV2.regions.entities || canvasV2.settings.maskOpacity !== prevCanvasV2.settings.maskOpacity || - canvasV2.tool.selected !== prevCanvasV2.tool.selected + canvasV2.tool.selected !== prevCanvasV2.tool.selected || + prevSelectedEntity?.id !== selectedEntity?.id ) { logIfDebugging('Rendering regions'); manager.renderRegions(); @@ -387,13 +402,18 @@ export const initializeRenderer = ( isFirstRender || canvasV2.inpaintMask !== prevCanvasV2.inpaintMask || canvasV2.settings.maskOpacity !== prevCanvasV2.settings.maskOpacity || - canvasV2.tool.selected !== prevCanvasV2.tool.selected + canvasV2.tool.selected !== prevCanvasV2.tool.selected || + prevSelectedEntity?.id !== selectedEntity?.id ) { logIfDebugging('Rendering inpaint mask'); manager.renderInpaintMask(); } - if (isFirstRender || canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities) { + if ( + isFirstRender || + canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities || + prevSelectedEntity?.id !== selectedEntity?.id + ) { logIfDebugging('Rendering control adapters'); manager.renderControlAdapters(); } @@ -427,7 +447,8 @@ export const initializeRenderer = ( isFirstRender || canvasV2.layers.entities !== prevCanvasV2.layers.entities || canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities || - canvasV2.regions.entities !== prevCanvasV2.regions.entities + canvasV2.regions.entities !== prevCanvasV2.regions.entities || + prevSelectedEntity?.id !== selectedEntity?.id ) { logIfDebugging('Arranging entities'); manager.arrangeEntities(); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 41df1dc7fbc..186d47946c5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -215,6 +215,7 @@ export const { layerImageAdded, layerAllDeleted, layerImageCacheChanged, + layerScaled, // IP Adapters ipaAdded, ipaRecalled, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index 9edabb1acfc..c81083273ac 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -172,6 +172,36 @@ export const layersReducers = { payload: { ...payload, lineId: uuidv4() }, }), }, + layerScaled: (state, action: PayloadAction<{ id: string; scale: number; x: number; y: number }>) => { + const { id, scale, x, y } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + for (const obj of layer.objects) { + if (obj.type === 'brush_line') { + obj.points = obj.points.map((point) => point * scale); + obj.strokeWidth *= scale; + } else if (obj.type === 'eraser_line') { + obj.points = obj.points.map((point) => point * scale); + obj.strokeWidth *= scale; + } else if (obj.type === 'rect_shape') { + obj.x *= scale; + obj.y *= scale; + obj.height *= scale; + obj.width *= scale; + } else if (obj.type === 'image') { + obj.x *= scale; + obj.y *= scale; + obj.height *= scale; + obj.width *= scale; + } + } + layer.x = x; + layer.y = y; + layer.bboxNeedsUpdate = true; + state.layers.imageCache = null; + }, layerEraserLineAdded: { reducer: (state, action: PayloadAction) => { const { id, points, lineId, width, clip } = action.payload; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index a5beb70ead6..4693654147a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -462,6 +462,9 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { const zTool = z.enum(['brush', 'eraser', 'move', 'rect', 'view', 'bbox']); export type Tool = z.infer; +export function isDrawingTool(tool: Tool): tool is 'brush' | 'eraser' | 'rect' { + return tool === 'brush' || tool === 'eraser' || tool === 'rect'; +} const zDrawingTool = zTool.extract(['brush', 'eraser']); @@ -891,6 +894,7 @@ export type CanvasV2State = { export type StageAttrs = { x: number; y: number; width: number; height: number; scale: number }; export type PosChangedArg = { id: string; x: number; y: number }; +export type ScaleChangedArg = { id: string; scale: number; x: number; y: number }; export type BboxChangedArg = { id: string; bbox: Rect | null }; export type EraserLineAddedArg = { id: string; From e55541ea87e8e433706e292d3a69b6e4fb8ebc14 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 3 Jul 2024 16:10:49 +1000 Subject: [PATCH 151/678] feat(ui): updated layer list component styling --- invokeai/frontend/web/public/locales/en.json | 7 ++++++- .../components/Layer/LayerHeader.tsx | 8 ++++++-- .../components/common/CanvasEntityContainer.tsx | 17 ++++++++--------- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 881bd1f52bc..0b8bb968705 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1679,12 +1679,17 @@ "globalIPAdapterLayer": "Global $t(common.ipAdapter) $t(unifiedCanvas.layer)", "globalInitialImage": "Global Initial Image", "globalInitialImageLayer": "$t(controlLayers.globalInitialImage) $t(unifiedCanvas.layer)", + "inpaintMask": "Inpaint Mask", + "layer": "Layer", "opacityFilter": "Opacity Filter", "clearProcessor": "Clear Processor", "resetProcessor": "Reset Processor to Defaults", "noLayersAdded": "No Layers Added", "layers_one": "Layer", - "layers_other": "Layers" + "layers_other": "Layers", + "objects_zero": "empty", + "objects_one": "{{count}} object", + "objects_other": "{{count}} objects" }, "upscaling": { "upscale": "Upscale", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx index c1f5606ebae..53a8d0dbfa4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx @@ -7,7 +7,7 @@ import { CanvasEntityTitle } from 'features/controlLayers/components/common/Canv import { LayerActionsMenu } from 'features/controlLayers/components/Layer/LayerActionsMenu'; import { layerDeleted, layerIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice'; import { selectLayerOrThrow } from 'features/controlLayers/store/layersReducers'; -import { memo, useCallback } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { LayerOpacity } from './LayerOpacity'; @@ -21,17 +21,21 @@ export const LayerHeader = memo(({ id, onToggleVisibility }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const isEnabled = useAppSelector((s) => selectLayerOrThrow(s.canvasV2, id).isEnabled); + const objectCount = useAppSelector((s) => selectLayerOrThrow(s.canvasV2, id).objects.length); const onToggleIsEnabled = useCallback(() => { dispatch(layerIsEnabledToggled({ id })); }, [dispatch, id]); const onDelete = useCallback(() => { dispatch(layerDeleted({ id })); }, [dispatch, id]); + const title = useMemo(() => { + return `${t('controlLayers.layer')} (${t('controlLayers.objects', { count: objectCount })})`; + }, [objectCount, t]); return ( - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx index 1cc72218310..9a244b697c0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx @@ -10,7 +10,7 @@ type Props = PropsWithChildren<{ }>; export const CanvasEntityContainer = memo(({ isSelected, onSelect, selectedBorderColor, children }: Props) => { - const bg = useMemo(() => { + const borderColor = useMemo(() => { if (isSelected) { return selectedBorderColor ?? 'base.400'; } @@ -25,19 +25,18 @@ export const CanvasEntityContainer = memo(({ isSelected, onSelect, selectedBorde return ( - - {children} - + {children} ); }); From e6723f194a222f393459771b8a42572e5df29554 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 3 Jul 2024 16:17:53 +1000 Subject: [PATCH 152/678] fix(ui): resetting layer resets position --- .../web/src/features/controlLayers/store/layersReducers.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index c81083273ac..c9ed79f0690 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -94,6 +94,8 @@ export const layersReducers = { layer.bbox = null; layer.bboxNeedsUpdate = false; state.layers.imageCache = null; + layer.x = 0; + layer.y = 0; }, layerDeleted: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; From 7d96b3e89ed67a73ed3fb4c47373430a7f43d536 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 3 Jul 2024 16:18:06 +1000 Subject: [PATCH 153/678] feat(ui): tweak layer ui component --- .../controlLayers/components/Layer/Layer.tsx | 9 ++++++++- .../components/Layer/LayerSettings.tsx | 16 ++-------------- .../components/common/CanvasEntityContainer.tsx | 1 + 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx index e49d76ebbee..9c1b315b804 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx @@ -1,10 +1,12 @@ import { useDisclosure } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import IAIDroppable from 'common/components/IAIDroppable'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { LayerHeader } from 'features/controlLayers/components/Layer/LayerHeader'; import { LayerSettings } from 'features/controlLayers/components/Layer/LayerSettings'; import { entitySelected } from 'features/controlLayers/store/canvasV2Slice'; -import { memo, useCallback } from 'react'; +import type { LayerImageDropData } from 'features/dnd/types'; +import { memo, useCallback, useMemo } from 'react'; type Props = { id: string; @@ -17,11 +19,16 @@ export const Layer = memo(({ id }: Props) => { const onSelect = useCallback(() => { dispatch(entitySelected({ id, type: 'layer' })); }, [dispatch, id]); + const droppableData = useMemo( + () => ({ id, actionType: 'ADD_LAYER_IMAGE', context: { id } }), + [id] + ); return ( {isOpen && } + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerSettings.tsx index 0ef45dc2234..8af3e818141 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerSettings.tsx @@ -1,24 +1,12 @@ -import IAIDroppable from 'common/components/IAIDroppable'; import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; -import type { LayerImageDropData } from 'features/dnd/types'; -import { memo, useMemo } from 'react'; +import { memo } from 'react'; type Props = { id: string; }; export const LayerSettings = memo(({ id }: Props) => { - const droppableData = useMemo( - () => ({ id, actionType: 'ADD_LAYER_IMAGE', context: { id } }), - [id] - ); - - return ( - - PLACEHOLDER - - - ); + return PLACEHOLDER; }); LayerSettings.displayName = 'LayerSettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx index 9a244b697c0..b1bc6a5aa14 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx @@ -25,6 +25,7 @@ export const CanvasEntityContainer = memo(({ isSelected, onSelect, selectedBorde return ( Date: Wed, 3 Jul 2024 18:50:46 +1000 Subject: [PATCH 154/678] fix(ui): staging area image offset --- .../listeners/addCommitStagingAreaImageListener.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts index 4aa6020d0a5..1da9b90b9e8 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts @@ -65,10 +65,9 @@ export const addStagingListeners = (startAppListening: AppStartListening) => { assert(layer, 'No layer found to stage image'); - const { x, y } = bbox; const { id } = layer; - api.dispatch(layerImageAdded({ id, imageDTO, pos: { x, y } })); + api.dispatch(layerImageAdded({ id, imageDTO, pos: { x: bbox.x - layer.x, y: bbox.y - layer.y } })); }, }); }; From bd679e018dcfae1916256422d20621004f557975 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 3 Jul 2024 18:51:04 +1000 Subject: [PATCH 155/678] fix(ui): stale selected entity --- .../controlLayers/konva/renderers/renderer.ts | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts index e5c67cbde74..9539a3403b4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -213,7 +213,6 @@ export const initializeRenderer = ( } else { selectedEntity = null; } - logIfDebugging('Selected entity changed'); return selectedEntity; }; @@ -228,7 +227,6 @@ export const initializeRenderer = ( } else { currentFill = canvasV2.tool.fill; } - logIfDebugging('Current fill changed'); return currentFill; }; @@ -243,10 +241,8 @@ export const initializeRenderer = ( // Read-only state, derived from redux let prevCanvasV2 = getState().canvasV2; let canvasV2 = getState().canvasV2; - let prevSelectedEntity: CanvasEntity | null = selectSelectedEntity(prevCanvasV2); - let prevCurrentFill: RgbaColor = selectCurrentFill(prevCanvasV2, prevSelectedEntity); - const getSelectedEntity = () => prevSelectedEntity; - const getCurrentFill = () => prevCurrentFill; + const getSelectedEntity = () => selectSelectedEntity(canvasV2); + const getCurrentFill = () => selectCurrentFill(canvasV2, getSelectedEntity()); const getBbox = () => canvasV2.bbox; const getDocument = () => canvasV2.document; const getToolState = () => canvasV2.tool; @@ -374,14 +370,11 @@ export const initializeRenderer = ( return; } - const selectedEntity = selectSelectedEntity(canvasV2); - const currentFill = selectCurrentFill(canvasV2, selectedEntity); - if ( isFirstRender || canvasV2.layers.entities !== prevCanvasV2.layers.entities || canvasV2.tool.selected !== prevCanvasV2.tool.selected || - prevSelectedEntity?.id !== selectedEntity?.id + canvasV2.selectedEntityIdentifier?.id !== prevCanvasV2.selectedEntityIdentifier?.id ) { logIfDebugging('Rendering layers'); manager.renderLayers(); @@ -392,7 +385,7 @@ export const initializeRenderer = ( canvasV2.regions.entities !== prevCanvasV2.regions.entities || canvasV2.settings.maskOpacity !== prevCanvasV2.settings.maskOpacity || canvasV2.tool.selected !== prevCanvasV2.tool.selected || - prevSelectedEntity?.id !== selectedEntity?.id + canvasV2.selectedEntityIdentifier?.id !== prevCanvasV2.selectedEntityIdentifier?.id ) { logIfDebugging('Rendering regions'); manager.renderRegions(); @@ -403,7 +396,7 @@ export const initializeRenderer = ( canvasV2.inpaintMask !== prevCanvasV2.inpaintMask || canvasV2.settings.maskOpacity !== prevCanvasV2.settings.maskOpacity || canvasV2.tool.selected !== prevCanvasV2.tool.selected || - prevSelectedEntity?.id !== selectedEntity?.id + canvasV2.selectedEntityIdentifier?.id !== prevCanvasV2.selectedEntityIdentifier?.id ) { logIfDebugging('Rendering inpaint mask'); manager.renderInpaintMask(); @@ -412,7 +405,7 @@ export const initializeRenderer = ( if ( isFirstRender || canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities || - prevSelectedEntity?.id !== selectedEntity?.id + canvasV2.selectedEntityIdentifier?.id !== prevCanvasV2.selectedEntityIdentifier?.id ) { logIfDebugging('Rendering control adapters'); manager.renderControlAdapters(); @@ -448,15 +441,13 @@ export const initializeRenderer = ( canvasV2.layers.entities !== prevCanvasV2.layers.entities || canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities || canvasV2.regions.entities !== prevCanvasV2.regions.entities || - prevSelectedEntity?.id !== selectedEntity?.id + canvasV2.selectedEntityIdentifier?.id !== prevCanvasV2.selectedEntityIdentifier?.id ) { logIfDebugging('Arranging entities'); manager.arrangeEntities(); } prevCanvasV2 = canvasV2; - prevSelectedEntity = selectedEntity; - prevCurrentFill = currentFill; if (isFirstRender) { isFirstRender = false; From cdfe0ca1504202b817b6dfc6a22a2de2e81ffce6 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 3 Jul 2024 18:51:16 +1000 Subject: [PATCH 156/678] fix(ui): staging area rendering --- .../konva/renderers/stagingArea.ts | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stagingArea.ts index 6bd5617aa17..191d650904d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stagingArea.ts @@ -1,17 +1,20 @@ import { KonvaImage, KonvaProgressImage } from 'features/controlLayers/konva/renderers/objects'; import type { CanvasV2State } from 'features/controlLayers/store/types'; import Konva from 'konva'; +import type { ImageDTO } from 'services/api/types'; import type { InvocationDenoiseProgressEvent } from 'services/events/types'; export class CanvasStagingArea { group: Konva.Group; image: KonvaImage | null; progressImage: KonvaProgressImage | null; + imageDTO: ImageDTO | null; constructor() { this.group = new Konva.Group({ listening: false }); this.image = null; this.progressImage = null; + this.imageDTO = null; } async render( @@ -21,21 +24,21 @@ export class CanvasStagingArea { lastProgressEvent: InvocationDenoiseProgressEvent | null, resetLastProgressEvent: () => void ) { - const imageDTO = stagingArea.images[stagingArea.selectedImageIndex]; + this.imageDTO = stagingArea.images[stagingArea.selectedImageIndex] ?? null; - if (imageDTO) { + if (this.imageDTO) { if (this.image) { - if (!this.image.isLoading && !this.image.isError && this.image.imageName !== imageDTO.image_name) { - await this.image.updateImageSource(imageDTO.image_name); + if (!this.image.isLoading && !this.image.isError && this.image.imageName !== this.imageDTO.image_name) { + await this.image.updateImageSource(this.imageDTO.image_name); } this.image.konvaImageGroup.x(bbox.x); this.image.konvaImageGroup.y(bbox.y); this.image.konvaImageGroup.visible(shouldShowStagedImage); this.progressImage?.konvaImageGroup.visible(false); } else { - const { image_name, width, height } = imageDTO; - this.image = new KonvaImage({ - imageObject: { + const { image_name, width, height } = this.imageDTO; + this.image = new KonvaImage( + { id: 'staging-area-image', type: 'image', x: bbox.x, @@ -49,12 +52,18 @@ export class CanvasStagingArea { height, }, }, - onLoad: () => { - resetLastProgressEvent(); - }, - }); + { + onLoad: (konvaImage) => { + if (this.imageDTO) { + konvaImage.width(this.imageDTO.width); + konvaImage.height(this.imageDTO.height); + } + resetLastProgressEvent(); + }, + } + ); this.group.add(this.image.konvaImageGroup); - await this.image.updateImageSource(imageDTO.image_name); + await this.image.updateImageSource(this.imageDTO.image_name); this.image.konvaImageGroup.visible(shouldShowStagedImage); this.progressImage?.konvaImageGroup.visible(false); } @@ -84,7 +93,7 @@ export class CanvasStagingArea { } } - if (!imageDTO && !lastProgressEvent) { + if (!this.imageDTO && !lastProgressEvent) { if (this.image) { this.image.konvaImageGroup.visible(false); } From bc525d29e1d2730b64296676ec4cf7ffef963128 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 3 Jul 2024 19:42:48 +1000 Subject: [PATCH 157/678] fix(ui): inpaint mask rendering --- .../controlLayers/konva/nodeManager.ts | 10 +- .../konva/renderers/entityBbox.ts | 14 +- .../konva/renderers/inpaintMask.ts | 259 +++++++++--------- .../controlLayers/konva/renderers/regions.ts | 4 +- .../controlLayers/konva/renderers/renderer.ts | 4 + .../controlLayers/store/canvasV2Slice.ts | 1 + .../store/inpaintMaskReducers.ts | 25 +- .../controlLayers/store/layersReducers.ts | 3 +- 8 files changed, 176 insertions(+), 144 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts index bea047c4c86..7dc7415b3e0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts @@ -150,7 +150,7 @@ export class KonvaNodeManager { this.background = new CanvasBackground(); this.stage.add(this.background.layer); - this.inpaintMask = new CanvasInpaintMask(this.stateApi.getInpaintMaskState(), this.stateApi.onPosChanged); + this.inpaintMask = new CanvasInpaintMask(this.stateApi.getInpaintMaskState(), this); this.stage.add(this.inpaintMask.layer); this.layers = new Map(); @@ -206,11 +206,7 @@ export class KonvaNodeManager { renderInpaintMask() { const inpaintMaskState = this.stateApi.getInpaintMaskState(); - const toolState = this.stateApi.getToolState(); - const selectedEntity = this.stateApi.getSelectedEntity(); - const maskOpacity = this.stateApi.getMaskOpacity(); - - this.inpaintMask.render(inpaintMaskState, toolState.selected, selectedEntity, maskOpacity); + this.inpaintMask.render(inpaintMaskState); } renderControlAdapters() { @@ -250,7 +246,7 @@ export class KonvaNodeManager { for (const rg of regions) { this.regions.get(rg.id)?.layer.zIndex(++zIndex); } - this.inpaintMask?.layer.zIndex(++zIndex); + this.inpaintMask.layer.zIndex(++zIndex); this.preview.layer.zIndex(++zIndex); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/entityBbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/entityBbox.ts index 5c226d017a1..1669df159ab 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/entityBbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/entityBbox.ts @@ -165,13 +165,13 @@ const getLayerBboxPixels = ( }; /** - * Get the bounding box of a konva layer. This function is faster than `getLayerBboxPixels` but less accurate. It - * should only be used when there are no eraser strokes or shapes in the layer. - * @param layer The konva layer to get the bounding box of. - * @returns The bounding box of the layer. + * Get the bounding box of a konva node. This function is faster than `getLayerBboxPixels` but less accurate. It + * should only be used when there are no eraser strokes or shapes in the node. + * @param node The konva node to get the bounding box of. + * @returns The bounding box of the node. */ -export const getLayerBboxFast = (layer: Konva.Layer): IRect => { - const bbox = layer.getClientRect(GET_CLIENT_RECT_CONFIG); +export const getNodeBboxFast = (node: Konva.Node): IRect => { + const bbox = node.getClientRect(GET_CLIENT_RECT_CONFIG); return { x: Math.floor(bbox.x), y: Math.floor(bbox.y), @@ -210,7 +210,7 @@ export const updateBboxes = ( if (entityState.type === 'layer') { if (entityState.objects.length === 0) { - // No objects - no bbox to calculate + // No objects - no bbox to calculate onBboxChanged({ id: entityState.id, bbox: null }, 'layer'); } else { onBboxChanged({ id: entityState.id, bbox: getLayerBboxPixels(konvaLayer, filterLayerChildren) }, 'layer'); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts index 85b6efff907..4f9c022240a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts @@ -1,42 +1,62 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { getObjectGroupId } from 'features/controlLayers/konva/naming'; -import type { StateApi } from 'features/controlLayers/konva/nodeManager'; -import { getLayerBboxFast } from 'features/controlLayers/konva/renderers/entityBbox'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import { getNodeBboxFast } from 'features/controlLayers/konva/renderers/entityBbox'; import { KonvaBrushLine, KonvaEraserLine, KonvaRect } from 'features/controlLayers/konva/renderers/objects'; import { mapId } from 'features/controlLayers/konva/util'; -import type { CanvasEntityIdentifier, InpaintMaskEntity, Tool } from 'features/controlLayers/store/types'; +import { type InpaintMaskEntity, isDrawingTool } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; export class CanvasInpaintMask { id: string; + manager: KonvaNodeManager; layer: Konva.Layer; group: Konva.Group; + objectsGroup: Konva.Group; compositingRect: Konva.Rect; + transformer: Konva.Transformer; objects: Map; - constructor(entity: InpaintMaskEntity, onPosChanged: StateApi['onPosChanged']) { + constructor(entity: InpaintMaskEntity, manager: KonvaNodeManager) { this.id = entity.id; - + this.manager = manager; this.layer = new Konva.Layer({ id: entity.id, draggable: true, dragDistance: 0, }); - // 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. - this.layer.on('dragend', function (e) { - onPosChanged({ id: entity.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'inpaint_mask'); - }); this.group = new Konva.Group({ id: getObjectGroupId(this.layer.id(), uuidv4()), listening: false, }); + this.objectsGroup = new Konva.Group({}); + this.group.add(this.objectsGroup); this.layer.add(this.group); + + this.transformer = new Konva.Transformer({ + shouldOverdrawWholeArea: true, + draggable: true, + dragDistance: 0, + enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], + rotateEnabled: false, + flipEnabled: false, + }); + this.transformer.on('transformend', () => { + this.manager.stateApi.onScaleChanged( + { id: this.id, scale: this.group.scaleX(), x: this.group.x(), y: this.group.y() }, + 'inpaint_mask' + ); + }); + this.transformer.on('dragend', () => { + this.manager.stateApi.onPosChanged({ id: this.id, x: this.group.x(), y: this.group.y() }, 'inpaint_mask'); + }); + this.layer.add(this.transformer); + this.compositingRect = new Konva.Rect({ listening: false }); - this.layer.add(this.compositingRect); + this.group.add(this.compositingRect); this.objects = new Map(); } @@ -44,24 +64,16 @@ export class CanvasInpaintMask { this.layer.destroy(); } - async render( - inpaintMaskState: InpaintMaskEntity, - selectedTool: Tool, - selectedEntityIdentifier: CanvasEntityIdentifier | null, - maskOpacity: number - ) { + async render(inpaintMaskState: InpaintMaskEntity) { // Update the layer's position and listening state - this.layer.setAttrs({ - listening: selectedTool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events - x: Math.floor(inpaintMaskState.x), - y: Math.floor(inpaintMaskState.y), + this.group.setAttrs({ + x: inpaintMaskState.x, + y: inpaintMaskState.y, + scaleX: 1, + scaleY: 1, }); - // Convert the color to a string, stripping the alpha - the object group will handle opacity. - const rgbColor = rgbColorToString(inpaintMaskState.fill); - - // We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required. - let groupNeedsCache = false; + let didDraw = false; const objectIds = inpaintMaskState.objects.map(mapId); // Destroy any objects that are no longer in state @@ -69,7 +81,7 @@ export class CanvasInpaintMask { if (!objectIds.includes(object.id)) { this.objects.delete(object.id); object.destroy(); - groupNeedsCache = true; + didDraw = true; } } @@ -81,13 +93,12 @@ export class CanvasInpaintMask { if (!brushLine) { brushLine = new KonvaBrushLine(obj); this.objects.set(brushLine.id, brushLine); - this.group.add(brushLine.konvaLineGroup); - groupNeedsCache = true; - } - - if (obj.points.length !== brushLine.konvaLine.points().length) { - brushLine.konvaLine.points(obj.points); - groupNeedsCache = true; + this.objectsGroup.add(brushLine.konvaLineGroup); + didDraw = true; + } else { + if (brushLine.update(obj)) { + didDraw = true; + } } } else if (obj.type === 'eraser_line') { let eraserLine = this.objects.get(obj.id); @@ -96,13 +107,12 @@ export class CanvasInpaintMask { if (!eraserLine) { eraserLine = new KonvaEraserLine(obj); this.objects.set(eraserLine.id, eraserLine); - this.group.add(eraserLine.konvaLineGroup); - groupNeedsCache = true; - } - - if (obj.points.length !== eraserLine.konvaLine.points().length) { - eraserLine.konvaLine.points(obj.points); - groupNeedsCache = true; + this.objectsGroup.add(eraserLine.konvaLineGroup); + didDraw = true; + } else { + if (eraserLine.update(obj)) { + didDraw = true; + } } } else if (obj.type === 'rect_shape') { let rect = this.objects.get(obj.id); @@ -111,8 +121,12 @@ export class CanvasInpaintMask { if (!rect) { rect = new KonvaRect(obj); this.objects.set(rect.id, rect); - this.group.add(rect.konvaRect); - groupNeedsCache = true; + this.objectsGroup.add(rect.konvaRect); + didDraw = true; + } else { + if (rect.update(obj)) { + didDraw = true; + } } } } @@ -120,96 +134,91 @@ export class CanvasInpaintMask { // Only update layer visibility if it has changed. if (this.layer.visible() !== inpaintMaskState.isEnabled) { this.layer.visible(inpaintMaskState.isEnabled); - groupNeedsCache = true; } + // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work + this.group.opacity(1); + + if (didDraw) { + // Convert the color to a string, stripping the alpha - the object group will handle opacity. + const rgbColor = rgbColorToString(inpaintMaskState.fill); + const maskOpacity = this.manager.stateApi.getMaskOpacity(); + + this.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 + ...getNodeBboxFast(this.objectsGroup), + fill: rgbColor, + opacity: maskOpacity, + // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) + globalCompositeOperation: 'source-in', + visible: true, + // This rect must always be on top of all other shapes + zIndex: this.objects.size + 1, + }); + } + + this.updateGroup(didDraw); + } + + updateGroup(didDraw: boolean) { + const isSelected = this.manager.stateApi.getIsSelected(this.id); + const selectedTool = this.manager.stateApi.getToolState().selected; + if (this.objects.size === 0) { - // No objects - clear the cache to reset the previous pixel data - this.group.clearCache(); + // If the layer is totally empty, reset the cache and bail out. + this.layer.listening(false); + this.transformer.nodes([]); + if (this.group.isCached()) { + this.group.clearCache(); + } return; } - // We must clear the cache first so Konva will re-draw the group with the new compositing rect - if (this.group.isCached()) { - this.group.clearCache(); + if (isSelected && selectedTool === 'move') { + // When the layer is selected and being moved, we should always cache it. + // We should update the cache if we drew to the layer. + if (!this.group.isCached() || didDraw) { + this.group.cache(); + } + // Activate the transformer + this.layer.listening(true); + this.transformer.nodes([this.group]); + this.transformer.forceUpdate(); + return; } - // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work - this.group.opacity(1); - this.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 - ...(!inpaintMaskState.bboxNeedsUpdate && inpaintMaskState.bbox - ? inpaintMaskState.bbox - : getLayerBboxFast(this.layer)), - fill: rgbColor, - opacity: maskOpacity, - // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) - globalCompositeOperation: 'source-in', - visible: true, - // This rect must always be on top of all other shapes - zIndex: this.objects.size + 1, - }); + if (isSelected && selectedTool !== 'move') { + // If the layer is selected but not using the move tool, we don't want the layer to be listening. + this.layer.listening(false); + // The transformer also does not need to be active. + this.transformer.nodes([]); + if (isDrawingTool(selectedTool)) { + // We are using a drawing tool (brush, eraser, rect). These tools change the layer's rendered appearance, so we + // should never be cached. + if (this.group.isCached()) { + this.group.clearCache(); + } + } else { + // We are using a non-drawing tool (move, view, bbox), so we should cache the layer. + // We should update the cache if we drew to the layer. + if (!this.group.isCached() || didDraw) { + this.group.cache(); + } + } + return; + } - // const isSelected = selectedEntityIdentifier?.id === inpaintMaskState.id; - - // /** - // * When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows - // * shapes to render as a "raster" layer with all pixels drawn at the same color and opacity. - // * - // * Without this special handling, each shape is drawn individually with the given opacity, atop the other shapes. The - // * effect is like if you have a Photoshop Group consisting of many shapes, each of which has the given opacity. - // * Overlapping shapes will have their colors blended together, and the final color is the result of all the shapes. - // * - // * 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. - // */ - // if (isSelected && selectedTool !== 'move') { - // // We must clear the cache first so Konva will re-draw the group with the new compositing rect - // if (this.konvaObjectGroup.isCached()) { - // this.konvaObjectGroup.clearCache(); - // } - // // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work - // this.konvaObjectGroup.opacity(1); - - // this.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 - // ...(!inpaintMaskState.bboxNeedsUpdate && inpaintMaskState.bbox - // ? inpaintMaskState.bbox - // : getLayerBboxFast(this.konvaLayer)), - // fill: rgbColor, - // opacity: maskOpacity, - // // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) - // globalCompositeOperation: 'source-in', - // visible: true, - // // This rect must always be on top of all other shapes - // zIndex: this.objects.size + 1, - // }); - // } else { - // // The compositing rect should only be shown when the layer is selected. - // this.compositingRect.visible(false); - // // Cache only if needed - or if we are on this code path and _don't_ have a cache - // if (groupNeedsCache || !this.konvaObjectGroup.isCached()) { - // this.konvaObjectGroup.cache(); - // } - // // Updating group opacity does not require re-caching - // this.konvaObjectGroup.opacity(maskOpacity); - // } - - // const bboxRect = - // regionMap.konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rg, regionMap.konvaLayer); - // if (rg.bbox) { - // const active = !rg.bboxNeedsUpdate && isSelected && tool === 'move'; - // bboxRect.setAttrs({ - // visible: active, - // listening: active, - // x: rg.bbox.x, - // y: rg.bbox.y, - // width: rg.bbox.width, - // height: rg.bbox.height, - // stroke: isSelected ? BBOX_SELECTED_STROKE : '', - // }); - // } else { - // bboxRect.visible(false); - // } + if (!isSelected) { + // Unselected layers should not be listening + this.layer.listening(false); + // The transformer also does not need to be active. + this.transformer.nodes([]); + // Update the layer's cache if it's not already cached or we drew to it. + if (!this.group.isCached() || didDraw) { + this.group.cache(); + } + + return; + } } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts index 0a2c5353d4c..2e8f154a986 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts @@ -1,7 +1,7 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { getObjectGroupId } from 'features/controlLayers/konva/naming'; import type { StateApi } from 'features/controlLayers/konva/nodeManager'; -import { getLayerBboxFast } from 'features/controlLayers/konva/renderers/entityBbox'; +import { getNodeBboxFast } from 'features/controlLayers/konva/renderers/entityBbox'; import { KonvaBrushLine, KonvaEraserLine, KonvaRect } from 'features/controlLayers/konva/renderers/objects'; import { mapId } from 'features/controlLayers/konva/util'; import type { CanvasEntityIdentifier, RegionEntity, Tool } from 'features/controlLayers/store/types'; @@ -138,7 +138,7 @@ export class CanvasRegion { this.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 - ...(!regionState.bboxNeedsUpdate && regionState.bbox ? regionState.bbox : getLayerBboxFast(this.layer)), + ...(!regionState.bboxNeedsUpdate && regionState.bbox ? regionState.bbox : getNodeBboxFast(this.layer)), fill: rgbColor, opacity: maskOpacity, // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts index 9539a3403b4..1ecd2621b88 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -21,6 +21,7 @@ import { imImageCacheChanged, imLinePointAdded, imRectAdded, + imScaled, imTranslated, layerBboxChanged, layerBrushLineAdded, @@ -107,6 +108,8 @@ export const initializeRenderer = ( logIfDebugging('onScaleChanged'); if (entityType === 'layer') { dispatch(layerScaled(arg)); + } else if (entityType === 'inpaint_mask') { + dispatch(imScaled(arg)); } }; const onBboxChanged = (arg: BboxChangedArg, entityType: CanvasEntity['type']) => { @@ -441,6 +444,7 @@ export const initializeRenderer = ( canvasV2.layers.entities !== prevCanvasV2.layers.entities || canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities || canvasV2.regions.entities !== prevCanvasV2.regions.entities || + canvasV2.inpaintMask !== prevCanvasV2.inpaintMask || canvasV2.selectedEntityIdentifier?.id !== prevCanvasV2.selectedEntityIdentifier?.id ) { logIfDebugging('Arranging entities'); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 186d47946c5..e9dabfafee1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -336,6 +336,7 @@ export const { imEraserLineAdded, imLinePointAdded, imRectAdded, + imScaled, // Staging stagingAreaStartedStaging, stagingAreaImageAdded, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts index 1b7a9346ee9..58343c3ebf3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts @@ -1,7 +1,7 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; -import type { CanvasV2State, InpaintMaskEntity } from 'features/controlLayers/store/types'; -import { imageDTOToImageWithDims,RGBA_RED } from 'features/controlLayers/store/types'; +import type { CanvasV2State, InpaintMaskEntity, ScaleChangedArg } from 'features/controlLayers/store/types'; +import { imageDTOToImageWithDims, RGBA_RED } from 'features/controlLayers/store/types'; import type { IRect } from 'konva/lib/types'; import type { ImageDTO } from 'services/api/types'; import { v4 as uuidv4 } from 'uuid'; @@ -29,6 +29,27 @@ export const inpaintMaskReducers = { state.inpaintMask.x = x; state.inpaintMask.y = y; }, + imScaled: (state, action: PayloadAction) => { + const { scale, x, y } = action.payload; + for (const obj of state.inpaintMask.objects) { + if (obj.type === 'brush_line') { + obj.points = obj.points.map((point) => point * scale); + obj.strokeWidth *= scale; + } else if (obj.type === 'eraser_line') { + obj.points = obj.points.map((point) => point * scale); + obj.strokeWidth *= scale; + } else if (obj.type === 'rect_shape') { + obj.x *= scale; + obj.y *= scale; + obj.height *= scale; + obj.width *= scale; + } + } + state.inpaintMask.x = x; + state.inpaintMask.y = y; + state.inpaintMask.bboxNeedsUpdate = true; + state.inpaintMask.imageCache = null; + }, imBboxChanged: (state, action: PayloadAction<{ bbox: IRect | null }>) => { const { bbox } = action.payload; state.inpaintMask.bbox = bbox; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index c9ed79f0690..c39a70fe011 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -14,6 +14,7 @@ import type { LayerEntity, PointAddedToLineArg, RectShapeAddedArg, + ScaleChangedArg, } from './types'; import { imageDTOToImageObject, imageDTOToImageWithDims, isLine } from './types'; @@ -174,7 +175,7 @@ export const layersReducers = { payload: { ...payload, lineId: uuidv4() }, }), }, - layerScaled: (state, action: PayloadAction<{ id: string; scale: number; x: number; y: number }>) => { + layerScaled: (state, action: PayloadAction) => { const { id, scale, x, y } = action.payload; const layer = selectLayer(state, id); if (!layer) { From cea5dc62161d7461e7a959cd38e41cc874f134f4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 3 Jul 2024 19:57:07 +1000 Subject: [PATCH 158/678] fix(ui): region rendering --- .../controlLayers/konva/nodeManager.ts | 7 +- .../konva/renderers/inpaintMask.ts | 6 +- .../controlLayers/konva/renderers/regions.ts | 262 +++++++++--------- .../controlLayers/konva/renderers/renderer.ts | 3 + .../controlLayers/store/canvasV2Slice.ts | 1 + .../controlLayers/store/regionsReducers.ts | 29 +- 6 files changed, 171 insertions(+), 137 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts index 7dc7415b3e0..de5c372498f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts @@ -181,9 +181,6 @@ export class KonvaNodeManager { renderRegions() { const { entities } = this.stateApi.getRegionsState(); - const maskOpacity = this.stateApi.getMaskOpacity(); - const toolState = this.stateApi.getToolState(); - const selectedEntity = this.stateApi.getSelectedEntity(); // Destroy the konva nodes for nonexistent entities for (const canvasRegion of this.regions.values()) { @@ -196,11 +193,11 @@ export class KonvaNodeManager { for (const entity of entities) { let adapter = this.regions.get(entity.id); if (!adapter) { - adapter = new CanvasRegion(entity, this.stateApi.onPosChanged); + adapter = new CanvasRegion(entity, this); this.regions.set(adapter.id, adapter); this.stage.add(adapter.layer); } - adapter.render(entity, toolState.selected, selectedEntity, maskOpacity); + adapter.render(entity); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts index 4f9c022240a..06d95993a3a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts @@ -22,11 +22,7 @@ export class CanvasInpaintMask { constructor(entity: InpaintMaskEntity, manager: KonvaNodeManager) { this.id = entity.id; this.manager = manager; - this.layer = new Konva.Layer({ - id: entity.id, - draggable: true, - dragDistance: 0, - }); + this.layer = new Konva.Layer({ id: entity.id }); this.group = new Konva.Group({ id: getObjectGroupId(this.layer.id(), uuidv4()), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts index 2e8f154a986..0f292dfacae 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts @@ -1,42 +1,61 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { getObjectGroupId } from 'features/controlLayers/konva/naming'; -import type { StateApi } from 'features/controlLayers/konva/nodeManager'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import { getNodeBboxFast } from 'features/controlLayers/konva/renderers/entityBbox'; import { KonvaBrushLine, KonvaEraserLine, KonvaRect } from 'features/controlLayers/konva/renderers/objects'; import { mapId } from 'features/controlLayers/konva/util'; -import type { CanvasEntityIdentifier, RegionEntity, Tool } from 'features/controlLayers/store/types'; +import { + isDrawingTool, + type RegionEntity, +} from 'features/controlLayers/store/types'; import Konva from 'konva'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; export class CanvasRegion { id: string; + manager: KonvaNodeManager; layer: Konva.Layer; group: Konva.Group; + objectsGroup: Konva.Group; compositingRect: Konva.Rect; + transformer: Konva.Transformer; objects: Map; - constructor(entity: RegionEntity, onPosChanged: StateApi['onPosChanged']) { + constructor(entity: RegionEntity, manager: KonvaNodeManager) { this.id = entity.id; + this.manager = manager; + this.layer = new Konva.Layer({ id: entity.id }); - this.layer = new Konva.Layer({ - id: entity.id, - draggable: true, - dragDistance: 0, - }); - - // 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. - this.layer.on('dragend', function (e) { - onPosChanged({ id: entity.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'regional_guidance'); - }); this.group = new Konva.Group({ id: getObjectGroupId(this.layer.id(), uuidv4()), listening: false, }); + this.objectsGroup = new Konva.Group({}); + this.group.add(this.objectsGroup); this.layer.add(this.group); + + this.transformer = new Konva.Transformer({ + shouldOverdrawWholeArea: true, + draggable: true, + dragDistance: 0, + enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], + rotateEnabled: false, + flipEnabled: false, + }); + this.transformer.on('transformend', () => { + this.manager.stateApi.onScaleChanged( + { id: this.id, scale: this.group.scaleX(), x: this.group.x(), y: this.group.y() }, + 'regional_guidance' + ); + }); + this.transformer.on('dragend', () => { + this.manager.stateApi.onPosChanged({ id: this.id, x: this.group.x(), y: this.group.y() }, 'regional_guidance'); + }); + this.layer.add(this.transformer); + this.compositingRect = new Konva.Rect({ listening: false }); - this.layer.add(this.compositingRect); + this.group.add(this.compositingRect); this.objects = new Map(); } @@ -44,24 +63,16 @@ export class CanvasRegion { this.layer.destroy(); } - async render( - regionState: RegionEntity, - selectedTool: Tool, - selectedEntityIdentifier: CanvasEntityIdentifier | null, - maskOpacity: number - ) { + async render(regionState: RegionEntity) { // Update the layer's position and listening state - this.layer.setAttrs({ - listening: selectedTool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events - x: Math.floor(regionState.x), - y: Math.floor(regionState.y), + this.group.setAttrs({ + x: regionState.x, + y: regionState.y, + scaleX: 1, + scaleY: 1, }); - // Convert the color to a string, stripping the alpha - the object group will handle opacity. - const rgbColor = rgbColorToString(regionState.fill); - - // We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required. - let groupNeedsCache = false; + let didDraw = false; const objectIds = regionState.objects.map(mapId); // Destroy any objects that are no longer in state @@ -69,7 +80,7 @@ export class CanvasRegion { if (!objectIds.includes(object.id)) { this.objects.delete(object.id); object.destroy(); - groupNeedsCache = true; + didDraw = true; } } @@ -81,13 +92,12 @@ export class CanvasRegion { if (!brushLine) { brushLine = new KonvaBrushLine(obj); this.objects.set(brushLine.id, brushLine); - this.group.add(brushLine.konvaLineGroup); - groupNeedsCache = true; - } - - if (obj.points.length !== brushLine.konvaLine.points().length) { - brushLine.konvaLine.points(obj.points); - groupNeedsCache = true; + this.objectsGroup.add(brushLine.konvaLineGroup); + didDraw = true; + } else { + if (brushLine.update(obj)) { + didDraw = true; + } } } else if (obj.type === 'eraser_line') { let eraserLine = this.objects.get(obj.id); @@ -96,13 +106,12 @@ export class CanvasRegion { if (!eraserLine) { eraserLine = new KonvaEraserLine(obj); this.objects.set(eraserLine.id, eraserLine); - this.group.add(eraserLine.konvaLineGroup); - groupNeedsCache = true; - } - - if (obj.points.length !== eraserLine.konvaLine.points().length) { - eraserLine.konvaLine.points(obj.points); - groupNeedsCache = true; + this.objectsGroup.add(eraserLine.konvaLineGroup); + didDraw = true; + } else { + if (eraserLine.update(obj)) { + didDraw = true; + } } } else if (obj.type === 'rect_shape') { let rect = this.objects.get(obj.id); @@ -111,8 +120,12 @@ export class CanvasRegion { if (!rect) { rect = new KonvaRect(obj); this.objects.set(rect.id, rect); - this.group.add(rect.konvaRect); - groupNeedsCache = true; + this.objectsGroup.add(rect.konvaRect); + didDraw = true; + } else { + if (rect.update(obj)) { + didDraw = true; + } } } } @@ -120,92 +133,91 @@ export class CanvasRegion { // Only update layer visibility if it has changed. if (this.layer.visible() !== regionState.isEnabled) { this.layer.visible(regionState.isEnabled); - groupNeedsCache = true; } + // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work + this.group.opacity(1); + + if (didDraw) { + // Convert the color to a string, stripping the alpha - the object group will handle opacity. + const rgbColor = rgbColorToString(regionState.fill); + const maskOpacity = this.manager.stateApi.getMaskOpacity(); + + this.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 + ...getNodeBboxFast(this.objectsGroup), + fill: rgbColor, + opacity: maskOpacity, + // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) + globalCompositeOperation: 'source-in', + visible: true, + // This rect must always be on top of all other shapes + zIndex: this.objects.size + 1, + }); + } + + this.updateGroup(didDraw); + } + + updateGroup(didDraw: boolean) { + const isSelected = this.manager.stateApi.getIsSelected(this.id); + const selectedTool = this.manager.stateApi.getToolState().selected; + if (this.objects.size === 0) { - // No objects - clear the cache to reset the previous pixel data - this.group.clearCache(); + // If the layer is totally empty, reset the cache and bail out. + this.layer.listening(false); + this.transformer.nodes([]); + if (this.group.isCached()) { + this.group.clearCache(); + } return; } - // We must clear the cache first so Konva will re-draw the group with the new compositing rect - if (this.group.isCached()) { - this.group.clearCache(); + if (isSelected && selectedTool === 'move') { + // When the layer is selected and being moved, we should always cache it. + // We should update the cache if we drew to the layer. + if (!this.group.isCached() || didDraw) { + this.group.cache(); + } + // Activate the transformer + this.layer.listening(true); + this.transformer.nodes([this.group]); + this.transformer.forceUpdate(); + return; } - // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work - this.group.opacity(1); - this.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 - ...(!regionState.bboxNeedsUpdate && regionState.bbox ? regionState.bbox : getNodeBboxFast(this.layer)), - fill: rgbColor, - opacity: maskOpacity, - // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) - globalCompositeOperation: 'source-in', - visible: true, - // This rect must always be on top of all other shapes - zIndex: this.objects.size + 1, - }); + if (isSelected && selectedTool !== 'move') { + // If the layer is selected but not using the move tool, we don't want the layer to be listening. + this.layer.listening(false); + // The transformer also does not need to be active. + this.transformer.nodes([]); + if (isDrawingTool(selectedTool)) { + // We are using a drawing tool (brush, eraser, rect). These tools change the layer's rendered appearance, so we + // should never be cached. + if (this.group.isCached()) { + this.group.clearCache(); + } + } else { + // We are using a non-drawing tool (move, view, bbox), so we should cache the layer. + // We should update the cache if we drew to the layer. + if (!this.group.isCached() || didDraw) { + this.group.cache(); + } + } + return; + } + + if (!isSelected) { + // Unselected layers should not be listening + this.layer.listening(false); + // The transformer also does not need to be active. + this.transformer.nodes([]); + // Update the layer's cache if it's not already cached or we drew to it. + if (!this.group.isCached() || didDraw) { + this.group.cache(); + } - // const isSelected = selectedEntityIdentifier?.id === regionState.id; - - // /** - // * When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows - // * shapes to render as a "raster" layer with all pixels drawn at the same color and opacity. - // * - // * Without this special handling, each shape is drawn individually with the given opacity, atop the other shapes. The - // * effect is like if you have a Photoshop Group consisting of many shapes, each of which has the given opacity. - // * Overlapping shapes will have their colors blended together, and the final color is the result of all the shapes. - // * - // * 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. - // */ - // if (isSelected && selectedTool !== 'move') { - // // We must clear the cache first so Konva will re-draw the group with the new compositing rect - // if (this.konvaObjectGroup.isCached()) { - // this.konvaObjectGroup.clearCache(); - // } - // // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work - // this.konvaObjectGroup.opacity(1); - - // this.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 - // ...(!regionState.bboxNeedsUpdate && regionState.bbox ? regionState.bbox : getLayerBboxFast(this.konvaLayer)), - // fill: rgbColor, - // opacity: maskOpacity, - // // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) - // globalCompositeOperation: 'source-in', - // visible: true, - // // This rect must always be on top of all other shapes - // zIndex: this.objects.size + 1, - // }); - // } else { - // // The compositing rect should only be shown when the layer is selected. - // this.compositingRect.visible(false); - // // Cache only if needed - or if we are on this code path and _don't_ have a cache - // if (groupNeedsCache || !this.konvaObjectGroup.isCached()) { - // this.konvaObjectGroup.cache(); - // } - // // Updating group opacity does not require re-caching - // this.konvaObjectGroup.opacity(maskOpacity); - // } - - // const bboxRect = - // regionMap.konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rg, regionMap.konvaLayer); - // if (rg.bbox) { - // const active = !rg.bboxNeedsUpdate && isSelected && tool === 'move'; - // bboxRect.setAttrs({ - // visible: active, - // listening: active, - // x: rg.bbox.x, - // y: rg.bbox.y, - // width: rg.bbox.width, - // height: rg.bbox.height, - // stroke: isSelected ? BBOX_SELECTED_STROKE : '', - // }); - // } else { - // bboxRect.visible(false); - // } + return; + } } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts index 1ecd2621b88..997d564c880 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -37,6 +37,7 @@ import { rgImageCacheChanged, rgLinePointAdded, rgRectAdded, + rgScaled, rgTranslated, toolBufferChanged, toolChanged, @@ -110,6 +111,8 @@ export const initializeRenderer = ( dispatch(layerScaled(arg)); } else if (entityType === 'inpaint_mask') { dispatch(imScaled(arg)); + } else if (entityType === 'regional_guidance') { + dispatch(rgScaled(arg)); } }; const onBboxChanged = (arg: BboxChangedArg, entityType: CanvasEntity['type']) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index e9dabfafee1..0b63056b3db 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -280,6 +280,7 @@ export const { rgEraserLineAdded, rgLinePointAdded, rgRectAdded, + rgScaled, // Compositing setInfillMethod, setInfillTileSize, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index 59ad0d5314f..9c7fb6e1e82 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -1,8 +1,8 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; -import type { CanvasV2State, CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types'; -import { imageDTOToImageObject, imageDTOToImageWithDims,RGBA_RED } from 'features/controlLayers/store/types'; +import type { CanvasV2State, CLIPVisionModelV2, IPMethodV2, ScaleChangedArg } from 'features/controlLayers/store/types'; +import { imageDTOToImageObject, imageDTOToImageWithDims, RGBA_RED } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas'; import type { IRect } from 'konva/lib/types'; @@ -107,6 +107,31 @@ export const regionsReducers = { rg.y = y; } }, + rgScaled: (state, action: PayloadAction) => { + const { id, scale, x, y } = action.payload; + const rg = selectRG(state, id); + if (!rg) { + return; + } + for (const obj of rg.objects) { + if (obj.type === 'brush_line') { + obj.points = obj.points.map((point) => point * scale); + obj.strokeWidth *= scale; + } else if (obj.type === 'eraser_line') { + obj.points = obj.points.map((point) => point * scale); + obj.strokeWidth *= scale; + } else if (obj.type === 'rect_shape') { + obj.x *= scale; + obj.y *= scale; + obj.height *= scale; + obj.width *= scale; + } + } + rg.x = x; + rg.y = y; + rg.bboxNeedsUpdate = true; + state.layers.imageCache = null; + }, rgBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { const { id, bbox } = action.payload; const rg = selectRG(state, id); From 25ab435129d0c85e6701f78c177a8a9867e5201f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 4 Jul 2024 09:56:47 +1000 Subject: [PATCH 159/678] fix(ui): merge conflicts in image deletion listener --- .../listeners/imageDeletionListeners.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts index 9278cf94b6c..4c9a045d60d 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts @@ -40,8 +40,8 @@ const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: Im }; const deleteControlAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { - state.canvasV2.controlAdapters.forEach(({ id, image, processedImage }) => { - if (image?.name === imageDTO.image_name || processedImage?.name === imageDTO.image_name) { + state.canvasV2.controlAdapters.entities.forEach(({ id, imageObject, processedImageObject }) => { + if (imageObject?.image.name === imageDTO.image_name || processedImageObject?.image.name === imageDTO.image_name) { dispatch(caImageChanged({ id, imageDTO: null })); dispatch(caProcessedImageChanged({ id, imageDTO: null })); } @@ -49,15 +49,15 @@ const deleteControlAdapterImages = (state: RootState, dispatch: AppDispatch, ima }; const deleteIPAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { - state.canvasV2.ipAdapters.forEach(({ id, imageObject: image }) => { - if (image?.name === imageDTO.image_name) { + state.canvasV2.ipAdapters.entities.forEach(({ id, imageObject }) => { + if (imageObject?.image.name === imageDTO.image_name) { dispatch(ipaImageChanged({ id, imageDTO: null })); } }); }; const deleteLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { - state.canvasV2.layers.forEach(({ id, objects }) => { + state.canvasV2.layers.entities.forEach(({ id, objects }) => { let shouldDelete = false; for (const obj of objects) { if (obj.type === 'image' && obj.image.name === imageDTO.image_name) { From dcac6028a20dbfaf70ea3a9c555d8de14bb87fc5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 4 Jul 2024 13:15:14 +1000 Subject: [PATCH 160/678] feat(ui): organize konva state and files --- .../components/StageComponent.tsx | 26 +- .../konva/{renderers => }/background.ts | 17 +- .../konva/{renderers => }/bbox.ts | 45 +- .../konva/{renderers => }/controlAdapters.ts | 0 .../{renderers => }/documentSizeOverlay.ts | 31 +- .../konva/{renderers => }/entityBbox.ts | 2 +- .../features/controlLayers/konva/events.ts | 35 +- .../konva/{renderers => }/inpaintMask.ts | 12 +- .../konva/{renderers => }/layers.ts | 2 +- .../features/controlLayers/konva/naming.ts | 10 +- .../controlLayers/konva/nodeManager.ts | 310 ++++++----- .../konva/{renderers => }/objects.ts | 0 .../konva/{renderers => }/preview.ts | 0 .../konva/{renderers => }/regions.ts | 9 +- .../controlLayers/konva/renderers/renderer.ts | 499 ------------------ .../konva/{renderers => }/stagingArea.ts | 26 +- .../features/controlLayers/konva/stateApi.ts | 237 +++++++++ .../konva/{renderers => }/tool.ts | 39 +- .../src/features/controlLayers/konva/util.ts | 4 +- .../controlLayers/store/canvasV2Slice.ts | 13 +- 20 files changed, 566 insertions(+), 751 deletions(-) rename invokeai/frontend/web/src/features/controlLayers/konva/{renderers => }/background.ts (86%) rename invokeai/frontend/web/src/features/controlLayers/konva/{renderers => }/bbox.ts (88%) rename invokeai/frontend/web/src/features/controlLayers/konva/{renderers => }/controlAdapters.ts (100%) rename invokeai/frontend/web/src/features/controlLayers/konva/{renderers => }/documentSizeOverlay.ts (66%) rename invokeai/frontend/web/src/features/controlLayers/konva/{renderers => }/entityBbox.ts (99%) rename invokeai/frontend/web/src/features/controlLayers/konva/{renderers => }/inpaintMask.ts (95%) rename invokeai/frontend/web/src/features/controlLayers/konva/{renderers => }/layers.ts (99%) rename invokeai/frontend/web/src/features/controlLayers/konva/{renderers => }/objects.ts (100%) rename invokeai/frontend/web/src/features/controlLayers/konva/{renderers => }/preview.ts (100%) rename invokeai/frontend/web/src/features/controlLayers/konva/{renderers => }/regions.ts (97%) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts rename invokeai/frontend/web/src/features/controlLayers/konva/{renderers => }/stagingArea.ts (83%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/stateApi.ts rename invokeai/frontend/web/src/features/controlLayers/konva/{renderers => }/tool.ts (88%) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index cb2e1070e13..10fceda1104 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -1,12 +1,16 @@ import { Flex } from '@invoke-ai/ui-library'; +import { logger } from 'app/logging/logger'; +import { $isDebugging } from 'app/store/nanostores/isDebugging'; import { useAppStore } from 'app/store/storeHooks'; import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay'; -import { initializeRenderer } from 'features/controlLayers/konva/renderers/renderer'; +import { KonvaNodeManager, setNodeManager } from 'features/controlLayers/konva/nodeManager'; import Konva from 'konva'; import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react'; import { useDevicePixelRatio } from 'use-device-pixel-ratio'; import { v4 as uuidv4 } from 'uuid'; +const log = logger('konva'); + // This will log warnings when layers > 5 - maybe use `import.meta.env.MODE === 'development'` instead? Konva.showWarnings = false; @@ -15,7 +19,25 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, const dpr = useDevicePixelRatio({ round: false }); useLayoutEffect(() => { - const cleanup = initializeRenderer(store, stage, container); + /** + * Logs a message to the console if debugging is enabled. + */ + const logIfDebugging = (message: string) => { + if ($isDebugging.get()) { + log.debug(message); + } + }; + + logIfDebugging('Initializing renderer'); + if (!container) { + // Nothing to clean up + logIfDebugging('No stage container, skipping initialization'); + return () => {}; + } + + const manager = new KonvaNodeManager(stage, container, store, logIfDebugging); + setNodeManager(manager); + const cleanup = manager.initialize(); return cleanup; }, [asPreview, container, stage, store]); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts b/invokeai/frontend/web/src/features/controlLayers/konva/background.ts similarity index 86% rename from invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/background.ts index 77dad03b27a..2e8b3da177d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/background.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/background.ts @@ -1,4 +1,5 @@ import { getArbitraryBaseColor } from '@invoke-ai/ui-library'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import Konva from 'konva'; const baseGridLineColor = getArbitraryBaseColor(27); @@ -30,19 +31,21 @@ const getGridSpacing = (scale: number): number => { export class CanvasBackground { layer: Konva.Layer; + manager: KonvaNodeManager; - constructor() { + constructor(manager: KonvaNodeManager) { + this.manager = manager; this.layer = new Konva.Layer({ listening: false }); } - renderBackground(stage: Konva.Stage): void { + renderBackground() { this.layer.zIndex(0); - const scale = stage.scaleX(); + const scale = this.manager.stage.scaleX(); const gridSpacing = getGridSpacing(scale); - const x = stage.x(); - const y = stage.y(); - const width = stage.width(); - const height = stage.height(); + const x = this.manager.stage.x(); + const y = this.manager.stage.y(); + const width = this.manager.stage.width(); + const height = this.manager.stage.height(); const stageRect = { x1: 0, y1: 0, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/bbox.ts similarity index 88% rename from invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/bbox.ts index f6e20708e35..f87689afffe 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/bbox.ts @@ -2,19 +2,19 @@ import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMult import { PREVIEW_GENERATION_BBOX_DUMMY_RECT, PREVIEW_GENERATION_BBOX_GROUP, - PREVIEW_GENERATION_BBOX_TRANSFORMER + PREVIEW_GENERATION_BBOX_TRANSFORMER, } from 'features/controlLayers/konva/naming'; -import type { CanvasV2State } from 'features/controlLayers/store/types'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; import { atom } from 'nanostores'; import { assert } from 'tsafe'; - export class CanvasBbox { group: Konva.Group; rect: Konva.Rect; transformer: Konva.Transformer; + manager: KonvaNodeManager; ALL_ANCHORS: string[] = [ 'top-left', @@ -29,17 +29,11 @@ export class CanvasBbox { CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right']; NO_ANCHORS: string[] = []; - constructor( - getBbox: () => IRect, - onBboxTransformed: (bbox: IRect) => void, - getShiftKey: () => boolean, - getCtrlKey: () => boolean, - getMetaKey: () => boolean, - getAltKey: () => boolean - ) { + constructor(manager: KonvaNodeManager) { + this.manager = manager; // Create a stash to hold onto the last aspect ratio of the bbox - this allows for locking the aspect ratio when // transforming the bbox. - const bbox = getBbox(); + const bbox = this.manager.stateApi.getBbox(); const $aspectRatioBuffer = atom(bbox.width / bbox.height); // Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully @@ -50,11 +44,11 @@ export class CanvasBbox { listening: false, strokeEnabled: false, draggable: true, - ...getBbox(), + ...this.manager.stateApi.getBbox(), }); this.rect.on('dragmove', () => { - const gridSize = getCtrlKey() || getMetaKey() ? 8 : 64; - const oldBbox = getBbox(); + const gridSize = this.manager.stateApi.getCtrlKey() || this.manager.stateApi.getMetaKey() ? 8 : 64; + const oldBbox = this.manager.stateApi.getBbox(); const newBbox: IRect = { ...oldBbox, x: roundToMultiple(this.rect.x(), gridSize), @@ -62,7 +56,7 @@ export class CanvasBbox { }; this.rect.setAttrs(newBbox); if (oldBbox.x !== newBbox.x || oldBbox.y !== newBbox.y) { - onBboxTransformed(newBbox); + this.manager.stateApi.onBboxTransformed(newBbox); } }); @@ -104,7 +98,7 @@ export class CanvasBbox { assert(stage, 'Stage must exist'); // We need to snap the anchors to the grid. If the user is holding ctrl/meta, we use the finer 8px grid. - const gridSize = getCtrlKey() || getMetaKey() ? 8 : 64; + const gridSize = this.manager.stateApi.getCtrlKey() || this.manager.stateApi.getMetaKey() ? 8 : 64; // Because we are working in absolute coordinates, we need to scale the grid size by the stage scale. const scaledGridSize = gridSize * stage.scaleX(); // To snap the anchor to the grid, we need to calculate an offset from the stage's absolute position. @@ -129,10 +123,10 @@ export class CanvasBbox { return; } - const alt = getAltKey(); - const ctrl = getCtrlKey(); - const meta = getMetaKey(); - const shift = getShiftKey(); + const alt = this.manager.stateApi.getAltKey(); + const ctrl = this.manager.stateApi.getCtrlKey(); + const meta = this.manager.stateApi.getMetaKey(); + const shift = this.manager.stateApi.getShiftKey(); // Grid size depends on the modifier keys let gridSize = ctrl || meta ? 8 : 64; @@ -141,7 +135,7 @@ export class CanvasBbox { // new dimensions so that each size scales in the correct increments and doesn't mis-place the bbox. For example, if // we snapped the width and height to 8px increments, the bbox would be mis-placed by 4px in the x and y axes. // Doubling the grid size ensures the bbox's coords remain aligned to the 8px/64px grid. - if (getAltKey()) { + if (this.manager.stateApi.getAltKey()) { gridSize = gridSize * 2; } @@ -196,7 +190,7 @@ export class CanvasBbox { this.rect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1 }); // Update the bbox in internal state. - onBboxTransformed(bbox); + this.manager.stateApi.onBboxTransformed(bbox); // Update the aspect ratio buffer whenever the shift key is not held - this allows for a nice UX where you can start // a transform, get the right aspect ratio, then hold shift to lock it in. @@ -217,7 +211,10 @@ export class CanvasBbox { this.group.add(this.transformer); } - render(bbox: CanvasV2State['bbox'], toolState: CanvasV2State['tool']) { + render() { + const bbox = this.manager.stateApi.getBbox(); + const toolState = this.manager.stateApi.getToolState(); + this.group.listening(toolState.selected === 'bbox'); this.rect.setAttrs({ x: bbox.x, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/konva/controlAdapters.ts similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/controlAdapters.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/documentSizeOverlay.ts b/invokeai/frontend/web/src/features/controlLayers/konva/documentSizeOverlay.ts similarity index 66% rename from invokeai/frontend/web/src/features/controlLayers/konva/renderers/documentSizeOverlay.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/documentSizeOverlay.ts index abf87c485b7..7259d2d6cb9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/documentSizeOverlay.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/documentSizeOverlay.ts @@ -1,6 +1,6 @@ import { getArbitraryBaseColor } from '@invoke-ai/ui-library'; import { DOCUMENT_FIT_PADDING_PX } from 'features/controlLayers/konva/constants'; -import type { CanvasV2State, StageAttrs } from 'features/controlLayers/store/types'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import Konva from 'konva'; export class CanvasDocumentSizeOverlay { @@ -8,8 +8,10 @@ export class CanvasDocumentSizeOverlay { outerRect: Konva.Rect; innerRect: Konva.Rect; padding: number; + manager: KonvaNodeManager; - constructor(padding?: number) { + constructor(manager: KonvaNodeManager, padding?: number) { + this.manager = manager; this.padding = padding ?? DOCUMENT_FIT_PADDING_PX; this.group = new Konva.Group({ id: 'document_overlay_group', listening: false }); this.outerRect = new Konva.Rect({ @@ -28,14 +30,15 @@ export class CanvasDocumentSizeOverlay { this.group.add(this.innerRect); } - render(stage: Konva.Stage, document: CanvasV2State['document']) { + render() { + const document = this.manager.stateApi.getDocument(); this.group.zIndex(0); - const x = stage.x(); - const y = stage.y(); - const width = stage.width(); - const height = stage.height(); - const scale = stage.scaleX(); + const x = this.manager.stage.x(); + const y = this.manager.stage.y(); + const width = this.manager.stage.width(); + const height = this.manager.stage.height(); + const scale = this.manager.stage.scaleX(); this.outerRect.setAttrs({ offsetX: x / scale, @@ -52,16 +55,18 @@ export class CanvasDocumentSizeOverlay { }); } - fitToStage(stage: Konva.Stage, document: CanvasV2State['document'], setStageAttrs: (attrs: StageAttrs) => void) { + fitToStage() { + const document = this.manager.stateApi.getDocument(); + // Fit & center the document on the stage - const width = stage.width(); - const height = stage.height(); + const width = this.manager.stage.width(); + const height = this.manager.stage.height(); const docWidthWithBuffer = document.width + this.padding * 2; const docHeightWithBuffer = document.height + this.padding * 2; const scale = Math.min(Math.min(width / docWidthWithBuffer, height / docHeightWithBuffer), 1); const x = (width - docWidthWithBuffer * scale) / 2 + this.padding * scale; const y = (height - docHeightWithBuffer * scale) / 2 + this.padding * scale; - stage.setAttrs({ x, y, width, height, scaleX: scale, scaleY: scale }); - setStageAttrs({ x, y, width, height, scale }); + this.manager.stage.setAttrs({ x, y, width, height, scaleX: scale, scaleY: scale }); + this.manager.stateApi.setStageAttrs({ x, y, width, height, scale }); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/entityBbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts similarity index 99% rename from invokeai/frontend/web/src/features/controlLayers/konva/renderers/entityBbox.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts index 1669df159ab..f0bb69bb328 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/entityBbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts @@ -5,7 +5,7 @@ import { RASTER_LAYER_OBJECT_GROUP_NAME, RG_LAYER_OBJECT_GROUP_NAME, } from 'features/controlLayers/konva/naming'; -import { createBboxRect } from 'features/controlLayers/konva/renderers/objects'; +import { createBboxRect } from 'features/controlLayers/konva/objects'; import { imageDataToDataURL } from 'features/controlLayers/konva/util'; import type { BboxChangedArg, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 938743a7354..c7c74e6105f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -128,7 +128,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) = //#region mouseenter stage.on('mouseenter', () => { - manager.renderToolPreview(); + manager.preview.tool.render(); }); //#region mousedown @@ -249,7 +249,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) = setLastAddedPoint(pos); } } - manager.renderToolPreview(); + manager.preview.tool.render(); }); //#region mouseup @@ -288,7 +288,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) = setLastMouseDownPos(null); } - manager.renderToolPreview(); + manager.preview.tool.render(); }); //#region mousemove @@ -394,7 +394,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) = } } } - manager.renderToolPreview(); + manager.preview.tool.render(); }); //#region mouseleave @@ -423,7 +423,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) = } } - manager.renderToolPreview(); + manager.preview.tool.render(); }); //#region wheel @@ -464,11 +464,11 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) = stage.scaleY(newScale); stage.position(newPos); setStageAttrs({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale }); - manager.renderBackground(); - manager.renderDocumentSizeOverlay(); + manager.preview.tool.render(); + manager.preview.documentSizeOverlay.render(); } } - manager.renderToolPreview(); + manager.preview.tool.render(); }); //#region dragmove @@ -480,9 +480,9 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) = height: stage.height(), scale: stage.scaleX(), }); - manager.renderBackground(); - manager.renderDocumentSizeOverlay(); - manager.renderToolPreview(); + manager.preview.tool.render(); + manager.preview.documentSizeOverlay.render(); + manager.preview.tool.render(); }); //#region dragend @@ -495,7 +495,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) = height: stage.height(), scale: stage.scaleX(), }); - manager.renderToolPreview(); + manager.preview.tool.render(); }); //#region key @@ -520,11 +520,12 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) = } else if (e.key === 'r') { setLastCursorPos(null); setLastMouseDownPos(null); - manager.fitDocument(); - manager.renderBackground(); - manager.renderDocumentSizeOverlay(); + manager.preview.documentSizeOverlay.fitToStage(); + manager.preview.tool.render(); + + manager.preview.documentSizeOverlay.render(); } - manager.renderToolPreview(); + manager.preview.tool.render(); }; window.addEventListener('keydown', onKeyDown); @@ -542,7 +543,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) = setToolBuffer(null); setSpaceKey(false); } - manager.renderToolPreview(); + manager.preview.tool.render(); }; window.addEventListener('keyup', onKeyUp); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/inpaintMask.ts similarity index 95% rename from invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/inpaintMask.ts index 06d95993a3a..4c7257879dd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/inpaintMask.ts @@ -1,8 +1,8 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; -import { getObjectGroupId } from 'features/controlLayers/konva/naming'; +import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox'; +import { getObjectGroupId,INPAINT_MASK_LAYER_ID } from 'features/controlLayers/konva/naming'; import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; -import { getNodeBboxFast } from 'features/controlLayers/konva/renderers/entityBbox'; -import { KonvaBrushLine, KonvaEraserLine, KonvaRect } from 'features/controlLayers/konva/renderers/objects'; +import { KonvaBrushLine, KonvaEraserLine, KonvaRect } from 'features/controlLayers/konva/objects'; import { mapId } from 'features/controlLayers/konva/util'; import { type InpaintMaskEntity, isDrawingTool } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -19,10 +19,10 @@ export class CanvasInpaintMask { transformer: Konva.Transformer; objects: Map; - constructor(entity: InpaintMaskEntity, manager: KonvaNodeManager) { - this.id = entity.id; + constructor(manager: KonvaNodeManager) { + this.id = INPAINT_MASK_LAYER_ID; this.manager = manager; - this.layer = new Konva.Layer({ id: entity.id }); + this.layer = new Konva.Layer({ id: INPAINT_MASK_LAYER_ID }); this.group = new Konva.Group({ id: getObjectGroupId(this.layer.id(), uuidv4()), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/layers.ts similarity index 99% rename from invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/layers.ts index 1935be13710..c902f248025 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/layers.ts @@ -1,6 +1,6 @@ import { getObjectGroupId } from 'features/controlLayers/konva/naming'; import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; -import { KonvaBrushLine, KonvaEraserLine, KonvaImage, KonvaRect } from 'features/controlLayers/konva/renderers/objects'; +import { KonvaBrushLine, KonvaEraserLine, KonvaImage, KonvaRect } from 'features/controlLayers/konva/objects'; import { mapId } from 'features/controlLayers/konva/util'; import { isDrawingTool, type LayerEntity } from 'features/controlLayers/store/types'; import Konva from 'konva'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts index 0d35ed86317..c96464092c6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts @@ -39,11 +39,11 @@ export const RASTER_LAYER_ERASER_LINE_NAME = `${RASTER_LAYER_NAME}.eraser_line`; export const RASTER_LAYER_RECT_SHAPE_NAME = `${RASTER_LAYER_NAME}.rect_shape`; export const RASTER_LAYER_IMAGE_NAME = `${RASTER_LAYER_NAME}.image`; -export const INPAINT_MASK_LAYER_NAME = 'inpaint_mask_layer'; -export const INPAINT_MASK_LAYER_OBJECT_GROUP_NAME = `${INPAINT_MASK_LAYER_NAME}.object_group`; -export const INPAINT_MASK_LAYER_BRUSH_LINE_NAME = `${INPAINT_MASK_LAYER_NAME}.brush_line`; -export const INPAINT_MASK_LAYER_ERASER_LINE_NAME = `${INPAINT_MASK_LAYER_NAME}.eraser_line`; -export const INPAINT_MASK_LAYER_RECT_SHAPE_NAME = `${INPAINT_MASK_LAYER_NAME}.rect_shape`; +export const INPAINT_MASK_LAYER_ID = 'inpaint_mask_layer'; +export const INPAINT_MASK_LAYER_OBJECT_GROUP_NAME = `${INPAINT_MASK_LAYER_ID}.object_group`; +export const INPAINT_MASK_LAYER_BRUSH_LINE_NAME = `${INPAINT_MASK_LAYER_ID}.brush_line`; +export const INPAINT_MASK_LAYER_ERASER_LINE_NAME = `${INPAINT_MASK_LAYER_ID}.eraser_line`; +export const INPAINT_MASK_LAYER_RECT_SHAPE_NAME = `${INPAINT_MASK_LAYER_ID}.rect_shape`; export const BACKGROUND_LAYER_ID = 'background_layer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts index de5c372498f..cdfde891f29 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts @@ -1,89 +1,28 @@ +import type { Store } from '@reduxjs/toolkit'; +import type { RootState } from 'app/store/store'; import { getImageDataTransparency } from 'common/util/arrayBuffer'; -import { CanvasBackground } from 'features/controlLayers/konva/renderers/background'; -import { CanvasPreview } from 'features/controlLayers/konva/renderers/preview'; +import { CanvasBackground } from 'features/controlLayers/konva/background'; +import { setStageEventHandlers } from 'features/controlLayers/konva/events'; +import { CanvasPreview } from 'features/controlLayers/konva/preview'; import { konvaNodeToBlob, konvaNodeToImageData, previewBlob } from 'features/controlLayers/konva/util'; -import type { - BrushLineAddedArg, - CanvasEntity, - CanvasV2State, - EraserLineAddedArg, - GenerationMode, - PointAddedToLineArg, - PosChangedArg, - Rect, - RectShapeAddedArg, - RgbaColor, - ScaleChangedArg, - StageAttrs, - Tool, -} from 'features/controlLayers/store/types'; +import { $lastProgressEvent, $shouldShowStagedImage } from 'features/controlLayers/store/canvasV2Slice'; +import type { CanvasV2State, GenerationMode, Rect } from 'features/controlLayers/store/types'; import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers'; import type Konva from 'konva'; -import type { Vector2d } from 'konva/lib/types'; import { atom } from 'nanostores'; import { getImageDTO as defaultGetImageDTO, uploadImage as defaultUploadImage } from 'services/api/endpoints/images'; import type { ImageCategory, ImageDTO } from 'services/api/types'; -import type { InvocationDenoiseProgressEvent } from 'services/events/types'; import { assert } from 'tsafe'; -import { CanvasBbox } from './renderers/bbox'; -import { CanvasControlAdapter } from './renderers/controlAdapters'; -import { CanvasDocumentSizeOverlay } from './renderers/documentSizeOverlay'; -import { CanvasInpaintMask } from './renderers/inpaintMask'; -import { CanvasLayer } from './renderers/layers'; -import { CanvasRegion } from './renderers/regions'; -import { CanvasStagingArea } from './renderers/stagingArea'; -import { CanvasTool } from './renderers/tool'; - -export type StateApi = { - getToolState: () => CanvasV2State['tool']; - getCurrentFill: () => RgbaColor; - setTool: (tool: Tool) => void; - setToolBuffer: (tool: Tool | null) => void; - getIsDrawing: () => boolean; - setIsDrawing: (isDrawing: boolean) => void; - getIsMouseDown: () => boolean; - setIsMouseDown: (isMouseDown: boolean) => void; - getLastMouseDownPos: () => Vector2d | null; - setLastMouseDownPos: (pos: Vector2d | null) => void; - getLastCursorPos: () => Vector2d | null; - setLastCursorPos: (pos: Vector2d | null) => void; - getLastAddedPoint: () => Vector2d | null; - setLastAddedPoint: (pos: Vector2d | null) => void; - setStageAttrs: (attrs: StageAttrs) => void; - getSelectedEntity: () => CanvasEntity | null; - getSpaceKey: () => boolean; - setSpaceKey: (val: boolean) => void; - getShouldShowStagedImage: () => boolean; - getBbox: () => CanvasV2State['bbox']; - getSettings: () => CanvasV2State['settings']; - onBrushLineAdded: (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => void; - onEraserLineAdded: (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => void; - onPointAddedToLine: (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => void; - onRectShapeAdded: (arg: RectShapeAddedArg, entityType: CanvasEntity['type']) => void; - onBrushWidthChanged: (size: number) => void; - onEraserWidthChanged: (size: number) => void; - getMaskOpacity: () => number; - getIsSelected: (id: string) => boolean; - onScaleChanged: (arg: ScaleChangedArg, entityType: CanvasEntity['type']) => void; - onPosChanged: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void; - onBboxTransformed: (bbox: Rect) => void; - getShiftKey: () => boolean; - getCtrlKey: () => boolean; - getMetaKey: () => boolean; - getAltKey: () => boolean; - getDocument: () => CanvasV2State['document']; - getLayersState: () => CanvasV2State['layers']; - getControlAdaptersState: () => CanvasV2State['controlAdapters']; - getRegionsState: () => CanvasV2State['regions']; - getInpaintMaskState: () => CanvasV2State['inpaintMask']; - getStagingAreaState: () => CanvasV2State['stagingArea']; - getLastProgressEvent: () => InvocationDenoiseProgressEvent | null; - resetLastProgressEvent: () => void; - onInpaintMaskImageCached: (imageDTO: ImageDTO) => void; - onRegionMaskImageCached: (id: string, imageDTO: ImageDTO) => void; - onLayerImageCached: (imageDTO: ImageDTO) => void; -}; +import { CanvasBbox } from './bbox'; +import { CanvasControlAdapter } from './controlAdapters'; +import { CanvasDocumentSizeOverlay } from './documentSizeOverlay'; +import { CanvasInpaintMask } from './inpaintMask'; +import { CanvasLayer } from './layers'; +import { CanvasRegion } from './regions'; +import { CanvasStagingArea } from './stagingArea'; +import { StateApi } from './stateApi'; +import { CanvasTool } from './tool'; type Util = { getImageDTO: (imageName: string) => Promise; @@ -116,41 +55,44 @@ export class KonvaNodeManager { stateApi: StateApi; preview: CanvasPreview; background: CanvasBackground; + private store: Store; + private isFirstRender: boolean; + private prevState: CanvasV2State; + private log: (message: string) => void; constructor( stage: Konva.Stage, container: HTMLDivElement, - stateApi: StateApi, + store: Store, + log: (message: string) => void, getImageDTO: Util['getImageDTO'] = defaultGetImageDTO, uploadImage: Util['uploadImage'] = defaultUploadImage ) { + this.log = log; this.stage = stage; this.container = container; - this.stateApi = stateApi; + this.store = store; + this.stateApi = new StateApi(this.store, this.log); + this.prevState = this.stateApi.getState(); + this.isFirstRender = true; + this.util = { getImageDTO, uploadImage, }; this.preview = new CanvasPreview( - new CanvasBbox( - this.stateApi.getBbox, - this.stateApi.onBboxTransformed, - this.stateApi.getShiftKey, - this.stateApi.getCtrlKey, - this.stateApi.getMetaKey, - this.stateApi.getAltKey - ), - new CanvasTool(), - new CanvasDocumentSizeOverlay(), - new CanvasStagingArea() + new CanvasBbox(this), + new CanvasTool(this), + new CanvasDocumentSizeOverlay(this), + new CanvasStagingArea(this) ); this.stage.add(this.preview.layer); - this.background = new CanvasBackground(); + this.background = new CanvasBackground(this); this.stage.add(this.background.layer); - this.inpaintMask = new CanvasInpaintMask(this.stateApi.getInpaintMaskState(), this); + this.inpaintMask = new CanvasInpaintMask(this); this.stage.add(this.inpaintMask.layer); this.layers = new Map(); @@ -247,46 +189,6 @@ export class KonvaNodeManager { this.preview.layer.zIndex(++zIndex); } - renderDocumentSizeOverlay() { - this.preview.documentSizeOverlay.render(this.stage, this.stateApi.getDocument()); - } - - renderBbox() { - this.preview.bbox.render(this.stateApi.getBbox(), this.stateApi.getToolState()); - } - - renderToolPreview() { - this.preview.tool.render( - this.stage, - 1, // TODO(psyche): this should be renderable entity count - this.stateApi.getToolState(), - this.stateApi.getCurrentFill(), - this.stateApi.getSelectedEntity(), - this.stateApi.getLastCursorPos(), - this.stateApi.getLastMouseDownPos(), - this.stateApi.getIsDrawing(), - this.stateApi.getIsMouseDown() - ); - } - - renderBackground() { - this.background.renderBackground(this.stage); - } - - renderStagingArea() { - this.preview.stagingArea.render( - this.stateApi.getStagingAreaState(), - this.stateApi.getBbox(), - this.stateApi.getShouldShowStagedImage(), - this.stateApi.getLastProgressEvent(), - this.stateApi.resetLastProgressEvent - ); - } - - fitDocument() { - this.preview.documentSizeOverlay.fitToStage(this.stage, this.stateApi.getDocument(), this.stateApi.setStageAttrs); - } - fitStageToContainer() { this.stage.width(this.container.offsetWidth); this.stage.height(this.container.offsetHeight); @@ -297,10 +199,150 @@ export class KonvaNodeManager { height: this.stage.height(), scale: this.stage.scaleX(), }); - this.renderBackground(); - this.renderDocumentSizeOverlay(); + this.background.renderBackground(); + this.preview.documentSizeOverlay.render(); } + render = async () => { + const state = this.stateApi.getState(); + + if (this.prevState === state && !this.isFirstRender) { + this.log('No changes detected, skipping render'); + return; + } + + if ( + this.isFirstRender || + state.layers.entities !== this.prevState.layers.entities || + state.tool.selected !== this.prevState.tool.selected || + state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id + ) { + this.log('Rendering layers'); + this.renderLayers(); + } + + if ( + this.isFirstRender || + state.regions.entities !== this.prevState.regions.entities || + state.settings.maskOpacity !== this.prevState.settings.maskOpacity || + state.tool.selected !== this.prevState.tool.selected || + state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id + ) { + this.log('Rendering regions'); + this.renderRegions(); + } + + if ( + this.isFirstRender || + state.inpaintMask !== this.prevState.inpaintMask || + state.settings.maskOpacity !== this.prevState.settings.maskOpacity || + state.tool.selected !== this.prevState.tool.selected || + state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id + ) { + this.log('Rendering inpaint mask'); + this.renderInpaintMask(); + } + + if ( + this.isFirstRender || + state.controlAdapters.entities !== this.prevState.controlAdapters.entities || + state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id + ) { + this.log('Rendering control adapters'); + this.renderControlAdapters(); + } + + if (this.isFirstRender || state.document !== this.prevState.document) { + this.log('Rendering document bounds overlay'); + this.preview.documentSizeOverlay.render(); + } + + if ( + this.isFirstRender || + state.bbox !== this.prevState.bbox || + state.tool.selected !== this.prevState.tool.selected + ) { + this.log('Rendering generation bbox'); + this.preview.bbox.render(); + } + + if ( + this.isFirstRender || + state.layers !== this.prevState.layers || + state.controlAdapters !== this.prevState.controlAdapters || + state.regions !== this.prevState.regions + ) { + // this.log('Updating entity bboxes'); + // debouncedUpdateBboxes(stage, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions, onBboxChanged); + } + + if (this.isFirstRender || state.stagingArea !== this.prevState.stagingArea) { + this.log('Rendering staging area'); + this.preview.stagingArea.render(); + } + + if ( + this.isFirstRender || + state.layers.entities !== this.prevState.layers.entities || + state.controlAdapters.entities !== this.prevState.controlAdapters.entities || + state.regions.entities !== this.prevState.regions.entities || + state.inpaintMask !== this.prevState.inpaintMask || + state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id + ) { + this.log('Arranging entities'); + this.arrangeEntities(); + } + + this.prevState = state; + + if (this.isFirstRender) { + this.isFirstRender = false; + } + }; + + initialize = () => { + this.log('Initializing renderer'); + this.stage.container(this.container); + + const cleanupListeners = setStageEventHandlers(this); + + // We can use a resize observer to ensure the stage always fits the container. We also need to re-render the bg and + // document bounds overlay when the stage is resized. + const resizeObserver = new ResizeObserver(this.fitStageToContainer.bind(this)); + resizeObserver.observe(this.container); + this.fitStageToContainer(); + + const unsubscribeRenderer = this.store.subscribe(this.render); + + // When we this flag, we need to render the staging area + $shouldShowStagedImage.subscribe((shouldShowStagedImage, prevShouldShowStagedImage) => { + this.log('Rendering staging area'); + if (shouldShowStagedImage !== prevShouldShowStagedImage) { + this.preview.stagingArea.render(); + } + }); + + $lastProgressEvent.subscribe(() => { + this.log('Rendering staging area'); + this.preview.stagingArea.render(); + }); + + this.log('First render of konva stage'); + // On first render, the document should be fit to the stage. + this.preview.documentSizeOverlay.render(); + this.preview.documentSizeOverlay.fitToStage(); + this.preview.tool.render(); + this.render(); + + return () => { + this.log('Cleaning up konva renderer'); + unsubscribeRenderer(); + cleanupListeners(); + $shouldShowStagedImage.off(); + resizeObserver.disconnect(); + }; + }; + getInpaintMaskLayerClone(): Konva.Layer { const layerClone = this.inpaintMask.layer.clone(); const objectGroupClone = this.inpaintMask.group.clone(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts b/invokeai/frontend/web/src/features/controlLayers/konva/objects.ts similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/objects.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/preview.ts similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/preview.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts b/invokeai/frontend/web/src/features/controlLayers/konva/regions.ts similarity index 97% rename from invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/regions.ts index 0f292dfacae..2d5ed1dd076 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/regions.ts @@ -1,13 +1,10 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { getObjectGroupId } from 'features/controlLayers/konva/naming'; import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; -import { getNodeBboxFast } from 'features/controlLayers/konva/renderers/entityBbox'; -import { KonvaBrushLine, KonvaEraserLine, KonvaRect } from 'features/controlLayers/konva/renderers/objects'; +import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox'; +import { KonvaBrushLine, KonvaEraserLine, KonvaRect } from 'features/controlLayers/konva/objects'; import { mapId } from 'features/controlLayers/konva/util'; -import { - isDrawingTool, - type RegionEntity, -} from 'features/controlLayers/store/types'; +import { isDrawingTool, type RegionEntity } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts deleted file mode 100644 index 997d564c880..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ /dev/null @@ -1,499 +0,0 @@ -import { $alt, $ctrl, $meta, $shift } from '@invoke-ai/ui-library'; -import type { Store } from '@reduxjs/toolkit'; -import { logger } from 'app/logging/logger'; -import { $isDebugging } from 'app/store/nanostores/isDebugging'; -import type { RootState } from 'app/store/store'; -import { setStageEventHandlers } from 'features/controlLayers/konva/events'; -import { KonvaNodeManager, setNodeManager } from 'features/controlLayers/konva/nodeManager'; -import { updateBboxes } from 'features/controlLayers/konva/renderers/entityBbox'; -import { - $lastProgressEvent, - $shouldShowStagedImage, - $stageAttrs, - bboxChanged, - brushWidthChanged, - caBboxChanged, - caTranslated, - eraserWidthChanged, - imBboxChanged, - imBrushLineAdded, - imEraserLineAdded, - imImageCacheChanged, - imLinePointAdded, - imRectAdded, - imScaled, - imTranslated, - layerBboxChanged, - layerBrushLineAdded, - layerEraserLineAdded, - layerImageCacheChanged, - layerLinePointAdded, - layerRectAdded, - layerScaled, - layerTranslated, - rgBboxChanged, - rgBrushLineAdded, - rgEraserLineAdded, - rgImageCacheChanged, - rgLinePointAdded, - rgRectAdded, - rgScaled, - rgTranslated, - toolBufferChanged, - toolChanged, -} from 'features/controlLayers/store/canvasV2Slice'; -import type { - BboxChangedArg, - BrushLineAddedArg, - CanvasEntity, - CanvasV2State, - EraserLineAddedArg, - PointAddedToLineArg, - PosChangedArg, - RectShapeAddedArg, - ScaleChangedArg, - Tool, -} from 'features/controlLayers/store/types'; -import type Konva from 'konva'; -import type { IRect, Vector2d } from 'konva/lib/types'; -import { debounce } from 'lodash-es'; -import type { RgbaColor } from 'react-colorful'; -import type { ImageDTO } from 'services/api/types'; - -/** - * Initializes the canvas renderer. It subscribes to the redux store and listens for changes directly, bypassing the - * react rendering cycle entirely, improving canvas performance. - * @param store The redux store - * @param stage The konva stage - * @param container The stage's target container element - * @returns A cleanup function - */ -export const initializeRenderer = ( - store: Store, - stage: Konva.Stage, - container: HTMLDivElement | null -): (() => void) => { - const _log = logger('konva'); - /** - * Logs a message to the console if debugging is enabled. - */ - const logIfDebugging = (message: string) => { - if ($isDebugging.get()) { - _log.debug(message); - } - }; - - logIfDebugging('Initializing renderer'); - if (!container) { - // Nothing to clean up - logIfDebugging('No stage container, skipping initialization'); - return () => {}; - } - - stage.container(container); - - // Set up callbacks for various events - const onPosChanged = (arg: PosChangedArg, entityType: CanvasEntity['type']) => { - logIfDebugging('onPosChanged'); - if (entityType === 'layer') { - dispatch(layerTranslated(arg)); - } else if (entityType === 'control_adapter') { - dispatch(caTranslated(arg)); - } else if (entityType === 'regional_guidance') { - dispatch(rgTranslated(arg)); - } else if (entityType === 'inpaint_mask') { - dispatch(imTranslated(arg)); - } - }; - const onScaleChanged = (arg: ScaleChangedArg, entityType: CanvasEntity['type']) => { - logIfDebugging('onScaleChanged'); - if (entityType === 'layer') { - dispatch(layerScaled(arg)); - } else if (entityType === 'inpaint_mask') { - dispatch(imScaled(arg)); - } else if (entityType === 'regional_guidance') { - dispatch(rgScaled(arg)); - } - }; - const onBboxChanged = (arg: BboxChangedArg, entityType: CanvasEntity['type']) => { - logIfDebugging('Entity bbox changed'); - if (entityType === 'layer') { - dispatch(layerBboxChanged(arg)); - } else if (entityType === 'control_adapter') { - dispatch(caBboxChanged(arg)); - } else if (entityType === 'regional_guidance') { - dispatch(rgBboxChanged(arg)); - } else if (entityType === 'inpaint_mask') { - dispatch(imBboxChanged(arg)); - } - }; - const onBrushLineAdded = (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => { - logIfDebugging('Brush line added'); - if (entityType === 'layer') { - dispatch(layerBrushLineAdded(arg)); - } else if (entityType === 'regional_guidance') { - dispatch(rgBrushLineAdded(arg)); - } else if (entityType === 'inpaint_mask') { - dispatch(imBrushLineAdded(arg)); - } - }; - const onEraserLineAdded = (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => { - logIfDebugging('Eraser line added'); - if (entityType === 'layer') { - dispatch(layerEraserLineAdded(arg)); - } else if (entityType === 'regional_guidance') { - dispatch(rgEraserLineAdded(arg)); - } else if (entityType === 'inpaint_mask') { - dispatch(imEraserLineAdded(arg)); - } - }; - const onPointAddedToLine = (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => { - logIfDebugging('Point added to line'); - if (entityType === 'layer') { - dispatch(layerLinePointAdded(arg)); - } else if (entityType === 'regional_guidance') { - dispatch(rgLinePointAdded(arg)); - } else if (entityType === 'inpaint_mask') { - dispatch(imLinePointAdded(arg)); - } - }; - const onRectShapeAdded = (arg: RectShapeAddedArg, entityType: CanvasEntity['type']) => { - logIfDebugging('Rect shape added'); - if (entityType === 'layer') { - dispatch(layerRectAdded(arg)); - } else if (entityType === 'regional_guidance') { - dispatch(rgRectAdded(arg)); - } else if (entityType === 'inpaint_mask') { - dispatch(imRectAdded(arg)); - } - }; - const onBboxTransformed = (bbox: IRect) => { - logIfDebugging('Generation bbox transformed'); - dispatch(bboxChanged(bbox)); - }; - const onBrushWidthChanged = (width: number) => { - logIfDebugging('Brush width changed'); - dispatch(brushWidthChanged(width)); - }; - const onEraserWidthChanged = (width: number) => { - logIfDebugging('Eraser width changed'); - dispatch(eraserWidthChanged(width)); - }; - const onRegionMaskImageCached = (id: string, imageDTO: ImageDTO) => { - logIfDebugging('Region mask image cached'); - dispatch(rgImageCacheChanged({ id, imageDTO })); - }; - const onInpaintMaskImageCached = (imageDTO: ImageDTO) => { - logIfDebugging('Inpaint mask image cached'); - dispatch(imImageCacheChanged({ imageDTO })); - }; - const onLayerImageCached = (imageDTO: ImageDTO) => { - logIfDebugging('Layer image cached'); - dispatch(layerImageCacheChanged({ imageDTO })); - }; - - const setTool = (tool: Tool) => { - logIfDebugging('Tool selection changed'); - dispatch(toolChanged(tool)); - }; - const setToolBuffer = (toolBuffer: Tool | null) => { - logIfDebugging('Tool buffer changed'); - dispatch(toolBufferChanged(toolBuffer)); - }; - - const selectSelectedEntity = (canvasV2: CanvasV2State): CanvasEntity | null => { - const identifier = canvasV2.selectedEntityIdentifier; - let selectedEntity: CanvasEntity | null = null; - if (!identifier) { - selectedEntity = null; - } else if (identifier.type === 'layer') { - selectedEntity = canvasV2.layers.entities.find((i) => i.id === identifier.id) ?? null; - } else if (identifier.type === 'control_adapter') { - selectedEntity = canvasV2.controlAdapters.entities.find((i) => i.id === identifier.id) ?? null; - } else if (identifier.type === 'ip_adapter') { - selectedEntity = canvasV2.ipAdapters.entities.find((i) => i.id === identifier.id) ?? null; - } else if (identifier.type === 'regional_guidance') { - selectedEntity = canvasV2.regions.entities.find((i) => i.id === identifier.id) ?? null; - } else if (identifier.type === 'inpaint_mask') { - selectedEntity = canvasV2.inpaintMask; - } else { - selectedEntity = null; - } - return selectedEntity; - }; - - const selectCurrentFill = (canvasV2: CanvasV2State, selectedEntity: CanvasEntity | null) => { - let currentFill: RgbaColor = canvasV2.tool.fill; - if (selectedEntity) { - if (selectedEntity.type === 'regional_guidance') { - currentFill = { ...selectedEntity.fill, a: canvasV2.settings.maskOpacity }; - } else if (selectedEntity.type === 'inpaint_mask') { - currentFill = { ...canvasV2.inpaintMask.fill, a: canvasV2.settings.maskOpacity }; - } - } else { - currentFill = canvasV2.tool.fill; - } - return currentFill; - }; - - const { getState, subscribe, dispatch } = store; - - // On the first render, we need to render everything. - let isFirstRender = true; - - // Stage interaction listeners need helpers to get and update current state. Some of the state is read-only, like - // bbox, document and tool state, while interaction state is read-write. - - // Read-only state, derived from redux - let prevCanvasV2 = getState().canvasV2; - let canvasV2 = getState().canvasV2; - const getSelectedEntity = () => selectSelectedEntity(canvasV2); - const getCurrentFill = () => selectCurrentFill(canvasV2, getSelectedEntity()); - const getBbox = () => canvasV2.bbox; - const getDocument = () => canvasV2.document; - const getToolState = () => canvasV2.tool; - const getSettings = () => canvasV2.settings; - const getRegionsState = () => canvasV2.regions; - const getLayersState = () => canvasV2.layers; - const getControlAdaptersState = () => canvasV2.controlAdapters; - const getInpaintMaskState = () => canvasV2.inpaintMask; - const getMaskOpacity = () => canvasV2.settings.maskOpacity; - const getStagingAreaState = () => canvasV2.stagingArea; - const getIsSelected = (id: string) => getSelectedEntity()?.id === id; - - // Read-only state, derived from nanostores - const resetLastProgressEvent = () => { - $lastProgressEvent.set(null); - }; - // Read-write state, ephemeral interaction state - let isDrawing = false; - const getIsDrawing = () => isDrawing; - const setIsDrawing = (val: boolean) => { - isDrawing = val; - }; - - let isMouseDown = false; - const getIsMouseDown = () => isMouseDown; - const setIsMouseDown = (val: boolean) => { - isMouseDown = val; - }; - - let lastAddedPoint: Vector2d | null = null; - const getLastAddedPoint = () => lastAddedPoint; - const setLastAddedPoint = (val: Vector2d | null) => { - lastAddedPoint = val; - }; - - let lastMouseDownPos: Vector2d | null = null; - const getLastMouseDownPos = () => lastMouseDownPos; - const setLastMouseDownPos = (val: Vector2d | null) => { - lastMouseDownPos = val; - }; - - let lastCursorPos: Vector2d | null = null; - const getLastCursorPos = () => lastCursorPos; - const setLastCursorPos = (val: Vector2d | null) => { - lastCursorPos = val; - }; - - let spaceKey = false; - const getSpaceKey = () => spaceKey; - const setSpaceKey = (val: boolean) => { - spaceKey = val; - }; - - const stateApi: KonvaNodeManager['stateApi'] = { - // Read-only state - getToolState, - getSelectedEntity, - getBbox, - getSettings, - getCurrentFill, - getAltKey: $alt.get, - getCtrlKey: $ctrl.get, - getMetaKey: $meta.get, - getShiftKey: $shift.get, - getControlAdaptersState, - getDocument, - getLayersState, - getRegionsState, - getMaskOpacity, - getInpaintMaskState, - getStagingAreaState, - getShouldShowStagedImage: $shouldShowStagedImage.get, - getLastProgressEvent: $lastProgressEvent.get, - resetLastProgressEvent, - getIsSelected, - - // Read-write state - setTool, - setToolBuffer, - getIsDrawing, - setIsDrawing, - getIsMouseDown, - setIsMouseDown, - getLastAddedPoint, - setLastAddedPoint, - getLastCursorPos, - setLastCursorPos, - getLastMouseDownPos, - setLastMouseDownPos, - getSpaceKey, - setSpaceKey, - setStageAttrs: $stageAttrs.set, - - // Callbacks - onBrushLineAdded, - onEraserLineAdded, - onPointAddedToLine, - onRectShapeAdded, - onBrushWidthChanged, - onEraserWidthChanged, - onPosChanged, - onBboxTransformed, - onRegionMaskImageCached, - onInpaintMaskImageCached, - onLayerImageCached, - onScaleChanged, - }; - - const manager = new KonvaNodeManager(stage, container, stateApi); - setNodeManager(manager); - console.log(manager); - - const cleanupListeners = setStageEventHandlers(manager); - - // Calculating bounding boxes is expensive, must be debounced to not block the UI thread during a user interaction. - // TODO(psyche): Figure out how to do this in a worker. Probably means running the renderer in a worker and sending - // the entire state over when needed. - const debouncedUpdateBboxes = debounce(updateBboxes, 300); - - const renderCanvas = async () => { - canvasV2 = store.getState().canvasV2; - - if (prevCanvasV2 === canvasV2 && !isFirstRender) { - logIfDebugging('No changes detected, skipping render'); - return; - } - - if ( - isFirstRender || - canvasV2.layers.entities !== prevCanvasV2.layers.entities || - canvasV2.tool.selected !== prevCanvasV2.tool.selected || - canvasV2.selectedEntityIdentifier?.id !== prevCanvasV2.selectedEntityIdentifier?.id - ) { - logIfDebugging('Rendering layers'); - manager.renderLayers(); - } - - if ( - isFirstRender || - canvasV2.regions.entities !== prevCanvasV2.regions.entities || - canvasV2.settings.maskOpacity !== prevCanvasV2.settings.maskOpacity || - canvasV2.tool.selected !== prevCanvasV2.tool.selected || - canvasV2.selectedEntityIdentifier?.id !== prevCanvasV2.selectedEntityIdentifier?.id - ) { - logIfDebugging('Rendering regions'); - manager.renderRegions(); - } - - if ( - isFirstRender || - canvasV2.inpaintMask !== prevCanvasV2.inpaintMask || - canvasV2.settings.maskOpacity !== prevCanvasV2.settings.maskOpacity || - canvasV2.tool.selected !== prevCanvasV2.tool.selected || - canvasV2.selectedEntityIdentifier?.id !== prevCanvasV2.selectedEntityIdentifier?.id - ) { - logIfDebugging('Rendering inpaint mask'); - manager.renderInpaintMask(); - } - - if ( - isFirstRender || - canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities || - canvasV2.selectedEntityIdentifier?.id !== prevCanvasV2.selectedEntityIdentifier?.id - ) { - logIfDebugging('Rendering control adapters'); - manager.renderControlAdapters(); - } - - if (isFirstRender || canvasV2.document !== prevCanvasV2.document) { - logIfDebugging('Rendering document bounds overlay'); - manager.renderDocumentSizeOverlay(); - } - - if (isFirstRender || canvasV2.bbox !== prevCanvasV2.bbox || canvasV2.tool.selected !== prevCanvasV2.tool.selected) { - logIfDebugging('Rendering generation bbox'); - manager.renderBbox(); - } - - if ( - isFirstRender || - canvasV2.layers !== prevCanvasV2.layers || - canvasV2.controlAdapters !== prevCanvasV2.controlAdapters || - canvasV2.regions !== prevCanvasV2.regions - ) { - // logIfDebugging('Updating entity bboxes'); - // debouncedUpdateBboxes(stage, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions, onBboxChanged); - } - - if (isFirstRender || canvasV2.stagingArea !== prevCanvasV2.stagingArea) { - logIfDebugging('Rendering staging area'); - manager.renderStagingArea(); - } - - if ( - isFirstRender || - canvasV2.layers.entities !== prevCanvasV2.layers.entities || - canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities || - canvasV2.regions.entities !== prevCanvasV2.regions.entities || - canvasV2.inpaintMask !== prevCanvasV2.inpaintMask || - canvasV2.selectedEntityIdentifier?.id !== prevCanvasV2.selectedEntityIdentifier?.id - ) { - logIfDebugging('Arranging entities'); - manager.arrangeEntities(); - } - - prevCanvasV2 = canvasV2; - - if (isFirstRender) { - isFirstRender = false; - } - }; - - // We can use a resize observer to ensure the stage always fits the container. We also need to re-render the bg and - // document bounds overlay when the stage is resized. - const resizeObserver = new ResizeObserver(manager.fitStageToContainer.bind(manager)); - resizeObserver.observe(container); - manager.fitStageToContainer(); - - const unsubscribeRenderer = subscribe(renderCanvas); - - // When we this flag, we need to render the staging area - $shouldShowStagedImage.subscribe((shouldShowStagedImage, prevShouldShowStagedImage) => { - logIfDebugging('Rendering staging area'); - if (shouldShowStagedImage !== prevShouldShowStagedImage) { - manager.renderStagingArea(); - } - }); - - $lastProgressEvent.subscribe(() => { - logIfDebugging('Rendering staging area'); - manager.renderStagingArea(); - }); - - logIfDebugging('First render of konva stage'); - // On first render, the document should be fit to the stage. - manager.renderDocumentSizeOverlay(); - manager.fitDocument(); - manager.renderToolPreview(); - renderCanvas(); - - return () => { - logIfDebugging('Cleaning up konva renderer'); - unsubscribeRenderer(); - cleanupListeners(); - $shouldShowStagedImage.off(); - resizeObserver.disconnect(); - }; -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/stagingArea.ts similarity index 83% rename from invokeai/frontend/web/src/features/controlLayers/konva/renderers/stagingArea.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/stagingArea.ts index 191d650904d..02b1a50fdb6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/stagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/stagingArea.ts @@ -1,29 +1,29 @@ -import { KonvaImage, KonvaProgressImage } from 'features/controlLayers/konva/renderers/objects'; -import type { CanvasV2State } from 'features/controlLayers/store/types'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import { KonvaImage, KonvaProgressImage } from 'features/controlLayers/konva/objects'; import Konva from 'konva'; import type { ImageDTO } from 'services/api/types'; -import type { InvocationDenoiseProgressEvent } from 'services/events/types'; export class CanvasStagingArea { group: Konva.Group; image: KonvaImage | null; progressImage: KonvaProgressImage | null; imageDTO: ImageDTO | null; + manager: KonvaNodeManager; - constructor() { + constructor(manager: KonvaNodeManager) { + this.manager = manager; this.group = new Konva.Group({ listening: false }); this.image = null; this.progressImage = null; this.imageDTO = null; } - async render( - stagingArea: CanvasV2State['stagingArea'], - bbox: CanvasV2State['bbox'], - shouldShowStagedImage: boolean, - lastProgressEvent: InvocationDenoiseProgressEvent | null, - resetLastProgressEvent: () => void - ) { + async render() { + const stagingArea = this.manager.stateApi.getStagingAreaState(); + const bbox = this.manager.stateApi.getBbox(); + const shouldShowStagedImage = this.manager.stateApi.getShouldShowStagedImage(); + const lastProgressEvent = this.manager.stateApi.getLastProgressEvent(); + this.imageDTO = stagingArea.images[stagingArea.selectedImageIndex] ?? null; if (this.imageDTO) { @@ -58,7 +58,7 @@ export class CanvasStagingArea { konvaImage.width(this.imageDTO.width); konvaImage.height(this.imageDTO.height); } - resetLastProgressEvent(); + this.manager.stateApi.resetLastProgressEvent(); }, } ); @@ -100,7 +100,7 @@ export class CanvasStagingArea { if (this.progressImage) { this.progressImage.konvaImageGroup.visible(false); } - resetLastProgressEvent(); + this.manager.stateApi.resetLastProgressEvent(); } } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/stateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/stateApi.ts new file mode 100644 index 00000000000..e29b79b572a --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/stateApi.ts @@ -0,0 +1,237 @@ +import { $alt, $ctrl, $meta, $shift } from "@invoke-ai/ui-library"; +import type { Store } from "@reduxjs/toolkit"; +import type { RootState } from "app/store/store"; +import { $isDrawing, $isMouseDown, $lastAddedPoint, $lastCursorPos, $lastMouseDownPos, $lastProgressEvent, $shouldShowStagedImage, $spaceKey, $stageAttrs, bboxChanged, brushWidthChanged, caBboxChanged, caTranslated, eraserWidthChanged, imBboxChanged, imBrushLineAdded, imEraserLineAdded, imImageCacheChanged, imLinePointAdded, imRectAdded, imScaled, imTranslated, layerBboxChanged, layerBrushLineAdded, layerEraserLineAdded, layerImageCacheChanged, layerLinePointAdded, layerRectAdded, layerScaled, layerTranslated, rgBboxChanged, rgBrushLineAdded, rgEraserLineAdded, rgImageCacheChanged, rgLinePointAdded, rgRectAdded, rgScaled, rgTranslated, toolBufferChanged, toolChanged } from "features/controlLayers/store/canvasV2Slice"; +import type { BboxChangedArg, BrushLineAddedArg, CanvasEntity, EraserLineAddedArg, PointAddedToLineArg, PosChangedArg, RectShapeAddedArg, ScaleChangedArg, Tool } from "features/controlLayers/store/types"; +import type { IRect } from "konva/lib/types"; +import type { RgbaColor } from "react-colorful"; +import type { ImageDTO } from "services/api/types"; + + +export class StateApi { + private store: Store; + private log: (message: string) => void; + + constructor(store: Store, log: (message: string) => void) { + this.store = store; + this.log = log; + } + + // Reminder - use arrow functions to avoid binding issues + getState = () => { + return this.store.getState().canvasV2; + }; + + onPosChanged = (arg: PosChangedArg, entityType: CanvasEntity['type']) => { + this.log('onPosChanged'); + if (entityType === 'layer') { + this.store.dispatch(layerTranslated(arg)); + } else if (entityType === 'control_adapter') { + this.store.dispatch(caTranslated(arg)); + } else if (entityType === 'regional_guidance') { + this.store.dispatch(rgTranslated(arg)); + } else if (entityType === 'inpaint_mask') { + this.store.dispatch(imTranslated(arg)); + } + }; + onScaleChanged = (arg: ScaleChangedArg, entityType: CanvasEntity['type']) => { + this.log('onScaleChanged'); + if (entityType === 'layer') { + this.store.dispatch(layerScaled(arg)); + } else if (entityType === 'inpaint_mask') { + this.store.dispatch(imScaled(arg)); + } else if (entityType === 'regional_guidance') { + this.store.dispatch(rgScaled(arg)); + } + }; + onBboxChanged = (arg: BboxChangedArg, entityType: CanvasEntity['type']) => { + this.log('Entity bbox changed'); + if (entityType === 'layer') { + this.store.dispatch(layerBboxChanged(arg)); + } else if (entityType === 'control_adapter') { + this.store.dispatch(caBboxChanged(arg)); + } else if (entityType === 'regional_guidance') { + this.store.dispatch(rgBboxChanged(arg)); + } else if (entityType === 'inpaint_mask') { + this.store.dispatch(imBboxChanged(arg)); + } + }; + onBrushLineAdded = (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => { + this.log('Brush line added'); + if (entityType === 'layer') { + this.store.dispatch(layerBrushLineAdded(arg)); + } else if (entityType === 'regional_guidance') { + this.store.dispatch(rgBrushLineAdded(arg)); + } else if (entityType === 'inpaint_mask') { + this.store.dispatch(imBrushLineAdded(arg)); + } + }; + onEraserLineAdded = (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => { + this.log('Eraser line added'); + if (entityType === 'layer') { + this.store.dispatch(layerEraserLineAdded(arg)); + } else if (entityType === 'regional_guidance') { + this.store.dispatch(rgEraserLineAdded(arg)); + } else if (entityType === 'inpaint_mask') { + this.store.dispatch(imEraserLineAdded(arg)); + } + }; + onPointAddedToLine = (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => { + this.log('Point added to line'); + if (entityType === 'layer') { + this.store.dispatch(layerLinePointAdded(arg)); + } else if (entityType === 'regional_guidance') { + this.store.dispatch(rgLinePointAdded(arg)); + } else if (entityType === 'inpaint_mask') { + this.store.dispatch(imLinePointAdded(arg)); + } + }; + onRectShapeAdded = (arg: RectShapeAddedArg, entityType: CanvasEntity['type']) => { + this.log('Rect shape added'); + if (entityType === 'layer') { + this.store.dispatch(layerRectAdded(arg)); + } else if (entityType === 'regional_guidance') { + this.store.dispatch(rgRectAdded(arg)); + } else if (entityType === 'inpaint_mask') { + this.store.dispatch(imRectAdded(arg)); + } + }; + onBboxTransformed = (bbox: IRect) => { + this.log('Generation bbox transformed'); + this.store.dispatch(bboxChanged(bbox)); + }; + onBrushWidthChanged = (width: number) => { + this.log('Brush width changed'); + this.store.dispatch(brushWidthChanged(width)); + }; + onEraserWidthChanged = (width: number) => { + this.log('Eraser width changed'); + this.store.dispatch(eraserWidthChanged(width)); + }; + onRegionMaskImageCached = (id: string, imageDTO: ImageDTO) => { + this.log('Region mask image cached'); + this.store.dispatch(rgImageCacheChanged({ id, imageDTO })); + }; + onInpaintMaskImageCached = (imageDTO: ImageDTO) => { + this.log('Inpaint mask image cached'); + this.store.dispatch(imImageCacheChanged({ imageDTO })); + }; + onLayerImageCached = (imageDTO: ImageDTO) => { + this.log('Layer image cached'); + this.store.dispatch(layerImageCacheChanged({ imageDTO })); + }; + setTool = (tool: Tool) => { + this.log('Tool selection changed'); + this.store.dispatch(toolChanged(tool)); + }; + setToolBuffer = (toolBuffer: Tool | null) => { + this.log('Tool buffer changed'); + this.store.dispatch(toolBufferChanged(toolBuffer)); + }; + + getSelectedEntity = (): CanvasEntity | null => { + const state = this.getState(); + const identifier = state.selectedEntityIdentifier; + let selectedEntity: CanvasEntity | null = null; + if (!identifier) { + selectedEntity = null; + } else if (identifier.type === 'layer') { + selectedEntity = state.layers.entities.find((i) => i.id === identifier.id) ?? null; + } else if (identifier.type === 'control_adapter') { + selectedEntity = state.controlAdapters.entities.find((i) => i.id === identifier.id) ?? null; + } else if (identifier.type === 'ip_adapter') { + selectedEntity = state.ipAdapters.entities.find((i) => i.id === identifier.id) ?? null; + } else if (identifier.type === 'regional_guidance') { + selectedEntity = state.regions.entities.find((i) => i.id === identifier.id) ?? null; + } else if (identifier.type === 'inpaint_mask') { + selectedEntity = state.inpaintMask; + } else { + selectedEntity = null; + } + return selectedEntity; + }; + + getCurrentFill = () => { + const state = this.getState(); + const selectedEntity = this.getSelectedEntity(); + let currentFill: RgbaColor = state.tool.fill; + if (selectedEntity) { + if (selectedEntity.type === 'regional_guidance') { + currentFill = { ...selectedEntity.fill, a: state.settings.maskOpacity }; + } else if (selectedEntity.type === 'inpaint_mask') { + currentFill = { ...state.inpaintMask.fill, a: state.settings.maskOpacity }; + } + } else { + currentFill = state.tool.fill; + } + return currentFill; + }; + getBbox = () => { + return this.getState().bbox; + }; + getDocument = () => { + return this.getState().document; + }; + getToolState = () => { + return this.getState().tool; + }; + getSettings = () => { + return this.getState().settings; + }; + getRegionsState = () => { + return this.getState().regions; + }; + getLayersState = () => { + return this.getState().layers; + }; + getControlAdaptersState = () => { + return this.getState().controlAdapters; + }; + getInpaintMaskState = () => { + return this.getState().inpaintMask; + }; + getMaskOpacity = () => { + return this.getState().settings.maskOpacity; + }; + getStagingAreaState = () => { + return this.getState().stagingArea; + }; + getIsSelected = (id: string) => { + return this.getSelectedEntity()?.id === id; + }; + + // Read-only state, derived from nanostores + resetLastProgressEvent = () => { + $lastProgressEvent.set(null); + }; + + // Read-write state, ephemeral interaction state + getIsDrawing = $isDrawing.get; + setIsDrawing = $isDrawing.set; + + getIsMouseDown = $isMouseDown.get; + setIsMouseDown = $isMouseDown.set; + + getLastAddedPoint = $lastAddedPoint.get; + setLastAddedPoint = $lastAddedPoint.set; + + getLastMouseDownPos = $lastMouseDownPos.get; + setLastMouseDownPos = $lastMouseDownPos.set; + + getLastCursorPos = $lastCursorPos.get; + setLastCursorPos = $lastCursorPos.set; + + getSpaceKey = $spaceKey.get; + setSpaceKey = $spaceKey.set; + + getLastProgressEvent = $lastProgressEvent.get; + setLastProgressEvent = $lastProgressEvent.set; + + getAltKey = $alt.get; + getCtrlKey = $ctrl.get; + getMetaKey = $meta.get; + getShiftKey = $shift.get; + + getShouldShowStagedImage = $shouldShowStagedImage.get; + setStageAttrs = $stageAttrs.set; +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/tool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/tool.ts similarity index 88% rename from invokeai/frontend/web/src/features/controlLayers/konva/renderers/tool.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/tool.ts index 38642b8f2ed..e1e99b50c60 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/tool.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/tool.ts @@ -5,10 +5,11 @@ import { BRUSH_ERASER_BORDER_WIDTH, } from 'features/controlLayers/konva/constants'; import { PREVIEW_RECT_ID } from 'features/controlLayers/konva/naming'; -import type { CanvasEntity, CanvasV2State, Position, RgbaColor } from 'features/controlLayers/store/types'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import Konva from 'konva'; export class CanvasTool { + manager: KonvaNodeManager; group: Konva.Group; brush: { group: Konva.Group; @@ -27,7 +28,8 @@ export class CanvasTool { fillRect: Konva.Rect; }; - constructor() { + constructor(manager: KonvaNodeManager) { + this.manager = manager; this.group = new Konva.Group(); // Create the brush preview group & circles @@ -94,8 +96,9 @@ export class CanvasTool { this.group.add(this.rect.group); } - scaleTool(stage: Konva.Stage, toolState: CanvasV2State['tool']) { - const scale = stage.scaleX(); + scaleTool = () => { + const toolState = this.manager.stateApi.getToolState(); + const scale = this.manager.stage.scaleX(); const brushRadius = toolState.brush.width / 2; this.brush.innerBorderCircle.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale); @@ -110,19 +113,19 @@ export class CanvasTool { strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, radius: eraserRadius + BRUSH_ERASER_BORDER_WIDTH / scale, }); - } + }; + + render() { + const stage = this.manager.stage; + const renderedEntityCount: number = 1; // TODO(psyche): this.manager should be renderable entity count + const toolState = this.manager.stateApi.getToolState(); + const currentFill = this.manager.stateApi.getCurrentFill(); + const selectedEntity = this.manager.stateApi.getSelectedEntity(); + const cursorPos = this.manager.stateApi.getLastCursorPos(); + const lastMouseDownPos = this.manager.stateApi.getLastMouseDownPos(); + const isDrawing = this.manager.stateApi.getIsDrawing(); + const isMouseDown = this.manager.stateApi.getIsMouseDown(); - render( - stage: Konva.Stage, - renderedEntityCount: number, - toolState: CanvasV2State['tool'], - currentFill: RgbaColor, - selectedEntity: CanvasEntity | null, - cursorPos: Position | null, - lastMouseDownPos: Position | null, - isDrawing: boolean, - isMouseDown: boolean - ) { const tool = toolState.selected; const isDrawableEntity = selectedEntity?.type === 'regional_guidance' || @@ -182,7 +185,7 @@ export class CanvasTool { radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, }); - this.scaleTool(stage, toolState); + this.scaleTool(); this.brush.group.visible(true); this.eraser.group.visible(false); @@ -208,7 +211,7 @@ export class CanvasTool { radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, }); - this.scaleTool(stage, toolState); + this.scaleTool(); this.brush.group.visible(false); this.eraser.group.visible(true); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index 1b4d603e0fc..ed4f70a6bb0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -1,6 +1,6 @@ import { CA_LAYER_NAME, - INPAINT_MASK_LAYER_NAME, + INPAINT_MASK_LAYER_ID, RASTER_LAYER_BRUSH_LINE_NAME, RASTER_LAYER_ERASER_LINE_NAME, RASTER_LAYER_IMAGE_NAME, @@ -102,7 +102,7 @@ export const selectRenderableLayers = (node: Konva.Node): boolean => node.name() === RG_LAYER_NAME || node.name() === CA_LAYER_NAME || node.name() === RASTER_LAYER_NAME || - node.name() === INPAINT_MASK_LAYER_NAME; + node.name() === INPAINT_MASK_LAYER_ID; /** * Konva selection callback to select RG mask objects. This includes lines and rects. diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 0b63056b3db..c341e5e152f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -3,6 +3,7 @@ import { createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { deepClone } from 'common/util/deepClone'; import { roundDownToMultiple } from 'common/util/roundDownToMultiple'; +import { INPAINT_MASK_LAYER_ID } from 'features/controlLayers/konva/naming'; import { bboxReducers } from 'features/controlLayers/store/bboxReducers'; import { compositingReducers } from 'features/controlLayers/store/compositingReducers'; import { controlAdaptersReducers } from 'features/controlLayers/store/controlAdaptersReducers'; @@ -20,19 +21,19 @@ import type { AspectRatioState } from 'features/parameters/components/ImageSize/ import { atom } from 'nanostores'; import type { InvocationDenoiseProgressEvent } from 'services/events/types'; -import type { CanvasEntityIdentifier, CanvasV2State, StageAttrs } from './types'; +import type { CanvasEntityIdentifier, CanvasV2State, Position, StageAttrs } from './types'; import { RGBA_RED } from './types'; const initialState: CanvasV2State = { _version: 3, - selectedEntityIdentifier: { type: 'inpaint_mask', id: 'inpaint_mask' }, + selectedEntityIdentifier: { type: 'inpaint_mask', id: INPAINT_MASK_LAYER_ID }, layers: { entities: [], imageCache: null }, controlAdapters: { entities: [] }, ipAdapters: { entities: [] }, regions: { entities: [] }, loras: [], inpaintMask: { - id: 'inpaint_mask', + id: INPAINT_MASK_LAYER_ID, type: 'inpaint_mask', bbox: null, bboxNeedsUpdate: false, @@ -366,6 +367,12 @@ export const $stageAttrs = atom({ }); export const $shouldShowStagedImage = atom(true); export const $lastProgressEvent = atom(null); +export const $isDrawing = atom(false); +export const $isMouseDown = atom(false); +export const $lastAddedPoint = atom(null); +export const $lastMouseDownPos = atom(null); +export const $lastCursorPos = atom(null); +export const $spaceKey = atom(false); export const canvasV2PersistConfig: PersistConfig = { name: canvasV2Slice.name, From 33ba8cabd1f492b50316066a67e622a2ba6c0127 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 4 Jul 2024 13:16:01 +1000 Subject: [PATCH 161/678] pkg(ui): remove unused deps react-konva & use-image --- invokeai/frontend/web/package.json | 2 - invokeai/frontend/web/pnpm-lock.yaml | 58 ---------------------------- 2 files changed, 60 deletions(-) diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 1b6432a3081..5ddb0d03faf 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -88,7 +88,6 @@ "react-hotkeys-hook": "4.5.0", "react-i18next": "^14.1.3", "react-icons": "^5.2.1", - "react-konva": "^18.2.10", "react-redux": "9.1.2", "react-resizable-panels": "^2.0.23", "react-select": "5.8.0", @@ -104,7 +103,6 @@ "socket.io-client": "^4.7.5", "use-debounce": "^10.0.2", "use-device-pixel-ratio": "^1.1.2", - "use-image": "^1.1.1", "uuid": "^10.0.0", "zod": "^3.23.8", "zod-validation-error": "^3.3.1" diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index bab6e3b1e5b..f9c906d0dc8 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -113,9 +113,6 @@ dependencies: react-icons: specifier: ^5.2.1 version: 5.2.1(react@18.3.1) - react-konva: - specifier: ^18.2.10 - version: 18.2.10(konva@9.3.14)(react-dom@18.3.1)(react@18.3.1) react-redux: specifier: 9.1.2 version: 9.1.2(@types/react@18.3.3)(react@18.3.1)(redux@5.0.1) @@ -161,9 +158,6 @@ dependencies: use-device-pixel-ratio: specifier: ^1.1.2 version: 1.1.2(react@18.3.1) - use-image: - specifier: ^1.1.1 - version: 1.1.1(react-dom@18.3.1)(react@18.3.1) uuid: specifier: ^10.0.0 version: 10.0.0 @@ -5202,12 +5196,6 @@ packages: '@types/react': 18.3.3 dev: true - /@types/react-reconciler@0.28.8: - resolution: {integrity: sha512-SN9c4kxXZonFhbX4hJrZy37yw9e7EIxcpHCxQv5JUS18wDE5ovkQKlqQEkufdJCCMfuI9BnjUJvhYeJ9x5Ra7g==} - dependencies: - '@types/react': 18.3.3 - dev: false - /@types/react-transition-group@4.4.10: resolution: {integrity: sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==} dependencies: @@ -8384,15 +8372,6 @@ packages: set-function-name: 2.0.2 dev: true - /its-fine@1.2.5(react@18.3.1): - resolution: {integrity: sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==} - peerDependencies: - react: '>=18.0' - dependencies: - '@types/react-reconciler': 0.28.8 - react: 18.3.1 - dev: false - /jackspeak@2.3.6: resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} engines: {node: '>=14'} @@ -9801,33 +9780,6 @@ packages: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} dev: true - /react-konva@18.2.10(konva@9.3.14)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-ohcX1BJINL43m4ynjZ24MxFI1syjBdrXhqVxYVDw2rKgr3yuS0x/6m1Y2Z4sl4T/gKhfreBx8KHisd0XC6OT1g==} - peerDependencies: - konva: ^8.0.1 || ^7.2.5 || ^9.0.0 - react: '>=18.0.0' - react-dom: '>=18.0.0' - dependencies: - '@types/react-reconciler': 0.28.8 - its-fine: 1.2.5(react@18.3.1) - konva: 9.3.14 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-reconciler: 0.29.2(react@18.3.1) - scheduler: 0.23.2 - dev: false - - /react-reconciler@0.29.2(react@18.3.1): - resolution: {integrity: sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==} - engines: {node: '>=0.10.0'} - peerDependencies: - react: ^18.3.1 - dependencies: - loose-envify: 1.4.0 - react: 18.3.1 - scheduler: 0.23.2 - dev: false - /react-redux@9.1.2(@types/react@18.3.3)(react@18.3.1)(redux@5.0.1): resolution: {integrity: sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==} peerDependencies: @@ -11331,16 +11283,6 @@ packages: react: 18.3.1 dev: false - /use-image@1.1.1(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-n4YO2k8AJG/BcDtxmBx8Aa+47kxY5m335dJiCQA5tTeVU4XdhrhqR6wT0WISRXwdMEOv5CSjqekDZkEMiiWaYQ==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - /use-isomorphic-layout-effect@1.1.2(@types/react@18.3.3)(react@18.3.1): resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==} peerDependencies: From 510981118235505d52c97d5cef215336eaf682ef Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 4 Jul 2024 13:20:25 +1000 Subject: [PATCH 162/678] fix(ui): background rendering --- .../web/src/features/controlLayers/konva/background.ts | 2 +- .../web/src/features/controlLayers/konva/events.ts | 7 +++---- .../web/src/features/controlLayers/konva/nodeManager.ts | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/background.ts b/invokeai/frontend/web/src/features/controlLayers/konva/background.ts index 2e8b3da177d..dfa614a96d5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/background.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/background.ts @@ -38,7 +38,7 @@ export class CanvasBackground { this.layer = new Konva.Layer({ listening: false }); } - renderBackground() { + render() { this.layer.zIndex(0); const scale = this.manager.stage.scaleX(); const gridSpacing = getGridSpacing(scale); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index c7c74e6105f..32483ee4ee0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -464,7 +464,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) = stage.scaleY(newScale); stage.position(newPos); setStageAttrs({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale }); - manager.preview.tool.render(); + manager.background.render(); manager.preview.documentSizeOverlay.render(); } } @@ -480,7 +480,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) = height: stage.height(), scale: stage.scaleX(), }); - manager.preview.tool.render(); + manager.background.render(); manager.preview.documentSizeOverlay.render(); manager.preview.tool.render(); }); @@ -521,8 +521,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) = setLastCursorPos(null); setLastMouseDownPos(null); manager.preview.documentSizeOverlay.fitToStage(); - manager.preview.tool.render(); - + manager.background.render(); manager.preview.documentSizeOverlay.render(); } manager.preview.tool.render(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts index cdfde891f29..9f705cd27f5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts @@ -199,7 +199,7 @@ export class KonvaNodeManager { height: this.stage.height(), scale: this.stage.scaleX(), }); - this.background.renderBackground(); + this.background.render(); this.preview.documentSizeOverlay.render(); } From cd02638db6d439cd739ff8949026a021a37932c8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 4 Jul 2024 20:55:55 +1000 Subject: [PATCH 163/678] tidy(ui): organise files --- .../listeners/enqueueRequestedLinear.ts | 2 +- .../components/StageComponent.tsx | 2 +- .../{background.ts => CanvasBackground.ts} | 2 +- .../konva/{bbox.ts => CanvasBbox.ts} | 2 +- ...rolAdapters.ts => CanvasControlAdapter.ts} | 0 ...verlay.ts => CanvasDocumentSizeOverlay.ts} | 2 +- .../{inpaintMask.ts => CanvasInpaintMask.ts} | 2 +- .../konva/{layers.ts => CanvasLayer.ts} | 2 +- .../konva/{preview.ts => CanvasPreview.ts} | 8 +++--- .../konva/{regions.ts => CanvasRegion.ts} | 4 +-- .../{stagingArea.ts => CanvasStagingArea.ts} | 2 +- .../konva/{tool.ts => CanvasTool.ts} | 2 +- .../{nodeManager.ts => KonvaNodeManager.ts} | 26 +++++++++---------- .../konva/{stateApi.ts => StateApi.ts} | 0 .../features/controlLayers/konva/events.ts | 2 +- .../util/graph/generation/addImageToImage.ts | 2 +- .../nodes/util/graph/generation/addInpaint.ts | 2 +- .../util/graph/generation/addOutpaint.ts | 2 +- .../nodes/util/graph/generation/addRegions.ts | 2 +- .../generation/buildImageToImageSDXLGraph.ts | 2 +- .../util/graph/generation/buildSD1Graph.ts | 2 +- .../util/graph/generation/buildSDXLGraph.ts | 2 +- .../generation/buildTextToImageSD1SD2Graph.ts | 2 +- 23 files changed, 37 insertions(+), 37 deletions(-) rename invokeai/frontend/web/src/features/controlLayers/konva/{background.ts => CanvasBackground.ts} (99%) rename invokeai/frontend/web/src/features/controlLayers/konva/{bbox.ts => CanvasBbox.ts} (99%) rename invokeai/frontend/web/src/features/controlLayers/konva/{controlAdapters.ts => CanvasControlAdapter.ts} (100%) rename invokeai/frontend/web/src/features/controlLayers/konva/{documentSizeOverlay.ts => CanvasDocumentSizeOverlay.ts} (99%) rename invokeai/frontend/web/src/features/controlLayers/konva/{inpaintMask.ts => CanvasInpaintMask.ts} (99%) rename invokeai/frontend/web/src/features/controlLayers/konva/{layers.ts => CanvasLayer.ts} (99%) rename invokeai/frontend/web/src/features/controlLayers/konva/{preview.ts => CanvasPreview.ts} (76%) rename invokeai/frontend/web/src/features/controlLayers/konva/{regions.ts => CanvasRegion.ts} (99%) rename invokeai/frontend/web/src/features/controlLayers/konva/{stagingArea.ts => CanvasStagingArea.ts} (99%) rename invokeai/frontend/web/src/features/controlLayers/konva/{tool.ts => CanvasTool.ts} (99%) rename invokeai/frontend/web/src/features/controlLayers/konva/{nodeManager.ts => KonvaNodeManager.ts} (95%) rename invokeai/frontend/web/src/features/controlLayers/konva/{stateApi.ts => StateApi.ts} (100%) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index d15a48da480..00646146670 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -1,6 +1,6 @@ import { enqueueRequested } from 'app/store/actions'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { getNodeManager } from 'features/controlLayers/konva/nodeManager'; +import { getNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; import { stagingAreaCanceledStaging, stagingAreaStartedStaging } from 'features/controlLayers/store/canvasV2Slice'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index 10fceda1104..6cafcd10665 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -3,7 +3,7 @@ import { logger } from 'app/logging/logger'; import { $isDebugging } from 'app/store/nanostores/isDebugging'; import { useAppStore } from 'app/store/storeHooks'; import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay'; -import { KonvaNodeManager, setNodeManager } from 'features/controlLayers/konva/nodeManager'; +import { KonvaNodeManager, setNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; import Konva from 'konva'; import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react'; import { useDevicePixelRatio } from 'use-device-pixel-ratio'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/background.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts similarity index 99% rename from invokeai/frontend/web/src/features/controlLayers/konva/background.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts index dfa614a96d5..01c5980cab7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/background.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts @@ -1,5 +1,5 @@ import { getArbitraryBaseColor } from '@invoke-ai/ui-library'; -import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; import Konva from 'konva'; const baseGridLineColor = getArbitraryBaseColor(27); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/bbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts similarity index 99% rename from invokeai/frontend/web/src/features/controlLayers/konva/bbox.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts index f87689afffe..d6dd391484a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/bbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts @@ -1,10 +1,10 @@ import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMultiple'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; import { PREVIEW_GENERATION_BBOX_DUMMY_RECT, PREVIEW_GENERATION_BBOX_GROUP, PREVIEW_GENERATION_BBOX_TRANSFORMER, } from 'features/controlLayers/konva/naming'; -import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; import { atom } from 'nanostores'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/konva/controlAdapters.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/documentSizeOverlay.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasDocumentSizeOverlay.ts similarity index 99% rename from invokeai/frontend/web/src/features/controlLayers/konva/documentSizeOverlay.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/CanvasDocumentSizeOverlay.ts index 7259d2d6cb9..be780449db7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/documentSizeOverlay.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasDocumentSizeOverlay.ts @@ -1,6 +1,6 @@ import { getArbitraryBaseColor } from '@invoke-ai/ui-library'; import { DOCUMENT_FIT_PADDING_PX } from 'features/controlLayers/konva/constants'; -import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; import Konva from 'konva'; export class CanvasDocumentSizeOverlay { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/inpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts similarity index 99% rename from invokeai/frontend/web/src/features/controlLayers/konva/inpaintMask.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts index 4c7257879dd..0151e896b25 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/inpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts @@ -1,7 +1,7 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; import { getObjectGroupId,INPAINT_MASK_LAYER_ID } from 'features/controlLayers/konva/naming'; -import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import { KonvaBrushLine, KonvaEraserLine, KonvaRect } from 'features/controlLayers/konva/objects'; import { mapId } from 'features/controlLayers/konva/util'; import { type InpaintMaskEntity, isDrawingTool } from 'features/controlLayers/store/types'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts similarity index 99% rename from invokeai/frontend/web/src/features/controlLayers/konva/layers.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index c902f248025..a5660bdfc47 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -1,5 +1,5 @@ +import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; import { getObjectGroupId } from 'features/controlLayers/konva/naming'; -import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import { KonvaBrushLine, KonvaEraserLine, KonvaImage, KonvaRect } from 'features/controlLayers/konva/objects'; import { mapId } from 'features/controlLayers/konva/util'; import { isDrawingTool, type LayerEntity } from 'features/controlLayers/store/types'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/preview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts similarity index 76% rename from invokeai/frontend/web/src/features/controlLayers/konva/preview.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts index beddbcc10bb..f5f08429411 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/preview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts @@ -1,9 +1,9 @@ import Konva from 'konva'; -import type { CanvasBbox } from './bbox'; -import type { CanvasDocumentSizeOverlay } from './documentSizeOverlay'; -import type { CanvasStagingArea } from './stagingArea'; -import type { CanvasTool } from './tool'; +import type { CanvasBbox } from './CanvasBbox'; +import type { CanvasDocumentSizeOverlay } from './CanvasDocumentSizeOverlay'; +import type { CanvasStagingArea } from './CanvasStagingArea'; +import type { CanvasTool } from './CanvasTool'; export class CanvasPreview { layer: Konva.Layer; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/regions.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts similarity index 99% rename from invokeai/frontend/web/src/features/controlLayers/konva/regions.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts index 2d5ed1dd076..9d72f027446 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/regions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts @@ -1,7 +1,7 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; -import { getObjectGroupId } from 'features/controlLayers/konva/naming'; -import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; +import { getObjectGroupId } from 'features/controlLayers/konva/naming'; import { KonvaBrushLine, KonvaEraserLine, KonvaRect } from 'features/controlLayers/konva/objects'; import { mapId } from 'features/controlLayers/konva/util'; import { isDrawingTool, type RegionEntity } from 'features/controlLayers/store/types'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/stagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts similarity index 99% rename from invokeai/frontend/web/src/features/controlLayers/konva/stagingArea.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts index 02b1a50fdb6..9f5ce759e63 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/stagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts @@ -1,4 +1,4 @@ -import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; import { KonvaImage, KonvaProgressImage } from 'features/controlLayers/konva/objects'; import Konva from 'konva'; import type { ImageDTO } from 'services/api/types'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/tool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts similarity index 99% rename from invokeai/frontend/web/src/features/controlLayers/konva/tool.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts index e1e99b50c60..ed6e0d35ee1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/tool.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts @@ -4,8 +4,8 @@ import { BRUSH_BORDER_OUTER_COLOR, BRUSH_ERASER_BORDER_WIDTH, } from 'features/controlLayers/konva/constants'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; import { PREVIEW_RECT_ID } from 'features/controlLayers/konva/naming'; -import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import Konva from 'konva'; export class CanvasTool { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/KonvaNodeManager.ts similarity index 95% rename from invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/KonvaNodeManager.ts index 9f705cd27f5..796efae4a77 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/KonvaNodeManager.ts @@ -1,10 +1,6 @@ import type { Store } from '@reduxjs/toolkit'; import type { RootState } from 'app/store/store'; import { getImageDataTransparency } from 'common/util/arrayBuffer'; -import { CanvasBackground } from 'features/controlLayers/konva/background'; -import { setStageEventHandlers } from 'features/controlLayers/konva/events'; -import { CanvasPreview } from 'features/controlLayers/konva/preview'; -import { konvaNodeToBlob, konvaNodeToImageData, previewBlob } from 'features/controlLayers/konva/util'; import { $lastProgressEvent, $shouldShowStagedImage } from 'features/controlLayers/store/canvasV2Slice'; import type { CanvasV2State, GenerationMode, Rect } from 'features/controlLayers/store/types'; import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers'; @@ -14,15 +10,19 @@ import { getImageDTO as defaultGetImageDTO, uploadImage as defaultUploadImage } import type { ImageCategory, ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; -import { CanvasBbox } from './bbox'; -import { CanvasControlAdapter } from './controlAdapters'; -import { CanvasDocumentSizeOverlay } from './documentSizeOverlay'; -import { CanvasInpaintMask } from './inpaintMask'; -import { CanvasLayer } from './layers'; -import { CanvasRegion } from './regions'; -import { CanvasStagingArea } from './stagingArea'; -import { StateApi } from './stateApi'; -import { CanvasTool } from './tool'; +import { CanvasBackground } from './CanvasBackground'; +import { CanvasBbox } from './CanvasBbox'; +import { CanvasControlAdapter } from './CanvasControlAdapter'; +import { CanvasDocumentSizeOverlay } from './CanvasDocumentSizeOverlay'; +import { CanvasInpaintMask } from './CanvasInpaintMask'; +import { CanvasLayer } from './CanvasLayer'; +import { CanvasPreview } from './CanvasPreview'; +import { CanvasRegion } from './CanvasRegion'; +import { CanvasStagingArea } from './CanvasStagingArea'; +import { CanvasTool } from './CanvasTool'; +import { setStageEventHandlers } from './events'; +import { StateApi } from './StateApi'; +import { konvaNodeToBlob, konvaNodeToImageData, previewBlob } from './util'; type Util = { getImageDTO: (imageName: string) => Promise; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/stateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/StateApi.ts similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/konva/stateApi.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/StateApi.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 32483ee4ee0..bf76ffbfe96 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -1,4 +1,4 @@ -import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; import { getScaledCursorPosition } from 'features/controlLayers/konva/util'; import type { CanvasEntity } from 'features/controlLayers/store/types'; import type Konva from 'konva'; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts index fa6dd026a59..c3b2953113d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts @@ -1,4 +1,4 @@ -import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; import type { CanvasV2State, Size } from 'features/controlLayers/store/types'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { isEqual, pick } from 'lodash-es'; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts index 450f6832426..f578ff82028 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts @@ -1,4 +1,4 @@ -import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; import type { CanvasV2State, Size } from 'features/controlLayers/store/types'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import type { ParameterPrecision } from 'features/parameters/types/parameterSchemas'; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index 02559d0b5b7..c4b828e2750 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts @@ -1,4 +1,4 @@ -import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; import type { CanvasV2State, Size } from 'features/controlLayers/store/types'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { getInfill } from 'features/nodes/util/graph/graphBuilderUtils'; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts index 30a399d211c..86c1d66491a 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts @@ -1,5 +1,5 @@ import { deepClone } from 'common/util/deepClone'; -import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; import type { IPAdapterEntity, Rect, RegionEntity } from 'features/controlLayers/store/types'; import { PROMPT_REGION_INVERT_TENSOR_MASK_PREFIX, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImageToImageSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImageToImageSDXLGraph.ts index 4dd0e1e0565..5b8bc5d5500 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImageToImageSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImageToImageSDXLGraph.ts @@ -1,5 +1,5 @@ import type { RootState } from 'app/store/store'; -import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { LATENTS_TO_IMAGE, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index 9b929cc9ceb..ddc8114c22c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -1,5 +1,5 @@ import type { RootState } from 'app/store/store'; -import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { CANVAS_OUTPUT, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index 04c0f0cf2f8..f3eedd42c09 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -1,5 +1,5 @@ import type { RootState } from 'app/store/store'; -import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { CANVAS_OUTPUT, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildTextToImageSD1SD2Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildTextToImageSD1SD2Graph.ts index 044cca05ce2..2c6f95b4fad 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildTextToImageSD1SD2Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildTextToImageSD1SD2Graph.ts @@ -1,5 +1,5 @@ import type { RootState } from 'app/store/store'; -import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { CLIP_SKIP, From 3b08250331f476e65689fa0dfc8ee47c5b457f6a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 4 Jul 2024 21:07:05 +1000 Subject: [PATCH 164/678] tidy(ui): organise files --- .../listeners/enqueueRequestedLinear.ts | 4 +- .../components/StageComponent.tsx | 6 +- .../controlLayers/konva/CanvasBackground.ts | 6 +- .../controlLayers/konva/CanvasBbox.ts | 6 +- .../controlLayers/konva/CanvasBrushLine.ts | 53 +++ .../konva/CanvasControlAdapter.ts | 7 +- .../konva/CanvasDocumentSizeOverlay.ts | 6 +- .../controlLayers/konva/CanvasEraserLine.ts | 70 ++++ .../controlLayers/konva/CanvasImage.ts | 158 ++++++++ .../controlLayers/konva/CanvasInpaintMask.ts | 26 +- .../controlLayers/konva/CanvasLayer.ts | 29 +- .../{KonvaNodeManager.ts => CanvasManager.ts} | 12 +- .../konva/CanvasProgressImage.ts | 60 +++ .../controlLayers/konva/CanvasRect.ts | 46 +++ .../controlLayers/konva/CanvasRegion.ts | 24 +- .../controlLayers/konva/CanvasStagingArea.ts | 17 +- .../controlLayers/konva/CanvasTool.ts | 6 +- .../controlLayers/konva/entityBbox.ts | 18 +- .../features/controlLayers/konva/events.ts | 14 +- .../features/controlLayers/konva/objects.ts | 378 ------------------ .../util/graph/generation/addImageToImage.ts | 4 +- .../nodes/util/graph/generation/addInpaint.ts | 4 +- .../util/graph/generation/addOutpaint.ts | 4 +- .../nodes/util/graph/generation/addRegions.ts | 4 +- .../generation/buildImageToImageSDXLGraph.ts | 4 +- .../util/graph/generation/buildSD1Graph.ts | 4 +- .../util/graph/generation/buildSDXLGraph.ts | 4 +- .../generation/buildTextToImageSD1SD2Graph.ts | 4 +- 28 files changed, 505 insertions(+), 473 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts rename invokeai/frontend/web/src/features/controlLayers/konva/{KonvaNodeManager.ts => CanvasManager.ts} (98%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts delete mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/objects.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index 00646146670..6359e55e408 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -1,6 +1,6 @@ import { enqueueRequested } from 'app/store/actions'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { getNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; +import { getCanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { stagingAreaCanceledStaging, stagingAreaStartedStaging } from 'features/controlLayers/store/canvasV2Slice'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph'; @@ -26,7 +26,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) try { let g; - const manager = getNodeManager(); + const manager = getCanvasManager(); assert(model, 'No model found in state'); const base = model.base; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index 6cafcd10665..8604d951421 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -3,7 +3,7 @@ import { logger } from 'app/logging/logger'; import { $isDebugging } from 'app/store/nanostores/isDebugging'; import { useAppStore } from 'app/store/storeHooks'; import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay'; -import { KonvaNodeManager, setNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; +import { CanvasManager, setCanvasManager } from 'features/controlLayers/konva/CanvasManager'; import Konva from 'konva'; import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react'; import { useDevicePixelRatio } from 'use-device-pixel-ratio'; @@ -35,8 +35,8 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, return () => {}; } - const manager = new KonvaNodeManager(stage, container, store, logIfDebugging); - setNodeManager(manager); + const manager = new CanvasManager(stage, container, store, logIfDebugging); + setCanvasManager(manager); const cleanup = manager.initialize(); return cleanup; }, [asPreview, container, stage, store]); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts index 01c5980cab7..5bf87510d82 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts @@ -1,5 +1,5 @@ import { getArbitraryBaseColor } from '@invoke-ai/ui-library'; -import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import Konva from 'konva'; const baseGridLineColor = getArbitraryBaseColor(27); @@ -31,9 +31,9 @@ const getGridSpacing = (scale: number): number => { export class CanvasBackground { layer: Konva.Layer; - manager: KonvaNodeManager; + manager: CanvasManager; - constructor(manager: KonvaNodeManager) { + constructor(manager: CanvasManager) { this.manager = manager; this.layer = new Konva.Layer({ listening: false }); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts index d6dd391484a..7ab216c4bf7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts @@ -1,5 +1,5 @@ import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMultiple'; -import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { PREVIEW_GENERATION_BBOX_DUMMY_RECT, PREVIEW_GENERATION_BBOX_GROUP, @@ -14,7 +14,7 @@ export class CanvasBbox { group: Konva.Group; rect: Konva.Rect; transformer: Konva.Transformer; - manager: KonvaNodeManager; + manager: CanvasManager; ALL_ANCHORS: string[] = [ 'top-left', @@ -29,7 +29,7 @@ export class CanvasBbox { CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right']; NO_ANCHORS: string[] = []; - constructor(manager: KonvaNodeManager) { + constructor(manager: CanvasManager) { this.manager = manager; // Create a stash to hold onto the last aspect ratio of the bbox - this allows for locking the aspect ratio when // transforming the bbox. diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts new file mode 100644 index 00000000000..9ce10450a13 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts @@ -0,0 +1,53 @@ +import { rgbaColorToString } from 'common/util/colorCodeTransformers'; +import type { BrushLine } from 'features/controlLayers/store/types'; +import Konva from 'konva'; + +export class CanvasBrushLine { + id: string; + konvaLineGroup: Konva.Group; + konvaLine: Konva.Line; + lastBrushLine: BrushLine; + + constructor(brushLine: BrushLine) { + const { id, strokeWidth, clip, color, points } = brushLine; + this.id = id; + this.konvaLineGroup = new Konva.Group({ + clip, + listening: false, + }); + this.konvaLine = new Konva.Line({ + id, + listening: false, + shadowForStrokeEnabled: false, + strokeWidth, + tension: 0, + lineCap: 'round', + lineJoin: 'round', + globalCompositeOperation: 'source-over', + stroke: rgbaColorToString(color), + points, + }); + this.konvaLineGroup.add(this.konvaLine); + this.lastBrushLine = brushLine; + } + + update(brushLine: BrushLine, force?: boolean): boolean { + if (this.lastBrushLine !== brushLine || force) { + const { points, color, clip, strokeWidth } = brushLine; + this.konvaLine.setAttrs({ + points, + stroke: rgbaColorToString(color), + clip, + strokeWidth, + }); + this.lastBrushLine = brushLine; + return true; + } else { + return false; + } + } + + destroy() { + this.konvaLineGroup.destroy(); + } +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts index 421ec4ad9aa..aae0a5fcb85 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts @@ -1,3 +1,4 @@ +import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; import { getObjectGroupId } from 'features/controlLayers/konva/naming'; import type { ControlAdapterEntity } from 'features/controlLayers/store/types'; @@ -5,13 +6,11 @@ import Konva from 'konva'; import { isEqual } from 'lodash-es'; import { v4 as uuidv4 } from 'uuid'; -import { KonvaImage } from './objects'; - export class CanvasControlAdapter { id: string; layer: Konva.Layer; group: Konva.Group; - image: KonvaImage | null; + image: CanvasImage | null; constructor(entity: ControlAdapterEntity) { const { id } = entity; @@ -43,7 +42,7 @@ export class CanvasControlAdapter { const filters = entity.filter === 'LightnessToAlphaFilter' ? [LightnessToAlphaFilter] : []; if (!this.image) { - this.image = await new KonvaImage(imageObject, { + this.image = await new CanvasImage(imageObject, { onLoad: (konvaImage) => { konvaImage.filters(filters); konvaImage.cache(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasDocumentSizeOverlay.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasDocumentSizeOverlay.ts index be780449db7..8b278a56367 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasDocumentSizeOverlay.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasDocumentSizeOverlay.ts @@ -1,6 +1,6 @@ import { getArbitraryBaseColor } from '@invoke-ai/ui-library'; import { DOCUMENT_FIT_PADDING_PX } from 'features/controlLayers/konva/constants'; -import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import Konva from 'konva'; export class CanvasDocumentSizeOverlay { @@ -8,9 +8,9 @@ export class CanvasDocumentSizeOverlay { outerRect: Konva.Rect; innerRect: Konva.Rect; padding: number; - manager: KonvaNodeManager; + manager: CanvasManager; - constructor(manager: KonvaNodeManager, padding?: number) { + constructor(manager: CanvasManager, padding?: number) { this.manager = manager; this.padding = padding ?? DOCUMENT_FIT_PADDING_PX; this.group = new Konva.Group({ id: 'document_overlay_group', listening: false }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts new file mode 100644 index 00000000000..f1ce21afd10 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts @@ -0,0 +1,70 @@ +import { rgbaColorToString } from 'common/util/colorCodeTransformers'; +import { getLayerBboxId, LAYER_BBOX_NAME } from 'features/controlLayers/konva/naming'; +import type { CanvasEntity, EraserLine } from 'features/controlLayers/store/types'; +import { RGBA_RED } from 'features/controlLayers/store/types'; +import Konva from 'konva'; + +/** + * Creates a bounding box rect for a layer. + * @param entity The layer state for the layer to create the bounding box for + * @param konvaLayer The konva layer to attach the bounding box to + */ +export const createBboxRect = (entity: CanvasEntity, konvaLayer: Konva.Layer): Konva.Rect => { + const rect = new Konva.Rect({ + id: getLayerBboxId(entity.id), + name: LAYER_BBOX_NAME, + strokeWidth: 1, + visible: false, + }); + konvaLayer.add(rect); + return rect; +}; + +export class CanvasEraserLine { + id: string; + konvaLineGroup: Konva.Group; + konvaLine: Konva.Line; + lastEraserLine: EraserLine; + + constructor(eraserLine: EraserLine) { + const { id, strokeWidth, clip, points } = eraserLine; + this.id = id; + this.konvaLineGroup = new Konva.Group({ + clip, + listening: false, + }); + this.konvaLine = new Konva.Line({ + id, + listening: false, + shadowForStrokeEnabled: false, + strokeWidth, + tension: 0, + lineCap: 'round', + lineJoin: 'round', + globalCompositeOperation: 'destination-out', + stroke: rgbaColorToString(RGBA_RED), + points, + }); + this.konvaLineGroup.add(this.konvaLine); + this.lastEraserLine = eraserLine; + } + + update(eraserLine: EraserLine, force?: boolean): boolean { + if (this.lastEraserLine !== eraserLine || force) { + const { points, clip, strokeWidth } = eraserLine; + this.konvaLine.setAttrs({ + points, + clip, + strokeWidth, + }); + this.lastEraserLine = eraserLine; + return true; + } else { + return false; + } + } + + destroy() { + this.konvaLineGroup.destroy(); + } +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts new file mode 100644 index 00000000000..575fcc4e64f --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -0,0 +1,158 @@ +import type { ImageObject } from 'features/controlLayers/store/types'; +import { t } from 'i18next'; +import Konva from 'konva'; +import { getImageDTO as defaultGetImageDTO } from 'services/api/endpoints/images'; +import type { ImageDTO } from 'services/api/types'; + +export class CanvasImage { + id: string; + konvaImageGroup: Konva.Group; + konvaPlaceholderGroup: Konva.Group; + konvaPlaceholderRect: Konva.Rect; + konvaPlaceholderText: Konva.Text; + imageName: string | null; + konvaImage: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately + isLoading: boolean; + isError: boolean; + getImageDTO: (imageName: string) => Promise; + onLoading: () => void; + onLoad: (imageName: string, imageEl: HTMLImageElement) => void; + onError: () => void; + lastImageObject: ImageObject; + + constructor( + imageObject: ImageObject, + options: { + getImageDTO?: (imageName: string) => Promise; + onLoading?: () => void; + onLoad?: (konvaImage: Konva.Image) => void; + onError?: () => void; + } + ) { + const { getImageDTO, onLoading, onLoad, onError } = options; + const { id, width, height, x, y } = imageObject; + this.konvaImageGroup = new Konva.Group({ id, listening: false, x, y }); + this.konvaPlaceholderGroup = new Konva.Group({ listening: false }); + this.konvaPlaceholderRect = new Konva.Rect({ + fill: 'hsl(220 12% 45% / 1)', // 'base.500' + width, + height, + listening: false, + }); + this.konvaPlaceholderText = new Konva.Text({ + fill: 'hsl(220 12% 10% / 1)', // 'base.900' + width, + height, + align: 'center', + verticalAlign: 'middle', + fontFamily: '"Inter Variable", sans-serif', + fontSize: width / 16, + fontStyle: '600', + text: t('common.loadingImage', 'Loading Image'), + listening: false, + }); + + this.konvaPlaceholderGroup.add(this.konvaPlaceholderRect); + this.konvaPlaceholderGroup.add(this.konvaPlaceholderText); + this.konvaImageGroup.add(this.konvaPlaceholderGroup); + + this.id = id; + this.imageName = null; + this.konvaImage = null; + this.isLoading = false; + this.isError = false; + this.getImageDTO = getImageDTO ?? defaultGetImageDTO; + this.onLoading = function () { + this.isLoading = true; + if (!this.konvaImage) { + this.konvaPlaceholderGroup.visible(true); + this.konvaPlaceholderText.text(t('common.loadingImage', 'Loading Image')); + } + this.konvaImageGroup.visible(true); + if (onLoading) { + onLoading(); + } + }; + this.onLoad = function (imageName: string, imageEl: HTMLImageElement) { + if (this.konvaImage) { + this.konvaImage.setAttrs({ + image: imageEl, + }); + } else { + this.konvaImage = new Konva.Image({ + id: this.id, + listening: false, + image: imageEl, + width, + height, + }); + this.konvaImageGroup.add(this.konvaImage); + } + this.imageName = imageName; + this.isLoading = false; + this.isError = false; + this.konvaPlaceholderGroup.visible(false); + this.konvaImageGroup.visible(true); + + if (onLoad) { + onLoad(this.konvaImage); + } + }; + this.onError = function () { + this.imageName = null; + this.isLoading = false; + this.isError = true; + this.konvaPlaceholderGroup.visible(true); + this.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load')); + this.konvaImageGroup.visible(true); + + if (onError) { + onError(); + } + }; + this.lastImageObject = imageObject; + } + + async updateImageSource(imageName: string) { + try { + this.onLoading(); + + const imageDTO = await this.getImageDTO(imageName); + if (!imageDTO) { + this.onError(); + return; + } + const imageEl = new Image(); + imageEl.onload = () => { + this.onLoad(imageName, imageEl); + }; + imageEl.onerror = () => { + this.onError(); + }; + imageEl.id = imageName; + imageEl.src = imageDTO.image_url; + } catch { + this.onError(); + } + } + + async update(imageObject: ImageObject, force?: boolean): Promise { + if (this.lastImageObject !== imageObject || force) { + const { width, height, x, y, image } = imageObject; + if (this.lastImageObject.image.name !== image.name || force) { + await this.updateImageSource(image.name); + } + this.konvaImage?.setAttrs({ x, y, width, height }); + this.konvaPlaceholderRect.setAttrs({ width, height }); + this.konvaPlaceholderText.setAttrs({ width, height, fontSize: width / 16 }); + this.lastImageObject = imageObject; + return true; + } else { + return false; + } + } + + destroy() { + this.konvaImageGroup.destroy(); + } +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts index 0151e896b25..59ef9e77594 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts @@ -1,8 +1,10 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; +import { CanvasBrushLine } from 'features/controlLayers/konva/CanvasBrushLine'; +import { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox'; -import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; -import { getObjectGroupId,INPAINT_MASK_LAYER_ID } from 'features/controlLayers/konva/naming'; -import { KonvaBrushLine, KonvaEraserLine, KonvaRect } from 'features/controlLayers/konva/objects'; +import { getObjectGroupId, INPAINT_MASK_LAYER_ID } from 'features/controlLayers/konva/naming'; import { mapId } from 'features/controlLayers/konva/util'; import { type InpaintMaskEntity, isDrawingTool } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -11,15 +13,15 @@ import { v4 as uuidv4 } from 'uuid'; export class CanvasInpaintMask { id: string; - manager: KonvaNodeManager; + manager: CanvasManager; layer: Konva.Layer; group: Konva.Group; objectsGroup: Konva.Group; compositingRect: Konva.Rect; transformer: Konva.Transformer; - objects: Map; + objects: Map; - constructor(manager: KonvaNodeManager) { + constructor(manager: CanvasManager) { this.id = INPAINT_MASK_LAYER_ID; this.manager = manager; this.layer = new Konva.Layer({ id: INPAINT_MASK_LAYER_ID }); @@ -84,10 +86,10 @@ export class CanvasInpaintMask { for (const obj of inpaintMaskState.objects) { if (obj.type === 'brush_line') { let brushLine = this.objects.get(obj.id); - assert(brushLine instanceof KonvaBrushLine || brushLine === undefined); + assert(brushLine instanceof CanvasBrushLine || brushLine === undefined); if (!brushLine) { - brushLine = new KonvaBrushLine(obj); + brushLine = new CanvasBrushLine(obj); this.objects.set(brushLine.id, brushLine); this.objectsGroup.add(brushLine.konvaLineGroup); didDraw = true; @@ -98,10 +100,10 @@ export class CanvasInpaintMask { } } else if (obj.type === 'eraser_line') { let eraserLine = this.objects.get(obj.id); - assert(eraserLine instanceof KonvaEraserLine || eraserLine === undefined); + assert(eraserLine instanceof CanvasEraserLine || eraserLine === undefined); if (!eraserLine) { - eraserLine = new KonvaEraserLine(obj); + eraserLine = new CanvasEraserLine(obj); this.objects.set(eraserLine.id, eraserLine); this.objectsGroup.add(eraserLine.konvaLineGroup); didDraw = true; @@ -112,10 +114,10 @@ export class CanvasInpaintMask { } } else if (obj.type === 'rect_shape') { let rect = this.objects.get(obj.id); - assert(rect instanceof KonvaRect || rect === undefined); + assert(rect instanceof CanvasRect || rect === undefined); if (!rect) { - rect = new KonvaRect(obj); + rect = new CanvasRect(obj); this.objects.set(rect.id, rect); this.objectsGroup.add(rect.konvaRect); didDraw = true; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index a5660bdfc47..3288e710a48 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -1,6 +1,9 @@ -import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; +import { CanvasBrushLine } from 'features/controlLayers/konva/CanvasBrushLine'; +import { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine'; +import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; import { getObjectGroupId } from 'features/controlLayers/konva/naming'; -import { KonvaBrushLine, KonvaEraserLine, KonvaImage, KonvaRect } from 'features/controlLayers/konva/objects'; import { mapId } from 'features/controlLayers/konva/util'; import { isDrawingTool, type LayerEntity } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -9,13 +12,13 @@ import { v4 as uuidv4 } from 'uuid'; export class CanvasLayer { id: string; - manager: KonvaNodeManager; + manager: CanvasManager; layer: Konva.Layer; group: Konva.Group; transformer: Konva.Transformer; - objects: Map; + objects: Map; - constructor(entity: LayerEntity, manager: KonvaNodeManager) { + constructor(entity: LayerEntity, manager: CanvasManager) { this.id = entity.id; this.manager = manager; this.layer = new Konva.Layer({ @@ -79,10 +82,10 @@ export class CanvasLayer { for (const obj of layerState.objects) { if (obj.type === 'brush_line') { let brushLine = this.objects.get(obj.id); - assert(brushLine instanceof KonvaBrushLine || brushLine === undefined); + assert(brushLine instanceof CanvasBrushLine || brushLine === undefined); if (!brushLine) { - brushLine = new KonvaBrushLine(obj); + brushLine = new CanvasBrushLine(obj); this.objects.set(brushLine.id, brushLine); this.group.add(brushLine.konvaLineGroup); didDraw = true; @@ -93,10 +96,10 @@ export class CanvasLayer { } } else if (obj.type === 'eraser_line') { let eraserLine = this.objects.get(obj.id); - assert(eraserLine instanceof KonvaEraserLine || eraserLine === undefined); + assert(eraserLine instanceof CanvasEraserLine || eraserLine === undefined); if (!eraserLine) { - eraserLine = new KonvaEraserLine(obj); + eraserLine = new CanvasEraserLine(obj); this.objects.set(eraserLine.id, eraserLine); this.group.add(eraserLine.konvaLineGroup); didDraw = true; @@ -107,10 +110,10 @@ export class CanvasLayer { } } else if (obj.type === 'rect_shape') { let rect = this.objects.get(obj.id); - assert(rect instanceof KonvaRect || rect === undefined); + assert(rect instanceof CanvasRect || rect === undefined); if (!rect) { - rect = new KonvaRect(obj); + rect = new CanvasRect(obj); this.objects.set(rect.id, rect); this.group.add(rect.konvaRect); didDraw = true; @@ -121,10 +124,10 @@ export class CanvasLayer { } } else if (obj.type === 'image') { let image = this.objects.get(obj.id); - assert(image instanceof KonvaImage || image === undefined); + assert(image instanceof CanvasImage || image === undefined); if (!image) { - image = await new KonvaImage(obj, { + image = await new CanvasImage(obj, { onLoad: () => { this.updateGroup(true); }, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/KonvaNodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts similarity index 98% rename from invokeai/frontend/web/src/features/controlLayers/konva/KonvaNodeManager.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 796efae4a77..6d5fbcf7c48 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/KonvaNodeManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -34,17 +34,17 @@ type Util = { ) => Promise; }; -const $nodeManager = atom(null); -export function getNodeManager() { - const nodeManager = $nodeManager.get(); +const $canvasManager = atom(null); +export function getCanvasManager() { + const nodeManager = $canvasManager.get(); assert(nodeManager !== null, 'Node manager not initialized'); return nodeManager; } -export function setNodeManager(nodeManager: KonvaNodeManager) { - $nodeManager.set(nodeManager); +export function setCanvasManager(nodeManager: CanvasManager) { + $canvasManager.set(nodeManager); } -export class KonvaNodeManager { +export class CanvasManager { stage: Konva.Stage; container: HTMLDivElement; controlAdapters: Map; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts new file mode 100644 index 00000000000..e56f88e1644 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts @@ -0,0 +1,60 @@ +import Konva from 'konva'; + +export class CanvasProgressImage { + id: string; + progressImageId: string | null; + konvaImageGroup: Konva.Group; + konvaImage: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately + isLoading: boolean; + isError: boolean; + + constructor(arg: { id: string }) { + const { id } = arg; + this.konvaImageGroup = new Konva.Group({ id, listening: false }); + + this.id = id; + this.progressImageId = null; + this.konvaImage = null; + this.isLoading = false; + this.isError = false; + } + + async updateImageSource( + progressImageId: string, + dataURL: string, + x: number, + y: number, + width: number, + height: number + ) { + const imageEl = new Image(); + imageEl.onload = () => { + if (this.konvaImage) { + this.konvaImage.setAttrs({ + image: imageEl, + x, + y, + width, + height, + }); + } else { + this.konvaImage = new Konva.Image({ + id: this.id, + listening: false, + image: imageEl, + x, + y, + width, + height, + }); + this.konvaImageGroup.add(this.konvaImage); + } + }; + imageEl.id = progressImageId; + imageEl.src = dataURL; + } + + destroy() { + this.konvaImageGroup.destroy(); + } +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts new file mode 100644 index 00000000000..a5a8eea8b92 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts @@ -0,0 +1,46 @@ +import { rgbaColorToString } from 'common/util/colorCodeTransformers'; +import type { RectShape } from 'features/controlLayers/store/types'; +import Konva from 'konva'; + +export class CanvasRect { + id: string; + konvaRect: Konva.Rect; + lastRectShape: RectShape; + + constructor(rectShape: RectShape) { + const { id, x, y, width, height } = rectShape; + this.id = id; + const konvaRect = new Konva.Rect({ + id, + x, + y, + width, + height, + listening: false, + fill: rgbaColorToString(rectShape.color), + }); + this.konvaRect = konvaRect; + this.lastRectShape = rectShape; + } + + update(rectShape: RectShape, force?: boolean): boolean { + if (this.lastRectShape !== rectShape || force) { + const { x, y, width, height, color } = rectShape; + this.konvaRect.setAttrs({ + x, + y, + width, + height, + fill: rgbaColorToString(color), + }); + this.lastRectShape = rectShape; + return true; + } else { + return false; + } + } + + destroy() { + this.konvaRect.destroy(); + } +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts index 9d72f027446..57bda0a1efb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts @@ -1,8 +1,10 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; +import { CanvasBrushLine } from 'features/controlLayers/konva/CanvasBrushLine'; +import { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox'; -import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; import { getObjectGroupId } from 'features/controlLayers/konva/naming'; -import { KonvaBrushLine, KonvaEraserLine, KonvaRect } from 'features/controlLayers/konva/objects'; import { mapId } from 'features/controlLayers/konva/util'; import { isDrawingTool, type RegionEntity } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -11,15 +13,15 @@ import { v4 as uuidv4 } from 'uuid'; export class CanvasRegion { id: string; - manager: KonvaNodeManager; + manager: CanvasManager; layer: Konva.Layer; group: Konva.Group; objectsGroup: Konva.Group; compositingRect: Konva.Rect; transformer: Konva.Transformer; - objects: Map; + objects: Map; - constructor(entity: RegionEntity, manager: KonvaNodeManager) { + constructor(entity: RegionEntity, manager: CanvasManager) { this.id = entity.id; this.manager = manager; this.layer = new Konva.Layer({ id: entity.id }); @@ -84,10 +86,10 @@ export class CanvasRegion { for (const obj of regionState.objects) { if (obj.type === 'brush_line') { let brushLine = this.objects.get(obj.id); - assert(brushLine instanceof KonvaBrushLine || brushLine === undefined); + assert(brushLine instanceof CanvasBrushLine || brushLine === undefined); if (!brushLine) { - brushLine = new KonvaBrushLine(obj); + brushLine = new CanvasBrushLine(obj); this.objects.set(brushLine.id, brushLine); this.objectsGroup.add(brushLine.konvaLineGroup); didDraw = true; @@ -98,10 +100,10 @@ export class CanvasRegion { } } else if (obj.type === 'eraser_line') { let eraserLine = this.objects.get(obj.id); - assert(eraserLine instanceof KonvaEraserLine || eraserLine === undefined); + assert(eraserLine instanceof CanvasEraserLine || eraserLine === undefined); if (!eraserLine) { - eraserLine = new KonvaEraserLine(obj); + eraserLine = new CanvasEraserLine(obj); this.objects.set(eraserLine.id, eraserLine); this.objectsGroup.add(eraserLine.konvaLineGroup); didDraw = true; @@ -112,10 +114,10 @@ export class CanvasRegion { } } else if (obj.type === 'rect_shape') { let rect = this.objects.get(obj.id); - assert(rect instanceof KonvaRect || rect === undefined); + assert(rect instanceof CanvasRect || rect === undefined); if (!rect) { - rect = new KonvaRect(obj); + rect = new CanvasRect(obj); this.objects.set(rect.id, rect); this.objectsGroup.add(rect.konvaRect); didDraw = true; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts index 9f5ce759e63..76d793ecb25 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts @@ -1,16 +1,17 @@ -import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; -import { KonvaImage, KonvaProgressImage } from 'features/controlLayers/konva/objects'; +import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasProgressImage } from 'features/controlLayers/konva/CanvasProgressImage'; import Konva from 'konva'; import type { ImageDTO } from 'services/api/types'; export class CanvasStagingArea { group: Konva.Group; - image: KonvaImage | null; - progressImage: KonvaProgressImage | null; + image: CanvasImage | null; + progressImage: CanvasProgressImage | null; imageDTO: ImageDTO | null; - manager: KonvaNodeManager; + manager: CanvasManager; - constructor(manager: KonvaNodeManager) { + constructor(manager: CanvasManager) { this.manager = manager; this.group = new Konva.Group({ listening: false }); this.image = null; @@ -37,7 +38,7 @@ export class CanvasStagingArea { this.progressImage?.konvaImageGroup.visible(false); } else { const { image_name, width, height } = this.imageDTO; - this.image = new KonvaImage( + this.image = new CanvasImage( { id: 'staging-area-image', type: 'image', @@ -85,7 +86,7 @@ export class CanvasStagingArea { this.progressImage.konvaImageGroup.visible(true); } } else { - this.progressImage = new KonvaProgressImage({ id: 'progress-image' }); + this.progressImage = new CanvasProgressImage({ id: 'progress-image' }); this.group.add(this.progressImage.konvaImageGroup); await this.progressImage.updateImageSource(progressImageId, dataURL, x, y, width, height); this.image?.konvaImageGroup.visible(false); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts index ed6e0d35ee1..825127a596c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts @@ -1,15 +1,15 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { BRUSH_BORDER_INNER_COLOR, BRUSH_BORDER_OUTER_COLOR, BRUSH_ERASER_BORDER_WIDTH, } from 'features/controlLayers/konva/constants'; -import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; import { PREVIEW_RECT_ID } from 'features/controlLayers/konva/naming'; import Konva from 'konva'; export class CanvasTool { - manager: KonvaNodeManager; + manager: CanvasManager; group: Konva.Group; brush: { group: Konva.Group; @@ -28,7 +28,7 @@ export class CanvasTool { fillRect: Konva.Rect; }; - constructor(manager: KonvaNodeManager) { + constructor(manager: CanvasManager) { this.manager = manager; this.group = new Konva.Group(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts index f0bb69bb328..1448d5a8f7f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts @@ -1,11 +1,11 @@ import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; import { CA_LAYER_IMAGE_NAME, + getLayerBboxId, LAYER_BBOX_NAME, RASTER_LAYER_OBJECT_GROUP_NAME, RG_LAYER_OBJECT_GROUP_NAME, } from 'features/controlLayers/konva/naming'; -import { createBboxRect } from 'features/controlLayers/konva/objects'; import { imageDataToDataURL } from 'features/controlLayers/konva/util'; import type { BboxChangedArg, @@ -18,6 +18,22 @@ import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; import { assert } from 'tsafe'; +/** + * Creates a bounding box rect for a layer. + * @param entity The layer state for the layer to create the bounding box for + * @param konvaLayer The konva layer to attach the bounding box to + */ +export const createBboxRect = (entity: CanvasEntity, konvaLayer: Konva.Layer): Konva.Rect => { + const rect = new Konva.Rect({ + id: getLayerBboxId(entity.id), + name: LAYER_BBOX_NAME, + strokeWidth: 1, + visible: false, + }); + konvaLayer.add(rect); + return rect; +}; + /** * Logic to create and render bounding boxes for layers. * Some utils are included for calculating bounding boxes. diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index bf76ffbfe96..4967982604f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -1,4 +1,4 @@ -import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getScaledCursorPosition } from 'features/controlLayers/konva/util'; import type { CanvasEntity } from 'features/controlLayers/store/types'; import type Konva from 'konva'; @@ -23,7 +23,7 @@ import { PREVIEW_TOOL_GROUP_ID } from './naming'; */ const updateLastCursorPos = ( stage: Konva.Stage, - setLastCursorPos: KonvaNodeManager['stateApi']['setLastCursorPos'] + setLastCursorPos: CanvasManager['stateApi']['setLastCursorPos'] ) => { const pos = getScaledCursorPosition(stage); if (!pos) { @@ -56,10 +56,10 @@ const calculateNewBrushSize = (brushSize: number, delta: number) => { const maybeAddNextPoint = ( selectedEntity: CanvasEntity, currentPos: Vector2d, - getToolState: KonvaNodeManager['stateApi']['getToolState'], - getLastAddedPoint: KonvaNodeManager['stateApi']['getLastAddedPoint'], - setLastAddedPoint: KonvaNodeManager['stateApi']['setLastAddedPoint'], - onPointAddedToLine: KonvaNodeManager['stateApi']['onPointAddedToLine'] + getToolState: CanvasManager['stateApi']['getToolState'], + getLastAddedPoint: CanvasManager['stateApi']['getLastAddedPoint'], + setLastAddedPoint: CanvasManager['stateApi']['setLastAddedPoint'], + onPointAddedToLine: CanvasManager['stateApi']['onPointAddedToLine'] ) => { const isDrawableEntity = selectedEntity?.type === 'regional_guidance' || @@ -95,7 +95,7 @@ const maybeAddNextPoint = ( ); }; -export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) => { +export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { const { stage, stateApi } = manager; const { getToolState, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/objects.ts b/invokeai/frontend/web/src/features/controlLayers/konva/objects.ts deleted file mode 100644 index 3a6ecbd0604..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/objects.ts +++ /dev/null @@ -1,378 +0,0 @@ -import { rgbaColorToString } from 'common/util/colorCodeTransformers'; -import { getLayerBboxId, LAYER_BBOX_NAME } from 'features/controlLayers/konva/naming'; -import type { BrushLine, CanvasEntity, EraserLine, ImageObject, RectShape } from 'features/controlLayers/store/types'; -import { RGBA_RED } from 'features/controlLayers/store/types'; -import { t } from 'i18next'; -import Konva from 'konva'; -import { getImageDTO as defaultGetImageDTO } from 'services/api/endpoints/images'; -import type { ImageDTO } from 'services/api/types'; - -/** - * Creates a bounding box rect for a layer. - * @param entity The layer state for the layer to create the bounding box for - * @param konvaLayer The konva layer to attach the bounding box to - */ -export const createBboxRect = (entity: CanvasEntity, konvaLayer: Konva.Layer): Konva.Rect => { - const rect = new Konva.Rect({ - id: getLayerBboxId(entity.id), - name: LAYER_BBOX_NAME, - strokeWidth: 1, - visible: false, - }); - konvaLayer.add(rect); - return rect; -}; - -export class KonvaBrushLine { - id: string; - konvaLineGroup: Konva.Group; - konvaLine: Konva.Line; - lastBrushLine: BrushLine; - - constructor(brushLine: BrushLine) { - const { id, strokeWidth, clip, color, points } = brushLine; - this.id = id; - this.konvaLineGroup = new Konva.Group({ - clip, - listening: false, - }); - this.konvaLine = new Konva.Line({ - id, - listening: false, - shadowForStrokeEnabled: false, - strokeWidth, - tension: 0, - lineCap: 'round', - lineJoin: 'round', - globalCompositeOperation: 'source-over', - stroke: rgbaColorToString(color), - points, - }); - this.konvaLineGroup.add(this.konvaLine); - this.lastBrushLine = brushLine; - } - - update(brushLine: BrushLine, force?: boolean): boolean { - if (this.lastBrushLine !== brushLine || force) { - const { points, color, clip, strokeWidth } = brushLine; - this.konvaLine.setAttrs({ - points, - stroke: rgbaColorToString(color), - clip, - strokeWidth, - }); - this.lastBrushLine = brushLine; - return true; - } else { - return false; - } - } - - destroy() { - this.konvaLineGroup.destroy(); - } -} - -export class KonvaEraserLine { - id: string; - konvaLineGroup: Konva.Group; - konvaLine: Konva.Line; - lastEraserLine: EraserLine; - - constructor(eraserLine: EraserLine) { - const { id, strokeWidth, clip, points } = eraserLine; - this.id = id; - this.konvaLineGroup = new Konva.Group({ - clip, - listening: false, - }); - this.konvaLine = new Konva.Line({ - id, - listening: false, - shadowForStrokeEnabled: false, - strokeWidth, - tension: 0, - lineCap: 'round', - lineJoin: 'round', - globalCompositeOperation: 'destination-out', - stroke: rgbaColorToString(RGBA_RED), - points, - }); - this.konvaLineGroup.add(this.konvaLine); - this.lastEraserLine = eraserLine; - } - - update(eraserLine: EraserLine, force?: boolean): boolean { - if (this.lastEraserLine !== eraserLine || force) { - const { points, clip, strokeWidth } = eraserLine; - this.konvaLine.setAttrs({ - points, - clip, - strokeWidth, - }); - this.lastEraserLine = eraserLine; - return true; - } else { - return false; - } - } - - destroy() { - this.konvaLineGroup.destroy(); - } -} - -export class KonvaRect { - id: string; - konvaRect: Konva.Rect; - lastRectShape: RectShape; - - constructor(rectShape: RectShape) { - const { id, x, y, width, height } = rectShape; - this.id = id; - const konvaRect = new Konva.Rect({ - id, - x, - y, - width, - height, - listening: false, - fill: rgbaColorToString(rectShape.color), - }); - this.konvaRect = konvaRect; - this.lastRectShape = rectShape; - } - - update(rectShape: RectShape, force?: boolean): boolean { - if (this.lastRectShape !== rectShape || force) { - const { x, y, width, height, color } = rectShape; - this.konvaRect.setAttrs({ - x, - y, - width, - height, - fill: rgbaColorToString(color), - }); - this.lastRectShape = rectShape; - return true; - } else { - return false; - } - } - - destroy() { - this.konvaRect.destroy(); - } -} - -export class KonvaImage { - id: string; - konvaImageGroup: Konva.Group; - konvaPlaceholderGroup: Konva.Group; - konvaPlaceholderRect: Konva.Rect; - konvaPlaceholderText: Konva.Text; - imageName: string | null; - konvaImage: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately - isLoading: boolean; - isError: boolean; - getImageDTO: (imageName: string) => Promise; - onLoading: () => void; - onLoad: (imageName: string, imageEl: HTMLImageElement) => void; - onError: () => void; - lastImageObject: ImageObject; - - constructor( - imageObject: ImageObject, - options: { - getImageDTO?: (imageName: string) => Promise; - onLoading?: () => void; - onLoad?: (konvaImage: Konva.Image) => void; - onError?: () => void; - } - ) { - const { getImageDTO, onLoading, onLoad, onError } = options; - const { id, width, height, x, y } = imageObject; - this.konvaImageGroup = new Konva.Group({ id, listening: false, x, y }); - this.konvaPlaceholderGroup = new Konva.Group({ listening: false }); - this.konvaPlaceholderRect = new Konva.Rect({ - fill: 'hsl(220 12% 45% / 1)', // 'base.500' - width, - height, - listening: false, - }); - this.konvaPlaceholderText = new Konva.Text({ - fill: 'hsl(220 12% 10% / 1)', // 'base.900' - width, - height, - align: 'center', - verticalAlign: 'middle', - fontFamily: '"Inter Variable", sans-serif', - fontSize: width / 16, - fontStyle: '600', - text: t('common.loadingImage', 'Loading Image'), - listening: false, - }); - - this.konvaPlaceholderGroup.add(this.konvaPlaceholderRect); - this.konvaPlaceholderGroup.add(this.konvaPlaceholderText); - this.konvaImageGroup.add(this.konvaPlaceholderGroup); - - this.id = id; - this.imageName = null; - this.konvaImage = null; - this.isLoading = false; - this.isError = false; - this.getImageDTO = getImageDTO ?? defaultGetImageDTO; - this.onLoading = function () { - this.isLoading = true; - if (!this.konvaImage) { - this.konvaPlaceholderGroup.visible(true); - this.konvaPlaceholderText.text(t('common.loadingImage', 'Loading Image')); - } - this.konvaImageGroup.visible(true); - if (onLoading) { - onLoading(); - } - }; - this.onLoad = function (imageName: string, imageEl: HTMLImageElement) { - if (this.konvaImage) { - this.konvaImage.setAttrs({ - image: imageEl, - }); - } else { - this.konvaImage = new Konva.Image({ - id: this.id, - listening: false, - image: imageEl, - width, - height, - }); - this.konvaImageGroup.add(this.konvaImage); - } - this.imageName = imageName; - this.isLoading = false; - this.isError = false; - this.konvaPlaceholderGroup.visible(false); - this.konvaImageGroup.visible(true); - - if (onLoad) { - onLoad(this.konvaImage); - } - }; - this.onError = function () { - this.imageName = null; - this.isLoading = false; - this.isError = true; - this.konvaPlaceholderGroup.visible(true); - this.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load')); - this.konvaImageGroup.visible(true); - - if (onError) { - onError(); - } - }; - this.lastImageObject = imageObject; - } - - async updateImageSource(imageName: string) { - try { - this.onLoading(); - - const imageDTO = await this.getImageDTO(imageName); - if (!imageDTO) { - this.onError(); - return; - } - const imageEl = new Image(); - imageEl.onload = () => { - this.onLoad(imageName, imageEl); - }; - imageEl.onerror = () => { - this.onError(); - }; - imageEl.id = imageName; - imageEl.src = imageDTO.image_url; - } catch { - this.onError(); - } - } - - async update(imageObject: ImageObject, force?: boolean): Promise { - if (this.lastImageObject !== imageObject || force) { - const { width, height, x, y, image } = imageObject; - if (this.lastImageObject.image.name !== image.name || force) { - await this.updateImageSource(image.name); - } - this.konvaImage?.setAttrs({ x, y, width, height }); - this.konvaPlaceholderRect.setAttrs({ width, height }); - this.konvaPlaceholderText.setAttrs({ width, height, fontSize: width / 16 }); - this.lastImageObject = imageObject; - return true; - } else { - return false; - } - } - - destroy() { - this.konvaImageGroup.destroy(); - } -} - -export class KonvaProgressImage { - id: string; - progressImageId: string | null; - konvaImageGroup: Konva.Group; - konvaImage: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately - isLoading: boolean; - isError: boolean; - - constructor(arg: { id: string }) { - const { id } = arg; - this.konvaImageGroup = new Konva.Group({ id, listening: false }); - - this.id = id; - this.progressImageId = null; - this.konvaImage = null; - this.isLoading = false; - this.isError = false; - } - - async updateImageSource( - progressImageId: string, - dataURL: string, - x: number, - y: number, - width: number, - height: number - ) { - const imageEl = new Image(); - imageEl.onload = () => { - if (this.konvaImage) { - this.konvaImage.setAttrs({ - image: imageEl, - x, - y, - width, - height, - }); - } else { - this.konvaImage = new Konva.Image({ - id: this.id, - listening: false, - image: imageEl, - x, - y, - width, - height, - }); - this.konvaImageGroup.add(this.konvaImage); - } - }; - imageEl.id = progressImageId; - imageEl.src = dataURL; - } - - destroy() { - this.konvaImageGroup.destroy(); - } -} diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts index c3b2953113d..953e3505ef5 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts @@ -1,4 +1,4 @@ -import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasV2State, Size } from 'features/controlLayers/store/types'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { isEqual, pick } from 'lodash-es'; @@ -6,7 +6,7 @@ import type { Invocation } from 'services/api/types'; export const addImageToImage = async ( g: Graph, - manager: KonvaNodeManager, + manager: CanvasManager, l2i: Invocation<'l2i'>, denoise: Invocation<'denoise_latents'>, vaeSource: Invocation<'main_model_loader' | 'sdxl_model_loader' | 'seamless' | 'vae_loader'>, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts index f578ff82028..0b4520385f2 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts @@ -1,4 +1,4 @@ -import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasV2State, Size } from 'features/controlLayers/store/types'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import type { ParameterPrecision } from 'features/parameters/types/parameterSchemas'; @@ -7,7 +7,7 @@ import type { Invocation } from 'services/api/types'; export const addInpaint = async ( g: Graph, - manager: KonvaNodeManager, + manager: CanvasManager, l2i: Invocation<'l2i'>, denoise: Invocation<'denoise_latents'>, vaeSource: Invocation<'main_model_loader' | 'sdxl_model_loader' | 'seamless' | 'vae_loader'>, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index c4b828e2750..e5c774d9533 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts @@ -1,4 +1,4 @@ -import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasV2State, Size } from 'features/controlLayers/store/types'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { getInfill } from 'features/nodes/util/graph/graphBuilderUtils'; @@ -8,7 +8,7 @@ import type { Invocation } from 'services/api/types'; export const addOutpaint = async ( g: Graph, - manager: KonvaNodeManager, + manager: CanvasManager, l2i: Invocation<'l2i'>, denoise: Invocation<'denoise_latents'>, vaeSource: Invocation<'main_model_loader' | 'sdxl_model_loader' | 'seamless' | 'vae_loader'>, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts index 86c1d66491a..55b283a7f1c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts @@ -1,5 +1,5 @@ import { deepClone } from 'common/util/deepClone'; -import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { IPAdapterEntity, Rect, RegionEntity } from 'features/controlLayers/store/types'; import { PROMPT_REGION_INVERT_TENSOR_MASK_PREFIX, @@ -27,7 +27,7 @@ import { assert } from 'tsafe'; */ export const addRegions = async ( - manager: KonvaNodeManager, + manager: CanvasManager, regions: RegionEntity[], g: Graph, bbox: Rect, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImageToImageSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImageToImageSDXLGraph.ts index 5b8bc5d5500..bbff0523f1a 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImageToImageSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildImageToImageSDXLGraph.ts @@ -1,5 +1,5 @@ import type { RootState } from 'app/store/store'; -import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { LATENTS_TO_IMAGE, @@ -30,7 +30,7 @@ import { addRegions } from './addRegions'; export const buildImageToImageSDXLGraph = async ( state: RootState, - manager: KonvaNodeManager + manager: CanvasManager ): Promise => { const { bbox, params } = state.canvasV2; const { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index ddc8114c22c..6966feef9e3 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -1,5 +1,5 @@ import type { RootState } from 'app/store/store'; -import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { CANVAS_OUTPUT, @@ -34,7 +34,7 @@ import { assert } from 'tsafe'; import { addRegions } from './addRegions'; -export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager): Promise => { +export const buildSD1Graph = async (state: RootState, manager: CanvasManager): Promise => { const generationMode = manager.getGenerationMode(); const { bbox, params } = state.canvasV2; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index f3eedd42c09..9177e9e745d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -1,5 +1,5 @@ import type { RootState } from 'app/store/store'; -import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { CANVAS_OUTPUT, @@ -33,7 +33,7 @@ import { assert } from 'tsafe'; import { addRegions } from './addRegions'; -export const buildSDXLGraph = async (state: RootState, manager: KonvaNodeManager): Promise => { +export const buildSDXLGraph = async (state: RootState, manager: CanvasManager): Promise => { const generationMode = manager.getGenerationMode(); const { bbox, params } = state.canvasV2; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildTextToImageSD1SD2Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildTextToImageSD1SD2Graph.ts index 2c6f95b4fad..352c1678635 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildTextToImageSD1SD2Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildTextToImageSD1SD2Graph.ts @@ -1,5 +1,5 @@ import type { RootState } from 'app/store/store'; -import type { KonvaNodeManager } from 'features/controlLayers/konva/KonvaNodeManager'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { CLIP_SKIP, @@ -31,7 +31,7 @@ import { assert } from 'tsafe'; import { addRegions } from './addRegions'; -export const buildTextToImageSD1SD2Graph = async (state: RootState, manager: KonvaNodeManager): Promise => { +export const buildTextToImageSD1SD2Graph = async (state: RootState, manager: CanvasManager): Promise => { const { bbox, params } = state.canvasV2; const { From 64e77578727ad4d81c7a5e05389bf3f0e5d116d5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 4 Jul 2024 21:28:43 +1000 Subject: [PATCH 165/678] tidy(ui): organise files --- .../components/StageComponent.tsx | 18 +- .../controlLayers/konva/CanvasManager.ts | 214 +++--------------- .../konva/{StateApi.ts => CanvasStateApi.ts} | 105 ++++++--- .../src/features/controlLayers/konva/util.ts | 184 ++++++++++++++- 4 files changed, 301 insertions(+), 220 deletions(-) rename invokeai/frontend/web/src/features/controlLayers/konva/{StateApi.ts => CanvasStateApi.ts} (77%) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index 8604d951421..6efb54b9caa 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -1,6 +1,5 @@ import { Flex } from '@invoke-ai/ui-library'; import { logger } from 'app/logging/logger'; -import { $isDebugging } from 'app/store/nanostores/isDebugging'; import { useAppStore } from 'app/store/storeHooks'; import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay'; import { CanvasManager, setCanvasManager } from 'features/controlLayers/konva/CanvasManager'; @@ -9,7 +8,7 @@ import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react'; import { useDevicePixelRatio } from 'use-device-pixel-ratio'; import { v4 as uuidv4 } from 'uuid'; -const log = logger('konva'); +const log = logger('canvas'); // This will log warnings when layers > 5 - maybe use `import.meta.env.MODE === 'development'` instead? Konva.showWarnings = false; @@ -19,23 +18,14 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, const dpr = useDevicePixelRatio({ round: false }); useLayoutEffect(() => { - /** - * Logs a message to the console if debugging is enabled. - */ - const logIfDebugging = (message: string) => { - if ($isDebugging.get()) { - log.debug(message); - } - }; - - logIfDebugging('Initializing renderer'); + log.debug('Initializing renderer'); if (!container) { // Nothing to clean up - logIfDebugging('No stage container, skipping initialization'); + log.debug('No stage container, skipping initialization'); return () => {}; } - const manager = new CanvasManager(stage, container, store, logIfDebugging); + const manager = new CanvasManager(stage, container, store); setCanvasManager(manager); const cleanup = manager.initialize(); return cleanup; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 6d5fbcf7c48..56af6548fe5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -1,9 +1,14 @@ import type { Store } from '@reduxjs/toolkit'; +import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; -import { getImageDataTransparency } from 'common/util/arrayBuffer'; +import { + getGenerationMode, + getImageSourceImage, + getInpaintMaskImage, + getRegionMaskImage, +} from 'features/controlLayers/konva/util'; import { $lastProgressEvent, $shouldShowStagedImage } from 'features/controlLayers/store/canvasV2Slice'; -import type { CanvasV2State, GenerationMode, Rect } from 'features/controlLayers/store/types'; -import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers'; +import type { CanvasV2State } from 'features/controlLayers/store/types'; import type Konva from 'konva'; import { atom } from 'nanostores'; import { getImageDTO as defaultGetImageDTO, uploadImage as defaultUploadImage } from 'services/api/endpoints/images'; @@ -19,10 +24,11 @@ import { CanvasLayer } from './CanvasLayer'; import { CanvasPreview } from './CanvasPreview'; import { CanvasRegion } from './CanvasRegion'; import { CanvasStagingArea } from './CanvasStagingArea'; +import { CanvasStateApi } from './CanvasStateApi'; import { CanvasTool } from './CanvasTool'; import { setStageEventHandlers } from './events'; -import { StateApi } from './StateApi'; -import { konvaNodeToBlob, konvaNodeToImageData, previewBlob } from './util'; + +const log = logger('canvas'); type Util = { getImageDTO: (imageName: string) => Promise; @@ -52,27 +58,24 @@ export class CanvasManager { regions: Map; inpaintMask: CanvasInpaintMask; util: Util; - stateApi: StateApi; + stateApi: CanvasStateApi; preview: CanvasPreview; background: CanvasBackground; private store: Store; private isFirstRender: boolean; private prevState: CanvasV2State; - private log: (message: string) => void; constructor( stage: Konva.Stage, container: HTMLDivElement, store: Store, - log: (message: string) => void, getImageDTO: Util['getImageDTO'] = defaultGetImageDTO, uploadImage: Util['uploadImage'] = defaultUploadImage ) { - this.log = log; this.stage = stage; this.container = container; this.store = store; - this.stateApi = new StateApi(this.store, this.log); + this.stateApi = new CanvasStateApi(this.store); this.prevState = this.stateApi.getState(); this.isFirstRender = true; @@ -207,7 +210,7 @@ export class CanvasManager { const state = this.stateApi.getState(); if (this.prevState === state && !this.isFirstRender) { - this.log('No changes detected, skipping render'); + log.debug('No changes detected, skipping render'); return; } @@ -217,7 +220,7 @@ export class CanvasManager { state.tool.selected !== this.prevState.tool.selected || state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id ) { - this.log('Rendering layers'); + log.debug('Rendering layers'); this.renderLayers(); } @@ -228,7 +231,7 @@ export class CanvasManager { state.tool.selected !== this.prevState.tool.selected || state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id ) { - this.log('Rendering regions'); + log.debug('Rendering regions'); this.renderRegions(); } @@ -239,7 +242,7 @@ export class CanvasManager { state.tool.selected !== this.prevState.tool.selected || state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id ) { - this.log('Rendering inpaint mask'); + log.debug('Rendering inpaint mask'); this.renderInpaintMask(); } @@ -248,12 +251,12 @@ export class CanvasManager { state.controlAdapters.entities !== this.prevState.controlAdapters.entities || state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id ) { - this.log('Rendering control adapters'); + log.debug('Rendering control adapters'); this.renderControlAdapters(); } if (this.isFirstRender || state.document !== this.prevState.document) { - this.log('Rendering document bounds overlay'); + log.debug('Rendering document bounds overlay'); this.preview.documentSizeOverlay.render(); } @@ -262,7 +265,7 @@ export class CanvasManager { state.bbox !== this.prevState.bbox || state.tool.selected !== this.prevState.tool.selected ) { - this.log('Rendering generation bbox'); + log.debug('Rendering generation bbox'); this.preview.bbox.render(); } @@ -272,12 +275,12 @@ export class CanvasManager { state.controlAdapters !== this.prevState.controlAdapters || state.regions !== this.prevState.regions ) { - // this.log('Updating entity bboxes'); + // log.debug('Updating entity bboxes'); // debouncedUpdateBboxes(stage, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions, onBboxChanged); } if (this.isFirstRender || state.stagingArea !== this.prevState.stagingArea) { - this.log('Rendering staging area'); + log.debug('Rendering staging area'); this.preview.stagingArea.render(); } @@ -289,7 +292,7 @@ export class CanvasManager { state.inpaintMask !== this.prevState.inpaintMask || state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id ) { - this.log('Arranging entities'); + log.debug('Arranging entities'); this.arrangeEntities(); } @@ -301,7 +304,7 @@ export class CanvasManager { }; initialize = () => { - this.log('Initializing renderer'); + log.debug('Initializing renderer'); this.stage.container(this.container); const cleanupListeners = setStageEventHandlers(this); @@ -316,18 +319,18 @@ export class CanvasManager { // When we this flag, we need to render the staging area $shouldShowStagedImage.subscribe((shouldShowStagedImage, prevShouldShowStagedImage) => { - this.log('Rendering staging area'); + log.debug('Rendering staging area'); if (shouldShowStagedImage !== prevShouldShowStagedImage) { this.preview.stagingArea.render(); } }); $lastProgressEvent.subscribe(() => { - this.log('Rendering staging area'); + log.debug('Rendering staging area'); this.preview.stagingArea.render(); }); - this.log('First render of konva stage'); + log.debug('First render of konva stage'); // On first render, the document should be fit to the stage. this.preview.documentSizeOverlay.render(); this.preview.documentSizeOverlay.fitToStage(); @@ -335,7 +338,7 @@ export class CanvasManager { this.render(); return () => { - this.log('Cleaning up konva renderer'); + log.debug('Cleaning up konva renderer'); unsubscribeRenderer(); cleanupListeners(); $shouldShowStagedImage.off(); @@ -343,164 +346,19 @@ export class CanvasManager { }; }; - getInpaintMaskLayerClone(): Konva.Layer { - const layerClone = this.inpaintMask.layer.clone(); - const objectGroupClone = this.inpaintMask.group.clone(); - - layerClone.destroyChildren(); - layerClone.add(objectGroupClone); - - objectGroupClone.opacity(1); - objectGroupClone.cache(); - - return layerClone; - } - - getRegionMaskLayerClone(arg: { id: string }): Konva.Layer { - const { id } = arg; - - const canvasRegion = this.regions.get(id); - assert(canvasRegion, `Canvas region with id ${id} not found`); - - const layerClone = canvasRegion.layer.clone(); - const objectGroupClone = canvasRegion.group.clone(); - - layerClone.destroyChildren(); - layerClone.add(objectGroupClone); - - objectGroupClone.opacity(1); - objectGroupClone.cache(); - - return layerClone; + getGenerationMode() { + return getGenerationMode({ manager: this }); } - getCompositeLayerStageClone(): Konva.Stage { - const layersState = this.stateApi.getLayersState(); - - const stageClone = this.stage.clone(); - - stageClone.scaleX(1); - stageClone.scaleY(1); - stageClone.x(0); - stageClone.y(0); - - const validLayers = layersState.entities.filter(isValidLayer); - - // Konva bug (?) - when iterating over the array returned from `stage.getLayers()`, if you destroy a layer, the array - // is mutated in-place and the next iteration will skip the next layer. To avoid this, we first collect the layers - // to delete in a separate array and then destroy them. - // TODO(psyche): Maybe report this? - const toDelete: Konva.Layer[] = []; - - for (const konvaLayer of stageClone.getLayers()) { - const layer = validLayers.find((l) => l.id === konvaLayer.id()); - if (!layer) { - toDelete.push(konvaLayer); - } - } - - for (const konvaLayer of toDelete) { - konvaLayer.destroy(); - } - - return stageClone; + getRegionMaskImage(arg: Omit[0], 'manager'>) { + return getRegionMaskImage({ ...arg, manager: this }); } - getGenerationMode(): GenerationMode { - const { x, y, width, height } = this.stateApi.getBbox(); - const inpaintMaskLayer = this.getInpaintMaskLayerClone(); - const inpaintMaskImageData = konvaNodeToImageData(inpaintMaskLayer, { x, y, width, height }); - const inpaintMaskTransparency = getImageDataTransparency(inpaintMaskImageData); - const compositeLayer = this.getCompositeLayerStageClone(); - const compositeLayerImageData = konvaNodeToImageData(compositeLayer, { x, y, width, height }); - const compositeLayerTransparency = getImageDataTransparency(compositeLayerImageData); - if (compositeLayerTransparency.isPartiallyTransparent) { - if (compositeLayerTransparency.isFullyTransparent) { - return 'txt2img'; - } - return 'outpaint'; - } else { - if (!inpaintMaskTransparency.isFullyTransparent) { - return 'inpaint'; - } - return 'img2img'; - } + getInpaintMaskImage(arg: Omit[0], 'manager'>) { + return getInpaintMaskImage({ ...arg, manager: this }); } - async getRegionMaskImage(arg: { id: string; bbox?: Rect; preview?: boolean }): Promise { - const { id, bbox, preview = false } = arg; - const region = this.stateApi.getRegionsState().entities.find((entity) => entity.id === id); - assert(region, `Region entity state with id ${id} not found`); - - // if (region.imageCache) { - // const imageDTO = await this.util.getImageDTO(region.imageCache.name); - // if (imageDTO) { - // return imageDTO; - // } - // } - - const layerClone = this.getRegionMaskLayerClone({ id }); - const blob = await konvaNodeToBlob(layerClone, bbox); - - if (preview) { - previewBlob(blob, `region ${region.id} mask`); - } - - layerClone.destroy(); - - const imageDTO = await this.util.uploadImage(blob, `${region.id}_mask.png`, 'mask', true); - this.stateApi.onRegionMaskImageCached(region.id, imageDTO); - return imageDTO; - } - - async getInpaintMaskImage(arg: { bbox?: Rect; preview?: boolean }): Promise { - const { bbox, preview = false } = arg; - // const inpaintMask = this.stateApi.getInpaintMaskState(); - - // if (inpaintMask.imageCache) { - // const imageDTO = await this.util.getImageDTO(inpaintMask.imageCache.name); - // if (imageDTO) { - // return imageDTO; - // } - // } - - const layerClone = this.getInpaintMaskLayerClone(); - const blob = await konvaNodeToBlob(layerClone, bbox); - - if (preview) { - previewBlob(blob, 'inpaint mask'); - } - - layerClone.destroy(); - - const imageDTO = await this.util.uploadImage(blob, 'inpaint_mask.png', 'mask', true); - this.stateApi.onInpaintMaskImageCached(imageDTO); - return imageDTO; - } - - async getImageSourceImage(arg: { bbox?: Rect; preview?: boolean }): Promise { - const { bbox, preview = false } = arg; - // const { imageCache } = this.stateApi.getLayersState(); - - // if (imageCache) { - // const imageDTO = await this.util.getImageDTO(imageCache.name); - // if (imageDTO) { - // return imageDTO; - // } - // } - - const stageClone = this.getCompositeLayerStageClone(); - - const blob = await konvaNodeToBlob(stageClone, bbox); - - if (preview) { - previewBlob(blob, 'image source'); - } - - stageClone.destroy(); - - const imageDTO = await this.util.uploadImage(blob, 'base_layer.png', 'general', true); - this.stateApi.onLayerImageCached(imageDTO); - return imageDTO; + getImageSourceImage(arg: Omit[0], 'manager'>) { + return getImageSourceImage({ ...arg, manager: this }); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/StateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts similarity index 77% rename from invokeai/frontend/web/src/features/controlLayers/konva/StateApi.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index e29b79b572a..083a882c442 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/StateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -1,20 +1,71 @@ -import { $alt, $ctrl, $meta, $shift } from "@invoke-ai/ui-library"; -import type { Store } from "@reduxjs/toolkit"; -import type { RootState } from "app/store/store"; -import { $isDrawing, $isMouseDown, $lastAddedPoint, $lastCursorPos, $lastMouseDownPos, $lastProgressEvent, $shouldShowStagedImage, $spaceKey, $stageAttrs, bboxChanged, brushWidthChanged, caBboxChanged, caTranslated, eraserWidthChanged, imBboxChanged, imBrushLineAdded, imEraserLineAdded, imImageCacheChanged, imLinePointAdded, imRectAdded, imScaled, imTranslated, layerBboxChanged, layerBrushLineAdded, layerEraserLineAdded, layerImageCacheChanged, layerLinePointAdded, layerRectAdded, layerScaled, layerTranslated, rgBboxChanged, rgBrushLineAdded, rgEraserLineAdded, rgImageCacheChanged, rgLinePointAdded, rgRectAdded, rgScaled, rgTranslated, toolBufferChanged, toolChanged } from "features/controlLayers/store/canvasV2Slice"; -import type { BboxChangedArg, BrushLineAddedArg, CanvasEntity, EraserLineAddedArg, PointAddedToLineArg, PosChangedArg, RectShapeAddedArg, ScaleChangedArg, Tool } from "features/controlLayers/store/types"; -import type { IRect } from "konva/lib/types"; -import type { RgbaColor } from "react-colorful"; -import type { ImageDTO } from "services/api/types"; +import { $alt, $ctrl, $meta, $shift } from '@invoke-ai/ui-library'; +import type { Store } from '@reduxjs/toolkit'; +import { logger } from 'app/logging/logger'; +import type { RootState } from 'app/store/store'; +import { + $isDrawing, + $isMouseDown, + $lastAddedPoint, + $lastCursorPos, + $lastMouseDownPos, + $lastProgressEvent, + $shouldShowStagedImage, + $spaceKey, + $stageAttrs, + bboxChanged, + brushWidthChanged, + caBboxChanged, + caTranslated, + eraserWidthChanged, + imBboxChanged, + imBrushLineAdded, + imEraserLineAdded, + imImageCacheChanged, + imLinePointAdded, + imRectAdded, + imScaled, + imTranslated, + layerBboxChanged, + layerBrushLineAdded, + layerEraserLineAdded, + layerImageCacheChanged, + layerLinePointAdded, + layerRectAdded, + layerScaled, + layerTranslated, + rgBboxChanged, + rgBrushLineAdded, + rgEraserLineAdded, + rgImageCacheChanged, + rgLinePointAdded, + rgRectAdded, + rgScaled, + rgTranslated, + toolBufferChanged, + toolChanged, +} from 'features/controlLayers/store/canvasV2Slice'; +import type { + BboxChangedArg, + BrushLineAddedArg, + CanvasEntity, + EraserLineAddedArg, + PointAddedToLineArg, + PosChangedArg, + RectShapeAddedArg, + ScaleChangedArg, + Tool, +} from 'features/controlLayers/store/types'; +import type { IRect } from 'konva/lib/types'; +import type { RgbaColor } from 'react-colorful'; +import type { ImageDTO } from 'services/api/types'; +const log = logger('canvas'); -export class StateApi { +export class CanvasStateApi { private store: Store; - private log: (message: string) => void; - constructor(store: Store, log: (message: string) => void) { + constructor(store: Store) { this.store = store; - this.log = log; } // Reminder - use arrow functions to avoid binding issues @@ -23,7 +74,7 @@ export class StateApi { }; onPosChanged = (arg: PosChangedArg, entityType: CanvasEntity['type']) => { - this.log('onPosChanged'); + log.debug('onPosChanged'); if (entityType === 'layer') { this.store.dispatch(layerTranslated(arg)); } else if (entityType === 'control_adapter') { @@ -35,7 +86,7 @@ export class StateApi { } }; onScaleChanged = (arg: ScaleChangedArg, entityType: CanvasEntity['type']) => { - this.log('onScaleChanged'); + log.debug('onScaleChanged'); if (entityType === 'layer') { this.store.dispatch(layerScaled(arg)); } else if (entityType === 'inpaint_mask') { @@ -45,7 +96,7 @@ export class StateApi { } }; onBboxChanged = (arg: BboxChangedArg, entityType: CanvasEntity['type']) => { - this.log('Entity bbox changed'); + log.debug('Entity bbox changed'); if (entityType === 'layer') { this.store.dispatch(layerBboxChanged(arg)); } else if (entityType === 'control_adapter') { @@ -57,7 +108,7 @@ export class StateApi { } }; onBrushLineAdded = (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => { - this.log('Brush line added'); + log.debug('Brush line added'); if (entityType === 'layer') { this.store.dispatch(layerBrushLineAdded(arg)); } else if (entityType === 'regional_guidance') { @@ -67,7 +118,7 @@ export class StateApi { } }; onEraserLineAdded = (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => { - this.log('Eraser line added'); + log.debug('Eraser line added'); if (entityType === 'layer') { this.store.dispatch(layerEraserLineAdded(arg)); } else if (entityType === 'regional_guidance') { @@ -77,7 +128,7 @@ export class StateApi { } }; onPointAddedToLine = (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => { - this.log('Point added to line'); + log.debug('Point added to line'); if (entityType === 'layer') { this.store.dispatch(layerLinePointAdded(arg)); } else if (entityType === 'regional_guidance') { @@ -87,7 +138,7 @@ export class StateApi { } }; onRectShapeAdded = (arg: RectShapeAddedArg, entityType: CanvasEntity['type']) => { - this.log('Rect shape added'); + log.debug('Rect shape added'); if (entityType === 'layer') { this.store.dispatch(layerRectAdded(arg)); } else if (entityType === 'regional_guidance') { @@ -97,35 +148,35 @@ export class StateApi { } }; onBboxTransformed = (bbox: IRect) => { - this.log('Generation bbox transformed'); + log.debug('Generation bbox transformed'); this.store.dispatch(bboxChanged(bbox)); }; onBrushWidthChanged = (width: number) => { - this.log('Brush width changed'); + log.debug('Brush width changed'); this.store.dispatch(brushWidthChanged(width)); }; onEraserWidthChanged = (width: number) => { - this.log('Eraser width changed'); + log.debug('Eraser width changed'); this.store.dispatch(eraserWidthChanged(width)); }; onRegionMaskImageCached = (id: string, imageDTO: ImageDTO) => { - this.log('Region mask image cached'); + log.debug('Region mask image cached'); this.store.dispatch(rgImageCacheChanged({ id, imageDTO })); }; onInpaintMaskImageCached = (imageDTO: ImageDTO) => { - this.log('Inpaint mask image cached'); + log.debug('Inpaint mask image cached'); this.store.dispatch(imImageCacheChanged({ imageDTO })); }; onLayerImageCached = (imageDTO: ImageDTO) => { - this.log('Layer image cached'); + log.debug('Layer image cached'); this.store.dispatch(layerImageCacheChanged({ imageDTO })); }; setTool = (tool: Tool) => { - this.log('Tool selection changed'); + log.debug('Tool selection changed'); this.store.dispatch(toolChanged(tool)); }; setToolBuffer = (toolBuffer: Tool | null) => { - this.log('Tool buffer changed'); + log.debug('Tool buffer changed'); this.store.dispatch(toolBufferChanged(toolBuffer)); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index ed4f70a6bb0..912cc168c28 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -1,3 +1,5 @@ +import { getImageDataTransparency } from 'common/util/arrayBuffer'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CA_LAYER_NAME, INPAINT_MASK_LAYER_ID, @@ -11,10 +13,12 @@ import { RG_LAYER_NAME, RG_LAYER_RECT_SHAPE_NAME, } from 'features/controlLayers/konva/naming'; -import type { Rect, RgbaColor } from 'features/controlLayers/store/types'; +import type { GenerationMode, Rect, RgbaColor } from 'features/controlLayers/store/types'; +import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers'; import Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; import type { Vector2d } from 'konva/lib/types'; +import type { ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; /** @@ -282,3 +286,181 @@ export const previewBlob = async (blob: Blob, label?: string) => { } w.document.write(``); }; + +export function getInpaintMaskLayerClone(arg: { manager: CanvasManager }): Konva.Layer { + const { manager } = arg; + const layerClone = manager.inpaintMask.layer.clone(); + const objectGroupClone = manager.inpaintMask.group.clone(); + + layerClone.destroyChildren(); + layerClone.add(objectGroupClone); + + objectGroupClone.opacity(1); + objectGroupClone.cache(); + + return layerClone; +} + +export function getRegionMaskLayerClone(arg: { manager: CanvasManager; id: string }): Konva.Layer { + const { id, manager } = arg; + + const canvasRegion = manager.regions.get(id); + assert(canvasRegion, `Canvas region with id ${id} not found`); + + const layerClone = canvasRegion.layer.clone(); + const objectGroupClone = canvasRegion.group.clone(); + + layerClone.destroyChildren(); + layerClone.add(objectGroupClone); + + objectGroupClone.opacity(1); + objectGroupClone.cache(); + + return layerClone; +} + +export function getCompositeLayerStageClone(arg: { manager: CanvasManager }): Konva.Stage { + const { manager } = arg; + + const layersState = manager.stateApi.getLayersState(); + + const stageClone = manager.stage.clone(); + + stageClone.scaleX(1); + stageClone.scaleY(1); + stageClone.x(0); + stageClone.y(0); + + const validLayers = layersState.entities.filter(isValidLayer); + + // Konva bug (?) - when iterating over the array returned from `stage.getLayers()`, if you destroy a layer, the array + // is mutated in-place and the next iteration will skip the next layer. To avoid this, we first collect the layers + // to delete in a separate array and then destroy them. + // TODO(psyche): Maybe report this? + const toDelete: Konva.Layer[] = []; + + for (const konvaLayer of stageClone.getLayers()) { + const layer = validLayers.find((l) => l.id === konvaLayer.id()); + if (!layer) { + toDelete.push(konvaLayer); + } + } + + for (const konvaLayer of toDelete) { + konvaLayer.destroy(); + } + + return stageClone; +} + +export function getGenerationMode(arg: { manager: CanvasManager }): GenerationMode { + const { manager } = arg; + const { x, y, width, height } = manager.stateApi.getBbox(); + const inpaintMaskLayer = getInpaintMaskLayerClone(arg); + const inpaintMaskImageData = konvaNodeToImageData(inpaintMaskLayer, { x, y, width, height }); + const inpaintMaskTransparency = getImageDataTransparency(inpaintMaskImageData); + const compositeLayer = getCompositeLayerStageClone(arg); + const compositeLayerImageData = konvaNodeToImageData(compositeLayer, { x, y, width, height }); + const compositeLayerTransparency = getImageDataTransparency(compositeLayerImageData); + if (compositeLayerTransparency.isPartiallyTransparent) { + if (compositeLayerTransparency.isFullyTransparent) { + return 'txt2img'; + } + return 'outpaint'; + } else { + if (!inpaintMaskTransparency.isFullyTransparent) { + return 'inpaint'; + } + return 'img2img'; + } +} + +export async function getRegionMaskImage(arg: { + manager: CanvasManager; + id: string; + bbox?: Rect; + preview?: boolean; +}): Promise { + const { manager, id, bbox, preview = false } = arg; + const region = manager.stateApi.getRegionsState().entities.find((entity) => entity.id === id); + assert(region, `Region entity state with id ${id} not found`); + + // if (region.imageCache) { + // const imageDTO = await this.util.getImageDTO(region.imageCache.name); + // if (imageDTO) { + // return imageDTO; + // } + // } + + const layerClone = getRegionMaskLayerClone({ id, manager }); + const blob = await konvaNodeToBlob(layerClone, bbox); + + if (preview) { + previewBlob(blob, `region ${region.id} mask`); + } + + layerClone.destroy(); + + const imageDTO = await manager.util.uploadImage(blob, `${region.id}_mask.png`, 'mask', true); + manager.stateApi.onRegionMaskImageCached(region.id, imageDTO); + return imageDTO; +} + +export async function getInpaintMaskImage(arg: { + manager: CanvasManager; + bbox?: Rect; + preview?: boolean; +}): Promise { + const { manager, bbox, preview = false } = arg; + // const inpaintMask = this.stateApi.getInpaintMaskState(); + + // if (inpaintMask.imageCache) { + // const imageDTO = await this.util.getImageDTO(inpaintMask.imageCache.name); + // if (imageDTO) { + // return imageDTO; + // } + // } + + const layerClone = getInpaintMaskLayerClone({ manager }); + const blob = await konvaNodeToBlob(layerClone, bbox); + + if (preview) { + previewBlob(blob, 'inpaint mask'); + } + + layerClone.destroy(); + + const imageDTO = await manager.util.uploadImage(blob, 'inpaint_mask.png', 'mask', true); + manager.stateApi.onInpaintMaskImageCached(imageDTO); + return imageDTO; +} + +export async function getImageSourceImage(arg: { + manager: CanvasManager; + bbox?: Rect; + preview?: boolean; +}): Promise { + const { manager, bbox, preview = false } = arg; + // const { imageCache } = this.stateApi.getLayersState(); + + // if (imageCache) { + // const imageDTO = await this.util.getImageDTO(imageCache.name); + // if (imageDTO) { + // return imageDTO; + // } + // } + + const stageClone = getCompositeLayerStageClone({ manager }); + + const blob = await konvaNodeToBlob(stageClone, bbox); + + if (preview) { + previewBlob(blob, 'image source'); + } + + stageClone.destroy(); + + const imageDTO = await manager.util.uploadImage(blob, 'base_layer.png', 'general', true); + manager.stateApi.onLayerImageCached(imageDTO); + return imageDTO; +} From 7c1afb6493521e91ef58d82c219eb047af70c0f6 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 5 Jul 2024 00:41:26 +1000 Subject: [PATCH 166/678] perf(ui): buffered drawing (wip) --- .../components/StageComponent.tsx | 1 + .../controlLayers/konva/CanvasInpaintMask.ts | 130 +++++--- .../controlLayers/konva/CanvasLayer.ts | 176 +++++++---- .../controlLayers/konva/CanvasManager.ts | 20 +- .../controlLayers/konva/CanvasRegion.ts | 129 +++++--- .../controlLayers/konva/CanvasStateApi.ts | 44 ++- .../features/controlLayers/konva/events.ts | 284 ++++++++++++------ .../controlLayers/store/canvasV2Slice.ts | 6 + .../store/inpaintMaskReducers.ts | 20 +- .../controlLayers/store/layersReducers.ts | 24 ++ .../controlLayers/store/regionsReducers.ts | 31 +- .../src/features/controlLayers/store/types.ts | 14 + 12 files changed, 614 insertions(+), 265 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index 6efb54b9caa..1db97cce034 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -27,6 +27,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, const manager = new CanvasManager(stage, container, store); setCanvasManager(manager); + console.log(manager); const cleanup = manager.initialize(); return cleanup; }, [asPreview, container, stage, store]); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts index 59ef9e77594..b22c66a6fda 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts @@ -6,7 +6,8 @@ import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox'; import { getObjectGroupId, INPAINT_MASK_LAYER_ID } from 'features/controlLayers/konva/naming'; import { mapId } from 'features/controlLayers/konva/util'; -import { type InpaintMaskEntity, isDrawingTool } from 'features/controlLayers/store/types'; +import type { BrushLine, EraserLine, InpaintMaskEntity } from 'features/controlLayers/store/types'; +import { isDrawingTool, RGBA_RED } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; @@ -20,8 +21,10 @@ export class CanvasInpaintMask { compositingRect: Konva.Rect; transformer: Konva.Transformer; objects: Map; + private drawingBuffer: BrushLine | EraserLine | null; + private prevInpaintMaskState: InpaintMaskEntity; - constructor(manager: CanvasManager) { + constructor(entity: InpaintMaskEntity, manager: CanvasManager) { this.id = INPAINT_MASK_LAYER_ID; this.manager = manager; this.layer = new Konva.Layer({ id: INPAINT_MASK_LAYER_ID }); @@ -56,12 +59,42 @@ export class CanvasInpaintMask { this.compositingRect = new Konva.Rect({ listening: false }); this.group.add(this.compositingRect); this.objects = new Map(); + this.drawingBuffer = null; + this.prevInpaintMaskState = entity; } destroy(): void { this.layer.destroy(); } + getDrawingBuffer() { + return this.drawingBuffer; + } + + async setDrawingBuffer(obj: BrushLine | EraserLine | null) { + this.drawingBuffer = obj; + if (this.drawingBuffer) { + if (this.drawingBuffer.type === 'brush_line') { + this.drawingBuffer.color = RGBA_RED; + } + + await this.renderObject(this.drawingBuffer, true); + this.updateGroup(true, this.prevInpaintMaskState); + } + } + + finalizeDrawingBuffer() { + if (!this.drawingBuffer) { + return; + } + if (this.drawingBuffer.type === 'brush_line') { + this.manager.stateApi.onBrushLineAdded2({ id: this.id, brushLine: this.drawingBuffer }, 'inpaint_mask'); + } else if (this.drawingBuffer.type === 'eraser_line') { + this.manager.stateApi.onEraserLineAdded2({ id: this.id, eraserLine: this.drawingBuffer }, 'inpaint_mask'); + } + this.setDrawingBuffer(null); + } + async render(inpaintMaskState: InpaintMaskEntity) { // Update the layer's position and listening state this.group.setAttrs({ @@ -84,51 +117,62 @@ export class CanvasInpaintMask { } for (const obj of inpaintMaskState.objects) { - if (obj.type === 'brush_line') { - let brushLine = this.objects.get(obj.id); - assert(brushLine instanceof CanvasBrushLine || brushLine === undefined); - - if (!brushLine) { - brushLine = new CanvasBrushLine(obj); - this.objects.set(brushLine.id, brushLine); - this.objectsGroup.add(brushLine.konvaLineGroup); - didDraw = true; - } else { - if (brushLine.update(obj)) { - didDraw = true; - } + didDraw = await this.renderObject(obj); + } + + this.updateGroup(didDraw, inpaintMaskState); + this.prevInpaintMaskState = inpaintMaskState; + } + + private async renderObject(obj: InpaintMaskEntity['objects'][number], force = false): Promise { + if (obj.type === 'brush_line') { + let brushLine = this.objects.get(obj.id); + assert(brushLine instanceof CanvasBrushLine || brushLine === undefined); + + if (!brushLine) { + brushLine = new CanvasBrushLine(obj); + this.objects.set(brushLine.id, brushLine); + this.objectsGroup.add(brushLine.konvaLineGroup); + return true; + } else { + if (brushLine.update(obj, force)) { + return true; } - } else if (obj.type === 'eraser_line') { - let eraserLine = this.objects.get(obj.id); - assert(eraserLine instanceof CanvasEraserLine || eraserLine === undefined); - - if (!eraserLine) { - eraserLine = new CanvasEraserLine(obj); - this.objects.set(eraserLine.id, eraserLine); - this.objectsGroup.add(eraserLine.konvaLineGroup); - didDraw = true; - } else { - if (eraserLine.update(obj)) { - didDraw = true; - } + } + } else if (obj.type === 'eraser_line') { + let eraserLine = this.objects.get(obj.id); + assert(eraserLine instanceof CanvasEraserLine || eraserLine === undefined); + + if (!eraserLine) { + eraserLine = new CanvasEraserLine(obj); + this.objects.set(eraserLine.id, eraserLine); + this.objectsGroup.add(eraserLine.konvaLineGroup); + return true; + } else { + if (eraserLine.update(obj, force)) { + return true; } - } else if (obj.type === 'rect_shape') { - let rect = this.objects.get(obj.id); - assert(rect instanceof CanvasRect || rect === undefined); - - if (!rect) { - rect = new CanvasRect(obj); - this.objects.set(rect.id, rect); - this.objectsGroup.add(rect.konvaRect); - didDraw = true; - } else { - if (rect.update(obj)) { - didDraw = true; - } + } + } else if (obj.type === 'rect_shape') { + let rect = this.objects.get(obj.id); + assert(rect instanceof CanvasRect || rect === undefined); + + if (!rect) { + rect = new CanvasRect(obj); + this.objects.set(rect.id, rect); + this.objectsGroup.add(rect.konvaRect); + return true; + } else { + if (rect.update(obj, force)) { + return true; } } } + return false; + } + + updateGroup(didDraw: boolean, inpaintMaskState: InpaintMaskEntity) { // Only update layer visibility if it has changed. if (this.layer.visible() !== inpaintMaskState.isEnabled) { this.layer.visible(inpaintMaskState.isEnabled); @@ -155,10 +199,6 @@ export class CanvasInpaintMask { }); } - this.updateGroup(didDraw); - } - - updateGroup(didDraw: boolean) { const isSelected = this.manager.stateApi.getIsSelected(this.id); const selectedTool = this.manager.stateApi.getToolState().selected; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 3288e710a48..f2a861a07d6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -5,7 +5,8 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; import { getObjectGroupId } from 'features/controlLayers/konva/naming'; import { mapId } from 'features/controlLayers/konva/util'; -import { isDrawingTool, type LayerEntity } from 'features/controlLayers/store/types'; +import type { BrushLine, EraserLine, LayerEntity } from 'features/controlLayers/store/types'; +import { isDrawingTool } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; @@ -15,8 +16,11 @@ export class CanvasLayer { manager: CanvasManager; layer: Konva.Layer; group: Konva.Group; + objectsGroup: Konva.Group; transformer: Konva.Transformer; objects: Map; + private drawingBuffer: BrushLine | EraserLine | null; + private prevLayerState: LayerEntity; constructor(entity: LayerEntity, manager: CanvasManager) { this.id = entity.id; @@ -30,6 +34,8 @@ export class CanvasLayer { id: getObjectGroupId(this.layer.id(), uuidv4()), listening: false, }); + this.objectsGroup = new Konva.Group({}); + this.group.add(this.objectsGroup); this.layer.add(this.group); this.transformer = new Konva.Transformer({ @@ -52,12 +58,40 @@ export class CanvasLayer { this.layer.add(this.transformer); this.objects = new Map(); + this.drawingBuffer = null; + this.prevLayerState = entity; } destroy(): void { this.layer.destroy(); } + getDrawingBuffer() { + return this.drawingBuffer; + } + + async setDrawingBuffer(obj: BrushLine | EraserLine | null) { + if (obj) { + this.drawingBuffer = obj; + await this.renderObject(this.drawingBuffer, true); + this.updateGroup(true, this.prevLayerState); + } else { + this.drawingBuffer = null; + } + } + + finalizeDrawingBuffer() { + if (!this.drawingBuffer) { + return; + } + if (this.drawingBuffer.type === 'brush_line') { + this.manager.stateApi.onBrushLineAdded2({ id: this.id, brushLine: this.drawingBuffer }, 'layer'); + } else if (this.drawingBuffer.type === 'eraser_line') { + this.manager.stateApi.onEraserLineAdded2({ id: this.id, eraserLine: this.drawingBuffer }, 'layer'); + } + this.setDrawingBuffer(null); + } + async render(layerState: LayerEntity) { // Update the layer's position and listening state this.group.setAttrs({ @@ -72,7 +106,7 @@ export class CanvasLayer { const objectIds = layerState.objects.map(mapId); // Destroy any objects that are no longer in state for (const object of this.objects.values()) { - if (!objectIds.includes(object.id)) { + if (!objectIds.includes(object.id) && object.id !== this.drawingBuffer?.id) { this.objects.delete(object.id); object.destroy(); didDraw = true; @@ -80,67 +114,11 @@ export class CanvasLayer { } for (const obj of layerState.objects) { - if (obj.type === 'brush_line') { - let brushLine = this.objects.get(obj.id); - assert(brushLine instanceof CanvasBrushLine || brushLine === undefined); - - if (!brushLine) { - brushLine = new CanvasBrushLine(obj); - this.objects.set(brushLine.id, brushLine); - this.group.add(brushLine.konvaLineGroup); - didDraw = true; - } else { - if (brushLine.update(obj)) { - didDraw = true; - } - } - } else if (obj.type === 'eraser_line') { - let eraserLine = this.objects.get(obj.id); - assert(eraserLine instanceof CanvasEraserLine || eraserLine === undefined); - - if (!eraserLine) { - eraserLine = new CanvasEraserLine(obj); - this.objects.set(eraserLine.id, eraserLine); - this.group.add(eraserLine.konvaLineGroup); - didDraw = true; - } else { - if (eraserLine.update(obj)) { - didDraw = true; - } - } - } else if (obj.type === 'rect_shape') { - let rect = this.objects.get(obj.id); - assert(rect instanceof CanvasRect || rect === undefined); - - if (!rect) { - rect = new CanvasRect(obj); - this.objects.set(rect.id, rect); - this.group.add(rect.konvaRect); - didDraw = true; - } else { - if (rect.update(obj)) { - didDraw = true; - } - } - } else if (obj.type === 'image') { - let image = this.objects.get(obj.id); - assert(image instanceof CanvasImage || image === undefined); - - if (!image) { - image = await new CanvasImage(obj, { - onLoad: () => { - this.updateGroup(true); - }, - }); - this.objects.set(image.id, image); - this.group.add(image.konvaImageGroup); - await image.updateImageSource(obj.image.name); - } else { - if (await image.update(obj)) { - didDraw = true; - } - } - } + didDraw = await this.renderObject(obj); + } + + if (this.drawingBuffer) { + didDraw = await this.renderObject(this.drawingBuffer); } // Only update layer visibility if it has changed. @@ -151,10 +129,78 @@ export class CanvasLayer { this.group.opacity(layerState.opacity); // The layer only listens when using the move tool - otherwise the stage is handling mouse events - this.updateGroup(didDraw); + this.updateGroup(didDraw, this.prevLayerState); + + this.prevLayerState = layerState; + } + + private async renderObject(obj: LayerEntity['objects'][number], force = false): Promise { + if (obj.type === 'brush_line') { + let brushLine = this.objects.get(obj.id); + assert(brushLine instanceof CanvasBrushLine || brushLine === undefined); + + if (!brushLine) { + brushLine = new CanvasBrushLine(obj); + this.objects.set(brushLine.id, brushLine); + this.objectsGroup.add(brushLine.konvaLineGroup); + return true; + } else { + if (brushLine.update(obj, force)) { + return true; + } + } + } else if (obj.type === 'eraser_line') { + let eraserLine = this.objects.get(obj.id); + assert(eraserLine instanceof CanvasEraserLine || eraserLine === undefined); + + if (!eraserLine) { + eraserLine = new CanvasEraserLine(obj); + this.objects.set(eraserLine.id, eraserLine); + this.objectsGroup.add(eraserLine.konvaLineGroup); + return true; + } else { + if (eraserLine.update(obj, force)) { + return true; + } + } + } else if (obj.type === 'rect_shape') { + let rect = this.objects.get(obj.id); + assert(rect instanceof CanvasRect || rect === undefined); + + if (!rect) { + rect = new CanvasRect(obj); + this.objects.set(rect.id, rect); + this.objectsGroup.add(rect.konvaRect); + return true; + } else { + if (rect.update(obj, force)) { + return true; + } + } + } else if (obj.type === 'image') { + let image = this.objects.get(obj.id); + assert(image instanceof CanvasImage || image === undefined); + + if (!image) { + image = await new CanvasImage(obj, { + onLoad: () => { + this.updateGroup(true, this.prevLayerState); + }, + }); + this.objects.set(image.id, image); + this.objectsGroup.add(image.konvaImageGroup); + await image.updateImageSource(obj.image.name); + } else { + if (await image.update(obj, force)) { + return true; + } + } + } + + return false; } - updateGroup(didDraw: boolean) { + updateGroup(didDraw: boolean, _: LayerEntity) { const isSelected = this.manager.stateApi.getIsSelected(this.id); const selectedTool = this.manager.stateApi.getToolState().selected; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 56af6548fe5..72640e7d716 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -95,7 +95,7 @@ export class CanvasManager { this.background = new CanvasBackground(this); this.stage.add(this.background.layer); - this.inpaintMask = new CanvasInpaintMask(this); + this.inpaintMask = new CanvasInpaintMask(this.stateApi.getInpaintMaskState(), this); this.stage.add(this.inpaintMask.layer); this.layers = new Map(); @@ -346,6 +346,24 @@ export class CanvasManager { }; }; + getSelectedEntityAdapter = (): CanvasLayer | CanvasRegion | CanvasControlAdapter | CanvasInpaintMask | null => { + const state = this.stateApi.getState(); + const identifier = state.selectedEntityIdentifier; + if (!identifier) { + return null; + } else if (identifier.type === 'layer') { + return this.layers.get(identifier.id) ?? null; + } else if (identifier.type === 'control_adapter') { + return this.controlAdapters.get(identifier.id) ?? null; + } else if (identifier.type === 'regional_guidance') { + return this.regions.get(identifier.id) ?? null; + } else if (identifier.type === 'inpaint_mask') { + return this.inpaintMask; + } else { + return null; + } + }; + getGenerationMode() { return getGenerationMode({ manager: this }); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts index 57bda0a1efb..c3a05f36cc7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts @@ -6,7 +6,8 @@ import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox'; import { getObjectGroupId } from 'features/controlLayers/konva/naming'; import { mapId } from 'features/controlLayers/konva/util'; -import { isDrawingTool, type RegionEntity } from 'features/controlLayers/store/types'; +import type { BrushLine, EraserLine, RegionEntity } from 'features/controlLayers/store/types'; +import { isDrawingTool, RGBA_RED } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; @@ -20,6 +21,8 @@ export class CanvasRegion { compositingRect: Konva.Rect; transformer: Konva.Transformer; objects: Map; + private drawingBuffer: BrushLine | EraserLine | null; + private prevRegionState: RegionEntity; constructor(entity: RegionEntity, manager: CanvasManager) { this.id = entity.id; @@ -56,12 +59,41 @@ export class CanvasRegion { this.compositingRect = new Konva.Rect({ listening: false }); this.group.add(this.compositingRect); this.objects = new Map(); + this.drawingBuffer = null; + this.prevRegionState = entity; } destroy(): void { this.layer.destroy(); } + getDrawingBuffer() { + return this.drawingBuffer; + } + + async setDrawingBuffer(obj: BrushLine | EraserLine | null) { + this.drawingBuffer = obj; + if (this.drawingBuffer) { + if (this.drawingBuffer.type === 'brush_line') { + this.drawingBuffer.color = RGBA_RED; + } + await this.renderObject(this.drawingBuffer, true); + this.updateGroup(true, this.prevRegionState); + } + } + + finalizeDrawingBuffer() { + if (!this.drawingBuffer) { + return; + } + if (this.drawingBuffer.type === 'brush_line') { + this.manager.stateApi.onBrushLineAdded2({ id: this.id, brushLine: this.drawingBuffer }, 'regional_guidance'); + } else if (this.drawingBuffer.type === 'eraser_line') { + this.manager.stateApi.onEraserLineAdded2({ id: this.id, eraserLine: this.drawingBuffer }, 'regional_guidance'); + } + this.setDrawingBuffer(null); + } + async render(regionState: RegionEntity) { // Update the layer's position and listening state this.group.setAttrs({ @@ -84,51 +116,62 @@ export class CanvasRegion { } for (const obj of regionState.objects) { - if (obj.type === 'brush_line') { - let brushLine = this.objects.get(obj.id); - assert(brushLine instanceof CanvasBrushLine || brushLine === undefined); - - if (!brushLine) { - brushLine = new CanvasBrushLine(obj); - this.objects.set(brushLine.id, brushLine); - this.objectsGroup.add(brushLine.konvaLineGroup); - didDraw = true; - } else { - if (brushLine.update(obj)) { - didDraw = true; - } + didDraw = await this.renderObject(obj); + } + + this.updateGroup(didDraw, regionState); + this.prevRegionState = regionState; + } + + private async renderObject(obj: RegionEntity['objects'][number], force = false): Promise { + if (obj.type === 'brush_line') { + let brushLine = this.objects.get(obj.id); + assert(brushLine instanceof CanvasBrushLine || brushLine === undefined); + + if (!brushLine) { + brushLine = new CanvasBrushLine(obj); + this.objects.set(brushLine.id, brushLine); + this.objectsGroup.add(brushLine.konvaLineGroup); + return true; + } else { + if (brushLine.update(obj, force)) { + return true; } - } else if (obj.type === 'eraser_line') { - let eraserLine = this.objects.get(obj.id); - assert(eraserLine instanceof CanvasEraserLine || eraserLine === undefined); - - if (!eraserLine) { - eraserLine = new CanvasEraserLine(obj); - this.objects.set(eraserLine.id, eraserLine); - this.objectsGroup.add(eraserLine.konvaLineGroup); - didDraw = true; - } else { - if (eraserLine.update(obj)) { - didDraw = true; - } + } + } else if (obj.type === 'eraser_line') { + let eraserLine = this.objects.get(obj.id); + assert(eraserLine instanceof CanvasEraserLine || eraserLine === undefined); + + if (!eraserLine) { + eraserLine = new CanvasEraserLine(obj); + this.objects.set(eraserLine.id, eraserLine); + this.objectsGroup.add(eraserLine.konvaLineGroup); + return true; + } else { + if (eraserLine.update(obj, force)) { + return true; } - } else if (obj.type === 'rect_shape') { - let rect = this.objects.get(obj.id); - assert(rect instanceof CanvasRect || rect === undefined); - - if (!rect) { - rect = new CanvasRect(obj); - this.objects.set(rect.id, rect); - this.objectsGroup.add(rect.konvaRect); - didDraw = true; - } else { - if (rect.update(obj)) { - didDraw = true; - } + } + } else if (obj.type === 'rect_shape') { + let rect = this.objects.get(obj.id); + assert(rect instanceof CanvasRect || rect === undefined); + + if (!rect) { + rect = new CanvasRect(obj); + this.objects.set(rect.id, rect); + this.objectsGroup.add(rect.konvaRect); + return true; + } else { + if (rect.update(obj, force)) { + return true; } } } + return false; + } + + updateGroup(didDraw: boolean, regionState: RegionEntity) { // Only update layer visibility if it has changed. if (this.layer.visible() !== regionState.isEnabled) { this.layer.visible(regionState.isEnabled); @@ -141,7 +184,6 @@ export class CanvasRegion { // Convert the color to a string, stripping the alpha - the object group will handle opacity. const rgbColor = rgbColorToString(regionState.fill); const maskOpacity = this.manager.stateApi.getMaskOpacity(); - this.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 ...getNodeBboxFast(this.objectsGroup), @@ -149,16 +191,11 @@ export class CanvasRegion { opacity: maskOpacity, // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) globalCompositeOperation: 'source-in', - visible: true, // This rect must always be on top of all other shapes zIndex: this.objects.size + 1, }); } - this.updateGroup(didDraw); - } - - updateGroup(didDraw: boolean) { const isSelected = this.manager.stateApi.getIsSelected(this.id); const selectedTool = this.manager.stateApi.getToolState().selected; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index 083a882c442..735fdb635e7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -19,7 +19,9 @@ import { eraserWidthChanged, imBboxChanged, imBrushLineAdded, + imBrushLineAdded2, imEraserLineAdded, + imEraserLineAdded2, imImageCacheChanged, imLinePointAdded, imRectAdded, @@ -27,7 +29,9 @@ import { imTranslated, layerBboxChanged, layerBrushLineAdded, + layerBrushLineAdded2, layerEraserLineAdded, + layerEraserLineAdded2, layerImageCacheChanged, layerLinePointAdded, layerRectAdded, @@ -35,7 +39,9 @@ import { layerTranslated, rgBboxChanged, rgBrushLineAdded, + rgBrushLineAdded2, rgEraserLineAdded, + rgEraserLineAdded2, rgImageCacheChanged, rgLinePointAdded, rgRectAdded, @@ -46,8 +52,10 @@ import { } from 'features/controlLayers/store/canvasV2Slice'; import type { BboxChangedArg, + BrushLine, BrushLineAddedArg, CanvasEntity, + EraserLine, EraserLineAddedArg, PointAddedToLineArg, PosChangedArg, @@ -127,6 +135,26 @@ export class CanvasStateApi { this.store.dispatch(imEraserLineAdded(arg)); } }; + onBrushLineAdded2 = (arg: { id: string; brushLine: BrushLine }, entityType: CanvasEntity['type']) => { + log.debug('Brush line added'); + if (entityType === 'layer') { + this.store.dispatch(layerBrushLineAdded2(arg)); + } else if (entityType === 'regional_guidance') { + this.store.dispatch(rgBrushLineAdded2(arg)); + } else if (entityType === 'inpaint_mask') { + this.store.dispatch(imBrushLineAdded2(arg)); + } + }; + onEraserLineAdded2 = (arg: { id: string; eraserLine: EraserLine }, entityType: CanvasEntity['type']) => { + log.debug('Eraser line added'); + if (entityType === 'layer') { + this.store.dispatch(layerEraserLineAdded2(arg)); + } else if (entityType === 'regional_guidance') { + this.store.dispatch(rgEraserLineAdded2(arg)); + } else if (entityType === 'inpaint_mask') { + this.store.dispatch(imEraserLineAdded2(arg)); + } + }; onPointAddedToLine = (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => { log.debug('Point added to line'); if (entityType === 'layer') { @@ -183,23 +211,21 @@ export class CanvasStateApi { getSelectedEntity = (): CanvasEntity | null => { const state = this.getState(); const identifier = state.selectedEntityIdentifier; - let selectedEntity: CanvasEntity | null = null; if (!identifier) { - selectedEntity = null; + return null; } else if (identifier.type === 'layer') { - selectedEntity = state.layers.entities.find((i) => i.id === identifier.id) ?? null; + return state.layers.entities.find((i) => i.id === identifier.id) ?? null; } else if (identifier.type === 'control_adapter') { - selectedEntity = state.controlAdapters.entities.find((i) => i.id === identifier.id) ?? null; + return state.controlAdapters.entities.find((i) => i.id === identifier.id) ?? null; } else if (identifier.type === 'ip_adapter') { - selectedEntity = state.ipAdapters.entities.find((i) => i.id === identifier.id) ?? null; + return state.ipAdapters.entities.find((i) => i.id === identifier.id) ?? null; } else if (identifier.type === 'regional_guidance') { - selectedEntity = state.regions.entities.find((i) => i.id === identifier.id) ?? null; + return state.regions.entities.find((i) => i.id === identifier.id) ?? null; } else if (identifier.type === 'inpaint_mask') { - selectedEntity = state.inpaintMask; + return state.inpaintMask; } else { - selectedEntity = null; + return null; } - return selectedEntity; }; getCurrentFill = () => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 4967982604f..53fb797222d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -1,19 +1,14 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getScaledCursorPosition } from 'features/controlLayers/konva/util'; -import type { CanvasEntity } from 'features/controlLayers/store/types'; +import type { CanvasEntity, CanvasV2State, Position } from 'features/controlLayers/store/types'; +import { isDrawableEntity, isDrawableEntityAdapter } from 'features/controlLayers/store/types'; import type Konva from 'konva'; import type { Vector2d } from 'konva/lib/types'; import { clamp } from 'lodash-es'; +import { v4 as uuidv4 } from 'uuid'; -import { - BRUSH_SPACING_TARGET_SCALE, - CANVAS_SCALE_BY, - MAX_BRUSH_SPACING_PX, - MAX_CANVAS_SCALE, - MIN_BRUSH_SPACING_PX, - MIN_CANVAS_SCALE, -} from './constants'; -import { PREVIEW_TOOL_GROUP_ID } from './naming'; +import { BRUSH_SPACING_TARGET_SCALE, CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from './constants'; +import { getBrushLineId, PREVIEW_TOOL_GROUP_ID } from './naming'; /** * Updates the last cursor position atom with the current cursor position, returning the new position or `null` if the @@ -21,10 +16,7 @@ import { PREVIEW_TOOL_GROUP_ID } from './naming'; * @param stage The konva stage * @param setLastCursorPos The callback to store the cursor pos */ -const updateLastCursorPos = ( - stage: Konva.Stage, - setLastCursorPos: CanvasManager['stateApi']['setLastCursorPos'] -) => { +const updateLastCursorPos = (stage: Konva.Stage, setLastCursorPos: CanvasManager['stateApi']['setLastCursorPos']) => { const pos = getScaledCursorPosition(stage); if (!pos) { return null; @@ -61,24 +53,18 @@ const maybeAddNextPoint = ( setLastAddedPoint: CanvasManager['stateApi']['setLastAddedPoint'], onPointAddedToLine: CanvasManager['stateApi']['onPointAddedToLine'] ) => { - const isDrawableEntity = - selectedEntity?.type === 'regional_guidance' || - selectedEntity?.type === 'layer' || - selectedEntity?.type === 'inpaint_mask'; - - if (!isDrawableEntity) { + if (!isDrawableEntity(selectedEntity)) { return; } + // Continue the last line const lastAddedPoint = getLastAddedPoint(); const toolState = getToolState(); - const minSpacingPx = clamp( + const minSpacingPx = toolState.selected === 'brush' ? toolState.brush.width * BRUSH_SPACING_TARGET_SCALE - : toolState.eraser.width * BRUSH_SPACING_TARGET_SCALE, - MIN_BRUSH_SPACING_PX, - MAX_BRUSH_SPACING_PX - ); + : toolState.eraser.width * BRUSH_SPACING_TARGET_SCALE; + if (lastAddedPoint) { // Dispatching redux events impacts perf substantially - using brush spacing keeps dispatches to a reasonable number if (Math.hypot(lastAddedPoint.x - currentPos.x, lastAddedPoint.y - currentPos.y) < minSpacingPx) { @@ -95,8 +81,29 @@ const maybeAddNextPoint = ( ); }; +const getNextPoint = ( + currentPos: Position, + toolState: CanvasV2State['tool'], + lastAddedPoint: Position | null +): Position | null => { + // Continue the last line + const minSpacingPx = + toolState.selected === 'brush' + ? toolState.brush.width * BRUSH_SPACING_TARGET_SCALE + : toolState.eraser.width * BRUSH_SPACING_TARGET_SCALE; + + if (lastAddedPoint) { + // Dispatching redux events impacts perf substantially - using brush spacing keeps dispatches to a reasonable number + if (Math.hypot(lastAddedPoint.x - currentPos.x, lastAddedPoint.y - currentPos.y) < minSpacingPx) { + return null; + } + } + + return currentPos; +}; + export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { - const { stage, stateApi } = manager; + const { stage, stateApi, getSelectedEntityAdapter } = manager; const { getToolState, getCurrentFill, @@ -132,17 +139,21 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { }); //#region mousedown - stage.on('mousedown', (e) => { + stage.on('mousedown', async (e) => { setIsMouseDown(true); const toolState = getToolState(); const pos = updateLastCursorPos(stage, setLastCursorPos); const selectedEntity = getSelectedEntity(); - const isDrawableEntity = - selectedEntity?.type === 'regional_guidance' || - selectedEntity?.type === 'layer' || - selectedEntity?.type === 'inpaint_mask'; - - if (pos && selectedEntity && isDrawableEntity && !getSpaceKey()) { + const selectedEntityAdapter = getSelectedEntityAdapter(); + + if ( + pos && + selectedEntity && + isDrawableEntity(selectedEntity) && + selectedEntityAdapter && + isDrawableEntityAdapter(selectedEntityAdapter) && + !getSpaceKey() + ) { setIsDrawing(true); setLastMouseDownPos(pos); @@ -180,21 +191,37 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { ); } } else { - onBrushLineAdded( - { - id: selectedEntity.id, - points: [ - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - ], - color: getCurrentFill(), - width: toolState.brush.width, - clip, - }, - selectedEntity.type - ); + if (selectedEntityAdapter.getDrawingBuffer()) { + selectedEntityAdapter.finalizeDrawingBuffer(); + } + await selectedEntityAdapter.setDrawingBuffer({ + id: getBrushLineId(selectedEntityAdapter.id, uuidv4()), + type: 'brush_line', + points: [ + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + ], + strokeWidth: toolState.brush.width, + color: getCurrentFill(), + clip, + }); + // onBrushLineAdded( + // { + // id: selectedEntity.id, + // points: [ + // pos.x - selectedEntity.x, + // pos.y - selectedEntity.y, + // pos.x - selectedEntity.x, + // pos.y - selectedEntity.y, + // ], + // color: getCurrentFill(), + // width: toolState.brush.width, + // clip, + // }, + // selectedEntity.type + // ); } setLastAddedPoint(pos); } @@ -231,20 +258,36 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { ); } } else { - onEraserLineAdded( - { - id: selectedEntity.id, - points: [ - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - ], - width: toolState.eraser.width, - clip, - }, - selectedEntity.type - ); + if (selectedEntityAdapter.getDrawingBuffer()) { + selectedEntityAdapter.finalizeDrawingBuffer(); + } + await selectedEntityAdapter.setDrawingBuffer({ + id: getBrushLineId(selectedEntityAdapter.id, uuidv4()), + type: 'eraser_line', + points: [ + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + ], + strokeWidth: toolState.eraser.width, + clip, + }); + + // onEraserLineAdded( + // { + // id: selectedEntity.id, + // points: [ + // pos.x - selectedEntity.x, + // pos.y - selectedEntity.y, + // pos.x - selectedEntity.x, + // pos.y - selectedEntity.y, + // ], + // width: toolState.eraser.width, + // clip, + // }, + // selectedEntity.type + // ); } setLastAddedPoint(pos); } @@ -253,18 +296,40 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { }); //#region mouseup - stage.on('mouseup', () => { + stage.on('mouseup', async () => { setIsMouseDown(false); const pos = getLastCursorPos(); const selectedEntity = getSelectedEntity(); - const isDrawableEntity = - selectedEntity?.type === 'regional_guidance' || - selectedEntity?.type === 'layer' || - selectedEntity?.type === 'inpaint_mask'; - - if (pos && selectedEntity && isDrawableEntity && !getSpaceKey()) { + const selectedEntityAdapter = getSelectedEntityAdapter(); + + if ( + pos && + selectedEntity && + isDrawableEntity(selectedEntity) && + selectedEntityAdapter && + isDrawableEntityAdapter(selectedEntityAdapter) && + !getSpaceKey() + ) { const toolState = getToolState(); + if (toolState.selected === 'brush') { + const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); + if (drawingBuffer?.type === 'brush_line') { + selectedEntityAdapter.finalizeDrawingBuffer(); + } else { + await selectedEntityAdapter.setDrawingBuffer(null); + } + } + + if (toolState.selected === 'eraser') { + const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); + if (drawingBuffer?.type === 'eraser_line') { + selectedEntityAdapter.finalizeDrawingBuffer(); + } else { + await selectedEntityAdapter.setDrawingBuffer(null); + } + } + if (toolState.selected === 'rect') { const lastMouseDownPos = getLastMouseDownPos(); if (lastMouseDownPos) { @@ -292,32 +357,48 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { }); //#region mousemove - stage.on('mousemove', () => { + stage.on('mousemove', async () => { const toolState = getToolState(); const pos = updateLastCursorPos(stage, setLastCursorPos); const selectedEntity = getSelectedEntity(); + const selectedEntityAdapter = getSelectedEntityAdapter(); stage .findOne(`#${PREVIEW_TOOL_GROUP_ID}`) ?.visible(toolState.selected === 'brush' || toolState.selected === 'eraser'); - const isDrawableEntity = - selectedEntity?.type === 'regional_guidance' || - selectedEntity?.type === 'layer' || - selectedEntity?.type === 'inpaint_mask'; - - if (pos && selectedEntity && isDrawableEntity && !getSpaceKey() && getIsMouseDown()) { + if ( + pos && + selectedEntity && + isDrawableEntity(selectedEntity) && + selectedEntityAdapter && + isDrawableEntityAdapter(selectedEntityAdapter) && + !getSpaceKey() && + getIsMouseDown() + ) { if (toolState.selected === 'brush') { if (getIsDrawing()) { + const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); + if (drawingBuffer?.type === 'brush_line') { + const lastAddedPoint = getLastAddedPoint(); + const nextPoint = getNextPoint(pos, toolState, lastAddedPoint); + if (nextPoint) { + drawingBuffer.points.push(nextPoint.x - selectedEntity.x, nextPoint.y - selectedEntity.y); + await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); + setLastAddedPoint(nextPoint); + } + } else { + await selectedEntityAdapter.setDrawingBuffer(null); + } // Continue the last line - maybeAddNextPoint( - selectedEntity, - pos, - getToolState, - getLastAddedPoint, - setLastAddedPoint, - onPointAddedToLine - ); + // maybeAddNextPoint( + // selectedEntity, + // pos, + // getToolState, + // getLastAddedPoint, + // setLastAddedPoint, + // onPointAddedToLine + // ); } else { const bbox = getBbox(); const settings = getSettings(); @@ -353,15 +434,28 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (toolState.selected === 'eraser') { if (getIsDrawing()) { + const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); + if (drawingBuffer?.type === 'eraser_line') { + const lastAddedPoint = getLastAddedPoint(); + const nextPoint = getNextPoint(pos, toolState, lastAddedPoint); + if (nextPoint) { + drawingBuffer.points.push(nextPoint.x - selectedEntity.x, nextPoint.y - selectedEntity.y); + await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); + setLastAddedPoint(nextPoint); + } + } else { + await selectedEntityAdapter.setDrawingBuffer(null); + } + // Continue the last line - maybeAddNextPoint( - selectedEntity, - pos, - getToolState, - getLastAddedPoint, - setLastAddedPoint, - onPointAddedToLine - ); + // maybeAddNextPoint( + // selectedEntity, + // pos, + // getToolState, + // getLastAddedPoint, + // setLastAddedPoint, + // onPointAddedToLine + // ); } else { const bbox = getBbox(); const settings = getSettings(); @@ -407,12 +501,8 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { const toolState = getToolState(); stage.findOne(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(false); - const isDrawableEntity = - selectedEntity?.type === 'regional_guidance' || - selectedEntity?.type === 'layer' || - selectedEntity?.type === 'inpaint_mask'; - if (pos && selectedEntity && isDrawableEntity && !getSpaceKey() && getIsMouseDown()) { + if (pos && selectedEntity && isDrawableEntity(selectedEntity) && !getSpaceKey() && getIsMouseDown()) { if (getIsMouseDown()) { if (toolState.selected === 'brush') { onPointAddedToLine({ id: selectedEntity.id, point: [pos.x, pos.y] }, selectedEntity.type); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index c341e5e152f..104a6d13b2e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -347,6 +347,12 @@ export const { stagingAreaCanceledStaging, stagingAreaNextImageSelected, stagingAreaPreviousImageSelected, + layerBrushLineAdded2, + layerEraserLineAdded2, + rgBrushLineAdded2, + rgEraserLineAdded2, + imBrushLineAdded2, + imEraserLineAdded2, } = canvasV2Slice.actions; export const selectCanvasV2Slice = (state: RootState) => state.canvasV2; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts index 58343c3ebf3..9dc78b61055 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts @@ -1,6 +1,12 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; -import type { CanvasV2State, InpaintMaskEntity, ScaleChangedArg } from 'features/controlLayers/store/types'; +import type { + BrushLine, + CanvasV2State, + EraserLine, + InpaintMaskEntity, + ScaleChangedArg, +} from 'features/controlLayers/store/types'; import { imageDTOToImageWithDims, RGBA_RED } from 'features/controlLayers/store/types'; import type { IRect } from 'konva/lib/types'; import type { ImageDTO } from 'services/api/types'; @@ -81,6 +87,18 @@ export const inpaintMaskReducers = { payload: { ...payload, lineId: uuidv4() }, }), }, + imBrushLineAdded2: (state, action: PayloadAction<{ brushLine: BrushLine }>) => { + const { brushLine } = action.payload; + state.inpaintMask.objects.push(brushLine); + state.inpaintMask.bboxNeedsUpdate = true; + state.layers.imageCache = null; + }, + imEraserLineAdded2: (state, action: PayloadAction<{ eraserLine: EraserLine }>) => { + const { eraserLine } = action.payload; + state.inpaintMask.objects.push(eraserLine); + state.inpaintMask.bboxNeedsUpdate = true; + state.layers.imageCache = null; + }, imEraserLineAdded: { reducer: (state, action: PayloadAction & { lineId: string }>) => { const { points, lineId, width, clip } = action.payload; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index c39a70fe011..11997808183 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -7,8 +7,10 @@ import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; import type { + BrushLine, BrushLineAddedArg, CanvasV2State, + EraserLine, EraserLineAddedArg, ImageObjectAddedArg, LayerEntity, @@ -152,6 +154,28 @@ export const layersReducers = { moveToStart(state.layers.entities, layer); state.layers.imageCache = null; }, + layerBrushLineAdded2: (state, action: PayloadAction<{ id: string; brushLine: BrushLine }>) => { + const { id, brushLine } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + + layer.objects.push(brushLine); + layer.bboxNeedsUpdate = true; + state.layers.imageCache = null; + }, + layerEraserLineAdded2: (state, action: PayloadAction<{ id: string; eraserLine: EraserLine }>) => { + const { id, eraserLine } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + + layer.objects.push(eraserLine); + layer.bboxNeedsUpdate = true; + state.layers.imageCache = null; + }, layerBrushLineAdded: { reducer: (state, action: PayloadAction) => { const { id, points, lineId, color, width, clip } = action.payload; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index 9c7fb6e1e82..b033d6f8134 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -1,7 +1,14 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; -import type { CanvasV2State, CLIPVisionModelV2, IPMethodV2, ScaleChangedArg } from 'features/controlLayers/store/types'; +import type { + BrushLine, + CanvasV2State, + CLIPVisionModelV2, + EraserLine, + IPMethodV2, + ScaleChangedArg, +} from 'features/controlLayers/store/types'; import { imageDTOToImageObject, imageDTOToImageWithDims, RGBA_RED } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas'; @@ -354,6 +361,28 @@ export const regionsReducers = { payload: { ...payload, lineId: uuidv4() }, }), }, + rgBrushLineAdded2: (state, action: PayloadAction<{ id: string; brushLine: BrushLine }>) => { + const { id, brushLine } = action.payload; + const rg = selectRG(state, id); + if (!rg) { + return; + } + + rg.objects.push(brushLine); + rg.bboxNeedsUpdate = true; + state.layers.imageCache = null; + }, + rgEraserLineAdded2: (state, action: PayloadAction<{ id: string; eraserLine: EraserLine }>) => { + const { id, eraserLine } = action.payload; + const rg = selectRG(state, id); + if (!rg) { + return; + } + + rg.objects.push(eraserLine); + rg.bboxNeedsUpdate = true; + state.layers.imageCache = null; + }, rgEraserLineAdded: { reducer: (state, action: PayloadAction) => { const { id, points, lineId, width, clip } = action.payload; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 4693654147a..872afbe56bb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,3 +1,7 @@ +import type { CanvasControlAdapter } from 'features/controlLayers/konva/CanvasControlAdapter'; +import { CanvasInpaintMask } from 'features/controlLayers/konva/CanvasInpaintMask'; +import { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; +import { CanvasRegion } from 'features/controlLayers/konva/CanvasRegion'; import { getImageObjectId } from 'features/controlLayers/konva/naming'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; @@ -924,3 +928,13 @@ export type RemoveIndexString = { }; export type GenerationMode = 'txt2img' | 'img2img' | 'inpaint' | 'outpaint'; + +export function isDrawableEntity(entity: CanvasEntity): entity is LayerEntity | RegionEntity | InpaintMaskEntity { + return entity.type === 'layer' || entity.type === 'regional_guidance' || entity.type === 'inpaint_mask'; +} + +export function isDrawableEntityAdapter( + adapter: CanvasLayer | CanvasRegion | CanvasControlAdapter | CanvasInpaintMask +): adapter is CanvasLayer | CanvasRegion | CanvasInpaintMask { + return adapter instanceof CanvasLayer || adapter instanceof CanvasRegion || adapter instanceof CanvasInpaintMask; +} From aa1727d16fbcbac3051a8e0819e4b3dba49d392c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 5 Jul 2024 00:41:36 +1000 Subject: [PATCH 167/678] perf(ui): object groups do not listen --- .../web/src/features/controlLayers/konva/CanvasInpaintMask.ts | 2 +- .../web/src/features/controlLayers/konva/CanvasLayer.ts | 2 +- .../web/src/features/controlLayers/konva/CanvasRegion.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts index b22c66a6fda..db944ee0ef1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts @@ -33,7 +33,7 @@ export class CanvasInpaintMask { id: getObjectGroupId(this.layer.id(), uuidv4()), listening: false, }); - this.objectsGroup = new Konva.Group({}); + this.objectsGroup = new Konva.Group({ listening: false }); this.group.add(this.objectsGroup); this.layer.add(this.group); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index f2a861a07d6..f869c009b83 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -34,7 +34,7 @@ export class CanvasLayer { id: getObjectGroupId(this.layer.id(), uuidv4()), listening: false, }); - this.objectsGroup = new Konva.Group({}); + this.objectsGroup = new Konva.Group({ listening: false }); this.group.add(this.objectsGroup); this.layer.add(this.group); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts index c3a05f36cc7..f7a704d4f7e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts @@ -33,7 +33,7 @@ export class CanvasRegion { id: getObjectGroupId(this.layer.id(), uuidv4()), listening: false, }); - this.objectsGroup = new Konva.Group({}); + this.objectsGroup = new Konva.Group({ listening: false }); this.group.add(this.objectsGroup); this.layer.add(this.group); From fef88734d07840df9b9f6bfe9384660d563760a4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 5 Jul 2024 00:43:55 +1000 Subject: [PATCH 168/678] perf(ui): do not use stage.find --- .../web/src/features/controlLayers/konva/events.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 53fb797222d..28a0af7801e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -8,7 +8,7 @@ import { clamp } from 'lodash-es'; import { v4 as uuidv4 } from 'uuid'; import { BRUSH_SPACING_TARGET_SCALE, CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from './constants'; -import { getBrushLineId, PREVIEW_TOOL_GROUP_ID } from './naming'; +import { getBrushLineId } from './naming'; /** * Updates the last cursor position atom with the current cursor position, returning the new position or `null` if the @@ -363,10 +363,6 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { const selectedEntity = getSelectedEntity(); const selectedEntityAdapter = getSelectedEntityAdapter(); - stage - .findOne(`#${PREVIEW_TOOL_GROUP_ID}`) - ?.visible(toolState.selected === 'brush' || toolState.selected === 'eraser'); - if ( pos && selectedEntity && @@ -500,8 +496,6 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { const selectedEntity = getSelectedEntity(); const toolState = getToolState(); - stage.findOne(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(false); - if (pos && selectedEntity && isDrawableEntity(selectedEntity) && !getSpaceKey() && getIsMouseDown()) { if (getIsMouseDown()) { if (toolState.selected === 'brush') { From ca5fde8ef5cc05e6a11b3a4a4c9edbb3d0fd1703 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 5 Jul 2024 10:00:47 +1000 Subject: [PATCH 169/678] fix(ui): buffered drawing edge cases --- .../components/HeadsUpDisplay.tsx | 28 +- .../controlLayers/konva/CanvasEraserLine.ts | 19 +- .../controlLayers/konva/CanvasInpaintMask.ts | 30 +- .../controlLayers/konva/CanvasLayer.ts | 35 +- .../controlLayers/konva/CanvasRegion.ts | 30 +- .../features/controlLayers/konva/events.ts | 319 +++++++----------- 6 files changed, 211 insertions(+), 250 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx index ba588583048..6e49df0ca7d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx @@ -1,12 +1,24 @@ import { Box, Flex, Text } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; -import { $stageAttrs } from 'features/controlLayers/store/canvasV2Slice'; +import { + $isDrawing, + $isMouseDown, + $lastAddedPoint, + $lastCursorPos, + $lastMouseDownPos, + $stageAttrs, +} from 'features/controlLayers/store/canvasV2Slice'; import { round } from 'lodash-es'; import { memo } from 'react'; export const HeadsUpDisplay = memo(() => { const stageAttrs = useStore($stageAttrs); + const cursorPos = useStore($lastCursorPos); + const isDrawing = useStore($isDrawing); + const isMouseDown = useStore($isMouseDown); + const lastMouseDownPos = useStore($lastMouseDownPos); + const lastAddedPoint = useStore($lastAddedPoint); const bbox = useAppSelector((s) => s.canvasV2.bbox); const document = useAppSelector((s) => s.canvasV2.document); @@ -25,6 +37,20 @@ export const HeadsUpDisplay = memo(() => { + + + + + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts index f1ce21afd10..76ebf13cfd4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts @@ -1,25 +1,8 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; -import { getLayerBboxId, LAYER_BBOX_NAME } from 'features/controlLayers/konva/naming'; -import type { CanvasEntity, EraserLine } from 'features/controlLayers/store/types'; +import type { EraserLine } from 'features/controlLayers/store/types'; import { RGBA_RED } from 'features/controlLayers/store/types'; import Konva from 'konva'; -/** - * Creates a bounding box rect for a layer. - * @param entity The layer state for the layer to create the bounding box for - * @param konvaLayer The konva layer to attach the bounding box to - */ -export const createBboxRect = (entity: CanvasEntity, konvaLayer: Konva.Layer): Konva.Rect => { - const rect = new Konva.Rect({ - id: getLayerBboxId(entity.id), - name: LAYER_BBOX_NAME, - strokeWidth: 1, - visible: false, - }); - konvaLayer.add(rect); - return rect; -}; - export class CanvasEraserLine { id: string; konvaLineGroup: Konva.Group; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts index db944ee0ef1..c510e3228ec 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts @@ -22,7 +22,7 @@ export class CanvasInpaintMask { transformer: Konva.Transformer; objects: Map; private drawingBuffer: BrushLine | EraserLine | null; - private prevInpaintMaskState: InpaintMaskEntity; + private inpaintMaskState: InpaintMaskEntity; constructor(entity: InpaintMaskEntity, manager: CanvasManager) { this.id = INPAINT_MASK_LAYER_ID; @@ -60,7 +60,7 @@ export class CanvasInpaintMask { this.group.add(this.compositingRect); this.objects = new Map(); this.drawingBuffer = null; - this.prevInpaintMaskState = entity; + this.inpaintMaskState = entity; } destroy(): void { @@ -79,7 +79,7 @@ export class CanvasInpaintMask { } await this.renderObject(this.drawingBuffer, true); - this.updateGroup(true, this.prevInpaintMaskState); + this.updateGroup(true); } } @@ -96,6 +96,8 @@ export class CanvasInpaintMask { } async render(inpaintMaskState: InpaintMaskEntity) { + this.inpaintMaskState = inpaintMaskState; + // Update the layer's position and listening state this.group.setAttrs({ x: inpaintMaskState.x, @@ -117,11 +119,18 @@ export class CanvasInpaintMask { } for (const obj of inpaintMaskState.objects) { - didDraw = await this.renderObject(obj); + if (await this.renderObject(obj)) { + didDraw = true; + } } - this.updateGroup(didDraw, inpaintMaskState); - this.prevInpaintMaskState = inpaintMaskState; + if (this.drawingBuffer) { + if (await this.renderObject(this.drawingBuffer)) { + didDraw = true; + } + } + + this.updateGroup(didDraw); } private async renderObject(obj: InpaintMaskEntity['objects'][number], force = false): Promise { @@ -172,18 +181,15 @@ export class CanvasInpaintMask { return false; } - updateGroup(didDraw: boolean, inpaintMaskState: InpaintMaskEntity) { - // Only update layer visibility if it has changed. - if (this.layer.visible() !== inpaintMaskState.isEnabled) { - this.layer.visible(inpaintMaskState.isEnabled); - } + updateGroup(didDraw: boolean) { + this.layer.visible(this.inpaintMaskState.isEnabled); // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work this.group.opacity(1); if (didDraw) { // Convert the color to a string, stripping the alpha - the object group will handle opacity. - const rgbColor = rgbColorToString(inpaintMaskState.fill); + const rgbColor = rgbColorToString(this.inpaintMaskState.fill); const maskOpacity = this.manager.stateApi.getMaskOpacity(); this.compositingRect.setAttrs({ diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index f869c009b83..fc074ddc401 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -20,7 +20,7 @@ export class CanvasLayer { transformer: Konva.Transformer; objects: Map; private drawingBuffer: BrushLine | EraserLine | null; - private prevLayerState: LayerEntity; + private layerState: LayerEntity; constructor(entity: LayerEntity, manager: CanvasManager) { this.id = entity.id; @@ -59,7 +59,7 @@ export class CanvasLayer { this.objects = new Map(); this.drawingBuffer = null; - this.prevLayerState = entity; + this.layerState = entity; } destroy(): void { @@ -74,7 +74,7 @@ export class CanvasLayer { if (obj) { this.drawingBuffer = obj; await this.renderObject(this.drawingBuffer, true); - this.updateGroup(true, this.prevLayerState); + this.updateGroup(true); } else { this.drawingBuffer = null; } @@ -93,6 +93,8 @@ export class CanvasLayer { } async render(layerState: LayerEntity) { + this.layerState = layerState; + // Update the layer's position and listening state this.group.setAttrs({ x: layerState.x, @@ -114,24 +116,18 @@ export class CanvasLayer { } for (const obj of layerState.objects) { - didDraw = await this.renderObject(obj); + if (await this.renderObject(obj)) { + didDraw = true; + } } if (this.drawingBuffer) { - didDraw = await this.renderObject(this.drawingBuffer); - } - - // Only update layer visibility if it has changed. - if (this.layer.visible() !== layerState.isEnabled) { - this.layer.visible(layerState.isEnabled); + if (await this.renderObject(this.drawingBuffer)) { + didDraw = true; + } } - this.group.opacity(layerState.opacity); - - // The layer only listens when using the move tool - otherwise the stage is handling mouse events - this.updateGroup(didDraw, this.prevLayerState); - - this.prevLayerState = layerState; + this.updateGroup(didDraw); } private async renderObject(obj: LayerEntity['objects'][number], force = false): Promise { @@ -184,7 +180,7 @@ export class CanvasLayer { if (!image) { image = await new CanvasImage(obj, { onLoad: () => { - this.updateGroup(true, this.prevLayerState); + this.updateGroup(true); }, }); this.objects.set(image.id, image); @@ -200,7 +196,10 @@ export class CanvasLayer { return false; } - updateGroup(didDraw: boolean, _: LayerEntity) { + updateGroup(didDraw: boolean) { + this.layer.visible(this.layerState.isEnabled); + + this.group.opacity(this.layerState.opacity); const isSelected = this.manager.stateApi.getIsSelected(this.id); const selectedTool = this.manager.stateApi.getToolState().selected; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts index f7a704d4f7e..2e735e4b5b7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts @@ -22,7 +22,7 @@ export class CanvasRegion { transformer: Konva.Transformer; objects: Map; private drawingBuffer: BrushLine | EraserLine | null; - private prevRegionState: RegionEntity; + private regionState: RegionEntity; constructor(entity: RegionEntity, manager: CanvasManager) { this.id = entity.id; @@ -60,7 +60,7 @@ export class CanvasRegion { this.group.add(this.compositingRect); this.objects = new Map(); this.drawingBuffer = null; - this.prevRegionState = entity; + this.regionState = entity; } destroy(): void { @@ -78,7 +78,7 @@ export class CanvasRegion { this.drawingBuffer.color = RGBA_RED; } await this.renderObject(this.drawingBuffer, true); - this.updateGroup(true, this.prevRegionState); + this.updateGroup(true); } } @@ -95,6 +95,8 @@ export class CanvasRegion { } async render(regionState: RegionEntity) { + this.regionState = regionState; + // Update the layer's position and listening state this.group.setAttrs({ x: regionState.x, @@ -116,11 +118,18 @@ export class CanvasRegion { } for (const obj of regionState.objects) { - didDraw = await this.renderObject(obj); + if (await this.renderObject(obj)) { + didDraw = true; + } } - this.updateGroup(didDraw, regionState); - this.prevRegionState = regionState; + if (this.drawingBuffer) { + if (await this.renderObject(this.drawingBuffer)) { + didDraw = true; + } + } + + this.updateGroup(didDraw); } private async renderObject(obj: RegionEntity['objects'][number], force = false): Promise { @@ -171,18 +180,15 @@ export class CanvasRegion { return false; } - updateGroup(didDraw: boolean, regionState: RegionEntity) { - // Only update layer visibility if it has changed. - if (this.layer.visible() !== regionState.isEnabled) { - this.layer.visible(regionState.isEnabled); - } + updateGroup(didDraw: boolean) { + this.layer.visible(this.regionState.isEnabled); // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work this.group.opacity(1); if (didDraw) { // Convert the color to a string, stripping the alpha - the object group will handle opacity. - const rgbColor = rgbColorToString(regionState.fill); + const rgbColor = rgbColorToString(this.regionState.fill); const maskOpacity = this.manager.stateApi.getMaskOpacity(); this.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 diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 28a0af7801e..82ba286a8a4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -1,14 +1,22 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getScaledCursorPosition } from 'features/controlLayers/konva/util'; -import type { CanvasEntity, CanvasV2State, Position } from 'features/controlLayers/store/types'; +import type { + CanvasEntity, + CanvasV2State, + InpaintMaskEntity, + LayerEntity, + Position, + RegionEntity, +} from 'features/controlLayers/store/types'; import { isDrawableEntity, isDrawableEntityAdapter } from 'features/controlLayers/store/types'; import type Konva from 'konva'; +import type { KonvaEventObject } from 'konva/lib/Node'; import type { Vector2d } from 'konva/lib/types'; import { clamp } from 'lodash-es'; import { v4 as uuidv4 } from 'uuid'; import { BRUSH_SPACING_TARGET_SCALE, CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from './constants'; -import { getBrushLineId } from './naming'; +import { getBrushLineId, getEraserLineId } from './naming'; /** * Updates the last cursor position atom with the current cursor position, returning the new position or `null` if the @@ -109,9 +117,6 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { getCurrentFill, setTool, setToolBuffer, - getIsDrawing, - setIsDrawing, - getIsMouseDown, setIsMouseDown, getLastMouseDownPos, setLastMouseDownPos, @@ -125,14 +130,36 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { setSpaceKey, getBbox, getSettings, - onBrushLineAdded, - onEraserLineAdded, - onPointAddedToLine, onRectShapeAdded, onBrushWidthChanged, onEraserWidthChanged, } = stateApi; + function getIsPrimaryMouseDown(e: KonvaEventObject) { + return e.evt.buttons === 1; + } + + function getClip(entity: RegionEntity | LayerEntity | InpaintMaskEntity) { + const settings = getSettings(); + const bbox = getBbox(); + + if (settings.clipToBbox) { + return { + x: bbox.x - entity.x, + y: bbox.y - entity.y, + width: bbox.width, + height: bbox.height, + }; + } else { + return { + x: -stage.x() / stage.scaleX() - entity.x, + y: -stage.y() / stage.scaleY() - entity.y, + width: stage.width() / stage.scaleX(), + height: stage.height() / stage.scaleY(), + }; + } + } + //#region mouseenter stage.on('mouseenter', () => { manager.preview.tool.render(); @@ -152,43 +179,32 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { isDrawableEntity(selectedEntity) && selectedEntityAdapter && isDrawableEntityAdapter(selectedEntityAdapter) && - !getSpaceKey() + !getSpaceKey() && + getIsPrimaryMouseDown(e) ) { - setIsDrawing(true); setLastMouseDownPos(pos); if (toolState.selected === 'brush') { - const bbox = getBbox(); - const settings = getSettings(); - - const clip = settings.clipToBbox - ? { - x: bbox.x, - y: bbox.y, - width: bbox.width, - height: bbox.height, - } - : null; - if (e.evt.shiftKey) { const lastAddedPoint = getLastAddedPoint(); // Create a straight line if holding shift if (lastAddedPoint) { - onBrushLineAdded( - { - id: selectedEntity.id, - points: [ - lastAddedPoint.x - selectedEntity.x, - lastAddedPoint.y - selectedEntity.y, - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - ], - color: getCurrentFill(), - width: toolState.brush.width, - clip, - }, - selectedEntity.type - ); + if (selectedEntityAdapter.getDrawingBuffer()) { + selectedEntityAdapter.finalizeDrawingBuffer(); + } + await selectedEntityAdapter.setDrawingBuffer({ + id: getBrushLineId(selectedEntityAdapter.id, uuidv4()), + type: 'brush_line', + points: [ + lastAddedPoint.x - selectedEntity.x, + lastAddedPoint.y - selectedEntity.y, + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + ], + strokeWidth: toolState.brush.width, + color: getCurrentFill(), + clip: getClip(selectedEntity), + }); } } else { if (selectedEntityAdapter.getDrawingBuffer()) { @@ -205,64 +221,39 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { ], strokeWidth: toolState.brush.width, color: getCurrentFill(), - clip, + clip: getClip(selectedEntity), }); - // onBrushLineAdded( - // { - // id: selectedEntity.id, - // points: [ - // pos.x - selectedEntity.x, - // pos.y - selectedEntity.y, - // pos.x - selectedEntity.x, - // pos.y - selectedEntity.y, - // ], - // color: getCurrentFill(), - // width: toolState.brush.width, - // clip, - // }, - // selectedEntity.type - // ); } setLastAddedPoint(pos); } if (toolState.selected === 'eraser') { - const bbox = getBbox(); - const settings = getSettings(); - - const clip = settings.clipToBbox - ? { - x: bbox.x, - y: bbox.y, - width: bbox.width, - height: bbox.height, - } - : null; if (e.evt.shiftKey) { // Create a straight line if holding shift const lastAddedPoint = getLastAddedPoint(); if (lastAddedPoint) { - onEraserLineAdded( - { - id: selectedEntity.id, - points: [ - lastAddedPoint.x - selectedEntity.x, - lastAddedPoint.y - selectedEntity.y, - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - ], - width: toolState.eraser.width, - clip, - }, - selectedEntity.type - ); + if (selectedEntityAdapter.getDrawingBuffer()) { + selectedEntityAdapter.finalizeDrawingBuffer(); + } + await selectedEntityAdapter.setDrawingBuffer({ + id: getBrushLineId(selectedEntityAdapter.id, uuidv4()), + type: 'eraser_line', + points: [ + lastAddedPoint.x - selectedEntity.x, + lastAddedPoint.y - selectedEntity.y, + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + ], + strokeWidth: toolState.eraser.width, + clip: getClip(selectedEntity), + }); } } else { if (selectedEntityAdapter.getDrawingBuffer()) { selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getBrushLineId(selectedEntityAdapter.id, uuidv4()), + id: getEraserLineId(selectedEntityAdapter.id, uuidv4()), type: 'eraser_line', points: [ pos.x - selectedEntity.x, @@ -271,23 +262,8 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { pos.y - selectedEntity.y, ], strokeWidth: toolState.eraser.width, - clip, + clip: getClip(selectedEntity), }); - - // onEraserLineAdded( - // { - // id: selectedEntity.id, - // points: [ - // pos.x - selectedEntity.x, - // pos.y - selectedEntity.y, - // pos.x - selectedEntity.x, - // pos.y - selectedEntity.y, - // ], - // width: toolState.eraser.width, - // clip, - // }, - // selectedEntity.type - // ); } setLastAddedPoint(pos); } @@ -296,7 +272,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { }); //#region mouseup - stage.on('mouseup', async () => { + stage.on('mouseup', async (e) => { setIsMouseDown(false); const pos = getLastCursorPos(); const selectedEntity = getSelectedEntity(); @@ -349,7 +325,6 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { } } - setIsDrawing(false); setLastMouseDownPos(null); } @@ -357,7 +332,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { }); //#region mousemove - stage.on('mousemove', async () => { + stage.on('mousemove', async (e) => { const toolState = getToolState(); const pos = updateLastCursorPos(stage, setLastCursorPos); const selectedEntity = getSelectedEntity(); @@ -370,11 +345,11 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { selectedEntityAdapter && isDrawableEntityAdapter(selectedEntityAdapter) && !getSpaceKey() && - getIsMouseDown() + getIsPrimaryMouseDown(e) ) { if (toolState.selected === 'brush') { - if (getIsDrawing()) { - const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); + const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); + if (drawingBuffer) { if (drawingBuffer?.type === 'brush_line') { const lastAddedPoint = getLastAddedPoint(); const nextPoint = getNextPoint(pos, toolState, lastAddedPoint); @@ -386,52 +361,31 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { } else { await selectedEntityAdapter.setDrawingBuffer(null); } - // Continue the last line - // maybeAddNextPoint( - // selectedEntity, - // pos, - // getToolState, - // getLastAddedPoint, - // setLastAddedPoint, - // onPointAddedToLine - // ); } else { - const bbox = getBbox(); - const settings = getSettings(); - - const clip = settings.clipToBbox - ? { - x: bbox.x, - y: bbox.y, - width: bbox.width, - height: bbox.height, - } - : null; - // Start a new line - onBrushLineAdded( - { - id: selectedEntity.id, - points: [ - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - ], - width: toolState.brush.width, - color: getCurrentFill(), - clip, - }, - selectedEntity.type - ); + if (selectedEntityAdapter.getDrawingBuffer()) { + selectedEntityAdapter.finalizeDrawingBuffer(); + } + await selectedEntityAdapter.setDrawingBuffer({ + id: getBrushLineId(selectedEntityAdapter.id, uuidv4()), + type: 'brush_line', + points: [ + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + ], + strokeWidth: toolState.brush.width, + color: getCurrentFill(), + clip: getClip(selectedEntity), + }); setLastAddedPoint(pos); - setIsDrawing(true); } } if (toolState.selected === 'eraser') { - if (getIsDrawing()) { - const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); - if (drawingBuffer?.type === 'eraser_line') { + const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); + if (drawingBuffer) { + if (drawingBuffer.type === 'eraser_line') { const lastAddedPoint = getLastAddedPoint(); const nextPoint = getNextPoint(pos, toolState, lastAddedPoint); if (nextPoint) { @@ -442,45 +396,23 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { } else { await selectedEntityAdapter.setDrawingBuffer(null); } - - // Continue the last line - // maybeAddNextPoint( - // selectedEntity, - // pos, - // getToolState, - // getLastAddedPoint, - // setLastAddedPoint, - // onPointAddedToLine - // ); } else { - const bbox = getBbox(); - const settings = getSettings(); - - const clip = settings.clipToBbox - ? { - x: bbox.x, - y: bbox.y, - width: bbox.width, - height: bbox.height, - } - : null; - // Start a new line - onEraserLineAdded( - { - id: selectedEntity.id, - points: [ - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - ], - width: toolState.eraser.width, - clip, - }, - selectedEntity.type - ); + if (selectedEntityAdapter.getDrawingBuffer()) { + selectedEntityAdapter.finalizeDrawingBuffer(); + } + await selectedEntityAdapter.setDrawingBuffer({ + id: getEraserLineId(selectedEntityAdapter.id, uuidv4()), + type: 'eraser_line', + points: [ + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + ], + strokeWidth: toolState.eraser.width, + clip: getClip(selectedEntity), + }); setLastAddedPoint(pos); - setIsDrawing(true); } } } @@ -488,22 +420,32 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { }); //#region mouseleave - stage.on('mouseleave', () => { + stage.on('mouseleave', async (e) => { const pos = updateLastCursorPos(stage, setLastCursorPos); - setIsDrawing(false); setLastCursorPos(null); setLastMouseDownPos(null); const selectedEntity = getSelectedEntity(); + const selectedEntityAdapter = getSelectedEntityAdapter(); const toolState = getToolState(); - if (pos && selectedEntity && isDrawableEntity(selectedEntity) && !getSpaceKey() && getIsMouseDown()) { - if (getIsMouseDown()) { - if (toolState.selected === 'brush') { - onPointAddedToLine({ id: selectedEntity.id, point: [pos.x, pos.y] }, selectedEntity.type); - } - if (toolState.selected === 'eraser') { - onPointAddedToLine({ id: selectedEntity.id, point: [pos.x, pos.y] }, selectedEntity.type); - } + if ( + pos && + selectedEntity && + isDrawableEntity(selectedEntity) && + selectedEntityAdapter && + isDrawableEntityAdapter(selectedEntityAdapter) && + !getSpaceKey() && + getIsPrimaryMouseDown(e) + ) { + const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); + if (toolState.selected === 'brush' && drawingBuffer?.type === 'brush_line') { + drawingBuffer.points.push(pos.x - selectedEntity.x, pos.y - selectedEntity.y); + await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); + selectedEntityAdapter.finalizeDrawingBuffer(); + } else if (toolState.selected === 'eraser' && drawingBuffer?.type === 'eraser_line') { + drawingBuffer.points.push(pos.x - selectedEntity.x, pos.y - selectedEntity.y); + await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); + selectedEntityAdapter.finalizeDrawingBuffer(); } } @@ -592,7 +534,6 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { } if (e.key === 'Escape') { // Cancel shape drawing on escape - setIsDrawing(false); setLastMouseDownPos(null); } else if (e.key === ' ') { // Select the view tool on space key down From 81b210cf14d3f7929eebcc52c7f2e2e667c1a6cd Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 5 Jul 2024 10:14:52 +1000 Subject: [PATCH 170/678] feat(ui): buffered rect drawing --- .../controlLayers/konva/CanvasInpaintMask.ts | 8 +- .../controlLayers/konva/CanvasLayer.ts | 8 +- .../controlLayers/konva/CanvasRegion.ts | 8 +- .../controlLayers/konva/CanvasStateApi.ts | 14 ++++ .../controlLayers/konva/CanvasTool.ts | 61 ++++++++------- .../features/controlLayers/konva/events.ts | 74 ++++++++++++++----- .../controlLayers/store/canvasV2Slice.ts | 3 + .../store/inpaintMaskReducers.ts | 7 ++ .../controlLayers/store/layersReducers.ts | 12 +++ .../controlLayers/store/regionsReducers.ts | 12 +++ 10 files changed, 150 insertions(+), 57 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts index c510e3228ec..0f14cc4fd91 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts @@ -6,7 +6,7 @@ import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox'; import { getObjectGroupId, INPAINT_MASK_LAYER_ID } from 'features/controlLayers/konva/naming'; import { mapId } from 'features/controlLayers/konva/util'; -import type { BrushLine, EraserLine, InpaintMaskEntity } from 'features/controlLayers/store/types'; +import type { BrushLine, EraserLine, InpaintMaskEntity, RectShape } from 'features/controlLayers/store/types'; import { isDrawingTool, RGBA_RED } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { assert } from 'tsafe'; @@ -21,7 +21,7 @@ export class CanvasInpaintMask { compositingRect: Konva.Rect; transformer: Konva.Transformer; objects: Map; - private drawingBuffer: BrushLine | EraserLine | null; + private drawingBuffer: BrushLine | EraserLine | RectShape | null; private inpaintMaskState: InpaintMaskEntity; constructor(entity: InpaintMaskEntity, manager: CanvasManager) { @@ -71,7 +71,7 @@ export class CanvasInpaintMask { return this.drawingBuffer; } - async setDrawingBuffer(obj: BrushLine | EraserLine | null) { + async setDrawingBuffer(obj: BrushLine | EraserLine | RectShape | null) { this.drawingBuffer = obj; if (this.drawingBuffer) { if (this.drawingBuffer.type === 'brush_line') { @@ -91,6 +91,8 @@ export class CanvasInpaintMask { this.manager.stateApi.onBrushLineAdded2({ id: this.id, brushLine: this.drawingBuffer }, 'inpaint_mask'); } else if (this.drawingBuffer.type === 'eraser_line') { this.manager.stateApi.onEraserLineAdded2({ id: this.id, eraserLine: this.drawingBuffer }, 'inpaint_mask'); + } else if (this.drawingBuffer.type === 'rect_shape') { + this.manager.stateApi.onRectShapeAdded2({ id: this.id, rectShape: this.drawingBuffer }, 'layer'); } this.setDrawingBuffer(null); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index fc074ddc401..92be18b40c3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -5,7 +5,7 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; import { getObjectGroupId } from 'features/controlLayers/konva/naming'; import { mapId } from 'features/controlLayers/konva/util'; -import type { BrushLine, EraserLine, LayerEntity } from 'features/controlLayers/store/types'; +import type { BrushLine, EraserLine, LayerEntity, RectShape } from 'features/controlLayers/store/types'; import { isDrawingTool } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { assert } from 'tsafe'; @@ -19,7 +19,7 @@ export class CanvasLayer { objectsGroup: Konva.Group; transformer: Konva.Transformer; objects: Map; - private drawingBuffer: BrushLine | EraserLine | null; + private drawingBuffer: BrushLine | EraserLine | RectShape | null; private layerState: LayerEntity; constructor(entity: LayerEntity, manager: CanvasManager) { @@ -70,7 +70,7 @@ export class CanvasLayer { return this.drawingBuffer; } - async setDrawingBuffer(obj: BrushLine | EraserLine | null) { + async setDrawingBuffer(obj: BrushLine | EraserLine | RectShape | null) { if (obj) { this.drawingBuffer = obj; await this.renderObject(this.drawingBuffer, true); @@ -88,6 +88,8 @@ export class CanvasLayer { this.manager.stateApi.onBrushLineAdded2({ id: this.id, brushLine: this.drawingBuffer }, 'layer'); } else if (this.drawingBuffer.type === 'eraser_line') { this.manager.stateApi.onEraserLineAdded2({ id: this.id, eraserLine: this.drawingBuffer }, 'layer'); + } else if (this.drawingBuffer.type === 'rect_shape') { + this.manager.stateApi.onRectShapeAdded2({ id: this.id, rectShape: this.drawingBuffer }, 'layer'); } this.setDrawingBuffer(null); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts index 2e735e4b5b7..90111cfa22b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts @@ -6,7 +6,7 @@ import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox'; import { getObjectGroupId } from 'features/controlLayers/konva/naming'; import { mapId } from 'features/controlLayers/konva/util'; -import type { BrushLine, EraserLine, RegionEntity } from 'features/controlLayers/store/types'; +import type { BrushLine, EraserLine, RectShape, RegionEntity } from 'features/controlLayers/store/types'; import { isDrawingTool, RGBA_RED } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { assert } from 'tsafe'; @@ -21,7 +21,7 @@ export class CanvasRegion { compositingRect: Konva.Rect; transformer: Konva.Transformer; objects: Map; - private drawingBuffer: BrushLine | EraserLine | null; + private drawingBuffer: BrushLine | EraserLine | RectShape | null; private regionState: RegionEntity; constructor(entity: RegionEntity, manager: CanvasManager) { @@ -71,7 +71,7 @@ export class CanvasRegion { return this.drawingBuffer; } - async setDrawingBuffer(obj: BrushLine | EraserLine | null) { + async setDrawingBuffer(obj: BrushLine | EraserLine | RectShape | null) { this.drawingBuffer = obj; if (this.drawingBuffer) { if (this.drawingBuffer.type === 'brush_line') { @@ -90,6 +90,8 @@ export class CanvasRegion { this.manager.stateApi.onBrushLineAdded2({ id: this.id, brushLine: this.drawingBuffer }, 'regional_guidance'); } else if (this.drawingBuffer.type === 'eraser_line') { this.manager.stateApi.onEraserLineAdded2({ id: this.id, eraserLine: this.drawingBuffer }, 'regional_guidance'); + } else if (this.drawingBuffer.type === 'rect_shape') { + this.manager.stateApi.onRectShapeAdded2({ id: this.id, rectShape: this.drawingBuffer }, 'layer'); } this.setDrawingBuffer(null); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index 735fdb635e7..f936cf15038 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -25,6 +25,7 @@ import { imImageCacheChanged, imLinePointAdded, imRectAdded, + imRectShapeAdded2, imScaled, imTranslated, layerBboxChanged, @@ -35,6 +36,7 @@ import { layerImageCacheChanged, layerLinePointAdded, layerRectAdded, + layerRectShapeAdded2, layerScaled, layerTranslated, rgBboxChanged, @@ -45,6 +47,7 @@ import { rgImageCacheChanged, rgLinePointAdded, rgRectAdded, + rgRectShapeAdded2, rgScaled, rgTranslated, toolBufferChanged, @@ -59,6 +62,7 @@ import type { EraserLineAddedArg, PointAddedToLineArg, PosChangedArg, + RectShape, RectShapeAddedArg, ScaleChangedArg, Tool, @@ -175,6 +179,16 @@ export class CanvasStateApi { this.store.dispatch(imRectAdded(arg)); } }; + onRectShapeAdded2 = (arg: { id: string; rectShape: RectShape }, entityType: CanvasEntity['type']) => { + log.debug('Rect shape added'); + if (entityType === 'layer') { + this.store.dispatch(layerRectShapeAdded2(arg)); + } else if (entityType === 'regional_guidance') { + this.store.dispatch(rgRectShapeAdded2(arg)); + } else if (entityType === 'inpaint_mask') { + this.store.dispatch(imRectShapeAdded2(arg)); + } + }; onBboxTransformed = (bbox: IRect) => { log.debug('Generation bbox transformed'); this.store.dispatch(bboxChanged(bbox)); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts index 825127a596c..54a9501559c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts @@ -5,7 +5,6 @@ import { BRUSH_BORDER_OUTER_COLOR, BRUSH_ERASER_BORDER_WIDTH, } from 'features/controlLayers/konva/constants'; -import { PREVIEW_RECT_ID } from 'features/controlLayers/konva/naming'; import Konva from 'konva'; export class CanvasTool { @@ -23,10 +22,10 @@ export class CanvasTool { innerBorderCircle: Konva.Circle; outerBorderCircle: Konva.Circle; }; - rect: { - group: Konva.Group; - fillRect: Konva.Rect; - }; + // rect: { + // group: Konva.Group; + // fillRect: Konva.Rect; + // }; constructor(manager: CanvasManager) { this.manager = manager; @@ -83,17 +82,17 @@ export class CanvasTool { this.eraser.group.add(this.eraser.outerBorderCircle); this.group.add(this.eraser.group); - // Create the rect preview - this is a rectangle drawn from the last mouse down position to the current cursor position - this.rect = { - group: new Konva.Group(), - fillRect: new Konva.Rect({ - id: PREVIEW_RECT_ID, - listening: false, - strokeEnabled: false, - }), - }; - this.rect.group.add(this.rect.fillRect); - this.group.add(this.rect.group); + // // Create the rect preview - this is a rectangle drawn from the last mouse down position to the current cursor position + // this.rect = { + // group: new Konva.Group(), + // fillRect: new Konva.Rect({ + // id: PREVIEW_RECT_ID, + // listening: false, + // strokeEnabled: false, + // }), + // }; + // this.rect.group.add(this.rect.fillRect); + // this.group.add(this.rect.group); } scaleTool = () => { @@ -189,7 +188,7 @@ export class CanvasTool { this.brush.group.visible(true); this.eraser.group.visible(false); - this.rect.group.visible(false); + // this.rect.group.visible(false); } else if (cursorPos && tool === 'eraser') { const scale = stage.scaleX(); // Update the fill circle @@ -215,23 +214,23 @@ export class CanvasTool { this.brush.group.visible(false); this.eraser.group.visible(true); - this.rect.group.visible(false); - } else if (cursorPos && lastMouseDownPos && tool === 'rect') { - this.rect.fillRect.setAttrs({ - x: Math.min(cursorPos.x, lastMouseDownPos.x), - y: Math.min(cursorPos.y, lastMouseDownPos.y), - width: Math.abs(cursorPos.x - lastMouseDownPos.x), - height: Math.abs(cursorPos.y - lastMouseDownPos.y), - fill: rgbaColorToString(currentFill), - visible: true, - }); - this.brush.group.visible(false); - this.eraser.group.visible(false); - this.rect.group.visible(true); + // this.rect.group.visible(false); + // } else if (cursorPos && lastMouseDownPos && tool === 'rect') { + // this.rect.fillRect.setAttrs({ + // x: Math.min(cursorPos.x, lastMouseDownPos.x), + // y: Math.min(cursorPos.y, lastMouseDownPos.y), + // width: Math.abs(cursorPos.x - lastMouseDownPos.x), + // height: Math.abs(cursorPos.y - lastMouseDownPos.y), + // fill: rgbaColorToString(currentFill), + // visible: true, + // }); + // this.brush.group.visible(false); + // this.eraser.group.visible(false); + // this.rect.group.visible(true); } else { this.brush.group.visible(false); this.eraser.group.visible(false); - this.rect.group.visible(false); + // this.rect.group.visible(false); } } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 82ba286a8a4..69ceb5a2917 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -16,7 +16,7 @@ import { clamp } from 'lodash-es'; import { v4 as uuidv4 } from 'uuid'; import { BRUSH_SPACING_TARGET_SCALE, CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from './constants'; -import { getBrushLineId, getEraserLineId } from './naming'; +import { getBrushLineId, getEraserLineId, getRectShapeId } from './naming'; /** * Updates the last cursor position atom with the current cursor position, returning the new position or `null` if the @@ -267,6 +267,21 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { } setLastAddedPoint(pos); } + + if (toolState.selected === 'rect') { + if (selectedEntityAdapter.getDrawingBuffer()) { + selectedEntityAdapter.finalizeDrawingBuffer(); + } + await selectedEntityAdapter.setDrawingBuffer({ + id: getRectShapeId(selectedEntityAdapter.id, uuidv4()), + type: 'rect_shape', + x: pos.x - selectedEntity.x, + y: pos.y - selectedEntity.y, + width: 0, + height: 0, + color: getCurrentFill(), + }); + } } manager.preview.tool.render(); }); @@ -284,7 +299,8 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { isDrawableEntity(selectedEntity) && selectedEntityAdapter && isDrawableEntityAdapter(selectedEntityAdapter) && - !getSpaceKey() + !getSpaceKey() && + getIsPrimaryMouseDown(e) ) { const toolState = getToolState(); @@ -307,22 +323,28 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { } if (toolState.selected === 'rect') { - const lastMouseDownPos = getLastMouseDownPos(); - if (lastMouseDownPos) { - onRectShapeAdded( - { - id: selectedEntity.id, - rect: { - x: Math.min(pos.x - selectedEntity.x, lastMouseDownPos.x - selectedEntity.x), - y: Math.min(pos.y - selectedEntity.y, lastMouseDownPos.y - selectedEntity.y), - width: Math.abs(pos.x - lastMouseDownPos.x), - height: Math.abs(pos.y - lastMouseDownPos.y), - }, - color: getCurrentFill(), - }, - selectedEntity.type - ); + const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); + if (drawingBuffer?.type === 'rect_shape') { + selectedEntityAdapter.finalizeDrawingBuffer(); + } else { + await selectedEntityAdapter.setDrawingBuffer(null); } + // const lastMouseDownPos = getLastMouseDownPos(); + // if (lastMouseDownPos) { + // onRectShapeAdded( + // { + // id: selectedEntity.id, + // rect: { + // x: Math.min(pos.x - selectedEntity.x, lastMouseDownPos.x - selectedEntity.x), + // y: Math.min(pos.y - selectedEntity.y, lastMouseDownPos.y - selectedEntity.y), + // width: Math.abs(pos.x - lastMouseDownPos.x), + // height: Math.abs(pos.y - lastMouseDownPos.y), + // }, + // color: getCurrentFill(), + // }, + // selectedEntity.type + // ); + // } } setLastMouseDownPos(null); @@ -415,6 +437,19 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { setLastAddedPoint(pos); } } + + if (toolState.selected === 'rect') { + const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); + if (drawingBuffer) { + if (drawingBuffer.type === 'rect_shape') { + drawingBuffer.width = pos.x - selectedEntity.x - drawingBuffer.x; + drawingBuffer.height = pos.y - selectedEntity.y - drawingBuffer.y; + await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); + } else { + await selectedEntityAdapter.setDrawingBuffer(null); + } + } + } } manager.preview.tool.render(); }); @@ -446,6 +481,11 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { drawingBuffer.points.push(pos.x - selectedEntity.x, pos.y - selectedEntity.y); await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); selectedEntityAdapter.finalizeDrawingBuffer(); + } else if (toolState.selected === 'rect' && drawingBuffer?.type === 'rect_shape') { + drawingBuffer.width = pos.x - selectedEntity.x - drawingBuffer.x; + drawingBuffer.height = pos.y - selectedEntity.y - drawingBuffer.y; + await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); + selectedEntityAdapter.finalizeDrawingBuffer(); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 104a6d13b2e..14107c60ffd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -349,10 +349,13 @@ export const { stagingAreaPreviousImageSelected, layerBrushLineAdded2, layerEraserLineAdded2, + layerRectShapeAdded2, rgBrushLineAdded2, rgEraserLineAdded2, + rgRectShapeAdded2, imBrushLineAdded2, imEraserLineAdded2, + imRectShapeAdded2, } = canvasV2Slice.actions; export const selectCanvasV2Slice = (state: RootState) => state.canvasV2; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts index 9dc78b61055..05e6bc4b222 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts @@ -5,6 +5,7 @@ import type { CanvasV2State, EraserLine, InpaintMaskEntity, + RectShape, ScaleChangedArg, } from 'features/controlLayers/store/types'; import { imageDTOToImageWithDims, RGBA_RED } from 'features/controlLayers/store/types'; @@ -99,6 +100,12 @@ export const inpaintMaskReducers = { state.inpaintMask.bboxNeedsUpdate = true; state.layers.imageCache = null; }, + imRectShapeAdded2: (state, action: PayloadAction<{ rectShape: RectShape }>) => { + const { rectShape } = action.payload; + state.inpaintMask.objects.push(rectShape); + state.inpaintMask.bboxNeedsUpdate = true; + state.layers.imageCache = null; + }, imEraserLineAdded: { reducer: (state, action: PayloadAction & { lineId: string }>) => { const { points, lineId, width, clip } = action.payload; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index 11997808183..6308f0d2ea9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -15,6 +15,7 @@ import type { ImageObjectAddedArg, LayerEntity, PointAddedToLineArg, + RectShape, RectShapeAddedArg, ScaleChangedArg, } from './types'; @@ -176,6 +177,17 @@ export const layersReducers = { layer.bboxNeedsUpdate = true; state.layers.imageCache = null; }, + layerRectShapeAdded2: (state, action: PayloadAction<{ id: string; rectShape: RectShape }>) => { + const { id, rectShape } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + + layer.objects.push(rectShape); + layer.bboxNeedsUpdate = true; + state.layers.imageCache = null; + }, layerBrushLineAdded: { reducer: (state, action: PayloadAction) => { const { id, points, lineId, color, width, clip } = action.payload; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index b033d6f8134..2fd0e69cfd9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -7,6 +7,7 @@ import type { CLIPVisionModelV2, EraserLine, IPMethodV2, + RectShape, ScaleChangedArg, } from 'features/controlLayers/store/types'; import { imageDTOToImageObject, imageDTOToImageWithDims, RGBA_RED } from 'features/controlLayers/store/types'; @@ -383,6 +384,17 @@ export const regionsReducers = { rg.bboxNeedsUpdate = true; state.layers.imageCache = null; }, + rgRectShapeAdded2: (state, action: PayloadAction<{ id: string; rectShape: RectShape }>) => { + const { id, rectShape } = action.payload; + const rg = selectRG(state, id); + if (!rg) { + return; + } + + rg.objects.push(rectShape); + rg.bboxNeedsUpdate = true; + state.layers.imageCache = null; + }, rgEraserLineAdded: { reducer: (state, action: PayloadAction) => { const { id, points, lineId, width, clip } = action.payload; From 1667603d1cfe8246671bad7560e7982af4300d9c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 5 Jul 2024 10:44:24 +1000 Subject: [PATCH 171/678] fix(ui): handle mouseup correctly --- .../features/controlLayers/konva/events.ts | 69 +------------------ 1 file changed, 2 insertions(+), 67 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 69ceb5a2917..95a5d8b3e8c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -1,7 +1,6 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getScaledCursorPosition } from 'features/controlLayers/konva/util'; import type { - CanvasEntity, CanvasV2State, InpaintMaskEntity, LayerEntity, @@ -11,7 +10,6 @@ import type { import { isDrawableEntity, isDrawableEntityAdapter } from 'features/controlLayers/store/types'; import type Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; -import type { Vector2d } from 'konva/lib/types'; import { clamp } from 'lodash-es'; import { v4 as uuidv4 } from 'uuid'; @@ -45,50 +43,6 @@ const calculateNewBrushSize = (brushSize: number, delta: number) => { return newBrushSize; }; -/** - * Adds the next point to a line if the cursor has moved far enough from the last point. - * @param layerId The layer to (maybe) add the point to - * @param currentPos The current cursor position - * @param $lastAddedPoint The last added line point as a nanostores atom - * @param $brushSpacingPx The brush spacing in pixels as a nanostores atom - * @param onPointAddedToLine The callback to add a point to a line - */ -const maybeAddNextPoint = ( - selectedEntity: CanvasEntity, - currentPos: Vector2d, - getToolState: CanvasManager['stateApi']['getToolState'], - getLastAddedPoint: CanvasManager['stateApi']['getLastAddedPoint'], - setLastAddedPoint: CanvasManager['stateApi']['setLastAddedPoint'], - onPointAddedToLine: CanvasManager['stateApi']['onPointAddedToLine'] -) => { - if (!isDrawableEntity(selectedEntity)) { - return; - } - - // Continue the last line - const lastAddedPoint = getLastAddedPoint(); - const toolState = getToolState(); - const minSpacingPx = - toolState.selected === 'brush' - ? toolState.brush.width * BRUSH_SPACING_TARGET_SCALE - : toolState.eraser.width * BRUSH_SPACING_TARGET_SCALE; - - if (lastAddedPoint) { - // Dispatching redux events impacts perf substantially - using brush spacing keeps dispatches to a reasonable number - if (Math.hypot(lastAddedPoint.x - currentPos.x, lastAddedPoint.y - currentPos.y) < minSpacingPx) { - return; - } - } - setLastAddedPoint(currentPos); - onPointAddedToLine( - { - id: selectedEntity.id, - point: [currentPos.x - selectedEntity.x, currentPos.y - selectedEntity.y], - }, - selectedEntity.type - ); -}; - const getNextPoint = ( currentPos: Position, toolState: CanvasV2State['tool'], @@ -118,7 +72,6 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { setTool, setToolBuffer, setIsMouseDown, - getLastMouseDownPos, setLastMouseDownPos, getLastCursorPos, setLastCursorPos, @@ -130,7 +83,6 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { setSpaceKey, getBbox, getSettings, - onRectShapeAdded, onBrushWidthChanged, onEraserWidthChanged, } = stateApi; @@ -287,7 +239,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { }); //#region mouseup - stage.on('mouseup', async (e) => { + stage.on('mouseup', async () => { setIsMouseDown(false); const pos = getLastCursorPos(); const selectedEntity = getSelectedEntity(); @@ -299,8 +251,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { isDrawableEntity(selectedEntity) && selectedEntityAdapter && isDrawableEntityAdapter(selectedEntityAdapter) && - !getSpaceKey() && - getIsPrimaryMouseDown(e) + !getSpaceKey() ) { const toolState = getToolState(); @@ -329,22 +280,6 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { } else { await selectedEntityAdapter.setDrawingBuffer(null); } - // const lastMouseDownPos = getLastMouseDownPos(); - // if (lastMouseDownPos) { - // onRectShapeAdded( - // { - // id: selectedEntity.id, - // rect: { - // x: Math.min(pos.x - selectedEntity.x, lastMouseDownPos.x - selectedEntity.x), - // y: Math.min(pos.y - selectedEntity.y, lastMouseDownPos.y - selectedEntity.y), - // width: Math.abs(pos.x - lastMouseDownPos.x), - // height: Math.abs(pos.y - lastMouseDownPos.y), - // }, - // color: getCurrentFill(), - // }, - // selectedEntity.type - // ); - // } } setLastMouseDownPos(null); From fc4779e095674e4bbe87e15fcc4d15e4ec8ce2b7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 5 Jul 2024 10:44:47 +1000 Subject: [PATCH 172/678] fix(ui): set buffered rect color to full alpha --- .../controlLayers/konva/CanvasInpaintMask.ts | 4 ++- .../controlLayers/konva/CanvasRegion.ts | 5 +++- .../controlLayers/konva/CanvasTool.ts | 25 +++++++++---------- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts index 0f14cc4fd91..63a932eac9d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts @@ -76,6 +76,8 @@ export class CanvasInpaintMask { if (this.drawingBuffer) { if (this.drawingBuffer.type === 'brush_line') { this.drawingBuffer.color = RGBA_RED; + } else if (this.drawingBuffer.type === 'rect_shape') { + this.drawingBuffer.color = RGBA_RED; } await this.renderObject(this.drawingBuffer, true); @@ -92,7 +94,7 @@ export class CanvasInpaintMask { } else if (this.drawingBuffer.type === 'eraser_line') { this.manager.stateApi.onEraserLineAdded2({ id: this.id, eraserLine: this.drawingBuffer }, 'inpaint_mask'); } else if (this.drawingBuffer.type === 'rect_shape') { - this.manager.stateApi.onRectShapeAdded2({ id: this.id, rectShape: this.drawingBuffer }, 'layer'); + this.manager.stateApi.onRectShapeAdded2({ id: this.id, rectShape: this.drawingBuffer }, 'inpaint_mask'); } this.setDrawingBuffer(null); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts index 90111cfa22b..609e5447bac 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts @@ -76,7 +76,10 @@ export class CanvasRegion { if (this.drawingBuffer) { if (this.drawingBuffer.type === 'brush_line') { this.drawingBuffer.color = RGBA_RED; + } else if (this.drawingBuffer.type === 'rect_shape') { + this.drawingBuffer.color = RGBA_RED; } + await this.renderObject(this.drawingBuffer, true); this.updateGroup(true); } @@ -91,7 +94,7 @@ export class CanvasRegion { } else if (this.drawingBuffer.type === 'eraser_line') { this.manager.stateApi.onEraserLineAdded2({ id: this.id, eraserLine: this.drawingBuffer }, 'regional_guidance'); } else if (this.drawingBuffer.type === 'rect_shape') { - this.manager.stateApi.onRectShapeAdded2({ id: this.id, rectShape: this.drawingBuffer }, 'layer'); + this.manager.stateApi.onRectShapeAdded2({ id: this.id, rectShape: this.drawingBuffer }, 'regional_guidance'); } this.setDrawingBuffer(null); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts index 54a9501559c..76145f954b7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts @@ -121,7 +121,6 @@ export class CanvasTool { const currentFill = this.manager.stateApi.getCurrentFill(); const selectedEntity = this.manager.stateApi.getSelectedEntity(); const cursorPos = this.manager.stateApi.getLastCursorPos(); - const lastMouseDownPos = this.manager.stateApi.getLastMouseDownPos(); const isDrawing = this.manager.stateApi.getIsDrawing(); const isMouseDown = this.manager.stateApi.getIsMouseDown(); @@ -215,18 +214,18 @@ export class CanvasTool { this.brush.group.visible(false); this.eraser.group.visible(true); // this.rect.group.visible(false); - // } else if (cursorPos && lastMouseDownPos && tool === 'rect') { - // this.rect.fillRect.setAttrs({ - // x: Math.min(cursorPos.x, lastMouseDownPos.x), - // y: Math.min(cursorPos.y, lastMouseDownPos.y), - // width: Math.abs(cursorPos.x - lastMouseDownPos.x), - // height: Math.abs(cursorPos.y - lastMouseDownPos.y), - // fill: rgbaColorToString(currentFill), - // visible: true, - // }); - // this.brush.group.visible(false); - // this.eraser.group.visible(false); - // this.rect.group.visible(true); + // } else if (cursorPos && lastMouseDownPos && tool === 'rect') { + // this.rect.fillRect.setAttrs({ + // x: Math.min(cursorPos.x, lastMouseDownPos.x), + // y: Math.min(cursorPos.y, lastMouseDownPos.y), + // width: Math.abs(cursorPos.x - lastMouseDownPos.x), + // height: Math.abs(cursorPos.y - lastMouseDownPos.y), + // fill: rgbaColorToString(currentFill), + // visible: true, + // }); + // this.brush.group.visible(false); + // this.eraser.group.visible(false); + // this.rect.group.visible(true); } else { this.brush.group.visible(false); this.eraser.group.visible(false); From 0869e23dc1654da5b34128b4e42ecfbf71f4473e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 5 Jul 2024 11:03:36 +1000 Subject: [PATCH 173/678] fix(ui): edge cases when holding shift and drawing lines --- .../features/controlLayers/konva/events.ts | 128 ++++++++++++------ 1 file changed, 84 insertions(+), 44 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 95a5d8b3e8c..4793faf7b71 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -6,6 +6,7 @@ import type { LayerEntity, Position, RegionEntity, + Tool, } from 'features/controlLayers/store/types'; import { isDrawableEntity, isDrawableEntityAdapter } from 'features/controlLayers/store/types'; import type Konva from 'konva'; @@ -64,6 +65,49 @@ const getNextPoint = ( return currentPos; }; +const getLastPointOfLine = (points: number[]): Position | null => { + if (points.length < 2) { + return null; + } + const x = points[points.length - 2]; + const y = points[points.length - 1]; + if (x === undefined || y === undefined) { + return null; + } + return { x, y }; +}; + +const getLastPointOfLastLineOfEntity = ( + entity: LayerEntity | RegionEntity | InpaintMaskEntity, + tool: Tool +): Position | null => { + const lastObject = entity.objects[entity.objects.length - 1]; + + if (!lastObject) { + return null; + } + + if ( + !( + (lastObject.type === 'brush_line' && tool === 'brush') || + (lastObject.type === 'eraser_line' && tool === 'eraser') + ) + ) { + // If the last object type and current tool do not match, we cannot continue the line + return null; + } + + if (lastObject.points.length < 2) { + return null; + } + const x = lastObject.points[lastObject.points.length - 2]; + const y = lastObject.points[lastObject.points.length - 1]; + if (x === undefined || y === undefined) { + return null; + } + return { x, y }; +}; + export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { const { stage, stateApi, getSelectedEntityAdapter } = manager; const { @@ -75,7 +119,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { setLastMouseDownPos, getLastCursorPos, setLastCursorPos, - getLastAddedPoint, + // getLastAddedPoint, setLastAddedPoint, setStageAttrs, getSelectedEntity, @@ -137,27 +181,26 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { setLastMouseDownPos(pos); if (toolState.selected === 'brush') { - if (e.evt.shiftKey) { - const lastAddedPoint = getLastAddedPoint(); - // Create a straight line if holding shift - if (lastAddedPoint) { - if (selectedEntityAdapter.getDrawingBuffer()) { - selectedEntityAdapter.finalizeDrawingBuffer(); - } - await selectedEntityAdapter.setDrawingBuffer({ - id: getBrushLineId(selectedEntityAdapter.id, uuidv4()), - type: 'brush_line', - points: [ - lastAddedPoint.x - selectedEntity.x, - lastAddedPoint.y - selectedEntity.y, - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - ], - strokeWidth: toolState.brush.width, - color: getCurrentFill(), - clip: getClip(selectedEntity), - }); + const lastLinePoint = getLastPointOfLastLineOfEntity(selectedEntity, toolState.selected); + if (e.evt.shiftKey && lastLinePoint) { + // Create a straight line from the last line point + if (selectedEntityAdapter.getDrawingBuffer()) { + selectedEntityAdapter.finalizeDrawingBuffer(); } + await selectedEntityAdapter.setDrawingBuffer({ + id: getBrushLineId(selectedEntityAdapter.id, uuidv4()), + type: 'brush_line', + points: [ + // The last point of the last line is already normalized to the entity's coordinates + lastLinePoint.x, + lastLinePoint.y, + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + ], + strokeWidth: toolState.brush.width, + color: getCurrentFill(), + clip: getClip(selectedEntity), + }); } else { if (selectedEntityAdapter.getDrawingBuffer()) { selectedEntityAdapter.finalizeDrawingBuffer(); @@ -180,26 +223,25 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { } if (toolState.selected === 'eraser') { - if (e.evt.shiftKey) { - // Create a straight line if holding shift - const lastAddedPoint = getLastAddedPoint(); - if (lastAddedPoint) { - if (selectedEntityAdapter.getDrawingBuffer()) { - selectedEntityAdapter.finalizeDrawingBuffer(); - } - await selectedEntityAdapter.setDrawingBuffer({ - id: getBrushLineId(selectedEntityAdapter.id, uuidv4()), - type: 'eraser_line', - points: [ - lastAddedPoint.x - selectedEntity.x, - lastAddedPoint.y - selectedEntity.y, - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - ], - strokeWidth: toolState.eraser.width, - clip: getClip(selectedEntity), - }); + const lastLinePoint = getLastPointOfLastLineOfEntity(selectedEntity, toolState.selected); + if (e.evt.shiftKey && lastLinePoint) { + // Create a straight line from the last line point + if (selectedEntityAdapter.getDrawingBuffer()) { + selectedEntityAdapter.finalizeDrawingBuffer(); } + await selectedEntityAdapter.setDrawingBuffer({ + id: getBrushLineId(selectedEntityAdapter.id, uuidv4()), + type: 'eraser_line', + points: [ + // The last point of the last line is already normalized to the entity's coordinates + lastLinePoint.x, + lastLinePoint.y, + pos.x - selectedEntity.x, + pos.y - selectedEntity.y, + ], + strokeWidth: toolState.eraser.width, + clip: getClip(selectedEntity), + }); } else { if (selectedEntityAdapter.getDrawingBuffer()) { selectedEntityAdapter.finalizeDrawingBuffer(); @@ -308,8 +350,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); if (drawingBuffer) { if (drawingBuffer?.type === 'brush_line') { - const lastAddedPoint = getLastAddedPoint(); - const nextPoint = getNextPoint(pos, toolState, lastAddedPoint); + const nextPoint = getNextPoint(pos, toolState, getLastPointOfLine(drawingBuffer.points)); if (nextPoint) { drawingBuffer.points.push(nextPoint.x - selectedEntity.x, nextPoint.y - selectedEntity.y); await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); @@ -343,8 +384,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); if (drawingBuffer) { if (drawingBuffer.type === 'eraser_line') { - const lastAddedPoint = getLastAddedPoint(); - const nextPoint = getNextPoint(pos, toolState, lastAddedPoint); + const nextPoint = getNextPoint(pos, toolState, getLastPointOfLine(drawingBuffer.points)); if (nextPoint) { drawingBuffer.points.push(nextPoint.x - selectedEntity.x, nextPoint.y - selectedEntity.y); await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); From 5663029ba6fc004e19f3313692520a2bb42153c7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 5 Jul 2024 11:06:28 +1000 Subject: [PATCH 174/678] feat(ui): fix issue where creating line needs 2 points --- .../controlLayers/konva/CanvasBrushLine.ts | 6 ++-- .../controlLayers/konva/CanvasEraserLine.ts | 6 ++-- .../features/controlLayers/konva/events.ts | 28 +++---------------- 3 files changed, 12 insertions(+), 28 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts index 9ce10450a13..e607e705ea1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts @@ -25,7 +25,8 @@ export class CanvasBrushLine { lineJoin: 'round', globalCompositeOperation: 'source-over', stroke: rgbaColorToString(color), - points, + // A line with only one point will not be rendered, so we duplicate the points to make it visible + points: points.length === 2 ? [...points, ...points] : points, }); this.konvaLineGroup.add(this.konvaLine); this.lastBrushLine = brushLine; @@ -35,7 +36,8 @@ export class CanvasBrushLine { if (this.lastBrushLine !== brushLine || force) { const { points, color, clip, strokeWidth } = brushLine; this.konvaLine.setAttrs({ - points, + // A line with only one point will not be rendered, so we duplicate the points to make it visible + points: points.length === 2 ? [...points, ...points] : points, stroke: rgbaColorToString(color), clip, strokeWidth, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts index 76ebf13cfd4..ead3b4e0906 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts @@ -26,7 +26,8 @@ export class CanvasEraserLine { lineJoin: 'round', globalCompositeOperation: 'destination-out', stroke: rgbaColorToString(RGBA_RED), - points, + // A line with only one point will not be rendered, so we duplicate the points to make it visible + points: points.length === 2 ? [...points, ...points] : points, }); this.konvaLineGroup.add(this.konvaLine); this.lastEraserLine = eraserLine; @@ -36,7 +37,8 @@ export class CanvasEraserLine { if (this.lastEraserLine !== eraserLine || force) { const { points, clip, strokeWidth } = eraserLine; this.konvaLine.setAttrs({ - points, + // A line with only one point will not be rendered, so we duplicate the points to make it visible + points: points.length === 2 ? [...points, ...points] : points, clip, strokeWidth, }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 4793faf7b71..53a0218ab6e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -208,12 +208,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.setDrawingBuffer({ id: getBrushLineId(selectedEntityAdapter.id, uuidv4()), type: 'brush_line', - points: [ - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - ], + points: [pos.x - selectedEntity.x, pos.y - selectedEntity.y], strokeWidth: toolState.brush.width, color: getCurrentFill(), clip: getClip(selectedEntity), @@ -249,12 +244,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.setDrawingBuffer({ id: getEraserLineId(selectedEntityAdapter.id, uuidv4()), type: 'eraser_line', - points: [ - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - ], + points: [pos.x - selectedEntity.x, pos.y - selectedEntity.y], strokeWidth: toolState.eraser.width, clip: getClip(selectedEntity), }); @@ -366,12 +356,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.setDrawingBuffer({ id: getBrushLineId(selectedEntityAdapter.id, uuidv4()), type: 'brush_line', - points: [ - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - ], + points: [pos.x - selectedEntity.x, pos.y - selectedEntity.y], strokeWidth: toolState.brush.width, color: getCurrentFill(), clip: getClip(selectedEntity), @@ -400,12 +385,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.setDrawingBuffer({ id: getEraserLineId(selectedEntityAdapter.id, uuidv4()), type: 'eraser_line', - points: [ - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, - ], + points: [pos.x - selectedEntity.x, pos.y - selectedEntity.y], strokeWidth: toolState.eraser.width, clip: getClip(selectedEntity), }); From 43f550e48b67b5b2812ea4b130d1397696e4d6d8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 5 Jul 2024 11:11:03 +1000 Subject: [PATCH 175/678] fix(ui): do not floor bbox calc, it cuts off the last pixels --- .../web/src/features/controlLayers/konva/entityBbox.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts index 1448d5a8f7f..0dcb9d2d3c5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts @@ -188,12 +188,7 @@ const getLayerBboxPixels = ( */ export const getNodeBboxFast = (node: Konva.Node): IRect => { const bbox = node.getClientRect(GET_CLIENT_RECT_CONFIG); - return { - x: Math.floor(bbox.x), - y: Math.floor(bbox.y), - width: Math.floor(bbox.width), - height: Math.floor(bbox.height), - }; + return bbox; }; const filterRGChildren = (node: Konva.Node): boolean => node.name() === RG_LAYER_OBJECT_GROUP_NAME; From 08e588a6c47acce7059fe1d9405f8b19b46d156f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 5 Jul 2024 17:12:56 +1000 Subject: [PATCH 176/678] feat(ui): control adapter image rendering --- .../konva/CanvasControlAdapter.ts | 140 ++++++++++++++---- .../controlLayers/konva/CanvasImage.ts | 8 +- .../controlLayers/konva/CanvasManager.ts | 2 +- .../features/controlLayers/konva/filters.ts | 4 + .../store/controlAdaptersReducers.ts | 6 +- .../src/features/controlLayers/store/types.ts | 8 +- 6 files changed, 131 insertions(+), 37 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts index aae0a5fcb85..40e4db2910e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts @@ -1,20 +1,24 @@ import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; -import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getObjectGroupId } from 'features/controlLayers/konva/naming'; -import type { ControlAdapterEntity } from 'features/controlLayers/store/types'; +import { type ControlAdapterEntity, isDrawingTool } from 'features/controlLayers/store/types'; import Konva from 'konva'; -import { isEqual } from 'lodash-es'; import { v4 as uuidv4 } from 'uuid'; export class CanvasControlAdapter { id: string; + manager: CanvasManager; layer: Konva.Layer; group: Konva.Group; + objectsGroup: Konva.Group; image: CanvasImage | null; + transformer: Konva.Transformer; + private controlAdapterState: ControlAdapterEntity; - constructor(entity: ControlAdapterEntity) { - const { id } = entity; + constructor(controlAdapterState: ControlAdapterEntity, manager: CanvasManager) { + const { id } = controlAdapterState; this.id = id; + this.manager = manager; this.layer = new Konva.Layer({ id, imageSmoothingEnabled: false, @@ -24,47 +28,123 @@ export class CanvasControlAdapter { id: getObjectGroupId(this.layer.id(), uuidv4()), listening: false, }); + this.objectsGroup = new Konva.Group({ listening: false }); + this.group.add(this.objectsGroup); this.layer.add(this.group); + + this.transformer = new Konva.Transformer({ + shouldOverdrawWholeArea: true, + draggable: true, + dragDistance: 0, + enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], + rotateEnabled: false, + flipEnabled: false, + }); + this.transformer.on('transformend', () => { + this.manager.stateApi.onScaleChanged( + { id: this.id, scale: this.group.scaleX(), x: this.group.x(), y: this.group.y() }, + 'layer' + ); + }); + this.transformer.on('dragend', () => { + this.manager.stateApi.onPosChanged({ id: this.id, x: this.group.x(), y: this.group.y() }, 'layer'); + }); + this.layer.add(this.transformer); + this.image = null; + this.controlAdapterState = controlAdapterState; } - async render(entity: ControlAdapterEntity) { - const imageObject = entity.processedImageObject ?? entity.imageObject; + async render(controlAdapterState: ControlAdapterEntity) { + this.controlAdapterState = controlAdapterState; + const imageObject = controlAdapterState.processedImageObject ?? controlAdapterState.imageObject; + + let didDraw = false; + if (!imageObject) { if (this.image) { this.image.konvaImageGroup.visible(false); + didDraw = true; } - return; - } - - const opacity = entity.opacity; - const visible = entity.isEnabled; - const filters = entity.filter === 'LightnessToAlphaFilter' ? [LightnessToAlphaFilter] : []; - - if (!this.image) { + } else if (!this.image) { this.image = await new CanvasImage(imageObject, { - onLoad: (konvaImage) => { - konvaImage.filters(filters); - konvaImage.cache(); - konvaImage.opacity(opacity); - konvaImage.visible(visible); + onLoad: () => { + this.updateGroup(true); }, }); - this.group.add(this.image.konvaImageGroup); + this.objectsGroup.add(this.image.konvaImageGroup); + await this.image.updateImageSource(imageObject.image.name); + } else if (!this.image.isLoading && !this.image.isError) { + if (await this.image.update(imageObject)) { + didDraw = true; + } + } + + this.updateGroup(didDraw); + } + + updateGroup(didDraw: boolean) { + this.layer.visible(this.controlAdapterState.isEnabled); + + this.group.opacity(this.controlAdapterState.opacity); + const isSelected = this.manager.stateApi.getIsSelected(this.id); + const selectedTool = this.manager.stateApi.getToolState().selected; + + if (!this.image?.konvaImage) { + // If the layer is totally empty, reset the cache and bail out. + this.layer.listening(false); + this.transformer.nodes([]); + if (this.group.isCached()) { + this.group.clearCache(); + } + return; } - if (this.image.isLoading || this.image.isError) { + + if (isSelected && selectedTool === 'move') { + // When the layer is selected and being moved, we should always cache it. + // We should update the cache if we drew to the layer. + if (!this.group.isCached() || didDraw) { + this.group.cache(); + } + // Activate the transformer + this.layer.listening(true); + this.transformer.nodes([this.group]); + this.transformer.forceUpdate(); return; } - if (this.image.imageName !== imageObject.image.name) { - this.image.updateImageSource(imageObject.image.name); + + if (isSelected && selectedTool !== 'move') { + // If the layer is selected but not using the move tool, we don't want the layer to be listening. + this.layer.listening(false); + // The transformer also does not need to be active. + this.transformer.nodes([]); + if (isDrawingTool(selectedTool)) { + // We are using a drawing tool (brush, eraser, rect). These tools change the layer's rendered appearance, so we + // should never be cached. + if (this.group.isCached()) { + this.group.clearCache(); + } + } else { + // We are using a non-drawing tool (move, view, bbox), so we should cache the layer. + // We should update the cache if we drew to the layer. + if (!this.group.isCached() || didDraw) { + this.group.cache(); + } + } + return; } - if (this.image.konvaImage) { - if (!isEqual(this.image.konvaImage.filters(), filters)) { - this.image.konvaImage.filters(filters); - this.image.konvaImage.cache(); + + if (!isSelected) { + // Unselected layers should not be listening + this.layer.listening(false); + // The transformer also does not need to be active. + this.transformer.nodes([]); + // Update the layer's cache if it's not already cached or we drew to it. + if (!this.group.isCached() || didDraw) { + this.group.cache(); } - this.image.konvaImage.opacity(opacity); - this.image.konvaImage.visible(visible); + + return; } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts index 575fcc4e64f..62e55f83f6e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -1,3 +1,4 @@ +import { FILTER_MAP } from 'features/controlLayers/konva/filters'; import type { ImageObject } from 'features/controlLayers/store/types'; import { t } from 'i18next'; import Konva from 'konva'; @@ -30,7 +31,7 @@ export class CanvasImage { } ) { const { getImageDTO, onLoading, onLoad, onError } = options; - const { id, width, height, x, y } = imageObject; + const { id, width, height, x, y, filters } = imageObject; this.konvaImageGroup = new Konva.Group({ id, listening: false, x, y }); this.konvaPlaceholderGroup = new Konva.Group({ listening: false }); this.konvaPlaceholderRect = new Konva.Rect({ @@ -85,6 +86,7 @@ export class CanvasImage { image: imageEl, width, height, + filters: filters.map((f) => FILTER_MAP[f]), }); this.konvaImageGroup.add(this.konvaImage); } @@ -138,11 +140,11 @@ export class CanvasImage { async update(imageObject: ImageObject, force?: boolean): Promise { if (this.lastImageObject !== imageObject || force) { - const { width, height, x, y, image } = imageObject; + const { width, height, x, y, image, filters } = imageObject; if (this.lastImageObject.image.name !== image.name || force) { await this.updateImageSource(image.name); } - this.konvaImage?.setAttrs({ x, y, width, height }); + this.konvaImage?.setAttrs({ x, y, width, height, filters: filters.map((f) => FILTER_MAP[f]) }); this.konvaPlaceholderRect.setAttrs({ width, height }); this.konvaPlaceholderText.setAttrs({ width, height, fontSize: width / 16 }); this.lastImageObject = imageObject; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 72640e7d716..9af96132394 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -164,7 +164,7 @@ export class CanvasManager { for (const entity of entities) { let adapter = this.controlAdapters.get(entity.id); if (!adapter) { - adapter = new CanvasControlAdapter(entity); + adapter = new CanvasControlAdapter(entity, this); this.controlAdapters.set(adapter.id, adapter); this.stage.add(adapter.layer); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/filters.ts b/invokeai/frontend/web/src/features/controlLayers/konva/filters.ts index 2fcdf4ce606..da374a762bf 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/filters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/filters.ts @@ -19,3 +19,7 @@ export const LightnessToAlphaFilter = (imageData: ImageData): void => { imageData.data[i * 4 + 3] = (cMin + cMax) / 2; } }; + +export const FILTER_MAP = { + LightnessToAlphaFilter: LightnessToAlphaFilter, +} as const; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts index 0601061f73d..2c1750a5c1c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts @@ -139,7 +139,7 @@ export const controlAdaptersReducers = { ca.bboxNeedsUpdate = true; ca.isEnabled = true; if (imageDTO) { - const newImageObject = imageDTOToImageObject(id, objectId, imageDTO); + const newImageObject = imageDTOToImageObject(id, objectId, imageDTO, { filters: ca.filter ? [ca.filter] : [] }); if (isEqual(newImageObject, ca.imageObject)) { return; } @@ -162,7 +162,9 @@ export const controlAdaptersReducers = { ca.bbox = null; ca.bboxNeedsUpdate = true; ca.isEnabled = true; - ca.processedImageObject = imageDTO ? imageDTOToImageObject(id, objectId, imageDTO) : null; + ca.processedImageObject = imageDTO + ? imageDTOToImageObject(id, objectId, imageDTO, { filters: ca.filter ? [ca.filter] : [] }) + : null; }, prepare: (payload: { id: string; imageDTO: ImageDTO | null }) => ({ payload: { ...payload, objectId: uuidv4() } }), }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 872afbe56bb..af00a59b61d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -761,7 +761,12 @@ export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO) height, }); -export const imageDTOToImageObject = (entityId: string, objectId: string, imageDTO: ImageDTO): ImageObject => { +export const imageDTOToImageObject = ( + entityId: string, + objectId: string, + imageDTO: ImageDTO, + overrides?: Partial +): ImageObject => { const { width, height, image_name } = imageDTO; return { id: getImageObjectId(entityId, objectId), @@ -776,6 +781,7 @@ export const imageDTOToImageObject = (entityId: string, objectId: string, imageD width, height, }, + ...overrides, }; }; From 8234e4e1684676eae55d267dfc6cbe42988537a0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 5 Jul 2024 17:36:16 +1000 Subject: [PATCH 177/678] tidy(ui): removed unused state related to non-buffered drawing --- .../controlLayers/konva/CanvasInpaintMask.ts | 6 +- .../controlLayers/konva/CanvasLayer.ts | 6 +- .../controlLayers/konva/CanvasRegion.ts | 6 +- .../controlLayers/konva/CanvasStateApi.ts | 81 +++------------ .../controlLayers/store/canvasV2Slice.ts | 27 ++--- .../store/inpaintMaskReducers.ts | 76 +------------- .../controlLayers/store/layersReducers.ts | 94 +----------------- .../controlLayers/store/regionsReducers.ts | 99 +------------------ 8 files changed, 45 insertions(+), 350 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts index 63a932eac9d..f9bd9895be1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts @@ -90,11 +90,11 @@ export class CanvasInpaintMask { return; } if (this.drawingBuffer.type === 'brush_line') { - this.manager.stateApi.onBrushLineAdded2({ id: this.id, brushLine: this.drawingBuffer }, 'inpaint_mask'); + this.manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: this.drawingBuffer }, 'inpaint_mask'); } else if (this.drawingBuffer.type === 'eraser_line') { - this.manager.stateApi.onEraserLineAdded2({ id: this.id, eraserLine: this.drawingBuffer }, 'inpaint_mask'); + this.manager.stateApi.onEraserLineAdded({ id: this.id, eraserLine: this.drawingBuffer }, 'inpaint_mask'); } else if (this.drawingBuffer.type === 'rect_shape') { - this.manager.stateApi.onRectShapeAdded2({ id: this.id, rectShape: this.drawingBuffer }, 'inpaint_mask'); + this.manager.stateApi.onRectShapeAdded({ id: this.id, rectShape: this.drawingBuffer }, 'inpaint_mask'); } this.setDrawingBuffer(null); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 92be18b40c3..5f6aafe4961 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -85,11 +85,11 @@ export class CanvasLayer { return; } if (this.drawingBuffer.type === 'brush_line') { - this.manager.stateApi.onBrushLineAdded2({ id: this.id, brushLine: this.drawingBuffer }, 'layer'); + this.manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: this.drawingBuffer }, 'layer'); } else if (this.drawingBuffer.type === 'eraser_line') { - this.manager.stateApi.onEraserLineAdded2({ id: this.id, eraserLine: this.drawingBuffer }, 'layer'); + this.manager.stateApi.onEraserLineAdded({ id: this.id, eraserLine: this.drawingBuffer }, 'layer'); } else if (this.drawingBuffer.type === 'rect_shape') { - this.manager.stateApi.onRectShapeAdded2({ id: this.id, rectShape: this.drawingBuffer }, 'layer'); + this.manager.stateApi.onRectShapeAdded({ id: this.id, rectShape: this.drawingBuffer }, 'layer'); } this.setDrawingBuffer(null); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts index 609e5447bac..1a331e0827d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts @@ -90,11 +90,11 @@ export class CanvasRegion { return; } if (this.drawingBuffer.type === 'brush_line') { - this.manager.stateApi.onBrushLineAdded2({ id: this.id, brushLine: this.drawingBuffer }, 'regional_guidance'); + this.manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: this.drawingBuffer }, 'regional_guidance'); } else if (this.drawingBuffer.type === 'eraser_line') { - this.manager.stateApi.onEraserLineAdded2({ id: this.id, eraserLine: this.drawingBuffer }, 'regional_guidance'); + this.manager.stateApi.onEraserLineAdded({ id: this.id, eraserLine: this.drawingBuffer }, 'regional_guidance'); } else if (this.drawingBuffer.type === 'rect_shape') { - this.manager.stateApi.onRectShapeAdded2({ id: this.id, rectShape: this.drawingBuffer }, 'regional_guidance'); + this.manager.stateApi.onRectShapeAdded({ id: this.id, rectShape: this.drawingBuffer }, 'regional_guidance'); } this.setDrawingBuffer(null); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index f936cf15038..869e165a505 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -15,39 +15,28 @@ import { bboxChanged, brushWidthChanged, caBboxChanged, + caScaled, caTranslated, eraserWidthChanged, imBboxChanged, imBrushLineAdded, - imBrushLineAdded2, imEraserLineAdded, - imEraserLineAdded2, imImageCacheChanged, - imLinePointAdded, - imRectAdded, - imRectShapeAdded2, + imRectShapeAdded, imScaled, imTranslated, layerBboxChanged, layerBrushLineAdded, - layerBrushLineAdded2, layerEraserLineAdded, - layerEraserLineAdded2, layerImageCacheChanged, - layerLinePointAdded, - layerRectAdded, - layerRectShapeAdded2, + layerRectShapeAdded, layerScaled, layerTranslated, rgBboxChanged, rgBrushLineAdded, - rgBrushLineAdded2, rgEraserLineAdded, - rgEraserLineAdded2, rgImageCacheChanged, - rgLinePointAdded, - rgRectAdded, - rgRectShapeAdded2, + rgRectShapeAdded, rgScaled, rgTranslated, toolBufferChanged, @@ -56,14 +45,10 @@ import { import type { BboxChangedArg, BrushLine, - BrushLineAddedArg, CanvasEntity, EraserLine, - EraserLineAddedArg, - PointAddedToLineArg, PosChangedArg, RectShape, - RectShapeAddedArg, ScaleChangedArg, Tool, } from 'features/controlLayers/store/types'; @@ -89,12 +74,12 @@ export class CanvasStateApi { log.debug('onPosChanged'); if (entityType === 'layer') { this.store.dispatch(layerTranslated(arg)); - } else if (entityType === 'control_adapter') { - this.store.dispatch(caTranslated(arg)); } else if (entityType === 'regional_guidance') { this.store.dispatch(rgTranslated(arg)); } else if (entityType === 'inpaint_mask') { this.store.dispatch(imTranslated(arg)); + } else if (entityType === 'control_adapter') { + this.store.dispatch(caTranslated(arg)); } }; onScaleChanged = (arg: ScaleChangedArg, entityType: CanvasEntity['type']) => { @@ -105,6 +90,8 @@ export class CanvasStateApi { this.store.dispatch(imScaled(arg)); } else if (entityType === 'regional_guidance') { this.store.dispatch(rgScaled(arg)); + } else if (entityType === 'control_adapter') { + this.store.dispatch(caScaled(arg)); } }; onBboxChanged = (arg: BboxChangedArg, entityType: CanvasEntity['type']) => { @@ -119,7 +106,7 @@ export class CanvasStateApi { this.store.dispatch(imBboxChanged(arg)); } }; - onBrushLineAdded = (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => { + onBrushLineAdded = (arg: { id: string; brushLine: BrushLine }, entityType: CanvasEntity['type']) => { log.debug('Brush line added'); if (entityType === 'layer') { this.store.dispatch(layerBrushLineAdded(arg)); @@ -129,7 +116,7 @@ export class CanvasStateApi { this.store.dispatch(imBrushLineAdded(arg)); } }; - onEraserLineAdded = (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => { + onEraserLineAdded = (arg: { id: string; eraserLine: EraserLine }, entityType: CanvasEntity['type']) => { log.debug('Eraser line added'); if (entityType === 'layer') { this.store.dispatch(layerEraserLineAdded(arg)); @@ -139,54 +126,14 @@ export class CanvasStateApi { this.store.dispatch(imEraserLineAdded(arg)); } }; - onBrushLineAdded2 = (arg: { id: string; brushLine: BrushLine }, entityType: CanvasEntity['type']) => { - log.debug('Brush line added'); - if (entityType === 'layer') { - this.store.dispatch(layerBrushLineAdded2(arg)); - } else if (entityType === 'regional_guidance') { - this.store.dispatch(rgBrushLineAdded2(arg)); - } else if (entityType === 'inpaint_mask') { - this.store.dispatch(imBrushLineAdded2(arg)); - } - }; - onEraserLineAdded2 = (arg: { id: string; eraserLine: EraserLine }, entityType: CanvasEntity['type']) => { - log.debug('Eraser line added'); - if (entityType === 'layer') { - this.store.dispatch(layerEraserLineAdded2(arg)); - } else if (entityType === 'regional_guidance') { - this.store.dispatch(rgEraserLineAdded2(arg)); - } else if (entityType === 'inpaint_mask') { - this.store.dispatch(imEraserLineAdded2(arg)); - } - }; - onPointAddedToLine = (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => { - log.debug('Point added to line'); - if (entityType === 'layer') { - this.store.dispatch(layerLinePointAdded(arg)); - } else if (entityType === 'regional_guidance') { - this.store.dispatch(rgLinePointAdded(arg)); - } else if (entityType === 'inpaint_mask') { - this.store.dispatch(imLinePointAdded(arg)); - } - }; - onRectShapeAdded = (arg: RectShapeAddedArg, entityType: CanvasEntity['type']) => { - log.debug('Rect shape added'); - if (entityType === 'layer') { - this.store.dispatch(layerRectAdded(arg)); - } else if (entityType === 'regional_guidance') { - this.store.dispatch(rgRectAdded(arg)); - } else if (entityType === 'inpaint_mask') { - this.store.dispatch(imRectAdded(arg)); - } - }; - onRectShapeAdded2 = (arg: { id: string; rectShape: RectShape }, entityType: CanvasEntity['type']) => { + onRectShapeAdded = (arg: { id: string; rectShape: RectShape }, entityType: CanvasEntity['type']) => { log.debug('Rect shape added'); if (entityType === 'layer') { - this.store.dispatch(layerRectShapeAdded2(arg)); + this.store.dispatch(layerRectShapeAdded(arg)); } else if (entityType === 'regional_guidance') { - this.store.dispatch(rgRectShapeAdded2(arg)); + this.store.dispatch(rgRectShapeAdded(arg)); } else if (entityType === 'inpaint_mask') { - this.store.dispatch(imRectShapeAdded2(arg)); + this.store.dispatch(imRectShapeAdded(arg)); } }; onBboxTransformed = (bbox: IRect) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 14107c60ffd..bf7f6929b45 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -209,14 +209,13 @@ export const { layerOpacityChanged, layerTranslated, layerBboxChanged, - layerBrushLineAdded, - layerEraserLineAdded, - layerLinePointAdded, - layerRectAdded, layerImageAdded, layerAllDeleted, layerImageCacheChanged, layerScaled, + layerBrushLineAdded, + layerEraserLineAdded, + layerRectShapeAdded, // IP Adapters ipaAdded, ipaRecalled, @@ -251,6 +250,7 @@ export const { caProcessorPendingBatchIdChanged, caWeightChanged, caBeginEndStepPctChanged, + caScaled, // Regions rgAdded, rgRecalled, @@ -277,11 +277,10 @@ export const { rgIPAdapterMethodChanged, rgIPAdapterModelChanged, rgIPAdapterCLIPVisionModelChanged, + rgScaled, rgBrushLineAdded, rgEraserLineAdded, - rgLinePointAdded, - rgRectAdded, - rgScaled, + rgRectShapeAdded, // Compositing setInfillMethod, setInfillTileSize, @@ -334,11 +333,10 @@ export const { imBboxChanged, imFillChanged, imImageCacheChanged, + imScaled, imBrushLineAdded, imEraserLineAdded, - imLinePointAdded, - imRectAdded, - imScaled, + imRectShapeAdded, // Staging stagingAreaStartedStaging, stagingAreaImageAdded, @@ -347,15 +345,6 @@ export const { stagingAreaCanceledStaging, stagingAreaNextImageSelected, stagingAreaPreviousImageSelected, - layerBrushLineAdded2, - layerEraserLineAdded2, - layerRectShapeAdded2, - rgBrushLineAdded2, - rgEraserLineAdded2, - rgRectShapeAdded2, - imBrushLineAdded2, - imEraserLineAdded2, - imRectShapeAdded2, } = canvasV2Slice.actions; export const selectCanvasV2Slice = (state: RootState) => state.canvasV2; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts index 05e6bc4b222..0e4c6bfc407 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts @@ -1,5 +1,4 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; import type { BrushLine, CanvasV2State, @@ -8,13 +7,11 @@ import type { RectShape, ScaleChangedArg, } from 'features/controlLayers/store/types'; -import { imageDTOToImageWithDims, RGBA_RED } from 'features/controlLayers/store/types'; +import { imageDTOToImageWithDims } from 'features/controlLayers/store/types'; import type { IRect } from 'konva/lib/types'; import type { ImageDTO } from 'services/api/types'; -import { v4 as uuidv4 } from 'uuid'; -import type { BrushLineAddedArg, EraserLineAddedArg, PointAddedToLineArg, RectShapeAddedArg, RgbColor } from './types'; -import { isLine } from './types'; +import type { RgbColor } from './types'; export const inpaintMaskReducers = { imReset: (state) => { @@ -70,85 +67,22 @@ export const inpaintMaskReducers = { const { imageDTO } = action.payload; state.inpaintMask.imageCache = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; }, - imBrushLineAdded: { - reducer: (state, action: PayloadAction & { lineId: string }>) => { - const { points, lineId, width, clip } = action.payload; - state.inpaintMask.objects.push({ - id: getBrushLineId(state.inpaintMask.id, lineId), - type: 'brush_line', - points, - strokeWidth: width, - color: RGBA_RED, - clip, - }); - state.inpaintMask.bboxNeedsUpdate = true; - state.inpaintMask.imageCache = null; - }, - prepare: (payload: Omit) => ({ - payload: { ...payload, lineId: uuidv4() }, - }), - }, - imBrushLineAdded2: (state, action: PayloadAction<{ brushLine: BrushLine }>) => { + imBrushLineAdded: (state, action: PayloadAction<{ brushLine: BrushLine }>) => { const { brushLine } = action.payload; state.inpaintMask.objects.push(brushLine); state.inpaintMask.bboxNeedsUpdate = true; state.layers.imageCache = null; }, - imEraserLineAdded2: (state, action: PayloadAction<{ eraserLine: EraserLine }>) => { + imEraserLineAdded: (state, action: PayloadAction<{ eraserLine: EraserLine }>) => { const { eraserLine } = action.payload; state.inpaintMask.objects.push(eraserLine); state.inpaintMask.bboxNeedsUpdate = true; state.layers.imageCache = null; }, - imRectShapeAdded2: (state, action: PayloadAction<{ rectShape: RectShape }>) => { + imRectShapeAdded: (state, action: PayloadAction<{ rectShape: RectShape }>) => { const { rectShape } = action.payload; state.inpaintMask.objects.push(rectShape); state.inpaintMask.bboxNeedsUpdate = true; state.layers.imageCache = null; }, - imEraserLineAdded: { - reducer: (state, action: PayloadAction & { lineId: string }>) => { - const { points, lineId, width, clip } = action.payload; - state.inpaintMask.objects.push({ - id: getEraserLineId(state.inpaintMask.id, lineId), - type: 'eraser_line', - points, - strokeWidth: width, - clip, - }); - state.inpaintMask.bboxNeedsUpdate = true; - state.inpaintMask.imageCache = null; - }, - prepare: (payload: Omit) => ({ - payload: { ...payload, lineId: uuidv4() }, - }), - }, - imLinePointAdded: (state, action: PayloadAction>) => { - const { point } = action.payload; - const lastObject = state.inpaintMask.objects[state.inpaintMask.objects.length - 1]; - if (!lastObject || !isLine(lastObject)) { - return; - } - lastObject.points.push(...point); - state.inpaintMask.bboxNeedsUpdate = true; - state.inpaintMask.imageCache = null; - }, - imRectAdded: { - reducer: (state, action: PayloadAction & { rectId: string }>) => { - const { rect, rectId } = action.payload; - if (rect.height === 0 || rect.width === 0) { - // Ignore zero-area rectangles - return; - } - state.inpaintMask.objects.push({ - type: 'rect_shape', - id: getRectShapeId(state.inpaintMask.id, rectId), - ...rect, - color: RGBA_RED, - }); - state.inpaintMask.bboxNeedsUpdate = true; - state.inpaintMask.imageCache = null; - }, - prepare: (payload: Omit) => ({ payload: { ...payload, rectId: uuidv4() } }), - }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index 6308f0d2ea9..d63f7e9c6b8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -1,6 +1,5 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; -import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; import type { IRect } from 'konva/lib/types'; import type { ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; @@ -8,18 +7,14 @@ import { v4 as uuidv4 } from 'uuid'; import type { BrushLine, - BrushLineAddedArg, CanvasV2State, EraserLine, - EraserLineAddedArg, ImageObjectAddedArg, LayerEntity, - PointAddedToLineArg, RectShape, - RectShapeAddedArg, ScaleChangedArg, } from './types'; -import { imageDTOToImageObject, imageDTOToImageWithDims, isLine } from './types'; +import { imageDTOToImageObject, imageDTOToImageWithDims } from './types'; export const selectLayer = (state: CanvasV2State, id: string) => state.layers.entities.find((layer) => layer.id === id); export const selectLayerOrThrow = (state: CanvasV2State, id: string) => { @@ -155,7 +150,7 @@ export const layersReducers = { moveToStart(state.layers.entities, layer); state.layers.imageCache = null; }, - layerBrushLineAdded2: (state, action: PayloadAction<{ id: string; brushLine: BrushLine }>) => { + layerBrushLineAdded: (state, action: PayloadAction<{ id: string; brushLine: BrushLine }>) => { const { id, brushLine } = action.payload; const layer = selectLayer(state, id); if (!layer) { @@ -166,7 +161,7 @@ export const layersReducers = { layer.bboxNeedsUpdate = true; state.layers.imageCache = null; }, - layerEraserLineAdded2: (state, action: PayloadAction<{ id: string; eraserLine: EraserLine }>) => { + layerEraserLineAdded: (state, action: PayloadAction<{ id: string; eraserLine: EraserLine }>) => { const { id, eraserLine } = action.payload; const layer = selectLayer(state, id); if (!layer) { @@ -177,7 +172,7 @@ export const layersReducers = { layer.bboxNeedsUpdate = true; state.layers.imageCache = null; }, - layerRectShapeAdded2: (state, action: PayloadAction<{ id: string; rectShape: RectShape }>) => { + layerRectShapeAdded: (state, action: PayloadAction<{ id: string; rectShape: RectShape }>) => { const { id, rectShape } = action.payload; const layer = selectLayer(state, id); if (!layer) { @@ -188,29 +183,6 @@ export const layersReducers = { layer.bboxNeedsUpdate = true; state.layers.imageCache = null; }, - layerBrushLineAdded: { - reducer: (state, action: PayloadAction) => { - const { id, points, lineId, color, width, clip } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - - layer.objects.push({ - id: getBrushLineId(id, lineId), - type: 'brush_line', - points, - strokeWidth: width, - color, - clip, - }); - layer.bboxNeedsUpdate = true; - state.layers.imageCache = null; - }, - prepare: (payload: BrushLineAddedArg) => ({ - payload: { ...payload, lineId: uuidv4() }, - }), - }, layerScaled: (state, action: PayloadAction) => { const { id, scale, x, y } = action.payload; const layer = selectLayer(state, id); @@ -241,64 +213,6 @@ export const layersReducers = { layer.bboxNeedsUpdate = true; state.layers.imageCache = null; }, - layerEraserLineAdded: { - reducer: (state, action: PayloadAction) => { - const { id, points, lineId, width, clip } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - - layer.objects.push({ - id: getEraserLineId(id, lineId), - type: 'eraser_line', - points, - strokeWidth: width, - clip, - }); - layer.bboxNeedsUpdate = true; - state.layers.imageCache = null; - }, - prepare: (payload: EraserLineAddedArg) => ({ - payload: { ...payload, lineId: uuidv4() }, - }), - }, - layerLinePointAdded: (state, action: PayloadAction) => { - const { id, point } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - const lastObject = layer.objects[layer.objects.length - 1]; - if (!lastObject || !isLine(lastObject)) { - return; - } - lastObject.points.push(...point); - layer.bboxNeedsUpdate = true; - state.layers.imageCache = null; - }, - layerRectAdded: { - reducer: (state, action: PayloadAction) => { - const { id, rect, rectId, color } = action.payload; - if (rect.height === 0 || rect.width === 0) { - // Ignore zero-area rectangles - return; - } - const layer = selectLayer(state, id); - if (!layer) { - return; - } - layer.objects.push({ - type: 'rect_shape', - id: getRectShapeId(id, rectId), - ...rect, - color, - }); - layer.bboxNeedsUpdate = true; - state.layers.imageCache = null; - }, - prepare: (payload: RectShapeAddedArg) => ({ payload: { ...payload, rectId: uuidv4() } }), - }, layerImageAdded: { reducer: ( state, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index 2fd0e69cfd9..1975d65681d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -1,6 +1,5 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; -import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; import type { BrushLine, CanvasV2State, @@ -10,7 +9,7 @@ import type { RectShape, ScaleChangedArg, } from 'features/controlLayers/store/types'; -import { imageDTOToImageObject, imageDTOToImageWithDims, RGBA_RED } from 'features/controlLayers/store/types'; +import { imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas'; import type { IRect } from 'konva/lib/types'; @@ -19,16 +18,7 @@ import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; -import type { - BrushLineAddedArg, - EraserLineAddedArg, - IPAdapterEntity, - PointAddedToLineArg, - RectShapeAddedArg, - RegionEntity, - RgbColor, -} from './types'; -import { isLine } from './types'; +import type { IPAdapterEntity, RegionEntity, RgbColor } from './types'; export const selectRG = (state: CanvasV2State, id: string) => state.regions.entities.find((rg) => rg.id === id); export const selectRGOrThrow = (state: CanvasV2State, id: string) => { @@ -340,29 +330,7 @@ export const regionsReducers = { } ipa.clipVisionModel = clipVisionModel; }, - rgBrushLineAdded: { - reducer: (state, action: PayloadAction) => { - const { id, points, lineId, width, clip } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - rg.objects.push({ - id: getBrushLineId(id, lineId), - type: 'brush_line', - points, - strokeWidth: width, - color: RGBA_RED, - clip, - }); - rg.bboxNeedsUpdate = true; - rg.imageCache = null; - }, - prepare: (payload: BrushLineAddedArg) => ({ - payload: { ...payload, lineId: uuidv4() }, - }), - }, - rgBrushLineAdded2: (state, action: PayloadAction<{ id: string; brushLine: BrushLine }>) => { + rgBrushLineAdded: (state, action: PayloadAction<{ id: string; brushLine: BrushLine }>) => { const { id, brushLine } = action.payload; const rg = selectRG(state, id); if (!rg) { @@ -373,7 +341,7 @@ export const regionsReducers = { rg.bboxNeedsUpdate = true; state.layers.imageCache = null; }, - rgEraserLineAdded2: (state, action: PayloadAction<{ id: string; eraserLine: EraserLine }>) => { + rgEraserLineAdded: (state, action: PayloadAction<{ id: string; eraserLine: EraserLine }>) => { const { id, eraserLine } = action.payload; const rg = selectRG(state, id); if (!rg) { @@ -384,7 +352,7 @@ export const regionsReducers = { rg.bboxNeedsUpdate = true; state.layers.imageCache = null; }, - rgRectShapeAdded2: (state, action: PayloadAction<{ id: string; rectShape: RectShape }>) => { + rgRectShapeAdded: (state, action: PayloadAction<{ id: string; rectShape: RectShape }>) => { const { id, rectShape } = action.payload; const rg = selectRG(state, id); if (!rg) { @@ -395,61 +363,4 @@ export const regionsReducers = { rg.bboxNeedsUpdate = true; state.layers.imageCache = null; }, - rgEraserLineAdded: { - reducer: (state, action: PayloadAction) => { - const { id, points, lineId, width, clip } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - rg.objects.push({ - id: getEraserLineId(id, lineId), - type: 'eraser_line', - points, - strokeWidth: width, - clip, - }); - rg.bboxNeedsUpdate = true; - rg.imageCache = null; - }, - prepare: (payload: EraserLineAddedArg) => ({ - payload: { ...payload, lineId: uuidv4() }, - }), - }, - rgLinePointAdded: (state, action: PayloadAction) => { - const { id, point } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - const lastObject = rg.objects[rg.objects.length - 1]; - if (!lastObject || !isLine(lastObject)) { - return; - } - lastObject.points.push(...point); - rg.bboxNeedsUpdate = true; - rg.imageCache = null; - }, - rgRectAdded: { - reducer: (state, action: PayloadAction) => { - const { id, rect, rectId } = action.payload; - if (rect.height === 0 || rect.width === 0) { - // Ignore zero-area rectangles - return; - } - const rg = selectRG(state, id); - if (!rg) { - return; - } - rg.objects.push({ - type: 'rect_shape', - id: getRectShapeId(id, rectId), - ...rect, - color: RGBA_RED, - }); - rg.bboxNeedsUpdate = true; - rg.imageCache = null; - }, - prepare: (payload: RectShapeAddedArg) => ({ payload: { ...payload, rectId: uuidv4() } }), - }, } satisfies SliceCaseReducers; From 778f703d3d7cee9653b3f56b95c057d1badc9bcb Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 5 Jul 2024 17:36:34 +1000 Subject: [PATCH 178/678] fix(ui): control adapter translate & scale --- .../konva/CanvasControlAdapter.ts | 13 ++++++++-- .../controlLayers/konva/CanvasManager.ts | 1 + .../store/controlAdaptersReducers.ts | 25 +++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts index 40e4db2910e..a69f4f3283a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts @@ -43,11 +43,11 @@ export class CanvasControlAdapter { this.transformer.on('transformend', () => { this.manager.stateApi.onScaleChanged( { id: this.id, scale: this.group.scaleX(), x: this.group.x(), y: this.group.y() }, - 'layer' + 'control_adapter' ); }); this.transformer.on('dragend', () => { - this.manager.stateApi.onPosChanged({ id: this.id, x: this.group.x(), y: this.group.y() }, 'layer'); + this.manager.stateApi.onPosChanged({ id: this.id, x: this.group.x(), y: this.group.y() }, 'control_adapter'); }); this.layer.add(this.transformer); @@ -57,6 +57,15 @@ export class CanvasControlAdapter { async render(controlAdapterState: ControlAdapterEntity) { this.controlAdapterState = controlAdapterState; + + // Update the layer's position and listening state + this.group.setAttrs({ + x: controlAdapterState.x, + y: controlAdapterState.y, + scaleX: 1, + scaleY: 1, + }); + const imageObject = controlAdapterState.processedImageObject ?? controlAdapterState.imageObject; let didDraw = false; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 9af96132394..22d5413c8a6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -249,6 +249,7 @@ export class CanvasManager { if ( this.isFirstRender || state.controlAdapters.entities !== this.prevState.controlAdapters.entities || + state.tool.selected !== this.prevState.tool.selected || state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id ) { log.debug('Rendering control adapters'); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts index 2c1750a5c1c..28402f48611 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts @@ -15,6 +15,7 @@ import type { ControlNetData, Filter, ProcessorConfig, + ScaleChangedArg, T2IAdapterConfig, T2IAdapterData, } from './types'; @@ -72,6 +73,30 @@ export const controlAdaptersReducers = { ca.x = x; ca.y = y; }, + caScaled: (state, action: PayloadAction) => { + const { id, scale, x, y } = action.payload; + const ca = selectCA(state, id); + if (!ca) { + return; + } + if (ca.imageObject) { + ca.imageObject.x *= scale; + ca.imageObject.y *= scale; + ca.imageObject.height *= scale; + ca.imageObject.width *= scale; + } + + if (ca.processedImageObject) { + ca.processedImageObject.x *= scale; + ca.processedImageObject.y *= scale; + ca.processedImageObject.height *= scale; + ca.processedImageObject.width *= scale; + } + ca.x = x; + ca.y = y; + ca.bboxNeedsUpdate = true; + state.layers.imageCache = null; + }, caBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { const { id, bbox } = action.payload; const ca = selectCA(state, id); From 09ad8b623849ea7422820e4e3e7f8cd4d7634ffe Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 5 Jul 2024 18:41:01 +1000 Subject: [PATCH 179/678] feat(ui): use canvas as source for control images (wip) --- .../controlLayers/konva/CanvasManager.ts | 5 ++ .../src/features/controlLayers/konva/util.ts | 49 ++++++++++++++++++ .../graph/generation/addControlAdapters.ts | 50 +++++++++++-------- .../util/graph/generation/buildSD1Graph.ts | 9 +++- .../util/graph/generation/buildSDXLGraph.ts | 9 +++- 5 files changed, 100 insertions(+), 22 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 22d5413c8a6..7f42b6d5669 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -2,6 +2,7 @@ import type { Store } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import { + getControlAdapterImage, getGenerationMode, getImageSourceImage, getInpaintMaskImage, @@ -369,6 +370,10 @@ export class CanvasManager { return getGenerationMode({ manager: this }); } + getControlAdapterImage(arg: Omit[0], 'manager'>) { + return getControlAdapterImage({ ...arg, manager: this }); + } + getRegionMaskImage(arg: Omit[0], 'manager'>) { return getRegionMaskImage({ ...arg, manager: this }); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index 912cc168c28..236a5e864a5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -319,6 +319,24 @@ export function getRegionMaskLayerClone(arg: { manager: CanvasManager; id: strin return layerClone; } +export function getControlAdapterLayerClone(arg: { manager: CanvasManager; id: string }): Konva.Layer { + const { id, manager } = arg; + + const controlAdapter = manager.controlAdapters.get(id); + assert(controlAdapter, `Canvas region with id ${id} not found`); + + const controlAdapterClone = controlAdapter.layer.clone(); + const objectGroupClone = controlAdapter.group.clone(); + + controlAdapterClone.destroyChildren(); + controlAdapterClone.add(objectGroupClone); + + objectGroupClone.opacity(1); + objectGroupClone.cache(); + + return controlAdapterClone; +} + export function getCompositeLayerStageClone(arg: { manager: CanvasManager }): Konva.Stage { const { manager } = arg; @@ -406,6 +424,37 @@ export async function getRegionMaskImage(arg: { return imageDTO; } +export async function getControlAdapterImage(arg: { + manager: CanvasManager; + id: string; + bbox?: Rect; + preview?: boolean; +}): Promise { + const { manager, id, bbox, preview = false } = arg; + const ca = manager.stateApi.getControlAdaptersState().entities.find((entity) => entity.id === id); + assert(ca, `Control adapter entity state with id ${id} not found`); + + // if (region.imageCache) { + // const imageDTO = await this.util.getImageDTO(region.imageCache.name); + // if (imageDTO) { + // return imageDTO; + // } + // } + + const layerClone = getControlAdapterLayerClone({ id, manager }); + const blob = await konvaNodeToBlob(layerClone, bbox); + + if (preview) { + previewBlob(blob, `region ${ca.id} mask`); + } + + layerClone.destroy(); + + const imageDTO = await manager.util.uploadImage(blob, `${ca.id}_control_image.png`, 'control', true); + // manager.stateApi.onRegionMaskImageCached(ca.id, imageDTO); + return imageDTO; +} + export async function getInpaintMaskImage(arg: { manager: CanvasManager; bbox?: Rect; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts index 3759a0822b4..7e55beca0a0 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts @@ -1,8 +1,10 @@ +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { ControlAdapterEntity, ControlNetData, ImageWithDims, ProcessorConfig, + Rect, T2IAdapterData, } from 'features/controlLayers/store/types'; import type { ImageField } from 'features/nodes/types/common'; @@ -11,18 +13,20 @@ import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import type { BaseModelType, Invocation } from 'services/api/types'; import { assert } from 'tsafe'; -export const addControlAdapters = ( +export const addControlAdapters = async ( + manager: CanvasManager, controlAdapters: ControlAdapterEntity[], g: Graph, + bbox: Rect, denoise: Invocation<'denoise_latents'>, base: BaseModelType -): ControlAdapterEntity[] => { +): Promise => { const validControlAdapters = controlAdapters.filter((ca) => isValidControlAdapter(ca, base)); for (const ca of validControlAdapters) { if (ca.adapterType === 'controlnet') { - addControlNetToGraph(ca, g, denoise); + await addControlNetToGraph(manager, ca, g, bbox, denoise); } else { - addT2IAdapterToGraph(ca, g, denoise); + await addT2IAdapterToGraph(manager, ca, g, bbox, denoise); } } return validControlAdapters; @@ -45,14 +49,17 @@ const addControlNetCollectorSafe = (g: Graph, denoise: Invocation<'denoise_laten } }; -const addControlNetToGraph = (ca: ControlNetData, g: Graph, denoise: Invocation<'denoise_latents'>) => { - const { id, beginEndStepPct, controlMode, imageObject, model, processedImageObject, processorConfig, weight } = ca; +const addControlNetToGraph = async ( + manager: CanvasManager, + ca: ControlNetData, + g: Graph, + bbox: Rect, + denoise: Invocation<'denoise_latents'> +) => { + const { id, beginEndStepPct, controlMode, model, weight } = ca; assert(model, 'ControlNet model is required'); - const controlImage = buildControlImage( - imageObject?.image ?? null, - processedImageObject?.image ?? null, - processorConfig - ); + const { image_name } = await manager.getControlAdapterImage({ id: ca.id, bbox, preview: true }); + const controlNetCollect = addControlNetCollectorSafe(g, denoise); const controlNet = g.addNode({ @@ -64,7 +71,7 @@ const addControlNetToGraph = (ca: ControlNetData, g: Graph, denoise: Invocation< resize_mode: 'just_resize', control_model: model, control_weight: weight, - image: controlImage, + image: { image_name }, }); g.addEdge(controlNet, 'control', controlNetCollect, 'item'); }; @@ -87,14 +94,17 @@ const addT2IAdapterCollectorSafe = (g: Graph, denoise: Invocation<'denoise_laten } }; -const addT2IAdapterToGraph = (ca: T2IAdapterData, g: Graph, denoise: Invocation<'denoise_latents'>) => { - const { id, beginEndStepPct, imageObject, model, processedImageObject, processorConfig, weight } = ca; +const addT2IAdapterToGraph = async ( + manager: CanvasManager, + ca: T2IAdapterData, + g: Graph, + bbox: Rect, + denoise: Invocation<'denoise_latents'> +) => { + const { id, beginEndStepPct, model, weight } = ca; assert(model, 'T2I Adapter model is required'); - const controlImage = buildControlImage( - imageObject?.image ?? null, - processedImageObject?.image ?? null, - processorConfig - ); + const { image_name } = await manager.getControlAdapterImage({ id: ca.id, bbox, preview: true }); + const t2iAdapterCollect = addT2IAdapterCollectorSafe(g, denoise); const t2iAdapter = g.addNode({ @@ -105,7 +115,7 @@ const addT2IAdapterToGraph = (ca: T2IAdapterData, g: Graph, denoise: Invocation< resize_mode: 'just_resize', t2i_adapter_model: model, weight: weight, - image: controlImage, + image: { image_name }, }); g.addEdge(t2iAdapter, 't2i_adapter', t2iAdapterCollect, 'item'); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index 6966feef9e3..30cbd48f9e0 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -210,7 +210,14 @@ export const buildSD1Graph = async (state: RootState, manager: CanvasManager): P ); } - const _addedCAs = addControlAdapters(state.canvasV2.controlAdapters.entities, g, denoise, modelConfig.base); + const _addedCAs = await addControlAdapters( + manager, + state.canvasV2.controlAdapters.entities, + g, + state.canvasV2.bbox, + denoise, + modelConfig.base + ); const _addedIPAs = addIPAdapters(state.canvasV2.ipAdapters.entities, g, denoise, modelConfig.base); const _addedRegions = await addRegions( manager, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index 9177e9e745d..2233445a254 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -214,7 +214,14 @@ export const buildSDXLGraph = async (state: RootState, manager: CanvasManager): ); } - const _addedCAs = addControlAdapters(state.canvasV2.controlAdapters.entities, g, denoise, modelConfig.base); + const _addedCAs = await addControlAdapters( + manager, + state.canvasV2.controlAdapters.entities, + g, + state.canvasV2.bbox, + denoise, + modelConfig.base + ); const _addedIPAs = addIPAdapters(state.canvasV2.ipAdapters.entities, g, denoise, modelConfig.base); const _addedRegions = await addRegions( manager, From 233a449e53cc18d1164f118216a6138a523bd041 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 5 Jul 2024 19:13:20 +1000 Subject: [PATCH 180/678] feat(ui): no animation on layer selection Felt sluggish --- .../controlLayers/components/common/CanvasEntityContainer.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx index b1bc6a5aa14..7c0fd050904 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx @@ -34,8 +34,6 @@ export const CanvasEntityContainer = memo(({ isSelected, onSelect, selectedBorde borderColor={borderColor} opacity={isSelected ? 1 : 0.6} borderRadius="base" - transitionProperty="all" - transitionDuration="0.2s" > {children} From 748cb0bb8d1203e147a8359756e3a6e572d7b14a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 5 Jul 2024 19:14:09 +1000 Subject: [PATCH 181/678] feat(ui): temp disable doc size overlay --- .../konva/CanvasDocumentSizeOverlay.ts | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasDocumentSizeOverlay.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasDocumentSizeOverlay.ts index 8b278a56367..ce7c8a6827b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasDocumentSizeOverlay.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasDocumentSizeOverlay.ts @@ -1,6 +1,6 @@ import { getArbitraryBaseColor } from '@invoke-ai/ui-library'; -import { DOCUMENT_FIT_PADDING_PX } from 'features/controlLayers/konva/constants'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { DOCUMENT_FIT_PADDING_PX } from 'features/controlLayers/konva/constants'; import Konva from 'konva'; export class CanvasDocumentSizeOverlay { @@ -31,28 +31,29 @@ export class CanvasDocumentSizeOverlay { } render() { - const document = this.manager.stateApi.getDocument(); - this.group.zIndex(0); + return; + // const document = this.manager.stateApi.getDocument(); + // this.group.zIndex(0); - const x = this.manager.stage.x(); - const y = this.manager.stage.y(); - const width = this.manager.stage.width(); - const height = this.manager.stage.height(); - const scale = this.manager.stage.scaleX(); + // const x = this.manager.stage.x(); + // const y = this.manager.stage.y(); + // const width = this.manager.stage.width(); + // const height = this.manager.stage.height(); + // const scale = this.manager.stage.scaleX(); - this.outerRect.setAttrs({ - offsetX: x / scale, - offsetY: y / scale, - width: width / scale, - height: height / scale, - }); + // this.outerRect.setAttrs({ + // offsetX: x / scale, + // offsetY: y / scale, + // width: width / scale, + // height: height / scale, + // }); - this.innerRect.setAttrs({ - x: 0, - y: 0, - width: document.width, - height: document.height, - }); + // this.innerRect.setAttrs({ + // x: 0, + // y: 0, + // width: document.width, + // height: document.height, + // }); } fitToStage() { From e397efb622b84ecd402406c5d75b5a53c8009ceb Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 5 Jul 2024 19:15:36 +1000 Subject: [PATCH 182/678] fix(ui): fiddle with control adapter filters some jank still --- .../ControlAdapter/CAOpacityAndFilter.tsx | 6 ++++-- .../controlLayers/konva/CanvasImage.ts | 17 +++++++++++++++-- .../store/controlAdaptersReducers.ts | 18 ++++++++++++------ .../src/features/controlLayers/store/types.ts | 2 +- 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAOpacityAndFilter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAOpacityAndFilter.tsx index f6bfbdf6f87..25117459f65 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAOpacityAndFilter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAOpacityAndFilter.tsx @@ -32,7 +32,9 @@ export const CAOpacityAndFilter = memo(({ id }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const opacity = useAppSelector((s) => Math.round(selectCAOrThrow(s.canvasV2, id).opacity * 100)); - const isFilterEnabled = useAppSelector((s) => selectCAOrThrow(s.canvasV2, id).filter === 'LightnessToAlphaFilter'); + const isFilterEnabled = useAppSelector((s) => + selectCAOrThrow(s.canvasV2, id).filters.includes('LightnessToAlphaFilter') + ); const onChangeOpacity = useCallback( (v: number) => { dispatch(caOpacityChanged({ id, opacity: v / 100 })); @@ -41,7 +43,7 @@ export const CAOpacityAndFilter = memo(({ id }: Props) => { ); const onChangeFilter = useCallback( (e: ChangeEvent) => { - dispatch(caFilterChanged({ id, filter: e.target.checked ? 'LightnessToAlphaFilter' : 'none' })); + dispatch(caFilterChanged({ id, filters: e.target.checked ? ['LightnessToAlphaFilter'] : [] })); }, [dispatch, id] ); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts index 62e55f83f6e..d102869c3a2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -86,10 +86,16 @@ export class CanvasImage { image: imageEl, width, height, - filters: filters.map((f) => FILTER_MAP[f]), }); this.konvaImageGroup.add(this.konvaImage); } + if (filters.length > 0) { + this.konvaImage.cache(); + this.konvaImage.filters(filters.map((f) => FILTER_MAP[f])); + } else { + this.konvaImage.clearCache(); + this.konvaImage.filters([]); + } this.imageName = imageName; this.isLoading = false; this.isError = false; @@ -144,7 +150,14 @@ export class CanvasImage { if (this.lastImageObject.image.name !== image.name || force) { await this.updateImageSource(image.name); } - this.konvaImage?.setAttrs({ x, y, width, height, filters: filters.map((f) => FILTER_MAP[f]) }); + this.konvaImage?.setAttrs({ x, y, width, height }); + if (filters.length > 0) { + this.konvaImage?.cache(); + this.konvaImage?.filters(filters.map((f) => FILTER_MAP[f])); + } else { + this.konvaImage?.clearCache(); + this.konvaImage?.filters([]); + } this.konvaPlaceholderRect.setAttrs({ width, height }); this.konvaPlaceholderText.setAttrs({ width, height, fontSize: width / 16 }); this.lastImageObject = imageObject; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts index 28402f48611..e82b7aa2acd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts @@ -41,7 +41,7 @@ export const controlAdaptersReducers = { bboxNeedsUpdate: false, isEnabled: true, opacity: 1, - filter: 'LightnessToAlphaFilter', + filters: ['LightnessToAlphaFilter'], processorPendingBatchId: null, ...config, }); @@ -164,7 +164,7 @@ export const controlAdaptersReducers = { ca.bboxNeedsUpdate = true; ca.isEnabled = true; if (imageDTO) { - const newImageObject = imageDTOToImageObject(id, objectId, imageDTO, { filters: ca.filter ? [ca.filter] : [] }); + const newImageObject = imageDTOToImageObject(id, objectId, imageDTO, { filters: ca.filters }); if (isEqual(newImageObject, ca.imageObject)) { return; } @@ -188,7 +188,7 @@ export const controlAdaptersReducers = { ca.bboxNeedsUpdate = true; ca.isEnabled = true; ca.processedImageObject = imageDTO - ? imageDTOToImageObject(id, objectId, imageDTO, { filters: ca.filter ? [ca.filter] : [] }) + ? imageDTOToImageObject(id, objectId, imageDTO, { filters: ca.filters }) : null; }, prepare: (payload: { id: string; imageDTO: ImageDTO | null }) => ({ payload: { ...payload, objectId: uuidv4() } }), @@ -248,13 +248,19 @@ export const controlAdaptersReducers = { ca.processedImageObject = null; } }, - caFilterChanged: (state, action: PayloadAction<{ id: string; filter: Filter }>) => { - const { id, filter } = action.payload; + caFilterChanged: (state, action: PayloadAction<{ id: string; filters: Filter[] }>) => { + const { id, filters } = action.payload; const ca = selectCA(state, id); if (!ca) { return; } - ca.filter = filter; + ca.filters = filters; + if (ca.imageObject) { + ca.imageObject.filters = filters; + } + if (ca.processedImageObject) { + ca.processedImageObject.filters = filters; + } }, caProcessorPendingBatchIdChanged: (state, action: PayloadAction<{ id: string; batchId: string | null }>) => { const { id, batchId } = action.payload; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index af00a59b61d..92697568ea2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -677,7 +677,7 @@ const zControlAdapterEntityBase = z.object({ bbox: zRect.nullable(), bboxNeedsUpdate: z.boolean(), opacity: zOpacity, - filter: zFilter, + filters: z.array(zFilter), weight: z.number().gte(-1).lte(2), imageObject: zImageObject.nullable(), processedImageObject: zImageObject.nullable(), From 1a6ebb9606cd8ef37ecca49c1150ef41eab732e4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 8 Jul 2024 11:32:52 +1000 Subject: [PATCH 183/678] feat(ui): add snapToRect util --- .../src/features/controlLayers/konva/util.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index 236a5e864a5..e4d52922f1c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -78,6 +78,28 @@ export const snapPosToStage = (pos: Vector2d, stage: Konva.Stage, snapPx = 10): return snappedPos; }; +/** + * Snaps a position to the edge of the given rect if within a threshold of the edge + * @param pos The position to snap + * @param rect The rect to snap to + * @param threshold The snap threshold in pixels + */ +export const snapToRect = (pos: Vector2d, rect: Rect, threshold = 10): Vector2d => { + const snappedPos = { ...pos }; + // Snap to the edge of the rect if within threshold + if (pos.x - threshold < rect.x) { + snappedPos.x = rect.x; + } else if (pos.x + threshold > rect.x + rect.width) { + snappedPos.x = rect.x + rect.width; + } + if (pos.y - threshold < rect.y) { + snappedPos.y = rect.y; + } else if (pos.y + threshold > rect.y + rect.height) { + snappedPos.y = rect.y + rect.height; + } + return snappedPos; +}; + /** * Checks if the left mouse button is currently pressed * @param e The konva event From daffb950c3e0fdc1b20ac69262ac3bb89a7217ee Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 8 Jul 2024 11:49:15 +1000 Subject: [PATCH 184/678] feat(ui): add reset button to canvas --- .../components/ControlLayersToolbar.tsx | 2 ++ .../components/ResetCanvasButton.tsx | 15 +++++++++++++++ .../features/controlLayers/store/canvasV2Slice.ts | 12 ++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ResetCanvasButton.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index 9313a312ae6..8554c63b2b9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -5,6 +5,7 @@ import { BrushWidth } from 'features/controlLayers/components/BrushWidth'; import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover'; import { EraserWidth } from 'features/controlLayers/components/EraserWidth'; import { FillColorPicker } from 'features/controlLayers/components/FillColorPicker'; +import { ResetCanvasButton } from 'features/controlLayers/components/ResetCanvasButton'; import { ToolChooser } from 'features/controlLayers/components/ToolChooser'; import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup'; import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton'; @@ -30,6 +31,7 @@ export const ControlLayersToolbar = memo(() => { +
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ResetCanvasButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ResetCanvasButton.tsx new file mode 100644 index 00000000000..f0880b6e020 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ResetCanvasButton.tsx @@ -0,0 +1,15 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { canvasReset } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; +import { PiTrashBold } from 'react-icons/pi'; + +export const ResetCanvasButton = memo(() => { + const dispatch = useAppDispatch(); + const onClick = useCallback(() => { + dispatch(canvasReset()); + }, [dispatch]); + return } aria-label="Reset canvas" colorScheme="error" />; +}); + +ResetCanvasButton.displayName = 'ResetCanvasButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index bf7f6929b45..be800e82d88 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -176,6 +176,17 @@ export const canvasV2Slice = createSlice({ state.ipAdapters.entities = []; state.controlAdapters.entities = []; }, + canvasReset: (state) => { + state.bbox = deepClone(initialState.bbox); + state.controlAdapters = deepClone(initialState.controlAdapters); + state.document = deepClone(initialState.document); + state.ipAdapters = deepClone(initialState.ipAdapters); + state.layers = deepClone(initialState.layers); + state.regions = deepClone(initialState.regions); + state.selectedEntityIdentifier = deepClone(initialState.selectedEntityIdentifier); + state.stagingArea = deepClone(initialState.stagingArea); + state.tool = deepClone(initialState.tool); + }, }, }); @@ -196,6 +207,7 @@ export const { scaledBboxChanged, bboxScaleMethodChanged, clipToBboxChanged, + canvasReset, // layers layerAdded, layerRecalled, From 4dc5b18671b6341b559131daaad697ad07763deb Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 8 Jul 2024 15:44:42 +1000 Subject: [PATCH 185/678] feat(ui): "stagingArea" -> "session" --- .../addCommitStagingAreaImageListener.ts | 8 +-- .../listeners/enqueueRequestedLinear.ts | 10 +-- .../socketio/socketInvocationComplete.ts | 6 +- .../StagingArea/StagingAreaToolbar.tsx | 36 +++++------ .../controlLayers/components/ToolChooser.tsx | 2 +- .../controlLayers/konva/CanvasManager.ts | 2 +- .../controlLayers/konva/CanvasStagingArea.ts | 2 +- .../controlLayers/konva/CanvasStateApi.ts | 2 +- .../controlLayers/store/canvasV2Slice.ts | 28 +++++---- .../controlLayers/store/sessionReducers.ts | 62 +++++++++++++++++++ .../store/stagingAreaReducers.ts | 57 ----------------- .../src/features/controlLayers/store/types.ts | 7 ++- .../gallery/hooks/useGalleryHotkeys.ts | 2 +- 13 files changed, 116 insertions(+), 108 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts delete mode 100644 invokeai/frontend/web/src/features/controlLayers/store/stagingAreaReducers.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts index 1da9b90b9e8..3dbc159e760 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts @@ -4,8 +4,8 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware' import { layerAdded, layerImageAdded, - stagingAreaCanceledStaging, - stagingAreaImageAccepted, + sessionStagingCanceled, + sessionStagedImageAccepted, } from 'features/controlLayers/store/canvasV2Slice'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; @@ -14,7 +14,7 @@ import { assert } from 'tsafe'; export const addStagingListeners = (startAppListening: AppStartListening) => { startAppListening({ - matcher: isAnyOf(stagingAreaCanceledStaging, stagingAreaImageAccepted), + matcher: isAnyOf(sessionStagingCanceled, sessionStagedImageAccepted), effect: async (_, { dispatch }) => { const log = logger('canvas'); @@ -47,7 +47,7 @@ export const addStagingListeners = (startAppListening: AppStartListening) => { }); startAppListening({ - actionCreator: stagingAreaImageAccepted, + actionCreator: sessionStagedImageAccepted, effect: async (action, api) => { const { imageDTO } = action.payload; const { layers, selectedEntityIdentifier, bbox } = api.getState().canvasV2; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index 6359e55e408..df29d7cb9cc 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -1,7 +1,7 @@ import { enqueueRequested } from 'app/store/actions'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { getCanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { stagingAreaCanceledStaging, stagingAreaStartedStaging } from 'features/controlLayers/store/canvasV2Slice'; +import { sessionStagingCanceled, sessionStartedStaging } from 'features/controlLayers/store/canvasV2Slice'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph'; import { buildSDXLGraph } from 'features/nodes/util/graph/generation/buildSDXLGraph'; @@ -18,8 +18,8 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) const { prepend } = action.payload; let didStartStaging = false; - if (!state.canvasV2.stagingArea.isStaging) { - dispatch(stagingAreaStartedStaging()); + if (!state.canvasV2.session.isStaging) { + dispatch(sessionStartedStaging()); didStartStaging = true; } @@ -48,8 +48,8 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) req.reset(); await req.unwrap(); } catch { - if (didStartStaging && getState().canvasV2.stagingArea.isStaging) { - dispatch(stagingAreaCanceledStaging()); + if (didStartStaging && getState().canvasV2.session.isStaging) { + dispatch(sessionStagingCanceled()); } } }, diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts index e963023522f..9b36a2b3f90 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts @@ -2,7 +2,7 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { deepClone } from 'common/util/deepClone'; import { parseify } from 'common/util/serialize'; -import { stagingAreaImageAdded } from 'features/controlLayers/store/canvasV2Slice'; +import { sessionImageStaged } from 'features/controlLayers/store/canvasV2Slice'; import { boardIdSelected, galleryViewChanged, imageSelected, offsetChanged } from 'features/gallery/store/gallerySlice'; import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; import { zNodeStatus } from 'features/nodes/types/invocation'; @@ -42,8 +42,8 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi // handle tab-specific logic if (data.origin === 'canvas') { - if (data.invocation_source_id === CANVAS_OUTPUT && canvasV2.stagingArea.isStaging) { - dispatch(stagingAreaImageAdded({ imageDTO })); + if (data.invocation_source_id === CANVAS_OUTPUT && canvasV2.session.isStaging) { + dispatch(sessionImageStaged({ imageDTO })); } } else if (data.origin === 'workflows') { const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx index f8e00a9176b..62b243157c5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx @@ -3,11 +3,11 @@ import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { $shouldShowStagedImage, - stagingAreaCanceledStaging, - stagingAreaImageAccepted, - stagingAreaImageDiscarded, - stagingAreaNextImageSelected, - stagingAreaPreviousImageSelected, + sessionStagingCanceled, + sessionStagedImageAccepted, + sessionStagedImageDiscarded, + sessionNextStagedImageSelected, + sessionPrevStagedImageSelected, } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; @@ -24,7 +24,7 @@ import { } from 'react-icons/pi'; export const StagingAreaToolbar = memo(() => { - const isStaging = useAppSelector((s) => s.canvasV2.stagingArea.isStaging); + const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); if (!isStaging) { return null; @@ -37,30 +37,30 @@ StagingAreaToolbar.displayName = 'StagingAreaToolbar'; export const StagingAreaToolbarContent = memo(() => { const dispatch = useAppDispatch(); - const stagingArea = useAppSelector((s) => s.canvasV2.stagingArea); + const stagingArea = useAppSelector((s) => s.canvasV2.session); const shouldShowStagedImage = useStore($shouldShowStagedImage); - const images = useMemo(() => stagingArea.images, [stagingArea]); + const images = useMemo(() => stagingArea.stagedImages, [stagingArea]); const selectedImageDTO = useMemo(() => { - return images[stagingArea.selectedImageIndex] ?? null; - }, [images, stagingArea.selectedImageIndex]); + return images[stagingArea.selectedStagedImageIndex] ?? null; + }, [images, stagingArea.selectedStagedImageIndex]); // const [changeIsImageIntermediate] = useChangeImageIsIntermediateMutation(); const { t } = useTranslation(); const onPrev = useCallback(() => { - dispatch(stagingAreaPreviousImageSelected()); + dispatch(sessionPrevStagedImageSelected()); }, [dispatch]); const onNext = useCallback(() => { - dispatch(stagingAreaNextImageSelected()); + dispatch(sessionNextStagedImageSelected()); }, [dispatch]); const onAccept = useCallback(() => { if (!selectedImageDTO) { return; } - dispatch(stagingAreaImageAccepted({ imageDTO: selectedImageDTO })); + dispatch(sessionStagedImageAccepted({ imageDTO: selectedImageDTO })); }, [dispatch, selectedImageDTO]); const onDiscardOne = useCallback(() => { @@ -68,14 +68,14 @@ export const StagingAreaToolbarContent = memo(() => { return; } if (images.length === 1) { - dispatch(stagingAreaCanceledStaging()); + dispatch(sessionStagingCanceled()); } else { - dispatch(stagingAreaImageDiscarded({ imageDTO: selectedImageDTO })); + dispatch(sessionStagedImageDiscarded({ imageDTO: selectedImageDTO })); } }, [dispatch, selectedImageDTO, images.length]); const onDiscardAll = useCallback(() => { - dispatch(stagingAreaCanceledStaging()); + dispatch(sessionStagingCanceled()); }, [dispatch]); const onToggleShouldShowStagedImage = useCallback(() => { @@ -109,11 +109,11 @@ export const StagingAreaToolbarContent = memo(() => { const counterText = useMemo(() => { if (images.length > 0) { - return `${(stagingArea.selectedImageIndex ?? 0) + 1} of ${images.length}`; + return `${(stagingArea.selectedStagedImageIndex ?? 0) + 1} of ${images.length}`; } else { return `0 of 0`; } - }, [images.length, stagingArea.selectedImageIndex]); + }, [images.length, stagingArea.selectedStagedImageIndex]); return ( <> diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx index 89901bde2d4..dc07df3581f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx @@ -43,7 +43,7 @@ export const ToolChooser: React.FC = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); - const isStaging = useAppSelector((s) => s.canvasV2.stagingArea.isStaging); + const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); const isDrawingToolDisabled = useMemo( () => !getIsDrawingToolEnabled(selectedEntityIdentifier), [selectedEntityIdentifier] diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 7f42b6d5669..54681d5132b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -281,7 +281,7 @@ export class CanvasManager { // debouncedUpdateBboxes(stage, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions, onBboxChanged); } - if (this.isFirstRender || state.stagingArea !== this.prevState.stagingArea) { + if (this.isFirstRender || state.session !== this.prevState.session) { log.debug('Rendering staging area'); this.preview.stagingArea.render(); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts index 76d793ecb25..b4f1c25a167 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts @@ -25,7 +25,7 @@ export class CanvasStagingArea { const shouldShowStagedImage = this.manager.stateApi.getShouldShowStagedImage(); const lastProgressEvent = this.manager.stateApi.getLastProgressEvent(); - this.imageDTO = stagingArea.images[stagingArea.selectedImageIndex] ?? null; + this.imageDTO = stagingArea.stagedImages[stagingArea.selectedStagedImageIndex] ?? null; if (this.imageDTO) { if (this.image) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index 869e165a505..bfbc58ae9e1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -232,7 +232,7 @@ export class CanvasStateApi { return this.getState().settings.maskOpacity; }; getStagingAreaState = () => { - return this.getState().stagingArea; + return this.getState().session; }; getIsSelected = (id: string) => { return this.getSelectedEntity()?.id === id; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index be800e82d88..8b6a80fe67b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -13,8 +13,8 @@ import { layersReducers } from 'features/controlLayers/store/layersReducers'; import { lorasReducers } from 'features/controlLayers/store/lorasReducers'; import { paramsReducers } from 'features/controlLayers/store/paramsReducers'; import { regionsReducers } from 'features/controlLayers/store/regionsReducers'; +import { sessionReducers } from 'features/controlLayers/store/sessionReducers'; import { settingsReducers } from 'features/controlLayers/store/settingsReducers'; -import { stagingAreaReducers } from 'features/controlLayers/store/stagingAreaReducers'; import { toolReducers } from 'features/controlLayers/store/toolReducers'; import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; @@ -122,10 +122,11 @@ const initialState: CanvasV2State = { refinerNegativeAestheticScore: 2.5, refinerStart: 0.8, }, - stagingArea: { + session: { + isActive: false, isStaging: false, - images: [], - selectedImageIndex: 0, + stagedImages: [], + selectedStagedImageIndex: 0, }, }; @@ -144,7 +145,7 @@ export const canvasV2Slice = createSlice({ ...toolReducers, ...bboxReducers, ...inpaintMaskReducers, - ...stagingAreaReducers, + ...sessionReducers, widthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }>) => { const { width, updateAspectRatio, clamp } = action.payload; state.document.width = clamp ? Math.max(roundDownToMultiple(width, 8), 64) : width; @@ -184,7 +185,7 @@ export const canvasV2Slice = createSlice({ state.layers = deepClone(initialState.layers); state.regions = deepClone(initialState.regions); state.selectedEntityIdentifier = deepClone(initialState.selectedEntityIdentifier); - state.stagingArea = deepClone(initialState.stagingArea); + state.session = deepClone(initialState.session); state.tool = deepClone(initialState.tool); }, }, @@ -350,13 +351,14 @@ export const { imEraserLineAdded, imRectShapeAdded, // Staging - stagingAreaStartedStaging, - stagingAreaImageAdded, - stagingAreaImageDiscarded, - stagingAreaImageAccepted, - stagingAreaCanceledStaging, - stagingAreaNextImageSelected, - stagingAreaPreviousImageSelected, + sessionStarted, + sessionStartedStaging, + sessionImageStaged, + sessionStagedImageDiscarded, + sessionStagedImageAccepted, + sessionStagingCanceled, + sessionNextStagedImageSelected, + sessionPrevStagedImageSelected, } = canvasV2Slice.actions; export const selectCanvasV2Slice = (state: RootState) => state.canvasV2; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts new file mode 100644 index 00000000000..f9910eacf06 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts @@ -0,0 +1,62 @@ +import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import type { CanvasV2State } from 'features/controlLayers/store/types'; +import type { ImageDTO } from 'services/api/types'; + +export const sessionReducers = { + sessionStarted: (state) => { + state.session.isActive = true; + }, + sessionStartedStaging: (state) => { + state.session.isStaging = true; + state.session.selectedStagedImageIndex = 0; + // When we start staging, the user should not be interacting with the stage except to move it around. Set the tool + // to view. + state.tool.selectedBuffer = state.tool.selected; + state.tool.selected = 'view'; + }, + sessionImageStaged: (state, action: PayloadAction<{ imageDTO: ImageDTO }>) => { + const { imageDTO } = action.payload; + state.session.stagedImages.push(imageDTO); + state.session.selectedStagedImageIndex = state.session.stagedImages.length - 1; + }, + sessionNextStagedImageSelected: (state) => { + state.session.selectedStagedImageIndex = + (state.session.selectedStagedImageIndex + 1) % state.session.stagedImages.length; + }, + sessionPrevStagedImageSelected: (state) => { + state.session.selectedStagedImageIndex = + (state.session.selectedStagedImageIndex - 1 + state.session.stagedImages.length) % + state.session.stagedImages.length; + }, + sessionStagedImageDiscarded: (state, action: PayloadAction<{ imageDTO: ImageDTO }>) => { + const { imageDTO } = action.payload; + state.session.stagedImages = state.session.stagedImages.filter((image) => image.image_name !== imageDTO.image_name); + state.session.selectedStagedImageIndex = Math.min( + state.session.selectedStagedImageIndex, + state.session.stagedImages.length - 1 + ); + if (state.session.stagedImages.length === 0) { + state.session.isStaging = false; + } + }, + sessionStagedImageAccepted: (state, _: PayloadAction<{ imageDTO: ImageDTO }>) => { + // When we finish staging, reset the tool back to the previous selection. + state.session.isStaging = false; + state.session.stagedImages = []; + state.session.selectedStagedImageIndex = 0; + if (state.tool.selectedBuffer) { + state.tool.selected = state.tool.selectedBuffer; + state.tool.selectedBuffer = null; + } + }, + sessionStagingCanceled: (state) => { + state.session.isStaging = false; + state.session.stagedImages = []; + state.session.selectedStagedImageIndex = 0; + // When we finish staging, reset the tool back to the previous selection. + if (state.tool.selectedBuffer) { + state.tool.selected = state.tool.selectedBuffer; + state.tool.selectedBuffer = null; + } + }, +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/stagingAreaReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/stagingAreaReducers.ts deleted file mode 100644 index 9e6168d9668..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/store/stagingAreaReducers.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import type { CanvasV2State } from 'features/controlLayers/store/types'; -import type { ImageDTO } from 'services/api/types'; - -export const stagingAreaReducers = { - stagingAreaStartedStaging: (state) => { - state.stagingArea.isStaging = true; - state.stagingArea.selectedImageIndex = 0; - // When we start staging, the user should not be interacting with the stage except to move it around. Set the tool - // to view. - state.tool.selectedBuffer = state.tool.selected; - state.tool.selected = 'view'; - }, - stagingAreaImageAdded: (state, action: PayloadAction<{ imageDTO: ImageDTO }>) => { - const { imageDTO } = action.payload; - state.stagingArea.images.push(imageDTO); - state.stagingArea.selectedImageIndex = state.stagingArea.images.length - 1; - }, - stagingAreaNextImageSelected: (state) => { - state.stagingArea.selectedImageIndex = (state.stagingArea.selectedImageIndex + 1) % state.stagingArea.images.length; - }, - stagingAreaPreviousImageSelected: (state) => { - state.stagingArea.selectedImageIndex = - (state.stagingArea.selectedImageIndex - 1 + state.stagingArea.images.length) % state.stagingArea.images.length; - }, - stagingAreaImageDiscarded: (state, action: PayloadAction<{ imageDTO: ImageDTO }>) => { - const { imageDTO } = action.payload; - state.stagingArea.images = state.stagingArea.images.filter((image) => image.image_name !== imageDTO.image_name); - state.stagingArea.selectedImageIndex = Math.min( - state.stagingArea.selectedImageIndex, - state.stagingArea.images.length - 1 - ); - if (state.stagingArea.images.length === 0) { - state.stagingArea.isStaging = false; - } - }, - stagingAreaImageAccepted: (state, _: PayloadAction<{ imageDTO: ImageDTO }>) => { - // When we finish staging, reset the tool back to the previous selection. - state.stagingArea.isStaging = false; - state.stagingArea.images = []; - state.stagingArea.selectedImageIndex = 0; - if (state.tool.selectedBuffer) { - state.tool.selected = state.tool.selectedBuffer; - state.tool.selectedBuffer = null; - } - }, - stagingAreaCanceledStaging: (state) => { - state.stagingArea.isStaging = false; - state.stagingArea.images = []; - state.stagingArea.selectedImageIndex = 0; - // When we finish staging, reset the tool back to the previous selection. - if (state.tool.selectedBuffer) { - state.tool.selected = state.tool.selectedBuffer; - state.tool.selectedBuffer = null; - } - }, -} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 92697568ea2..18c89289408 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -895,10 +895,11 @@ export type CanvasV2State = { refinerNegativeAestheticScore: number; refinerStart: number; }; - stagingArea: { + session: { + isActive: boolean; isStaging: boolean; - images: ImageDTO[]; - selectedImageIndex: number; + stagedImages: ImageDTO[]; + selectedStagedImageIndex: number; }; }; diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts index e6b68e7b772..8199d8f63ee 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts @@ -9,7 +9,7 @@ import { useListImagesQuery } from 'services/api/endpoints/images'; * Registers gallery hotkeys. This hook is a singleton. */ export const useGalleryHotkeys = () => { - const isStaging = useAppSelector((s) => s.canvasV2.stagingArea.isStaging); + const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); const { goNext, goPrev, isNextEnabled, isPrevEnabled } = useGalleryPagination(); const queryArgs = useAppSelector(selectListImagesQueryArgs); From 0a5cf8ae424004cb074332230bba0a75e4a2d6d7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 8 Jul 2024 16:55:12 +1000 Subject: [PATCH 186/678] feat(ui): add useAssertSingleton util hook This simple hook asserts that it is only ever called once. Particularly useful for things like hotkeys hooks. From 70248b9684efc8fab5d61275ff0a0c056d7e0c62 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 8 Jul 2024 17:08:59 +1000 Subject: [PATCH 187/678] feat(ui): split up tool chooser buttons Prep for distinct toolbars for generation vs canvas modes --- .../components/BboxToolButton.tsx | 33 +++ .../components/BrushToolButton.tsx | 39 ++++ .../components/EraserToolButton.tsx | 39 ++++ .../components/MoveToolButton.tsx | 35 +++ .../components/RectToolButton.tsx | 39 ++++ .../controlLayers/components/ToolChooser.tsx | 208 ++---------------- .../components/ViewToolButton.tsx | 32 +++ .../hooks/useCanvasDeleteLayerHotkey.ts | 50 +++++ .../hooks/useCanvasResetLayerHotkey.ts | 53 +++++ .../src/features/controlLayers/store/types.ts | 8 +- 10 files changed, 344 insertions(+), 192 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/BboxToolButton.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/BrushToolButton.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/EraserToolButton.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/MoveToolButton.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RectToolButton.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ViewToolButton.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/components/BboxToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/BboxToolButton.tsx new file mode 100644 index 00000000000..70440f10ac2 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/BboxToolButton.tsx @@ -0,0 +1,33 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; +import { PiBoundingBoxBold } from 'react-icons/pi'; + +export const BboxToolButton = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isDisabled = useAppSelector((s) => s.canvasV2.session.isStaging); + const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'bbox'); + + const onClick = useCallback(() => { + dispatch(toolChanged('bbox')); + }, [dispatch]); + + useHotkeys('q', onClick, [onClick]); + + return ( + } + variant={isSelected ? 'solid' : 'outline'} + onClick={onClick} + isDisabled={isDisabled} + /> + ); +}); + +BboxToolButton.displayName = 'BboxToolButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/BrushToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/BrushToolButton.tsx new file mode 100644 index 00000000000..0dcaa7fa7c4 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/BrushToolButton.tsx @@ -0,0 +1,39 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { isDrawableEntityType } from 'features/controlLayers/store/types'; +import { memo, useCallback } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; +import { PiPaintBrushBold } from 'react-icons/pi'; + +export const BrushToolButton = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'brush'); + const isDisabled = useAppSelector((s) => { + const entityType = s.canvasV2.selectedEntityIdentifier?.type; + const isDrawingToolAllowed = entityType ? isDrawableEntityType(entityType) : false; + const isStaging = s.canvasV2.session.isStaging; + return !isDrawingToolAllowed || isStaging; + }); + + const onClick = useCallback(() => { + dispatch(toolChanged('brush')); + }, [dispatch]); + + useHotkeys('b', onClick, { enabled: !isDisabled }, [isDisabled, onClick]); + + return ( + } + variant={isSelected ? 'solid' : 'outline'} + onClick={onClick} + isDisabled={isDisabled} + /> + ); +}); + +BrushToolButton.displayName = 'BrushToolButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/EraserToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/EraserToolButton.tsx new file mode 100644 index 00000000000..698b37c81f6 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/EraserToolButton.tsx @@ -0,0 +1,39 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { isDrawableEntityType } from 'features/controlLayers/store/types'; +import { memo, useCallback } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; +import { PiEraserBold } from 'react-icons/pi'; + +export const EraserToolButton = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'eraser'); + const isDisabled = useAppSelector((s) => { + const entityType = s.canvasV2.selectedEntityIdentifier?.type; + const isDrawingToolAllowed = entityType ? isDrawableEntityType(entityType) : false; + const isStaging = s.canvasV2.session.isStaging; + return !isDrawingToolAllowed || isStaging; + }); + + const onClick = useCallback(() => { + dispatch(toolChanged('eraser')); + }, [dispatch]); + + useHotkeys('e', onClick, { enabled: !isDisabled }, [isDisabled, onClick]); + + return ( + } + variant={isSelected ? 'solid' : 'outline'} + onClick={onClick} + isDisabled={isDisabled} + /> + ); +}); + +EraserToolButton.displayName = 'EraserToolButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/MoveToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/MoveToolButton.tsx new file mode 100644 index 00000000000..48dcfeb247a --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/MoveToolButton.tsx @@ -0,0 +1,35 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; +import { PiCursorBold } from 'react-icons/pi'; + +export const MoveToolButton = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'move'); + const isDisabled = useAppSelector( + (s) => s.canvasV2.selectedEntityIdentifier === null || s.canvasV2.session.isStaging + ); + + const onClick = useCallback(() => { + dispatch(toolChanged('move')); + }, [dispatch]); + + useHotkeys('v', onClick, { enabled: !isDisabled }, [isDisabled, onClick]); + + return ( + } + variant={isSelected ? 'solid' : 'outline'} + onClick={onClick} + isDisabled={isDisabled} + /> + ); +}); + +MoveToolButton.displayName = 'MoveToolButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RectToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RectToolButton.tsx new file mode 100644 index 00000000000..4a8ccadd095 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RectToolButton.tsx @@ -0,0 +1,39 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { isDrawableEntityType } from 'features/controlLayers/store/types'; +import { memo, useCallback } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; +import { PiRectangleBold } from 'react-icons/pi'; + +export const RectToolButton = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'rect'); + const isDisabled = useAppSelector((s) => { + const entityType = s.canvasV2.selectedEntityIdentifier?.type; + const isDrawingToolAllowed = entityType ? isDrawableEntityType(entityType) : false; + const isStaging = s.canvasV2.session.isStaging; + return !isDrawingToolAllowed || isStaging; + }); + + const onClick = useCallback(() => { + dispatch(toolChanged('rect')); + }, [dispatch]); + + useHotkeys('u', onClick, { enabled: !isDisabled }, [isDisabled, onClick]); + + return ( + } + variant={isSelected ? 'solid' : 'outline'} + onClick={onClick} + isDisabled={isDisabled} + /> + ); +}); + +RectToolButton.displayName = 'RectToolButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx index dc07df3581f..e1bfe85a1f4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx @@ -1,199 +1,25 @@ -import { ButtonGroup, IconButton } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { - caDeleted, - imReset, - ipaDeleted, - layerDeleted, - layerReset, - rgDeleted, - rgReset, - selectCanvasV2Slice, - toolChanged, -} from 'features/controlLayers/store/canvasV2Slice'; -import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; -import { useCallback, useMemo } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { - PiBoundingBoxBold, - PiCursorBold, - PiEraserBold, - PiHandBold, - PiPaintBrushBold, - PiRectangleBold, -} from 'react-icons/pi'; - -const DRAWING_TOOL_TYPES = ['layer', 'regional_guidance', 'inpaint_mask']; - -const getIsDrawingToolEnabled = (entityIdentifier: CanvasEntityIdentifier | null) => { - if (!entityIdentifier) { - return false; - } - return DRAWING_TOOL_TYPES.includes(entityIdentifier.type); -}; - -const selectSelectedEntityIdentifier = createMemoizedSelector( - selectCanvasV2Slice, - (canvasV2State) => canvasV2State.selectedEntityIdentifier -); +import { ButtonGroup } from '@invoke-ai/ui-library'; +import { BboxToolButton } from 'features/controlLayers/components/BboxToolButton'; +import { BrushToolButton } from 'features/controlLayers/components/BrushToolButton'; +import { EraserToolButton } from 'features/controlLayers/components/EraserToolButton'; +import { MoveToolButton } from 'features/controlLayers/components/MoveToolButton'; +import { RectToolButton } from 'features/controlLayers/components/RectToolButton'; +import { ViewToolButton } from 'features/controlLayers/components/ViewToolButton'; +import { useCanvasDeleteLayerHotkey } from 'features/controlLayers/hooks/useCanvasDeleteLayerHotkey'; +import { useCanvasResetLayerHotkey } from 'features/controlLayers/hooks/useCanvasResetLayerHotkey'; export const ToolChooser: React.FC = () => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); - const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); - const isDrawingToolDisabled = useMemo( - () => !getIsDrawingToolEnabled(selectedEntityIdentifier), - [selectedEntityIdentifier] - ); - const isMoveToolDisabled = useMemo(() => selectedEntityIdentifier === null, [selectedEntityIdentifier]); - const tool = useAppSelector((s) => s.canvasV2.tool.selected); - - const setToolToBrush = useCallback(() => { - dispatch(toolChanged('brush')); - }, [dispatch]); - useHotkeys('b', setToolToBrush, { enabled: !isDrawingToolDisabled && !isStaging }, [ - isDrawingToolDisabled, - isStaging, - setToolToBrush, - ]); - const setToolToEraser = useCallback(() => { - dispatch(toolChanged('eraser')); - }, [dispatch]); - useHotkeys('e', setToolToEraser, { enabled: !isDrawingToolDisabled && !isStaging }, [ - isDrawingToolDisabled, - isStaging, - setToolToEraser, - ]); - const setToolToRect = useCallback(() => { - dispatch(toolChanged('rect')); - }, [dispatch]); - useHotkeys('u', setToolToRect, { enabled: !isDrawingToolDisabled && !isStaging }, [ - isDrawingToolDisabled, - isStaging, - setToolToRect, - ]); - const setToolToMove = useCallback(() => { - dispatch(toolChanged('move')); - }, [dispatch]); - useHotkeys('v', setToolToMove, { enabled: !isMoveToolDisabled && !isStaging }, [ - isMoveToolDisabled, - isStaging, - setToolToMove, - ]); - const setToolToView = useCallback(() => { - dispatch(toolChanged('view')); - }, [dispatch]); - useHotkeys('h', setToolToView, [setToolToView]); - const setToolToBbox = useCallback(() => { - dispatch(toolChanged('bbox')); - }, [dispatch]); - useHotkeys('q', setToolToBbox, [setToolToBbox]); - - const resetSelectedLayer = useCallback(() => { - if (selectedEntityIdentifier === null) { - return; - } - const { type, id } = selectedEntityIdentifier; - if (type === 'layer') { - dispatch(layerReset({ id })); - } - if (type === 'regional_guidance') { - dispatch(rgReset({ id })); - } - if (type === 'inpaint_mask') { - dispatch(imReset()); - } - }, [dispatch, selectedEntityIdentifier]); - const isResetEnabled = useMemo( - () => - (!isStaging && selectedEntityIdentifier?.type === 'layer') || - selectedEntityIdentifier?.type === 'regional_guidance' || - selectedEntityIdentifier?.type === 'inpaint_mask', - [isStaging, selectedEntityIdentifier?.type] - ); - useHotkeys('shift+c', resetSelectedLayer, { enabled: isResetEnabled }, [ - isResetEnabled, - isStaging, - resetSelectedLayer, - ]); - - const deleteSelectedLayer = useCallback(() => { - if (selectedEntityIdentifier === null) { - return; - } - const { type, id } = selectedEntityIdentifier; - if (type === 'layer') { - dispatch(layerDeleted({ id })); - } - if (type === 'regional_guidance') { - dispatch(rgDeleted({ id })); - } - if (type === 'control_adapter') { - dispatch(caDeleted({ id })); - } - if (type === 'ip_adapter') { - dispatch(ipaDeleted({ id })); - } - }, [dispatch, selectedEntityIdentifier]); - const isDeleteEnabled = useMemo( - () => selectedEntityIdentifier !== null && !isStaging, - [selectedEntityIdentifier, isStaging] - ); - useHotkeys('shift+d', deleteSelectedLayer, { enabled: isDeleteEnabled }, [isDeleteEnabled, deleteSelectedLayer]); + useCanvasResetLayerHotkey(); + useCanvasDeleteLayerHotkey(); return ( - } - variant={tool === 'brush' ? 'solid' : 'outline'} - onClick={setToolToBrush} - isDisabled={isDrawingToolDisabled || isStaging} - /> - } - variant={tool === 'eraser' ? 'solid' : 'outline'} - onClick={setToolToEraser} - isDisabled={isDrawingToolDisabled || isStaging} - /> - } - variant={tool === 'rect' ? 'solid' : 'outline'} - onClick={setToolToRect} - isDisabled={isDrawingToolDisabled || isStaging} - /> - } - variant={tool === 'move' ? 'solid' : 'outline'} - onClick={setToolToMove} - isDisabled={isMoveToolDisabled || isStaging} - /> - } - variant={tool === 'view' ? 'solid' : 'outline'} - onClick={setToolToView} - isDisabled={isStaging} - /> - } - variant={tool === 'bbox' ? 'solid' : 'outline'} - onClick={setToolToBbox} - isDisabled={isStaging} - /> + + + + + + ); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ViewToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ViewToolButton.tsx new file mode 100644 index 00000000000..b9f6b1691dc --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ViewToolButton.tsx @@ -0,0 +1,32 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; +import { PiHandBold } from 'react-icons/pi'; + +export const ViewToolButton = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'view'); + const isDisabled = useAppSelector((s) => s.canvasV2.session.isStaging); + const onClick = useCallback(() => { + dispatch(toolChanged('view')); + }, [dispatch]); + + useHotkeys('h', onClick, [onClick]); + + return ( + } + variant={isSelected ? 'solid' : 'outline'} + onClick={onClick} + isDisabled={isDisabled} + /> + ); +}); + +ViewToolButton.displayName = 'ViewToolButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts new file mode 100644 index 00000000000..1e2fb57901e --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts @@ -0,0 +1,50 @@ +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; +import { + caDeleted, + ipaDeleted, + layerDeleted, + rgDeleted, + selectCanvasV2Slice, +} from 'features/controlLayers/store/canvasV2Slice'; +import { useCallback, useMemo } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; + +const selectSelectedEntityIdentifier = createMemoizedSelector( + selectCanvasV2Slice, + (canvasV2State) => canvasV2State.selectedEntityIdentifier +); + +export function useCanvasDeleteLayerHotkey() { + useAssertSingleton(useCanvasDeleteLayerHotkey.name); + const dispatch = useAppDispatch(); + const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); + const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); + + const deleteSelectedLayer = useCallback(() => { + if (selectedEntityIdentifier === null) { + return; + } + const { type, id } = selectedEntityIdentifier; + if (type === 'layer') { + dispatch(layerDeleted({ id })); + } + if (type === 'regional_guidance') { + dispatch(rgDeleted({ id })); + } + if (type === 'control_adapter') { + dispatch(caDeleted({ id })); + } + if (type === 'ip_adapter') { + dispatch(ipaDeleted({ id })); + } + }, [dispatch, selectedEntityIdentifier]); + + const isDeleteEnabled = useMemo( + () => selectedEntityIdentifier !== null && !isStaging, + [selectedEntityIdentifier, isStaging] + ); + + useHotkeys('shift+d', deleteSelectedLayer, { enabled: isDeleteEnabled }, [isDeleteEnabled, deleteSelectedLayer]); +} diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts new file mode 100644 index 00000000000..2d1c3c74f0c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts @@ -0,0 +1,53 @@ +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; +import { + imReset, + layerReset, + rgReset, + selectCanvasV2Slice, +} from 'features/controlLayers/store/canvasV2Slice'; +import { useCallback, useMemo } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; + +const selectSelectedEntityIdentifier = createMemoizedSelector( + selectCanvasV2Slice, + (canvasV2State) => canvasV2State.selectedEntityIdentifier +); + +export function useCanvasResetLayerHotkey() { + useAssertSingleton(useCanvasResetLayerHotkey.name); + const dispatch = useAppDispatch(); + const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); + const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); + + const resetSelectedLayer = useCallback(() => { + if (selectedEntityIdentifier === null) { + return; + } + const { type, id } = selectedEntityIdentifier; + if (type === 'layer') { + dispatch(layerReset({ id })); + } + if (type === 'regional_guidance') { + dispatch(rgReset({ id })); + } + if (type === 'inpaint_mask') { + dispatch(imReset()); + } + }, [dispatch, selectedEntityIdentifier]); + + const isResetEnabled = useMemo( + () => + (!isStaging && selectedEntityIdentifier?.type === 'layer') || + selectedEntityIdentifier?.type === 'regional_guidance' || + selectedEntityIdentifier?.type === 'inpaint_mask', + [isStaging, selectedEntityIdentifier?.type] + ); + + useHotkeys('shift+c', resetSelectedLayer, { enabled: isResetEnabled }, [ + isResetEnabled, + isStaging, + resetSelectedLayer, + ]); +} diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 18c89289408..f31ce9e998e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -655,7 +655,7 @@ const zImageFill = z.object({ }); const zFill = z.discriminatedUnion('type', [zColorFill, zImageFill]); const zInpaintMaskEntity = z.object({ - id: zId, + id: z.literal('inpaint_mask'), type: z.literal('inpaint_mask'), isEnabled: z.boolean(), x: z.number(), @@ -945,3 +945,9 @@ export function isDrawableEntityAdapter( ): adapter is CanvasLayer | CanvasRegion | CanvasInpaintMask { return adapter instanceof CanvasLayer || adapter instanceof CanvasRegion || adapter instanceof CanvasInpaintMask; } + +export function isDrawableEntityType( + entityType: CanvasEntity['type'] +): entityType is 'layer' | 'regional_guidance' | 'inpaint_mask' { + return entityType === 'layer' || entityType === 'regional_guidance' || entityType === 'inpaint_mask'; +} From f1cd6a06ec561e6c360494b5fca8f6f71be3a8b7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 8 Jul 2024 17:17:38 +1000 Subject: [PATCH 188/678] tidy(ui): remove unused naming objects/utils The canvas manager means we don't need to worry about konva node names as we never directly select konva nodes. --- .../controlLayers/konva/CanvasBbox.ts | 9 +--- .../controlLayers/konva/CanvasInpaintMask.ts | 6 +-- .../controlLayers/konva/entityBbox.ts | 19 +++---- .../features/controlLayers/konva/naming.ts | 51 ++----------------- .../src/features/controlLayers/konva/util.ts | 42 --------------- .../controlLayers/store/canvasV2Slice.ts | 6 +-- 6 files changed, 17 insertions(+), 116 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts index 7ab216c4bf7..797c97aba2c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts @@ -1,10 +1,5 @@ import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMultiple'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { - PREVIEW_GENERATION_BBOX_DUMMY_RECT, - PREVIEW_GENERATION_BBOX_GROUP, - PREVIEW_GENERATION_BBOX_TRANSFORMER, -} from 'features/controlLayers/konva/naming'; import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; import { atom } from 'nanostores'; @@ -38,9 +33,8 @@ export class CanvasBbox { // Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully // transparent rect for this purpose. - this.group = new Konva.Group({ id: PREVIEW_GENERATION_BBOX_GROUP, listening: false }); + this.group = new Konva.Group({ id: 'bbox_group', listening: false }); this.rect = new Konva.Rect({ - id: PREVIEW_GENERATION_BBOX_DUMMY_RECT, listening: false, strokeEnabled: false, draggable: true, @@ -61,7 +55,6 @@ export class CanvasBbox { }); this.transformer = new Konva.Transformer({ - id: PREVIEW_GENERATION_BBOX_TRANSFORMER, borderDash: [5, 5], borderStroke: 'rgba(212,216,234,1)', borderEnabled: true, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts index f9bd9895be1..4dcd7be36be 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts @@ -4,7 +4,7 @@ import { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine' import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox'; -import { getObjectGroupId, INPAINT_MASK_LAYER_ID } from 'features/controlLayers/konva/naming'; +import { getObjectGroupId } from 'features/controlLayers/konva/naming'; import { mapId } from 'features/controlLayers/konva/util'; import type { BrushLine, EraserLine, InpaintMaskEntity, RectShape } from 'features/controlLayers/store/types'; import { isDrawingTool, RGBA_RED } from 'features/controlLayers/store/types'; @@ -25,9 +25,9 @@ export class CanvasInpaintMask { private inpaintMaskState: InpaintMaskEntity; constructor(entity: InpaintMaskEntity, manager: CanvasManager) { - this.id = INPAINT_MASK_LAYER_ID; + this.id = 'inpaint_mask'; this.manager = manager; - this.layer = new Konva.Layer({ id: INPAINT_MASK_LAYER_ID }); + this.layer = new Konva.Layer({ id: this.id }); this.group = new Konva.Group({ id: getObjectGroupId(this.layer.id(), uuidv4()), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts index 0dcb9d2d3c5..e68131c386c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts @@ -1,11 +1,5 @@ import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; -import { - CA_LAYER_IMAGE_NAME, - getLayerBboxId, - LAYER_BBOX_NAME, - RASTER_LAYER_OBJECT_GROUP_NAME, - RG_LAYER_OBJECT_GROUP_NAME, -} from 'features/controlLayers/konva/naming'; +import { getLayerBboxId } from 'features/controlLayers/konva/naming'; import { imageDataToDataURL } from 'features/controlLayers/konva/util'; import type { BboxChangedArg, @@ -26,7 +20,7 @@ import { assert } from 'tsafe'; export const createBboxRect = (entity: CanvasEntity, konvaLayer: Konva.Layer): Konva.Rect => { const rect = new Konva.Rect({ id: getLayerBboxId(entity.id), - name: LAYER_BBOX_NAME, + name: 'bbox', strokeWidth: 1, visible: false, }); @@ -191,9 +185,10 @@ export const getNodeBboxFast = (node: Konva.Node): IRect => { return bbox; }; -const filterRGChildren = (node: Konva.Node): boolean => node.name() === RG_LAYER_OBJECT_GROUP_NAME; -const filterLayerChildren = (node: Konva.Node): boolean => node.name() === RASTER_LAYER_OBJECT_GROUP_NAME; -const filterCAChildren = (node: Konva.Node): boolean => node.name() === CA_LAYER_IMAGE_NAME; +// TODO(psyche): fix this +const filterRGChildren = (node: Konva.Node): boolean => true; +const filterLayerChildren = (node: Konva.Node): boolean => true; +const filterCAChildren = (node: Konva.Node): boolean => true; /** * Calculates the bbox of each regional guidance layer. Only calculates if the mask has changed. @@ -213,7 +208,7 @@ export const updateBboxes = ( assert(konvaLayer, `Layer ${entityState.id} not found in stage`); // We only need to recalculate the bbox if the layer has changed if (entityState.bboxNeedsUpdate) { - const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(entityState, konvaLayer); + const bboxRect = konvaLayer.findOne('.bbox') ?? createBboxRect(entityState, konvaLayer); // Hide the bbox while we calculate the new bbox, else the bbox will be included in the calculation const visible = bboxRect.visible(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts index c96464092c6..c9888d27dfd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts @@ -2,59 +2,14 @@ * This file contains IDs, names, and ID getters for konva layers and objects. */ -// IDs for singleton Konva layers and objects -export const PREVIEW_LAYER_ID = 'preview_layer'; -export const PREVIEW_TOOL_GROUP_ID = `${PREVIEW_LAYER_ID}.tool_group`; -export const PREVIEW_BRUSH_GROUP_ID = `${PREVIEW_LAYER_ID}.brush_group`; -export const PREVIEW_BRUSH_FILL_ID = `${PREVIEW_LAYER_ID}.brush_fill`; -export const PREVIEW_BRUSH_BORDER_INNER_ID = `${PREVIEW_LAYER_ID}.brush_border_inner`; -export const PREVIEW_BRUSH_BORDER_OUTER_ID = `${PREVIEW_LAYER_ID}.brush_border_outer`; -export const PREVIEW_RECT_ID = `${PREVIEW_LAYER_ID}.rect`; -export const PREVIEW_GENERATION_BBOX_GROUP = `${PREVIEW_LAYER_ID}.gen_bbox_group`; -export const PREVIEW_GENERATION_BBOX_TRANSFORMER = `${PREVIEW_LAYER_ID}.gen_bbox_transformer`; -export const PREVIEW_GENERATION_BBOX_DUMMY_RECT = `${PREVIEW_LAYER_ID}.gen_bbox_dummy_rect`; -export const PREVIEW_DOCUMENT_SIZE_GROUP = `${PREVIEW_LAYER_ID}.doc_size_group`; -export const PREVIEW_DOCUMENT_SIZE_STAGE_RECT = `${PREVIEW_LAYER_ID}.doc_size_stage_rect`; -export const PREVIEW_DOCUMENT_SIZE_DOCUMENT_RECT = `${PREVIEW_LAYER_ID}.doc_size_doc_rect`; - -// 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 IMAGE_PLACEHOLDER_NAME = 'image_placeholder'; - -export const CA_LAYER_NAME = 'control_adapter'; -export const CA_LAYER_OBJECT_GROUP_NAME = `${CA_LAYER_NAME}.object_group`; -export const CA_LAYER_IMAGE_NAME = `${CA_LAYER_NAME}.image`; - -export const RG_LAYER_NAME = 'regional_guidance_layer'; -export const RG_LAYER_OBJECT_GROUP_NAME = `${RG_LAYER_NAME}.object_group`; -export const RG_LAYER_BRUSH_LINE_NAME = `${RG_LAYER_NAME}.brush_line`; -export const RG_LAYER_ERASER_LINE_NAME = `${RG_LAYER_NAME}.eraser_line`; -export const RG_LAYER_RECT_SHAPE_NAME = `${RG_LAYER_NAME}.rect_shape`; - -export const RASTER_LAYER_NAME = 'raster_layer'; -export const RASTER_LAYER_OBJECT_GROUP_NAME = `${RASTER_LAYER_NAME}.object_group`; -export const RASTER_LAYER_BRUSH_LINE_NAME = `${RASTER_LAYER_NAME}.brush_line`; -export const RASTER_LAYER_ERASER_LINE_NAME = `${RASTER_LAYER_NAME}.eraser_line`; -export const RASTER_LAYER_RECT_SHAPE_NAME = `${RASTER_LAYER_NAME}.rect_shape`; -export const RASTER_LAYER_IMAGE_NAME = `${RASTER_LAYER_NAME}.image`; - -export const INPAINT_MASK_LAYER_ID = 'inpaint_mask_layer'; -export const INPAINT_MASK_LAYER_OBJECT_GROUP_NAME = `${INPAINT_MASK_LAYER_ID}.object_group`; -export const INPAINT_MASK_LAYER_BRUSH_LINE_NAME = `${INPAINT_MASK_LAYER_ID}.brush_line`; -export const INPAINT_MASK_LAYER_ERASER_LINE_NAME = `${INPAINT_MASK_LAYER_ID}.eraser_line`; -export const INPAINT_MASK_LAYER_RECT_SHAPE_NAME = `${INPAINT_MASK_LAYER_ID}.rect_shape`; - -export const BACKGROUND_LAYER_ID = 'background_layer'; - // Getters for non-singleton layer and object IDs -export const getRGId = (entityId: string) => `${RG_LAYER_NAME}_${entityId}`; -export const getLayerId = (entityId: string) => `${RASTER_LAYER_NAME}_${entityId}`; +export const getRGId = (entityId: string) => `region_${entityId}`; +export const getLayerId = (entityId: string) => `layer_${entityId}`; export const getBrushLineId = (entityId: string, lineId: string) => `${entityId}.brush_line_${lineId}`; export const getEraserLineId = (entityId: string, lineId: string) => `${entityId}.eraser_line_${lineId}`; export const getRectShapeId = (entityId: string, rectId: string) => `${entityId}.rect_${rectId}`; export const getImageObjectId = (entityId: string, imageId: string) => `${entityId}.image_${imageId}`; export const getObjectGroupId = (entityId: string, groupId: string) => `${entityId}.objectGroup_${groupId}`; export const getLayerBboxId = (entityId: string) => `${entityId}.bbox`; -export const getCAId = (entityId: string) => `${CA_LAYER_NAME}_${entityId}`; +export const getCAId = (entityId: string) => `control_adapter_${entityId}`; export const getIPAId = (entityId: string) => `ip_adapter_${entityId}`; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index e4d52922f1c..c5971d7b990 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -1,18 +1,5 @@ import { getImageDataTransparency } from 'common/util/arrayBuffer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { - CA_LAYER_NAME, - INPAINT_MASK_LAYER_ID, - 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, - RG_LAYER_ERASER_LINE_NAME, - RG_LAYER_NAME, - RG_LAYER_RECT_SHAPE_NAME, -} from 'features/controlLayers/konva/naming'; import type { GenerationMode, Rect, RgbaColor } from 'features/controlLayers/store/types'; import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers'; import Konva from 'konva'; @@ -120,35 +107,6 @@ export const getIsFocused = (stage: Konva.Stage): boolean => stage.container().c */ 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 = (node: Konva.Node): boolean => - node.name() === RG_LAYER_NAME || - node.name() === CA_LAYER_NAME || - node.name() === RASTER_LAYER_NAME || - node.name() === INPAINT_MASK_LAYER_ID; - -/** - * 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 => - 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 || - node.name() === RASTER_LAYER_IMAGE_NAME; - /** * Convert a Blob to a data URL. */ diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 8b6a80fe67b..c98486fb5f7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -3,7 +3,6 @@ import { createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { deepClone } from 'common/util/deepClone'; import { roundDownToMultiple } from 'common/util/roundDownToMultiple'; -import { INPAINT_MASK_LAYER_ID } from 'features/controlLayers/konva/naming'; import { bboxReducers } from 'features/controlLayers/store/bboxReducers'; import { compositingReducers } from 'features/controlLayers/store/compositingReducers'; import { controlAdaptersReducers } from 'features/controlLayers/store/controlAdaptersReducers'; @@ -26,14 +25,14 @@ import { RGBA_RED } from './types'; const initialState: CanvasV2State = { _version: 3, - selectedEntityIdentifier: { type: 'inpaint_mask', id: INPAINT_MASK_LAYER_ID }, + selectedEntityIdentifier: { type: 'inpaint_mask', id: 'inpaint_mask' }, layers: { entities: [], imageCache: null }, controlAdapters: { entities: [] }, ipAdapters: { entities: [] }, regions: { entities: [] }, loras: [], inpaintMask: { - id: INPAINT_MASK_LAYER_ID, + id: 'inpaint_mask', type: 'inpaint_mask', bbox: null, bboxNeedsUpdate: false, @@ -187,6 +186,7 @@ export const canvasV2Slice = createSlice({ state.selectedEntityIdentifier = deepClone(initialState.selectedEntityIdentifier); state.session = deepClone(initialState.session); state.tool = deepClone(initialState.tool); + state.inpaintMask = deepClone(initialState.inpaintMask); }, }, }); From cbc61daa563022356a58badb0e46d09d136e1cc3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 8 Jul 2024 17:17:51 +1000 Subject: [PATCH 189/678] feat(ui): hide bbox button when no canvas session active --- .../controlLayers/components/ToolChooser.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx index e1bfe85a1f4..706d51b74cb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx @@ -1,4 +1,5 @@ import { ButtonGroup } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; import { BboxToolButton } from 'features/controlLayers/components/BboxToolButton'; import { BrushToolButton } from 'features/controlLayers/components/BrushToolButton'; import { EraserToolButton } from 'features/controlLayers/components/EraserToolButton'; @@ -11,6 +12,20 @@ import { useCanvasResetLayerHotkey } from 'features/controlLayers/hooks/useCanva export const ToolChooser: React.FC = () => { useCanvasResetLayerHotkey(); useCanvasDeleteLayerHotkey(); + const isCanvasSessionActive = useAppSelector((s) => s.canvasV2.session.isActive); + + if (isCanvasSessionActive) { + return ( + + + + + + + + + ); + } return ( @@ -19,7 +34,6 @@ export const ToolChooser: React.FC = () => { - ); }; From ee4dc86c15245403b2721424127343cce7a6abda Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 8 Jul 2024 17:22:53 +1000 Subject: [PATCH 190/678] feat(ui): split out canvas entity list component --- .../components/CanvasEntityList.tsx | 53 +++++++++++++++++++ .../components/ControlLayersPanelContent.tsx | 46 ++-------------- 2 files changed, 57 insertions(+), 42 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx new file mode 100644 index 00000000000..5df4b0a4c7b --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx @@ -0,0 +1,53 @@ +/* eslint-disable i18next/no-literal-string */ +import { Flex } from '@invoke-ai/ui-library'; +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppSelector } from 'app/store/storeHooks'; +import { IAINoContentFallback } from 'common/components/IAIImageFallback'; +import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; +import { CA } from 'features/controlLayers/components/ControlAdapter/CA'; +import { IPA } from 'features/controlLayers/components/IPAdapter/IPA'; +import { Layer } from 'features/controlLayers/components/Layer/Layer'; +import { RG } from 'features/controlLayers/components/RegionalGuidance/RG'; +import { mapId } from 'features/controlLayers/konva/util'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { + const rgIds = canvasV2.regions.entities.map(mapId).reverse(); + const caIds = canvasV2.controlAdapters.entities.map(mapId).reverse(); + const ipaIds = canvasV2.ipAdapters.entities.map(mapId).reverse(); + const layerIds = canvasV2.layers.entities.map(mapId).reverse(); + const entityCount = rgIds.length + caIds.length + ipaIds.length + layerIds.length; + return { rgIds, caIds, ipaIds, layerIds, entityCount }; +}); + +export const CanvasEntityList = memo(() => { + const { t } = useTranslation(); + const { rgIds, caIds, ipaIds, layerIds, entityCount } = useAppSelector(selectEntityIds); + + if (entityCount > 0) { + return ( + + + {rgIds.map((id) => ( + + ))} + {caIds.map((id) => ( + + ))} + {ipaIds.map((id) => ( + + ))} + {layerIds.map((id) => ( + + ))} + + + ); + } + + return ; +}); + +CanvasEntityList.displayName = 'CanvasEntityList'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index 9e771eb48eb..4c8572a5f9a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -1,60 +1,22 @@ /* eslint-disable i18next/no-literal-string */ import { Flex } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { IAINoContentFallback } from 'common/components/IAIImageFallback'; -import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import { AddLayerButton } from 'features/controlLayers/components/AddLayerButton'; -import { CA } from 'features/controlLayers/components/ControlAdapter/CA'; +import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList'; import { DeleteAllLayersButton } from 'features/controlLayers/components/DeleteAllLayersButton'; import { IM } from 'features/controlLayers/components/InpaintMask/IM'; -import { IPA } from 'features/controlLayers/components/IPAdapter/IPA'; -import { Layer } from 'features/controlLayers/components/Layer/Layer'; -import { RG } from 'features/controlLayers/components/RegionalGuidance/RG'; -import { mapId } from 'features/controlLayers/konva/util'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; - -const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { - const rgIds = canvasV2.regions.entities.map(mapId).reverse(); - const caIds = canvasV2.controlAdapters.entities.map(mapId).reverse(); - const ipaIds = canvasV2.ipAdapters.entities.map(mapId).reverse(); - const layerIds = canvasV2.layers.entities.map(mapId).reverse(); - const entityCount = rgIds.length + caIds.length + ipaIds.length + layerIds.length; - return { rgIds, caIds, ipaIds, layerIds, entityCount }; -}); export const ControlLayersPanelContent = memo(() => { - const { t } = useTranslation(); - const { rgIds, caIds, ipaIds, layerIds, entityCount } = useAppSelector(selectEntityIds); - + const isCanvasSessionActive = useAppSelector((s) => s.canvasV2.session.isActive); return ( - - {entityCount > 0 && ( - - - {rgIds.map((id) => ( - - ))} - {caIds.map((id) => ( - - ))} - {ipaIds.map((id) => ( - - ))} - {layerIds.map((id) => ( - - ))} - - - )} - {entityCount === 0 && } + {isCanvasSessionActive && } + ); }); From 1b44520ff7fdb8e77872c97307858beb6d852ba0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 8 Jul 2024 17:23:14 +1000 Subject: [PATCH 191/678] feat(ui): initialState is for generation mode --- .../web/src/features/controlLayers/store/canvasV2Slice.ts | 4 ++-- .../web/src/features/controlLayers/store/sessionReducers.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index c98486fb5f7..ff345dfe136 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -25,7 +25,7 @@ import { RGBA_RED } from './types'; const initialState: CanvasV2State = { _version: 3, - selectedEntityIdentifier: { type: 'inpaint_mask', id: 'inpaint_mask' }, + selectedEntityIdentifier: null, layers: { entities: [], imageCache: null }, controlAdapters: { entities: [] }, ipAdapters: { entities: [] }, @@ -44,7 +44,7 @@ const initialState: CanvasV2State = { y: 0, }, tool: { - selected: 'bbox', + selected: 'view', selectedBuffer: null, invertScroll: false, fill: RGBA_RED, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts index f9910eacf06..77337938510 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts @@ -5,6 +5,7 @@ import type { ImageDTO } from 'services/api/types'; export const sessionReducers = { sessionStarted: (state) => { state.session.isActive = true; + state.selectedEntityIdentifier = { id: 'inpaint_mask', type: 'inpaint_mask' }; }, sessionStartedStaging: (state) => { state.session.isStaging = true; From 0cddbc24201a01e6ddb061137faf0d5de1eaf0c5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 8 Jul 2024 18:53:45 +1000 Subject: [PATCH 192/678] refactor(ui): remove modular imagesize components This is no longer necessary with canvas v2 and added a ton of extraneous redux actions when changing the image size. Also renamed to document size --- .../listeners/modelsLoaded.ts | 10 +- .../listeners/setDefaultSettings.ts | 8 +- .../ControlAdapter/CAImagePreview.tsx | 12 +- .../components/IPAdapter/IPAImagePreview.tsx | 12 +- .../controlLayers/store/canvasV2Slice.ts | 37 ++--- .../controlLayers/store/documentReducers.ts | 104 ++++++++++++ .../controlLayers/store/paramsReducers.ts | 2 +- .../src/features/controlLayers/store/types.ts | 2 +- .../src/features/metadata/util/recallers.ts | 8 +- .../components/Core/ParamHeight.tsx | 15 +- .../parameters/components/Core/ParamWidth.tsx | 15 +- .../AspectRatioCanvasPreview.tsx | 2 +- .../AspectRatioIconPreview.tsx | 20 +-- .../AspectRatioSelect.tsx | 19 +-- .../components/DocumentSize/DocumentSize.tsx | 38 +++++ .../LockAspectRatioButton.tsx | 14 +- .../SetOptimalSizeButton.tsx | 20 ++- .../SwapDimensionsButton.tsx | 9 +- .../calculateNewSize.ts | 0 .../{ImageSize => DocumentSize}/constants.ts | 0 .../{ImageSize => DocumentSize}/types.ts | 0 .../components/ImageSize/ImageSize.tsx | 47 ------ .../components/ImageSize/ImageSizeContext.ts | 156 ------------------ .../ImageSettingsAccordion.tsx | 4 +- .../ImageSizeLinear.tsx | 58 ------- 25 files changed, 241 insertions(+), 371 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/documentReducers.ts rename invokeai/frontend/web/src/features/parameters/components/{ImageSize => DocumentSize}/AspectRatioCanvasPreview.tsx (93%) rename invokeai/frontend/web/src/features/parameters/components/{ImageSize => DocumentSize}/AspectRatioIconPreview.tsx (75%) rename invokeai/frontend/web/src/features/parameters/components/{ImageSize => DocumentSize}/AspectRatioSelect.tsx (70%) create mode 100644 invokeai/frontend/web/src/features/parameters/components/DocumentSize/DocumentSize.tsx rename invokeai/frontend/web/src/features/parameters/components/{ImageSize => DocumentSize}/LockAspectRatioButton.tsx (55%) rename invokeai/frontend/web/src/features/parameters/components/{ImageSize => DocumentSize}/SetOptimalSizeButton.tsx (68%) rename invokeai/frontend/web/src/features/parameters/components/{ImageSize => DocumentSize}/SwapDimensionsButton.tsx (71%) rename invokeai/frontend/web/src/features/parameters/components/{ImageSize => DocumentSize}/calculateNewSize.ts (100%) rename invokeai/frontend/web/src/features/parameters/components/{ImageSize => DocumentSize}/constants.ts (100%) rename invokeai/frontend/web/src/features/parameters/components/{ImageSize => DocumentSize}/types.ts (100%) delete mode 100644 invokeai/frontend/web/src/features/parameters/components/ImageSize/ImageSize.tsx delete mode 100644 invokeai/frontend/web/src/features/parameters/components/ImageSize/ImageSizeContext.ts delete mode 100644 invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts index a94b0b45e36..fa1e453ff94 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts @@ -4,16 +4,16 @@ import type { AppDispatch, RootState } from 'app/store/store'; import type { JSONObject } from 'common/types'; import { caModelChanged, - heightChanged, + documentHeightChanged, + documentWidthChanged, ipaModelChanged, loraDeleted, modelChanged, refinerModelChanged, rgIPAdapterModelChanged, vaeSelected, - widthChanged, } from 'features/controlLayers/store/canvasV2Slice'; -import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; +import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; import { postProcessingModelChanged, upscaleModelChanged } from 'features/parameters/store/upscaleSlice'; import { zParameterModel, zParameterVAEModel } from 'features/parameters/types/parameterSchemas'; import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension'; @@ -91,8 +91,8 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => { optimalDimension * optimalDimension ); - dispatch(widthChanged({ width })); - dispatch(heightChanged({ height })); + dispatch(documentWidthChanged({ width })); + dispatch(documentHeightChanged({ height })); return; } } diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts index fa01ee9e650..f8ddadb4887 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts @@ -1,13 +1,13 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { - heightChanged, + documentHeightChanged, + documentWidthChanged, setCfgRescaleMultiplier, setCfgScale, setScheduler, setSteps, vaePrecisionChanged, vaeSelected, - widthChanged, } from 'features/controlLayers/store/canvasV2Slice'; import { setDefaultSettings } from 'features/parameters/store/actions'; import { @@ -99,13 +99,13 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni const setSizeOptions = { updateAspectRatio: true, clamp: true }; if (width) { if (isParameterWidth(width)) { - dispatch(widthChanged({ width, ...setSizeOptions })); + dispatch(documentWidthChanged({ width, ...setSizeOptions })); } } if (height) { if (isParameterHeight(height)) { - dispatch(heightChanged({ height, ...setSizeOptions })); + dispatch(documentHeightChanged({ height, ...setSizeOptions })); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx index b87362e573d..5d6d253fbf4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx @@ -3,11 +3,11 @@ import { skipToken } from '@reduxjs/toolkit/query'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; -import { heightChanged, widthChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { documentHeightChanged, documentWidthChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import type { ControlAdapterEntity } from 'features/controlLayers/store/types'; import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types'; -import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; +import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowCounterClockwiseBold, PiFloppyDiskBold, PiRulerBold } from 'react-icons/pi'; @@ -89,15 +89,15 @@ export const CAImagePreview = memo( if (shift) { const { width, height } = controlImage; - dispatch(widthChanged({ width, ...options })); - dispatch(heightChanged({ height, ...options })); + dispatch(documentWidthChanged({ width, ...options })); + dispatch(documentHeightChanged({ height, ...options })); } else { const { width, height } = calculateNewSize( controlImage.width / controlImage.height, optimalDimension * optimalDimension ); - dispatch(widthChanged({ width, ...options })); - dispatch(heightChanged({ height, ...options })); + dispatch(documentWidthChanged({ width, ...options })); + dispatch(documentHeightChanged({ height, ...options })); } }, [controlImage, dispatch, optimalDimension, shift]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx index 9de19ea3efe..1454e102b08 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx @@ -3,11 +3,11 @@ import { skipToken } from '@reduxjs/toolkit/query'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; -import { heightChanged, widthChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { documentHeightChanged, documentWidthChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import type { ImageWithDims } from 'features/controlLayers/store/types'; import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types'; -import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; +import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; import { memo, useCallback, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi'; @@ -42,15 +42,15 @@ export const IPAImagePreview = memo(({ image, onChangeImage, ipAdapterId, droppa const options = { updateAspectRatio: true, clamp: true }; if (shift) { const { width, height } = controlImage; - dispatch(widthChanged({ width, ...options })); - dispatch(heightChanged({ height, ...options })); + dispatch(documentWidthChanged({ width, ...options })); + dispatch(documentHeightChanged({ height, ...options })); } else { const { width, height } = calculateNewSize( controlImage.width / controlImage.height, optimalDimension * optimalDimension ); - dispatch(widthChanged({ width, ...options })); - dispatch(heightChanged({ height, ...options })); + dispatch(documentWidthChanged({ width, ...options })); + dispatch(documentHeightChanged({ height, ...options })); } }, [controlImage, dispatch, optimalDimension, shift]); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index ff345dfe136..5f07dde1233 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -2,10 +2,10 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { deepClone } from 'common/util/deepClone'; -import { roundDownToMultiple } from 'common/util/roundDownToMultiple'; import { bboxReducers } from 'features/controlLayers/store/bboxReducers'; import { compositingReducers } from 'features/controlLayers/store/compositingReducers'; import { controlAdaptersReducers } from 'features/controlLayers/store/controlAdaptersReducers'; +import { documentReducers } from 'features/controlLayers/store/documentReducers'; import { inpaintMaskReducers } from 'features/controlLayers/store/inpaintMaskReducers'; import { ipAdaptersReducers } from 'features/controlLayers/store/ipAdaptersReducers'; import { layersReducers } from 'features/controlLayers/store/layersReducers'; @@ -15,8 +15,7 @@ import { regionsReducers } from 'features/controlLayers/store/regionsReducers'; import { sessionReducers } from 'features/controlLayers/store/sessionReducers'; import { settingsReducers } from 'features/controlLayers/store/settingsReducers'; import { toolReducers } from 'features/controlLayers/store/toolReducers'; -import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants'; -import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; +import { initialAspectRatioState } from 'features/parameters/components/DocumentSize/constants'; import { atom } from 'nanostores'; import type { InvocationDenoiseProgressEvent } from 'services/events/types'; @@ -145,27 +144,7 @@ export const canvasV2Slice = createSlice({ ...bboxReducers, ...inpaintMaskReducers, ...sessionReducers, - widthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }>) => { - const { width, updateAspectRatio, clamp } = action.payload; - state.document.width = clamp ? Math.max(roundDownToMultiple(width, 8), 64) : width; - if (updateAspectRatio) { - state.document.aspectRatio.value = state.document.width / state.document.height; - state.document.aspectRatio.id = 'Free'; - state.document.aspectRatio.isLocked = false; - } - }, - heightChanged: (state, action: PayloadAction<{ height: number; updateAspectRatio?: boolean; clamp?: boolean }>) => { - const { height, updateAspectRatio, clamp } = action.payload; - state.document.height = clamp ? Math.max(roundDownToMultiple(height, 8), 64) : height; - if (updateAspectRatio) { - state.document.aspectRatio.value = state.document.width / state.document.height; - state.document.aspectRatio.id = 'Free'; - state.document.aspectRatio.isLocked = false; - } - }, - aspectRatioChanged: (state, action: PayloadAction) => { - state.document.aspectRatio = action.payload; - }, + ...documentReducers, entitySelected: (state, action: PayloadAction) => { state.selectedEntityIdentifier = action.payload; }, @@ -192,9 +171,6 @@ export const canvasV2Slice = createSlice({ }); export const { - widthChanged, - heightChanged, - aspectRatioChanged, bboxChanged, brushWidthChanged, eraserWidthChanged, @@ -209,6 +185,13 @@ export const { bboxScaleMethodChanged, clipToBboxChanged, canvasReset, + // document + documentWidthChanged, + documentHeightChanged, + documentAspectRatioLockToggled, + documentAspectRatioIdChanged, + documentDimensionsSwapped, + documentSizeOptimized, // layers layerAdded, layerRecalled, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/documentReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/documentReducers.ts new file mode 100644 index 00000000000..2d14f05cd14 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/documentReducers.ts @@ -0,0 +1,104 @@ +import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import { deepClone } from 'common/util/deepClone'; +import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple'; +import type { CanvasV2State } from 'features/controlLayers/store/types'; +import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; +import { ASPECT_RATIO_MAP, initialAspectRatioState } from 'features/parameters/components/DocumentSize/constants'; +import type { AspectRatioID } from 'features/parameters/components/DocumentSize/types'; +import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; + +export const documentReducers = { + documentWidthChanged: ( + state, + action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }> + ) => { + const { width, updateAspectRatio, clamp } = action.payload; + state.document.width = clamp ? Math.max(roundDownToMultiple(width, 8), 64) : width; + + if (state.document.aspectRatio.isLocked) { + state.document.height = roundToMultiple(state.document.width / state.document.aspectRatio.value, 8); + } + + if (updateAspectRatio || !state.document.aspectRatio.isLocked) { + state.document.aspectRatio.value = state.document.width / state.document.height; + state.document.aspectRatio.id = 'Free'; + state.document.aspectRatio.isLocked = false; + } + + if (!state.session.isActive) { + state.bbox.width = state.document.width; + state.bbox.height = state.document.height; + } + }, + documentHeightChanged: ( + state, + action: PayloadAction<{ height: number; updateAspectRatio?: boolean; clamp?: boolean }> + ) => { + const { height, updateAspectRatio, clamp } = action.payload; + + state.document.height = clamp ? Math.max(roundDownToMultiple(height, 8), 64) : height; + + if (state.document.aspectRatio.isLocked) { + state.document.width = roundToMultiple(state.document.height * state.document.aspectRatio.value, 8); + } + + if (updateAspectRatio || !state.document.aspectRatio.isLocked) { + state.document.aspectRatio.value = state.document.width / state.document.height; + state.document.aspectRatio.id = 'Free'; + state.document.aspectRatio.isLocked = false; + } + + if (!state.session.isActive) { + state.bbox.width = state.document.width; + state.bbox.height = state.document.height; + } + }, + documentAspectRatioLockToggled: (state) => { + state.document.aspectRatio.isLocked = !state.document.aspectRatio.isLocked; + }, + documentAspectRatioIdChanged: (state, action: PayloadAction<{ id: AspectRatioID }>) => { + const { id } = action.payload; + state.document.aspectRatio.id = id; + if (id === 'Free') { + state.document.aspectRatio.isLocked = false; + } else { + state.document.aspectRatio.isLocked = true; + state.document.aspectRatio.value = ASPECT_RATIO_MAP[id].ratio; + const { width, height } = calculateNewSize( + state.document.aspectRatio.value, + state.document.width * state.document.height + ); + state.document.width = width; + state.document.height = height; + } + }, + documentDimensionsSwapped: (state) => { + state.document.aspectRatio.value = 1 / state.document.aspectRatio.value; + if (state.document.aspectRatio.id === 'Free') { + const newWidth = state.document.height; + const newHeight = state.document.width; + state.document.width = newWidth; + state.document.height = newHeight; + } else { + const { width, height } = calculateNewSize( + state.document.aspectRatio.value, + state.document.width * state.document.height + ); + state.document.width = width; + state.document.height = height; + state.document.aspectRatio.id = ASPECT_RATIO_MAP[state.document.aspectRatio.id].inverseID; + } + }, + documentSizeOptimized: (state) => { + const optimalDimension = getOptimalDimension(state.params.model); + if (state.document.aspectRatio.isLocked) { + const { width, height } = calculateNewSize(state.document.aspectRatio.value, optimalDimension ** 2); + state.document.width = width; + state.document.height = height; + } else { + state.document.aspectRatio = deepClone(initialAspectRatioState); + state.document.width = optimalDimension; + state.document.height = optimalDimension; + } + }, +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts index fe7f895651e..ac7abd66156 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts @@ -1,7 +1,7 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import type { CanvasV2State } from 'features/controlLayers/store/types'; import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; -import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; +import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; import { CLIP_SKIP_MAP } from 'features/parameters/types/constants'; import type { ParameterCFGRescaleMultiplier, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index f31ce9e998e..23688e70910 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -4,7 +4,7 @@ import { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import { CanvasRegion } from 'features/controlLayers/konva/CanvasRegion'; import { getImageObjectId } from 'features/controlLayers/konva/naming'; import { zModelIdentifierField } from 'features/nodes/types/common'; -import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; +import type { AspectRatioState } from 'features/parameters/components/DocumentSize/types'; import type { ParameterCanvasCoherenceMode, ParameterCFGRescaleMultiplier, diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index e2f1c21e8a0..de9efe32ce3 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -12,7 +12,8 @@ import { } from 'features/controlLayers/konva/naming'; import { caRecalled, - heightChanged, + documentHeightChanged, + documentWidthChanged, ipaRecalled, layerAllDeleted, layerRecalled, @@ -37,7 +38,6 @@ import { setSeed, setSteps, vaeSelected, - widthChanged, } from 'features/controlLayers/store/canvasV2Slice'; import type { ControlAdapterEntity, @@ -115,11 +115,11 @@ const recallScheduler: MetadataRecallFunc = (scheduler) => { const setSizeOptions = { updateAspectRatio: true, clamp: true }; const recallWidth: MetadataRecallFunc = (width) => { - getStore().dispatch(widthChanged({ width, ...setSizeOptions })); + getStore().dispatch(documentWidthChanged({ width, ...setSizeOptions })); }; const recallHeight: MetadataRecallFunc = (height) => { - getStore().dispatch(heightChanged({ height, ...setSizeOptions })); + getStore().dispatch(documentHeightChanged({ height, ...setSizeOptions })); }; const recallSteps: MetadataRecallFunc = (steps) => { diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx index 63fc4ed632a..68a2c05c169 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx @@ -1,15 +1,16 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; +import { documentHeightChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; -import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; export const ParamHeight = memo(() => { const { t } = useTranslation(); - const ctx = useImageSizeContext(); + const dispatch = useAppDispatch(); const optimalDimension = useAppSelector(selectOptimalDimension); + const height = useAppSelector((s) => s.canvasV2.document.height); const sliderMin = useAppSelector((s) => s.config.sd.height.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.height.sliderMax); const numberInputMin = useAppSelector((s) => s.config.sd.height.numberInputMin); @@ -19,9 +20,9 @@ export const ParamHeight = memo(() => { const onChange = useCallback( (v: number) => { - ctx.heightChanged(v); + dispatch(documentHeightChanged({ height: v })); }, - [ctx] + [dispatch] ); const marks = useMemo(() => [sliderMin, optimalDimension, sliderMax], [sliderMin, optimalDimension, sliderMax]); @@ -32,7 +33,7 @@ export const ParamHeight = memo(() => { {t('parameters.height')} { marks={marks} /> { const { t } = useTranslation(); - const ctx = useImageSizeContext(); + const dispatch = useAppDispatch(); + const width = useAppSelector((s) => s.canvasV2.document.width); const optimalDimension = useAppSelector(selectOptimalDimension); const sliderMin = useAppSelector((s) => s.config.sd.width.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.width.sliderMax); @@ -19,9 +20,9 @@ export const ParamWidth = memo(() => { const onChange = useCallback( (v: number) => { - ctx.widthChanged(v); + dispatch(documentWidthChanged({ width: v })); }, - [ctx] + [dispatch] ); const marks = useMemo(() => [sliderMin, optimalDimension, sliderMax], [sliderMin, optimalDimension, sliderMax]); @@ -32,7 +33,7 @@ export const ParamWidth = memo(() => { {t('parameters.width')} { marks={marks} /> { diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioIconPreview.tsx b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/AspectRatioIconPreview.tsx similarity index 75% rename from invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioIconPreview.tsx rename to invokeai/frontend/web/src/features/parameters/components/DocumentSize/AspectRatioIconPreview.tsx index 3ed7d0d8028..b69cd0666d7 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioIconPreview.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/AspectRatioIconPreview.tsx @@ -1,6 +1,6 @@ import { useSize } from '@chakra-ui/react-use-size'; import { Flex, Icon } from '@invoke-ai/ui-library'; -import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext'; +import { useAppSelector } from 'app/store/storeHooks'; import { AnimatePresence, motion } from 'framer-motion'; import { memo, useMemo, useRef } from 'react'; import { PiFrameCorners } from 'react-icons/pi'; @@ -16,13 +16,13 @@ import { } from './constants'; export const AspectRatioIconPreview = memo(() => { - const ctx = useImageSizeContext(); + const document = useAppSelector((s) => s.canvasV2.document); const containerRef = useRef(null); const containerSize = useSize(containerRef); const shouldShowIcon = useMemo( - () => ctx.aspectRatioState.value < ICON_HIGH_CUTOFF && ctx.aspectRatioState.value > ICON_LOW_CUTOFF, - [ctx.aspectRatioState.value] + () => document.aspectRatio.value < ICON_HIGH_CUTOFF && document.aspectRatio.value > ICON_LOW_CUTOFF, + [document.aspectRatio.value] ); const { width, height } = useMemo(() => { @@ -30,19 +30,19 @@ export const AspectRatioIconPreview = memo(() => { return { width: 0, height: 0 }; } - let width = ctx.width; - let height = ctx.height; + let width = document.width; + let height = document.height; - if (ctx.width > ctx.height) { + if (document.width > document.height) { width = containerSize.width; - height = width / ctx.aspectRatioState.value; + height = width / document.aspectRatio.value; } else { height = containerSize.height; - width = height * ctx.aspectRatioState.value; + width = height * document.aspectRatio.value; } return { width, height }; - }, [containerSize, ctx.width, ctx.height, ctx.aspectRatioState.value]); + }, [containerSize, document.width, document.height, document.aspectRatio.value]); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/AspectRatioSelect.tsx similarity index 70% rename from invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioSelect.tsx rename to invokeai/frontend/web/src/features/parameters/components/DocumentSize/AspectRatioSelect.tsx index 211cdba500f..92256b3b3f2 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/AspectRatioSelect.tsx @@ -1,31 +1,30 @@ import type { ComboboxOption, SystemStyleObject } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import type { SingleValue } from 'chakra-react-select'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { ASPECT_RATIO_OPTIONS } from 'features/parameters/components/ImageSize/constants'; -import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext'; -import { isAspectRatioID } from 'features/parameters/components/ImageSize/types'; +import { documentAspectRatioIdChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { ASPECT_RATIO_OPTIONS } from 'features/parameters/components/DocumentSize/constants'; +import { isAspectRatioID } from 'features/parameters/components/DocumentSize/types'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; export const AspectRatioSelect = memo(() => { const { t } = useTranslation(); - const ctx = useImageSizeContext(); + const dispatch = useAppDispatch(); + const id = useAppSelector((s) => s.canvasV2.document.aspectRatio.id); const onChange = useCallback( (v: SingleValue) => { if (!v || !isAspectRatioID(v.value)) { return; } - ctx.aspectRatioSelected(v.value); + dispatch(documentAspectRatioIdChanged({ id: v.value })); }, - [ctx] + [dispatch] ); - const value = useMemo( - () => ASPECT_RATIO_OPTIONS.filter((o) => o.value === ctx.aspectRatioState.id)[0], - [ctx.aspectRatioState.id] - ); + const value = useMemo(() => ASPECT_RATIO_OPTIONS.filter((o) => o.value === id)[0], [id]); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/DocumentSize/DocumentSize.tsx b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/DocumentSize.tsx new file mode 100644 index 00000000000..97992f093c5 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/DocumentSize.tsx @@ -0,0 +1,38 @@ +import type { FormLabelProps } from '@invoke-ai/ui-library'; +import { Flex, FormControlGroup } from '@invoke-ai/ui-library'; +import { ParamHeight } from 'features/parameters/components/Core/ParamHeight'; +import { ParamWidth } from 'features/parameters/components/Core/ParamWidth'; +import { AspectRatioIconPreview } from 'features/parameters/components/DocumentSize/AspectRatioIconPreview'; +import { AspectRatioSelect } from 'features/parameters/components/DocumentSize/AspectRatioSelect'; +import { LockAspectRatioButton } from 'features/parameters/components/DocumentSize/LockAspectRatioButton'; +import { SetOptimalSizeButton } from 'features/parameters/components/DocumentSize/SetOptimalSizeButton'; +import { SwapDimensionsButton } from 'features/parameters/components/DocumentSize/SwapDimensionsButton'; +import { memo } from 'react'; + +export const DocumentSize = memo(() => { + return ( + + + + + + + + + + + + + + + + + + ); +}); + +DocumentSize.displayName = 'DocumentSize'; + +const formLabelProps: FormLabelProps = { + minW: 14, +}; diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/LockAspectRatioButton.tsx b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/LockAspectRatioButton.tsx similarity index 55% rename from invokeai/frontend/web/src/features/parameters/components/ImageSize/LockAspectRatioButton.tsx rename to invokeai/frontend/web/src/features/parameters/components/DocumentSize/LockAspectRatioButton.tsx index f24a52b12eb..1d239d201f1 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/LockAspectRatioButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/LockAspectRatioButton.tsx @@ -1,24 +1,26 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { documentAspectRatioLockToggled } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiLockSimpleFill, PiLockSimpleOpenBold } from 'react-icons/pi'; export const LockAspectRatioButton = memo(() => { const { t } = useTranslation(); - const ctx = useImageSizeContext(); + const dispatch = useAppDispatch(); + const isLocked = useAppSelector((s) => s.canvasV2.document.aspectRatio.isLocked); const onClick = useCallback(() => { - ctx.isLockedToggled(); - }, [ctx]); + dispatch(documentAspectRatioLockToggled()); + }, [dispatch]); return ( : } + icon={isLocked ? : } /> ); }); diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/SetOptimalSizeButton.tsx b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/SetOptimalSizeButton.tsx similarity index 68% rename from invokeai/frontend/web/src/features/parameters/components/ImageSize/SetOptimalSizeButton.tsx rename to invokeai/frontend/web/src/features/parameters/components/DocumentSize/SetOptimalSizeButton.tsx index 4ebdf583d05..7becf2e36fe 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/SetOptimalSizeButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/SetOptimalSizeButton.tsx @@ -1,7 +1,7 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { documentSizeOptimized } from 'features/controlLayers/store/canvasV2Slice'; import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; -import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext'; import { getIsSizeTooLarge, getIsSizeTooSmall } from 'features/parameters/util/optimalDimension'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,19 +9,21 @@ import { RiSparklingFill } from 'react-icons/ri'; export const SetOptimalSizeButton = memo(() => { const { t } = useTranslation(); - const ctx = useImageSizeContext(); + const dispatch = useAppDispatch(); + const width = useAppSelector((s) => s.canvasV2.document.width); + const height = useAppSelector((s) => s.canvasV2.document.height); const optimalDimension = useAppSelector(selectOptimalDimension); const isSizeTooSmall = useMemo( - () => getIsSizeTooSmall(ctx.width, ctx.height, optimalDimension), - [ctx.height, ctx.width, optimalDimension] + () => getIsSizeTooSmall(width, height, optimalDimension), + [height, width, optimalDimension] ); const isSizeTooLarge = useMemo( - () => getIsSizeTooLarge(ctx.width, ctx.height, optimalDimension), - [ctx.height, ctx.width, optimalDimension] + () => getIsSizeTooLarge(width, height, optimalDimension), + [height, width, optimalDimension] ); const onClick = useCallback(() => { - ctx.setOptimalSize(); - }, [ctx]); + dispatch(documentSizeOptimized()); + }, [dispatch]); const tooltip = useMemo(() => { if (isSizeTooSmall) { return t('parameters.setToOptimalSizeTooSmall'); diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/SwapDimensionsButton.tsx b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/SwapDimensionsButton.tsx similarity index 71% rename from invokeai/frontend/web/src/features/parameters/components/ImageSize/SwapDimensionsButton.tsx rename to invokeai/frontend/web/src/features/parameters/components/DocumentSize/SwapDimensionsButton.tsx index 80cfc6952c5..bc57ca3f51e 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/SwapDimensionsButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/SwapDimensionsButton.tsx @@ -1,15 +1,16 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { documentDimensionsSwapped } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowsDownUpBold } from 'react-icons/pi'; export const SwapDimensionsButton = memo(() => { const { t } = useTranslation(); - const ctx = useImageSizeContext(); + const dispatch = useAppDispatch(); const onClick = useCallback(() => { - ctx.dimensionsSwapped(); - }, [ctx]); + dispatch(documentDimensionsSwapped()); + }, [dispatch]); return ( { - const { widthComponent, heightComponent, previewComponent, ...ctx } = props; - return ( - - - - - - - - - - - {widthComponent} - {heightComponent} - - - - {previewComponent} - - - - ); -}); - -ImageSize.displayName = 'ImageSize'; - -const formLabelProps: FormLabelProps = { - minW: 14, -}; diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/ImageSizeContext.ts b/invokeai/frontend/web/src/features/parameters/components/ImageSize/ImageSizeContext.ts deleted file mode 100644 index fb2a3d9eeb8..00000000000 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/ImageSizeContext.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { useAppSelector } from 'app/store/storeHooks'; -import { roundToMultiple } from 'common/util/roundDownToMultiple'; -import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; -import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; -import { ASPECT_RATIO_MAP, initialAspectRatioState } from 'features/parameters/components/ImageSize/constants'; -import type { AspectRatioID, AspectRatioState } from 'features/parameters/components/ImageSize/types'; -import { createContext, useCallback, useContext, useMemo } from 'react'; - -export type ImageSizeContextInnerValue = { - width: number; - height: number; - aspectRatioState: AspectRatioState; - onChangeWidth: (width: number) => void; - onChangeHeight: (height: number) => void; - onChangeAspectRatioState: (aspectRatioState: AspectRatioState) => void; -}; - -export type ImageSizeContext = { - width: number; - height: number; - aspectRatioState: AspectRatioState; - aspectRatioSelected: (aspectRatioID: AspectRatioID) => void; - dimensionsSwapped: () => void; - widthChanged: (width: number) => void; - heightChanged: (height: number) => void; - isLockedToggled: () => void; - setOptimalSize: () => void; -}; - -export const ImageSizeContext = createContext(null); - -export const useImageSizeContext = (): ImageSizeContext => { - const _ctx = useContext(ImageSizeContext); - const optimalDimension = useAppSelector(selectOptimalDimension); - - if (!_ctx) { - throw new Error('useImageSizeContext must be used within a ImageSizeContext.Provider'); - } - - const aspectRatioSelected = useCallback( - (aspectRatioID: AspectRatioID) => { - const state: AspectRatioState = { - ..._ctx.aspectRatioState, - id: aspectRatioID, - }; - if (state.id === 'Free') { - // If the new aspect ratio is free, we only unlock - state.isLocked = false; - } else { - // The new aspect ratio not free, so we need to coerce the size & lock - state.isLocked = true; - state.value = ASPECT_RATIO_MAP[state.id].ratio; - const { width, height } = calculateNewSize(state.value, _ctx.width * _ctx.height); - _ctx.onChangeWidth(width); - _ctx.onChangeHeight(height); - } - _ctx.onChangeAspectRatioState(state); - }, - [_ctx] - ); - const dimensionsSwapped = useCallback(() => { - const state = { - ..._ctx.aspectRatioState, - }; - // We always invert the aspect ratio - state.value = 1 / state.value; - if (state.id === 'Free') { - // If the aspect ratio is free, we just swap the dimensions - const newWidth = _ctx.height; - const newHeight = _ctx.width; - _ctx.onChangeWidth(newWidth); - _ctx.onChangeHeight(newHeight); - } else { - // Else we need to calculate the new size - const { width, height } = calculateNewSize(state.value, _ctx.width * _ctx.height); - _ctx.onChangeWidth(width); - _ctx.onChangeHeight(height); - // Update the aspect ratio ID to match the new aspect ratio - state.id = ASPECT_RATIO_MAP[state.id].inverseID; - } - _ctx.onChangeAspectRatioState(state); - }, [_ctx]); - - const widthChanged = useCallback( - (width: number) => { - let height = _ctx.height; - const state = { ..._ctx.aspectRatioState }; - if (state.isLocked) { - // When locked, we calculate the new height based on the aspect ratio - height = roundToMultiple(width / state.value, 8); - } else { - // Else we unlock, set the aspect ratio to free, and update the aspect ratio itself - state.isLocked = false; - state.id = 'Free'; - state.value = width / height; - } - _ctx.onChangeWidth(width); - _ctx.onChangeHeight(height); - _ctx.onChangeAspectRatioState(state); - }, - [_ctx] - ); - - const heightChanged = useCallback( - (height: number) => { - let width = _ctx.width; - const state = { ..._ctx.aspectRatioState }; - if (state.isLocked) { - // When locked, we calculate the new width based on the aspect ratio - width = roundToMultiple(height * state.value, 8); - } else { - // Else we unlock, set the aspect ratio to free, and update the aspect ratio itself - state.isLocked = false; - state.id = 'Free'; - state.value = width / height; - } - _ctx.onChangeWidth(width); - _ctx.onChangeHeight(height); - _ctx.onChangeAspectRatioState(state); - }, - [_ctx] - ); - - const isLockedToggled = useCallback(() => { - const state = { ..._ctx.aspectRatioState }; - state.isLocked = !state.isLocked; - _ctx.onChangeAspectRatioState(state); - }, [_ctx]); - - const setOptimalSize = useCallback(() => { - if (_ctx.aspectRatioState.isLocked) { - const { width, height } = calculateNewSize(_ctx.aspectRatioState.value, optimalDimension * optimalDimension); - _ctx.onChangeWidth(width); - _ctx.onChangeHeight(height); - } else { - _ctx.onChangeAspectRatioState({ ...initialAspectRatioState }); - _ctx.onChangeWidth(optimalDimension); - _ctx.onChangeHeight(optimalDimension); - } - }, [_ctx, optimalDimension]); - - const ctx = useMemo( - () => ({ - ..._ctx, - aspectRatioSelected, - dimensionsSwapped, - widthChanged, - heightChanged, - isLockedToggled, - setOptimalSize, - }), - [_ctx, aspectRatioSelected, dimensionsSwapped, heightChanged, isLockedToggled, setOptimalSize, widthChanged] - ); - - return ctx; -}; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx index 1518e0ec2dd..866a20fb2dc 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx @@ -9,6 +9,7 @@ import ParamScaleBeforeProcessing from 'features/parameters/components/Canvas/In import ParamScaledHeight from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight'; import ParamScaledWidth from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth'; import ParamImageToImageStrength from 'features/parameters/components/Canvas/ParamImageToImageStrength'; +import { DocumentSize } from 'features/parameters/components/DocumentSize/DocumentSize'; import { ParamSeedNumberInput } from 'features/parameters/components/Seed/ParamSeedNumberInput'; import { ParamSeedRandomize } from 'features/parameters/components/Seed/ParamSeedRandomize'; import { ParamSeedShuffle } from 'features/parameters/components/Seed/ParamSeedShuffle'; @@ -17,7 +18,6 @@ import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/ import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -import { ImageSizeLinear } from './ImageSizeLinear'; const selector = createMemoizedSelector([selectHrfSlice, selectCanvasV2Slice], (hrf, canvasV2) => { const { shouldRandomizeSeed, model } = canvasV2.params; @@ -68,7 +68,7 @@ export const ImageSettingsAccordion = memo(() => { > - + diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx deleted file mode 100644 index 7c24c3d79b0..00000000000 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { aspectRatioChanged, heightChanged, widthChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { ParamHeight } from 'features/parameters/components/Core/ParamHeight'; -import { ParamWidth } from 'features/parameters/components/Core/ParamWidth'; -import { AspectRatioCanvasPreview } from 'features/parameters/components/ImageSize/AspectRatioCanvasPreview'; -import { ImageSize } from 'features/parameters/components/ImageSize/ImageSize'; -import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; -import { memo, useCallback } from 'react'; - -export const ImageSizeLinear = memo(() => { - const dispatch = useAppDispatch(); - const width = useAppSelector((s) => s.canvasV2.document.width); - const height = useAppSelector((s) => s.canvasV2.document.height); - const aspectRatioState = useAppSelector((s) => s.canvasV2.document.aspectRatio); - - const onChangeWidth = useCallback( - (width: number) => { - if (width === 0) { - return; - } - dispatch(widthChanged({ width })); - }, - [dispatch] - ); - - const onChangeHeight = useCallback( - (height: number) => { - if (height === 0) { - return; - } - dispatch(heightChanged({ height })); - }, - [dispatch] - ); - - const onChangeAspectRatioState = useCallback( - (aspectRatioState: AspectRatioState) => { - dispatch(aspectRatioChanged(aspectRatioState)); - }, - [dispatch] - ); - - return ( - } - widthComponent={} - previewComponent={} - onChangeAspectRatioState={onChangeAspectRatioState} - onChangeWidth={onChangeWidth} - onChangeHeight={onChangeHeight} - /> - ); -}); - -ImageSizeLinear.displayName = 'ImageSizeLinear'; From eaaeb356d754be99cf2cae3e8031315300f2511e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 8 Jul 2024 19:15:51 +1000 Subject: [PATCH 193/678] feat(ui): make documnet size a rect --- .../listeners/modelsLoaded.ts | 2 +- .../components/HeadsUpDisplay.tsx | 14 ++--- .../controlLayers/konva/CanvasBbox.ts | 32 +++++----- .../controlLayers/store/bboxReducers.ts | 6 +- .../controlLayers/store/canvasV2Slice.ts | 8 +-- .../controlLayers/store/documentReducers.ts | 60 +++++++++++-------- .../controlLayers/store/paramsReducers.ts | 6 +- .../src/features/controlLayers/store/types.ts | 18 ++++-- .../util/graph/generation/buildSD1Graph.ts | 4 +- .../util/graph/generation/buildSDXLGraph.ts | 4 +- .../nodes/util/graph/graphBuilderUtils.ts | 2 +- .../components/Core/ParamHeight.tsx | 2 +- .../parameters/components/Core/ParamWidth.tsx | 2 +- .../DocumentSize/AspectRatioIconPreview.tsx | 8 +-- .../DocumentSize/SetOptimalSizeButton.tsx | 4 +- .../ImageSettingsAccordion.tsx | 5 +- 16 files changed, 96 insertions(+), 81 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts index fa1e453ff94..a7b9f82d578 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts @@ -83,7 +83,7 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => { dispatch(modelChanged({ model: defaultModelInList, previousModel: currentModel })); const optimalDimension = getOptimalDimension(defaultModelInList); - if (getIsSizeOptimal(state.canvasV2.document.width, state.canvasV2.document.height, optimalDimension)) { + if (getIsSizeOptimal(state.canvasV2.document.rect.width, state.canvasV2.document.rect.height, optimalDimension)) { return; } const { width, height } = calculateNewSize( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx index 6e49df0ca7d..9de1876f60f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx @@ -25,18 +25,18 @@ export const HeadsUpDisplay = memo(() => { return ( - + - - - - - - + + + + + + { const gridSize = this.manager.stateApi.getCtrlKey() || this.manager.stateApi.getMetaKey() ? 8 : 64; - const oldBbox = this.manager.stateApi.getBbox(); - const newBbox: IRect = { - ...oldBbox, + const bbox = this.manager.stateApi.getBbox(); + const bboxRect: Rect = { + ...bbox.rect, x: roundToMultiple(this.rect.x(), gridSize), y: roundToMultiple(this.rect.y(), gridSize), }; - this.rect.setAttrs(newBbox); - if (oldBbox.x !== newBbox.x || oldBbox.y !== newBbox.y) { - this.manager.stateApi.onBboxTransformed(newBbox); + this.rect.setAttrs(bboxRect); + if (bbox.rect.x !== bboxRect.x || bbox.rect.y !== bboxRect.y) { + this.manager.stateApi.onBboxTransformed(bboxRect); } }); @@ -170,7 +170,7 @@ export class CanvasBbox { height = fittedHeight; } - const bbox = { + const bboxRect = { x: Math.round(x), y: Math.round(y), width, @@ -180,15 +180,15 @@ export class CanvasBbox { // Update the bboxRect's attrs directly with the new transform, and reset its scale to 1. // TODO(psyche): In `renderBboxPreview()` we also call setAttrs, need to do it twice to ensure it renders correctly. // Gotta be a way to avoid setting it twice... - this.rect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1 }); + this.rect.setAttrs({ ...bboxRect, scaleX: 1, scaleY: 1 }); // Update the bbox in internal state. - this.manager.stateApi.onBboxTransformed(bbox); + this.manager.stateApi.onBboxTransformed(bboxRect); // Update the aspect ratio buffer whenever the shift key is not held - this allows for a nice UX where you can start // a transform, get the right aspect ratio, then hold shift to lock it in. if (!shift) { - $aspectRatioBuffer.set(bbox.width / bbox.height); + $aspectRatioBuffer.set(bboxRect.width / bboxRect.height); } }); @@ -210,10 +210,10 @@ export class CanvasBbox { this.group.listening(toolState.selected === 'bbox'); this.rect.setAttrs({ - x: bbox.x, - y: bbox.y, - width: bbox.width, - height: bbox.height, + x: bbox.rect.x, + y: bbox.rect.y, + width: bbox.rect.width, + height: bbox.rect.height, scaleX: 1, scaleY: 1, listening: toolState.selected === 'bbox', diff --git a/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts index ded7d30a490..c01bdf5b1ad 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts @@ -16,17 +16,17 @@ export const bboxReducers = { if (action.payload === 'auto') { const optimalDimension = getOptimalDimension(state.params.model); - const size = pick(state.bbox, 'width', 'height'); + const size = pick(state.bbox.rect, 'width', 'height'); state.bbox.scaledSize = getScaledBoundingBoxDimensions(size, optimalDimension); } }, bboxChanged: (state, action: PayloadAction) => { - state.bbox = { ...state.bbox, ...action.payload }; + state.bbox.rect = action.payload; state.layers.imageCache = null; if (state.bbox.scaleMethod === 'auto') { const optimalDimension = getOptimalDimension(state.params.model); - const size = pick(state.bbox, 'width', 'height'); + const size = pick(state.bbox.rect, 'width', 'height'); state.bbox.scaledSize = getScaledBoundingBoxDimensions(size, optimalDimension); } }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 5f07dde1233..a19556ed3a9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -55,15 +55,11 @@ const initialState: CanvasV2State = { }, }, document: { - width: 512, - height: 512, + rect: { x: 0, y: 0, width: 512, height: 512 }, aspectRatio: deepClone(initialAspectRatioState), }, bbox: { - x: 0, - y: 0, - width: 512, - height: 512, + rect: { x: 0, y: 0, width: 512, height: 512 }, scaleMethod: 'auto', scaledSize: { width: 512, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/documentReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/documentReducers.ts index 2d14f05cd14..16fb34f7c07 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/documentReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/documentReducers.ts @@ -13,21 +13,21 @@ export const documentReducers = { action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }> ) => { const { width, updateAspectRatio, clamp } = action.payload; - state.document.width = clamp ? Math.max(roundDownToMultiple(width, 8), 64) : width; + state.document.rect.width = clamp ? Math.max(roundDownToMultiple(width, 8), 64) : width; if (state.document.aspectRatio.isLocked) { - state.document.height = roundToMultiple(state.document.width / state.document.aspectRatio.value, 8); + state.document.rect.height = roundToMultiple(state.document.rect.width / state.document.aspectRatio.value, 8); } if (updateAspectRatio || !state.document.aspectRatio.isLocked) { - state.document.aspectRatio.value = state.document.width / state.document.height; + state.document.aspectRatio.value = state.document.rect.width / state.document.rect.height; state.document.aspectRatio.id = 'Free'; state.document.aspectRatio.isLocked = false; } if (!state.session.isActive) { - state.bbox.width = state.document.width; - state.bbox.height = state.document.height; + state.bbox.rect.width = state.document.rect.width; + state.bbox.rect.height = state.document.rect.height; } }, documentHeightChanged: ( @@ -36,21 +36,21 @@ export const documentReducers = { ) => { const { height, updateAspectRatio, clamp } = action.payload; - state.document.height = clamp ? Math.max(roundDownToMultiple(height, 8), 64) : height; + state.document.rect.height = clamp ? Math.max(roundDownToMultiple(height, 8), 64) : height; if (state.document.aspectRatio.isLocked) { - state.document.width = roundToMultiple(state.document.height * state.document.aspectRatio.value, 8); + state.document.rect.width = roundToMultiple(state.document.rect.height * state.document.aspectRatio.value, 8); } if (updateAspectRatio || !state.document.aspectRatio.isLocked) { - state.document.aspectRatio.value = state.document.width / state.document.height; + state.document.aspectRatio.value = state.document.rect.width / state.document.rect.height; state.document.aspectRatio.id = 'Free'; state.document.aspectRatio.isLocked = false; } if (!state.session.isActive) { - state.bbox.width = state.document.width; - state.bbox.height = state.document.height; + state.bbox.rect.width = state.document.rect.width; + state.bbox.rect.height = state.document.rect.height; } }, documentAspectRatioLockToggled: (state) => { @@ -66,39 +66,51 @@ export const documentReducers = { state.document.aspectRatio.value = ASPECT_RATIO_MAP[id].ratio; const { width, height } = calculateNewSize( state.document.aspectRatio.value, - state.document.width * state.document.height + state.document.rect.width * state.document.rect.height ); - state.document.width = width; - state.document.height = height; + state.document.rect.width = width; + state.document.rect.height = height; + } + if (!state.session.isActive) { + state.bbox.rect.width = state.document.rect.width; + state.bbox.rect.height = state.document.rect.height; } }, documentDimensionsSwapped: (state) => { state.document.aspectRatio.value = 1 / state.document.aspectRatio.value; if (state.document.aspectRatio.id === 'Free') { - const newWidth = state.document.height; - const newHeight = state.document.width; - state.document.width = newWidth; - state.document.height = newHeight; + const newWidth = state.document.rect.height; + const newHeight = state.document.rect.width; + state.document.rect.width = newWidth; + state.document.rect.height = newHeight; } else { const { width, height } = calculateNewSize( state.document.aspectRatio.value, - state.document.width * state.document.height + state.document.rect.width * state.document.rect.height ); - state.document.width = width; - state.document.height = height; + state.document.rect.width = width; + state.document.rect.height = height; state.document.aspectRatio.id = ASPECT_RATIO_MAP[state.document.aspectRatio.id].inverseID; } + if (!state.session.isActive) { + state.bbox.rect.width = state.document.rect.width; + state.bbox.rect.height = state.document.rect.height; + } }, documentSizeOptimized: (state) => { const optimalDimension = getOptimalDimension(state.params.model); if (state.document.aspectRatio.isLocked) { const { width, height } = calculateNewSize(state.document.aspectRatio.value, optimalDimension ** 2); - state.document.width = width; - state.document.height = height; + state.document.rect.width = width; + state.document.rect.height = height; } else { state.document.aspectRatio = deepClone(initialAspectRatioState); - state.document.width = optimalDimension; - state.document.height = optimalDimension; + state.document.rect.width = optimalDimension; + state.document.rect.height = optimalDimension; + } + if (!state.session.isActive) { + state.bbox.rect.width = state.document.rect.width; + state.bbox.rect.height = state.document.rect.height; } }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts index ac7abd66156..6cf8fc68c3d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts @@ -62,10 +62,10 @@ export const paramsReducers = { // Update the bbox size to match the new model's optimal size // TODO(psyche): Should we change the document size too? const optimalDimension = getOptimalDimension(model); - if (!getIsSizeOptimal(state.document.width, state.document.height, optimalDimension)) { + if (!getIsSizeOptimal(state.document.rect.width, state.document.rect.height, optimalDimension)) { const bboxDims = calculateNewSize(state.document.aspectRatio.value, optimalDimension * optimalDimension); - state.bbox.width = bboxDims.width; - state.bbox.height = bboxDims.height; + state.bbox.rect.width = bboxDims.width; + state.bbox.rect.height = bboxDims.height; if (state.bbox.scaleMethod === 'auto') { state.bbox.scaledSize = getScaledBoundingBoxDimensions(bboxDims, optimalDimension); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 23688e70910..f215e1b194f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -831,8 +831,12 @@ export type CanvasV2State = { fill: RgbaColor; }; document: { - width: ParameterWidth; - height: ParameterHeight; + rect: { + x: number; + y: number; + width: ParameterWidth; + height: ParameterHeight; + }; aspectRatio: AspectRatioState; }; settings: { @@ -845,10 +849,12 @@ export type CanvasV2State = { clipToBbox: boolean; }; bbox: { - x: number; - y: number; - width: ParameterWidth; - height: ParameterHeight; + rect: { + x: number; + y: number; + width: ParameterWidth; + height: ParameterHeight; + }; scaledSize: { width: ParameterWidth; height: ParameterHeight; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index 30cbd48f9e0..ab3fcef4939 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -214,7 +214,7 @@ export const buildSD1Graph = async (state: RootState, manager: CanvasManager): P manager, state.canvasV2.controlAdapters.entities, g, - state.canvasV2.bbox, + state.canvasV2.bbox.rect, denoise, modelConfig.base ); @@ -223,7 +223,7 @@ export const buildSD1Graph = async (state: RootState, manager: CanvasManager): P manager, state.canvasV2.regions.entities, g, - state.canvasV2.bbox, + state.canvasV2.bbox.rect, modelConfig.base, denoise, posCond, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index 2233445a254..c6bb11b9baf 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -218,7 +218,7 @@ export const buildSDXLGraph = async (state: RootState, manager: CanvasManager): manager, state.canvasV2.controlAdapters.entities, g, - state.canvasV2.bbox, + state.canvasV2.bbox.rect, denoise, modelConfig.base ); @@ -227,7 +227,7 @@ export const buildSDXLGraph = async (state: RootState, manager: CanvasManager): manager, state.canvasV2.regions.entities, g, - state.canvasV2.bbox, + state.canvasV2.bbox.rect, modelConfig.base, denoise, posCond, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts index 7097029ca7e..9818357fc0a 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts @@ -75,7 +75,7 @@ export const getIsIntermediate = (state: RootState) => { }; export const getSizes = (bboxState: CanvasV2State['bbox']) => { - const originalSize = pick(bboxState, 'width', 'height'); + const originalSize = pick(bboxState.rect, 'width', 'height'); const scaledSize = ['auto', 'manual'].includes(bboxState.scaleMethod) ? bboxState.scaledSize : originalSize; return { originalSize, scaledSize }; }; diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx index 68a2c05c169..dbf8d9346a9 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx @@ -10,7 +10,7 @@ export const ParamHeight = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const optimalDimension = useAppSelector(selectOptimalDimension); - const height = useAppSelector((s) => s.canvasV2.document.height); + const height = useAppSelector((s) => s.canvasV2.document.rect.height); const sliderMin = useAppSelector((s) => s.config.sd.height.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.height.sliderMax); const numberInputMin = useAppSelector((s) => s.config.sd.height.numberInputMin); diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamWidth.tsx index 2d1b6935b44..ddf4975fc95 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamWidth.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamWidth.tsx @@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next'; export const ParamWidth = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const width = useAppSelector((s) => s.canvasV2.document.width); + const width = useAppSelector((s) => s.canvasV2.document.rect.width); const optimalDimension = useAppSelector(selectOptimalDimension); const sliderMin = useAppSelector((s) => s.config.sd.width.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.width.sliderMax); diff --git a/invokeai/frontend/web/src/features/parameters/components/DocumentSize/AspectRatioIconPreview.tsx b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/AspectRatioIconPreview.tsx index b69cd0666d7..9b8af7e16e7 100644 --- a/invokeai/frontend/web/src/features/parameters/components/DocumentSize/AspectRatioIconPreview.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/AspectRatioIconPreview.tsx @@ -30,10 +30,10 @@ export const AspectRatioIconPreview = memo(() => { return { width: 0, height: 0 }; } - let width = document.width; - let height = document.height; + let width = document.rect.width; + let height = document.rect.height; - if (document.width > document.height) { + if (document.rect.width > document.rect.height) { width = containerSize.width; height = width / document.aspectRatio.value; } else { @@ -42,7 +42,7 @@ export const AspectRatioIconPreview = memo(() => { } return { width, height }; - }, [containerSize, document.width, document.height, document.aspectRatio.value]); + }, [containerSize, document.rect.width, document.rect.height, document.aspectRatio.value]); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/DocumentSize/SetOptimalSizeButton.tsx b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/SetOptimalSizeButton.tsx index 7becf2e36fe..a5e88513f8a 100644 --- a/invokeai/frontend/web/src/features/parameters/components/DocumentSize/SetOptimalSizeButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/SetOptimalSizeButton.tsx @@ -10,8 +10,8 @@ import { RiSparklingFill } from 'react-icons/ri'; export const SetOptimalSizeButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const width = useAppSelector((s) => s.canvasV2.document.width); - const height = useAppSelector((s) => s.canvasV2.document.height); + const width = useAppSelector((s) => s.canvasV2.document.rect.width); + const height = useAppSelector((s) => s.canvasV2.document.rect.height); const optimalDimension = useAppSelector(selectOptimalDimension); const isSizeTooSmall = useMemo( () => getIsSizeTooSmall(width, height, optimalDimension), diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx index 866a20fb2dc..145a0213af7 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx @@ -18,14 +18,15 @@ import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/ import { memo } from 'react'; import { useTranslation } from 'react-i18next'; - const selector = createMemoizedSelector([selectHrfSlice, selectCanvasV2Slice], (hrf, canvasV2) => { const { shouldRandomizeSeed, model } = canvasV2.params; const { hrfEnabled } = hrf; const badges: string[] = []; const isSDXL = model?.base === 'sdxl'; - const { aspectRatio, width, height } = canvasV2.document; + const { aspectRatio } = canvasV2.document; + const { width, height } = canvasV2.document.rect; + badges.push(`${width}×${height}`); badges.push(aspectRatio.id); From fedcebbe4de008858a57614f053f570c77fad72e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 8 Jul 2024 19:16:05 +1000 Subject: [PATCH 194/678] feat(ui): restore document size overlay renderer --- .../konva/CanvasDocumentSizeOverlay.ts | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasDocumentSizeOverlay.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasDocumentSizeOverlay.ts index ce7c8a6827b..00e51ab6cc3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasDocumentSizeOverlay.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasDocumentSizeOverlay.ts @@ -31,29 +31,23 @@ export class CanvasDocumentSizeOverlay { } render() { - return; - // const document = this.manager.stateApi.getDocument(); - // this.group.zIndex(0); + const document = this.manager.stateApi.getDocument(); + this.group.zIndex(0); - // const x = this.manager.stage.x(); - // const y = this.manager.stage.y(); - // const width = this.manager.stage.width(); - // const height = this.manager.stage.height(); - // const scale = this.manager.stage.scaleX(); + const x = this.manager.stage.x(); + const y = this.manager.stage.y(); + const width = this.manager.stage.width(); + const height = this.manager.stage.height(); + const scale = this.manager.stage.scaleX(); - // this.outerRect.setAttrs({ - // offsetX: x / scale, - // offsetY: y / scale, - // width: width / scale, - // height: height / scale, - // }); + this.outerRect.setAttrs({ + offsetX: x / scale, + offsetY: y / scale, + width: width / scale, + height: height / scale, + }); - // this.innerRect.setAttrs({ - // x: 0, - // y: 0, - // width: document.width, - // height: document.height, - // }); + this.innerRect.setAttrs(document.rect); } fitToStage() { @@ -62,8 +56,8 @@ export class CanvasDocumentSizeOverlay { // Fit & center the document on the stage const width = this.manager.stage.width(); const height = this.manager.stage.height(); - const docWidthWithBuffer = document.width + this.padding * 2; - const docHeightWithBuffer = document.height + this.padding * 2; + const docWidthWithBuffer = document.rect.width + this.padding * 2; + const docHeightWithBuffer = document.rect.height + this.padding * 2; const scale = Math.min(Math.min(width / docWidthWithBuffer, height / docHeightWithBuffer), 1); const x = (width - docWidthWithBuffer * scale) / 2 + this.padding * scale; const y = (height - docHeightWithBuffer * scale) / 2 + this.padding * scale; From db67ae2de4394ffc7530b3ca09b54d9e830fc7e5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 8 Jul 2024 21:39:51 +1000 Subject: [PATCH 195/678] feat(ui): log invocation source id on socket event --- .../listeners/socketio/socketGeneratorProgress.ts | 4 ++-- .../listeners/socketio/socketInvocationComplete.ts | 5 ++++- .../listeners/socketio/socketInvocationError.ts | 2 +- .../listeners/socketio/socketInvocationStarted.ts | 4 ++-- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts index cc81dbdf753..e28235da594 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts @@ -13,8 +13,8 @@ export const addGeneratorProgressEventListener = (startAppListening: AppStartLis startAppListening({ actionCreator: socketGeneratorProgress, effect: (action) => { - log.trace(parseify(action.payload), `Generator progress`); - const { invocation_source_id, step, total_steps, progress_image, origin } = action.payload.data; + const { invocation_source_id, invocation, step, total_steps, progress_image, origin } = action.payload.data; + log.trace(parseify(action.payload), `Generator progress (${invocation.type}, ${invocation_source_id})`); if (origin === 'workflows') { const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts index 9b36a2b3f90..1b382c77df6 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts @@ -22,7 +22,10 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi actionCreator: socketInvocationComplete, effect: async (action, { dispatch, getState }) => { const { data } = action.payload; - log.debug({ data: parseify(data) }, `Invocation complete (${data.invocation.type})`); + log.debug( + { data: parseify(data) }, + `Invocation complete (${data.invocation.type}, ${data.invocation_source_id})` + ); const { result, invocation_source_id } = data; // This complete event has an associated image output diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationError.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationError.ts index b34f34a079e..cb3e1d2fa5b 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationError.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationError.ts @@ -13,7 +13,7 @@ export const addInvocationErrorEventListener = (startAppListening: AppStartListe actionCreator: socketInvocationError, effect: (action) => { const { invocation_source_id, invocation, error_type, error_message, error_traceback } = action.payload.data; - log.error(parseify(action.payload), `Invocation error (${invocation.type})`); + log.error(parseify(action.payload), `Invocation error (${invocation.type}, ${invocation_source_id})`); const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]); if (nes) { nes.status = zNodeStatus.enum.FAILED; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationStarted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationStarted.ts index 7dae869ce21..d32a43b8f9f 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationStarted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationStarted.ts @@ -12,8 +12,8 @@ export const addInvocationStartedEventListener = (startAppListening: AppStartLis startAppListening({ actionCreator: socketInvocationStarted, effect: (action) => { - log.debug(parseify(action.payload), `Invocation started (${action.payload.data.invocation.type})`); - const { invocation_source_id } = action.payload.data; + const { invocation_source_id, invocation } = action.payload.data; + log.debug(parseify(action.payload), `Invocation started (${invocation.type}, ${invocation_source_id})`); const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]); if (nes) { nes.status = zNodeStatus.enum.IN_PROGRESS; From 2fd6fd4624c86f5af1141ccf962266482dfa580a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 8 Jul 2024 23:56:21 +1000 Subject: [PATCH 196/678] UNDO ME WIP --- .../listeners/enqueueRequestedLinear.ts | 2 +- .../components/CanvasResizer.tsx | 147 ++++++++++++++++++ .../components/ControlLayersEditor.tsx | 3 + .../components/ControlLayersToolbar.tsx | 2 + .../components/NewSessionButton.tsx | 16 ++ .../controlLayers/konva/CanvasBbox.ts | 8 + .../controlLayers/konva/CanvasManager.ts | 3 +- .../controlLayers/konva/CanvasStagingArea.ts | 2 +- .../controlLayers/konva/CanvasStateApi.ts | 2 +- .../src/features/controlLayers/konva/util.ts | 2 +- .../components/DocumentSize/DocumentSize.tsx | 6 +- 11 files changed, 185 insertions(+), 8 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasResizer.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/NewSessionButton.tsx diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index df29d7cb9cc..74425a4600b 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -18,7 +18,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) const { prepend } = action.payload; let didStartStaging = false; - if (!state.canvasV2.session.isStaging) { + if (!state.canvasV2.session.isStaging && state.canvasV2.session.isActive) { dispatch(sessionStartedStaging()); didStartStaging = true; } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasResizer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasResizer.tsx new file mode 100644 index 00000000000..d5fc3852d2d --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasResizer.tsx @@ -0,0 +1,147 @@ +import { Flex, Grid, GridItem, IconButton } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { memo, useCallback, useState } from 'react'; +import { + PiArrowDownBold, + PiArrowDownLeftBold, + PiArrowDownRightBold, + PiArrowLeftBold, + PiArrowRightBold, + PiArrowUpBold, + PiArrowUpLeftBold, + PiArrowUpRightBold, + PiSquareBold, +} from 'react-icons/pi'; + +type ResizeDirection = + | 'up-left' + | 'up' + | 'up-right' + | 'left' + | 'center-out' + | 'right' + | 'down-left' + | 'down' + | 'down-right'; + +export const CanvasResizer = memo(() => { + const document = useAppSelector((s) => s.canvasV2.document); + const [resizeDirection, setResizeDirection] = useState('center-out'); + + const setDirUpLeft = useCallback(() => { + setResizeDirection('up-left'); + }, []); + + const setDirUp = useCallback(() => { + setResizeDirection('up'); + }, []); + + const setDirUpRight = useCallback(() => { + setResizeDirection('up-right'); + }, []); + + const setDirLeft = useCallback(() => { + setResizeDirection('left'); + }, []); + + const setDirCenterOut = useCallback(() => { + setResizeDirection('center-out'); + }, []); + + const setDirRight = useCallback(() => { + setResizeDirection('right'); + }, []); + + const setDirDownLeft = useCallback(() => { + setResizeDirection('down-left'); + }, []); + + const setDirDown = useCallback(() => { + setResizeDirection('down'); + }, []); + + const setDirDownRight = useCallback(() => { + setResizeDirection('down-right'); + }, []); + + return ( + + + + } + variant={resizeDirection === 'up-left' ? 'solid' : 'ghost'} + /> + + + } + variant={resizeDirection === 'up' ? 'solid' : 'ghost'} + /> + + + } + variant={resizeDirection === 'up-right' ? 'solid' : 'ghost'} + /> + + + } + variant={resizeDirection === 'left' ? 'solid' : 'ghost'} + /> + + + } + variant={resizeDirection === 'center-out' ? 'solid' : 'ghost'} + /> + + + } + variant={resizeDirection === 'right' ? 'solid' : 'ghost'} + /> + + + } + variant={resizeDirection === 'down-left' ? 'solid' : 'ghost'} + /> + + + } + variant={resizeDirection === 'down' ? 'solid' : 'ghost'} + /> + + + } + variant={resizeDirection === 'down-right' ? 'solid' : 'ghost'} + /> + + + + ); +}); + +CanvasResizer.displayName = 'CanvasResizer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx index 1a3b0c20f99..a2a4bf9126e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx @@ -21,6 +21,9 @@ export const ControlLayersEditor = memo(() => { + {/* + + */} ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index 8554c63b2b9..e3cc5cb5fac 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -5,6 +5,7 @@ import { BrushWidth } from 'features/controlLayers/components/BrushWidth'; import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover'; import { EraserWidth } from 'features/controlLayers/components/EraserWidth'; import { FillColorPicker } from 'features/controlLayers/components/FillColorPicker'; +import { NewSessionButton } from 'features/controlLayers/components/NewSessionButton'; import { ResetCanvasButton } from 'features/controlLayers/components/ResetCanvasButton'; import { ToolChooser } from 'features/controlLayers/components/ToolChooser'; import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup'; @@ -32,6 +33,7 @@ export const ControlLayersToolbar = memo(() => { + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/NewSessionButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/NewSessionButton.tsx new file mode 100644 index 00000000000..6befb0a59fe --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/NewSessionButton.tsx @@ -0,0 +1,16 @@ +import { Button } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { sessionStarted } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; + +export const NewSessionButton = memo(() => { + const dispatch = useAppDispatch(); + + const onClick = useCallback(() => { + dispatch(sessionStarted()); + }, [dispatch]); + + return ; +}); + +NewSessionButton.displayName = 'NewSessionButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts index c6f22f3fe04..f5bc028a1bd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts @@ -205,9 +205,17 @@ export class CanvasBbox { } render() { + const session = this.manager.stateApi.getSession(); const bbox = this.manager.stateApi.getBbox(); const toolState = this.manager.stateApi.getToolState(); + if (!session.isActive) { + this.group.listening(false); + this.group.visible(false); + return; + } + + this.group.visible(true); this.group.listening(toolState.selected === 'bbox'); this.rect.setAttrs({ x: bbox.rect.x, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 54681d5132b..804faa45e41 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -265,7 +265,8 @@ export class CanvasManager { if ( this.isFirstRender || state.bbox !== this.prevState.bbox || - state.tool.selected !== this.prevState.tool.selected + state.tool.selected !== this.prevState.tool.selected || + state.session.isActive !== this.prevState.session.isActive ) { log.debug('Rendering generation bbox'); this.preview.bbox.render(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts index b4f1c25a167..8623c097529 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts @@ -20,7 +20,7 @@ export class CanvasStagingArea { } async render() { - const stagingArea = this.manager.stateApi.getStagingAreaState(); + const stagingArea = this.manager.stateApi.getSession(); const bbox = this.manager.stateApi.getBbox(); const shouldShowStagedImage = this.manager.stateApi.getShouldShowStagedImage(); const lastProgressEvent = this.manager.stateApi.getLastProgressEvent(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index bfbc58ae9e1..d485290c159 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -231,7 +231,7 @@ export class CanvasStateApi { getMaskOpacity = () => { return this.getState().settings.maskOpacity; }; - getStagingAreaState = () => { + getSession = () => { return this.getState().session; }; getIsSelected = (id: string) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index c5971d7b990..e729306ca0c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -353,7 +353,7 @@ export function getCompositeLayerStageClone(arg: { manager: CanvasManager }): Ko export function getGenerationMode(arg: { manager: CanvasManager }): GenerationMode { const { manager } = arg; - const { x, y, width, height } = manager.stateApi.getBbox(); + const { x, y, width, height } = manager.stateApi.getBbox().rect; const inpaintMaskLayer = getInpaintMaskLayerClone(arg); const inpaintMaskImageData = konvaNodeToImageData(inpaintMaskLayer, { x, y, width, height }); const inpaintMaskTransparency = getImageDataTransparency(inpaintMaskImageData); diff --git a/invokeai/frontend/web/src/features/parameters/components/DocumentSize/DocumentSize.tsx b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/DocumentSize.tsx index 97992f093c5..3d710bc47f5 100644 --- a/invokeai/frontend/web/src/features/parameters/components/DocumentSize/DocumentSize.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/DocumentSize.tsx @@ -1,8 +1,8 @@ import type { FormLabelProps } from '@invoke-ai/ui-library'; import { Flex, FormControlGroup } from '@invoke-ai/ui-library'; +import { CanvasResizer } from 'features/controlLayers/components/CanvasResizer'; import { ParamHeight } from 'features/parameters/components/Core/ParamHeight'; import { ParamWidth } from 'features/parameters/components/Core/ParamWidth'; -import { AspectRatioIconPreview } from 'features/parameters/components/DocumentSize/AspectRatioIconPreview'; import { AspectRatioSelect } from 'features/parameters/components/DocumentSize/AspectRatioSelect'; import { LockAspectRatioButton } from 'features/parameters/components/DocumentSize/LockAspectRatioButton'; import { SetOptimalSizeButton } from 'features/parameters/components/DocumentSize/SetOptimalSizeButton'; @@ -24,8 +24,8 @@ export const DocumentSize = memo(() => { - - + + ); From 0db5c6ac8e25cb7a710dbe8e1b9a97c8977a94ab Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 11 Jul 2024 15:31:21 +1000 Subject: [PATCH 197/678] feat(ui): rough out img2img on canvas --- .../listeners/imageDropped.ts | 13 +++ .../components/ControlLayersPanelContent.tsx | 2 + .../components/InitialImage/InitialImage.tsx | 25 +++++ .../InitialImage/InitialImageActionsMenu.tsx | 0 .../InitialImage/InitialImageHeader.tsx | 34 ++++++ .../InitialImage/InitialImagePreview.tsx | 100 ++++++++++++++++++ .../InitialImage/InitialImageSettings.tsx | 13 +++ .../controlLayers/konva/CanvasInitialImage.ts | 73 +++++++++++++ .../controlLayers/konva/CanvasInpaintMask.ts | 3 +- .../controlLayers/konva/CanvasManager.ts | 49 +++++++-- .../controlLayers/konva/CanvasStateApi.ts | 3 + .../src/features/controlLayers/konva/util.ts | 47 +++++++- .../controlLayers/store/canvasV2Slice.ts | 15 +++ .../controlLayers/store/documentReducers.ts | 25 +++++ .../store/initialImageReducers.ts | 38 +++++++ .../src/features/controlLayers/store/types.ts | 19 +++- .../web/src/features/dnd/types/index.ts | 7 +- .../web/src/features/dnd/util/isValidDrop.ts | 2 + .../util/graph/generation/addImageToImage.ts | 4 +- .../nodes/util/graph/generation/addInpaint.ts | 4 +- .../util/graph/generation/addOutpaint.ts | 2 +- .../util/graph/generation/buildSD1Graph.ts | 3 + .../util/graph/generation/buildSDXLGraph.ts | 3 + 23 files changed, 468 insertions(+), 16 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImage.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageActionsMenu.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageHeader.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImagePreview.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageSettings.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/initialImageReducers.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index fb4ffbca7cb..cb93b9a2551 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -4,6 +4,7 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware' import { parseify } from 'common/util/serialize'; import { caImageChanged, + iiImageChanged, ipaImageChanged, layerImageAdded, rgIPAdapterImageChanged, @@ -110,6 +111,18 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => return; } + /** + * Image dropped on Raster layer + */ + if ( + overData.actionType === 'SET_INITIAL_IMAGE' && + activeData.payloadType === 'IMAGE_DTO' && + activeData.payload.imageDTO + ) { + dispatch(iiImageChanged({ imageDTO: activeData.payload.imageDTO })); + return; + } + /** * Image dropped on node image field */ diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index 4c8572a5f9a..94e706b68a0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -4,6 +4,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { AddLayerButton } from 'features/controlLayers/components/AddLayerButton'; import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList'; import { DeleteAllLayersButton } from 'features/controlLayers/components/DeleteAllLayersButton'; +import { InitialImage } from 'features/controlLayers/components/InitialImage/InitialImage'; import { IM } from 'features/controlLayers/components/InpaintMask/IM'; import { memo } from 'react'; @@ -17,6 +18,7 @@ export const ControlLayersPanelContent = memo(() => { {isCanvasSessionActive && } + {!isCanvasSessionActive && } ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImage.tsx new file mode 100644 index 00000000000..00fdc673c0f --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImage.tsx @@ -0,0 +1,25 @@ +import { useDisclosure } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; +import { InitialImageHeader } from 'features/controlLayers/components/InitialImage/InitialImageHeader'; +import { InitialImageSettings } from 'features/controlLayers/components/InitialImage/InitialImageSettings'; +import { entitySelected } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; + +export const InitialImage = memo(() => { + const dispatch = useAppDispatch(); + const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === 'initial_image'); + const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); + const onSelect = useCallback(() => { + dispatch(entitySelected({ id: 'initial_image', type: 'initial_image' })); + }, [dispatch]); + + return ( + + + {isOpen && } + + ); +}); + +InitialImage.displayName = 'InitialImage'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageActionsMenu.tsx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageHeader.tsx new file mode 100644 index 00000000000..8af3e98f408 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageHeader.tsx @@ -0,0 +1,34 @@ +import { Spacer } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; +import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; +import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; +import { iiIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +type Props = { + onToggleVisibility: () => void; +}; + +export const InitialImageHeader = memo(({ onToggleVisibility }: Props) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isEnabled = useAppSelector((s) => s.canvasV2.initialImage.isEnabled); + const onToggleIsEnabled = useCallback(() => { + dispatch(iiIsEnabledToggled()); + }, [dispatch]); + const title = useMemo(() => { + return `${t('controlLayers.initialImage')}`; + }, [t]); + + return ( + + + + + + ); +}); + +InitialImageHeader.displayName = 'InitialImageHeader'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImagePreview.tsx new file mode 100644 index 00000000000..248c09a6605 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImagePreview.tsx @@ -0,0 +1,100 @@ +import { Flex, useShiftModifier } from '@invoke-ai/ui-library'; +import { skipToken } from '@reduxjs/toolkit/query'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import IAIDndImage from 'common/components/IAIDndImage'; +import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; +import { documentHeightChanged, documentWidthChanged, iiReset } from 'features/controlLayers/store/canvasV2Slice'; +import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; +import type { ImageDraggableData, InitialImageDropData } from 'features/dnd/types'; +import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; +import { memo, useCallback, useEffect, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi'; +import { useGetImageDTOQuery } from 'services/api/endpoints/images'; + +export const InitialImagePreview = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const initialImage = useAppSelector((s) => s.canvasV2.initialImage); + const isConnected = useAppSelector((s) => s.system.isConnected); + const optimalDimension = useAppSelector(selectOptimalDimension); + const shift = useShiftModifier(); + + const { currentData: imageDTO, isError: isErrorControlImage } = useGetImageDTOQuery( + initialImage.imageObject?.image.name ?? skipToken + ); + + const onReset = useCallback(() => { + dispatch(iiReset()); + }, [dispatch]); + + const onUseSize = useCallback(() => { + if (!imageDTO) { + return; + } + + const options = { updateAspectRatio: true, clamp: true }; + if (shift) { + const { width, height } = imageDTO; + dispatch(documentWidthChanged({ width, ...options })); + dispatch(documentHeightChanged({ height, ...options })); + } else { + const { width, height } = calculateNewSize(imageDTO.width / imageDTO.height, optimalDimension * optimalDimension); + dispatch(documentWidthChanged({ width, ...options })); + dispatch(documentHeightChanged({ height, ...options })); + } + }, [imageDTO, dispatch, optimalDimension, shift]); + + const draggableData = useMemo(() => { + if (imageDTO) { + return { + id: 'initial_image', + payloadType: 'IMAGE_DTO', + payload: { imageDTO }, + }; + } + }, [imageDTO]); + + const droppableData = useMemo( + () => ({ id: 'initial_image', actionType: 'SET_INITIAL_IMAGE' }), + [] + ); + + useEffect(() => { + if (isConnected && isErrorControlImage) { + onReset(); + } + }, [onReset, isConnected, isErrorControlImage]); + + return ( + + + + + {imageDTO && ( + + } + tooltip={t('controlnet.resetControlImage')} + /> + } + tooltip={ + shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions') + } + /> + + )} + + + ); +}); + +InitialImagePreview.displayName = 'InitialImagePreview'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageSettings.tsx new file mode 100644 index 00000000000..9c9da2f5366 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageSettings.tsx @@ -0,0 +1,13 @@ +import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; +import { InitialImagePreview } from 'features/controlLayers/components/InitialImage/InitialImagePreview'; +import { memo } from 'react'; + +export const InitialImageSettings = memo(() => { + return ( + + + + ); +}); + +InitialImageSettings.displayName = 'InitialImageSettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts new file mode 100644 index 00000000000..e09fc428550 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts @@ -0,0 +1,73 @@ +import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { getObjectGroupId } from 'features/controlLayers/konva/naming'; +import type { InitialImageEntity } from 'features/controlLayers/store/types'; +import Konva from 'konva'; +import { v4 as uuidv4 } from 'uuid'; + +export class CanvasInitialImage { + id = 'initial_image'; + manager: CanvasManager; + layer: Konva.Layer; + group: Konva.Group; + objectsGroup: Konva.Group; + image: CanvasImage | null; + private initialImageState: InitialImageEntity; + + constructor(initialImageState: InitialImageEntity, manager: CanvasManager) { + this.manager = manager; + this.layer = new Konva.Layer({ + id: this.id, + imageSmoothingEnabled: true, + listening: false, + }); + this.group = new Konva.Group({ + id: getObjectGroupId(this.layer.id(), uuidv4()), + listening: false, + }); + this.objectsGroup = new Konva.Group({ listening: false }); + this.group.add(this.objectsGroup); + this.layer.add(this.group); + + this.image = null; + this.initialImageState = initialImageState; + } + + async render(initialImageState: InitialImageEntity) { + this.initialImageState = initialImageState; + + if (!this.initialImageState.imageObject) { + this.layer.visible(false); + return; + } + + const imageObject = this.initialImageState.imageObject; + + if (!imageObject) { + if (this.image) { + this.image.konvaImageGroup.visible(false); + } + } else if (!this.image) { + this.image = await new CanvasImage(imageObject, { + onLoad: () => { + this.updateGroup(); + }, + }); + this.objectsGroup.add(this.image.konvaImageGroup); + await this.image.updateImageSource(imageObject.image.name); + } else if (!this.image.isLoading && !this.image.isError) { + await this.image.update(imageObject); + } + + this.updateGroup(); + } + + updateGroup() { + const visible = this.initialImageState ? this.initialImageState.isEnabled : false; + this.layer.visible(visible); + } + + destroy(): void { + this.layer.destroy(); + } +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts index 4dcd7be36be..b30dae3f5d2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts @@ -13,7 +13,7 @@ import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; export class CanvasInpaintMask { - id: string; + id = 'inpaint_mask'; manager: CanvasManager; layer: Konva.Layer; group: Konva.Group; @@ -25,7 +25,6 @@ export class CanvasInpaintMask { private inpaintMaskState: InpaintMaskEntity; constructor(entity: InpaintMaskEntity, manager: CanvasManager) { - this.id = 'inpaint_mask'; this.manager = manager; this.layer = new Konva.Layer({ id: this.id }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 804faa45e41..17b5394602a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -1,15 +1,17 @@ import type { Store } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; +import { CanvasInitialImage } from 'features/controlLayers/konva/CanvasInitialImage'; import { + getCompositeLayerImage, getControlAdapterImage, getGenerationMode, - getImageSourceImage, + getInitialImage, getInpaintMaskImage, getRegionMaskImage, } from 'features/controlLayers/konva/util'; import { $lastProgressEvent, $shouldShowStagedImage } from 'features/controlLayers/store/canvasV2Slice'; -import type { CanvasV2State } from 'features/controlLayers/store/types'; +import type { CanvasV2State, GenerationMode } from 'features/controlLayers/store/types'; import type Konva from 'konva'; import { atom } from 'nanostores'; import { getImageDTO as defaultGetImageDTO, uploadImage as defaultUploadImage } from 'services/api/endpoints/images'; @@ -58,6 +60,7 @@ export class CanvasManager { layers: Map; regions: Map; inpaintMask: CanvasInpaintMask; + initialImage: CanvasInitialImage; util: Util; stateApi: CanvasStateApi; preview: CanvasPreview; @@ -102,6 +105,13 @@ export class CanvasManager { this.layers = new Map(); this.regions = new Map(); this.controlAdapters = new Map(); + + this.initialImage = new CanvasInitialImage(this.stateApi.getInitialImageState(), this); + this.stage.add(this.initialImage.layer); + } + + async renderInitialImage() { + this.initialImage.render(this.stateApi.getInitialImageState()); } async renderLayers() { @@ -180,6 +190,7 @@ export class CanvasManager { const regions = getRegionsState().entities; let zIndex = 0; this.background.layer.zIndex(++zIndex); + this.initialImage.layer.zIndex(++zIndex); for (const layer of layers) { this.layers.get(layer.id)?.layer.zIndex(++zIndex); } @@ -225,6 +236,17 @@ export class CanvasManager { this.renderLayers(); } + if ( + this.isFirstRender || + state.initialImage !== this.prevState.initialImage || + state.document !== this.prevState.document || + state.tool.selected !== this.prevState.tool.selected || + state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id + ) { + log.debug('Rendering intial image'); + this.renderInitialImage(); + } + if ( this.isFirstRender || state.regions.entities !== this.prevState.regions.entities || @@ -367,8 +389,19 @@ export class CanvasManager { } }; - getGenerationMode() { - return getGenerationMode({ manager: this }); + getGenerationMode(): GenerationMode { + const session = this.stateApi.getSession(); + if (session.isActive) { + return getGenerationMode({ manager: this }); + } + + const initialImageState = this.stateApi.getInitialImageState(); + + if (initialImageState.imageObject && initialImageState.isEnabled) { + return 'img2img'; + } + + return 'txt2img'; } getControlAdapterImage(arg: Omit[0], 'manager'>) { @@ -383,7 +416,11 @@ export class CanvasManager { return getInpaintMaskImage({ ...arg, manager: this }); } - getImageSourceImage(arg: Omit[0], 'manager'>) { - return getImageSourceImage({ ...arg, manager: this }); + getInitialImage(arg: Omit[0], 'manager'>) { + if (this.stateApi.getSession().isActive) { + return getCompositeLayerImage({ ...arg, manager: this }); + } else { + return getInitialImage({ ...arg, manager: this }); + } } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index d485290c159..cac8dcce272 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -228,6 +228,9 @@ export class CanvasStateApi { getInpaintMaskState = () => { return this.getState().inpaintMask; }; + getInitialImageState = () => { + return this.getState().initialImage; + }; getMaskOpacity = () => { return this.getState().settings.maskOpacity; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index e729306ca0c..103ef2811d3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -317,6 +317,23 @@ export function getControlAdapterLayerClone(arg: { manager: CanvasManager; id: s return controlAdapterClone; } +export function getInitialImageLayerClone(arg: { manager: CanvasManager }): Konva.Layer { + const { manager } = arg; + + const initialImage = manager.initialImage; + + const initialImageClone = initialImage.layer.clone(); + const objectGroupClone = initialImage.group.clone(); + + initialImageClone.destroyChildren(); + initialImageClone.add(objectGroupClone); + + objectGroupClone.opacity(1); + objectGroupClone.cache(); + + return initialImageClone; +} + export function getCompositeLayerStageClone(arg: { manager: CanvasManager }): Konva.Stage { const { manager } = arg; @@ -435,6 +452,34 @@ export async function getControlAdapterImage(arg: { return imageDTO; } +export async function getInitialImage(arg: { + manager: CanvasManager; + bbox?: Rect; + preview?: boolean; +}): Promise { + const { manager, bbox, preview = false } = arg; + + // if (region.imageCache) { + // const imageDTO = await this.util.getImageDTO(region.imageCache.name); + // if (imageDTO) { + // return imageDTO; + // } + // } + + const layerClone = getInitialImageLayerClone({ manager }); + const blob = await konvaNodeToBlob(layerClone, bbox); + + if (preview) { + previewBlob(blob, 'initial image'); + } + + layerClone.destroy(); + + const imageDTO = await manager.util.uploadImage(blob, 'initial_image.png', 'other', true); + // manager.stateApi.onRegionMaskImageCached(ca.id, imageDTO); + return imageDTO; +} + export async function getInpaintMaskImage(arg: { manager: CanvasManager; bbox?: Rect; @@ -464,7 +509,7 @@ export async function getInpaintMaskImage(arg: { return imageDTO; } -export async function getImageSourceImage(arg: { +export async function getCompositeLayerImage(arg: { manager: CanvasManager; bbox?: Rect; preview?: boolean; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index a19556ed3a9..28781563fd5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -6,6 +6,7 @@ import { bboxReducers } from 'features/controlLayers/store/bboxReducers'; import { compositingReducers } from 'features/controlLayers/store/compositingReducers'; import { controlAdaptersReducers } from 'features/controlLayers/store/controlAdaptersReducers'; import { documentReducers } from 'features/controlLayers/store/documentReducers'; +import { initialImageReducers } from 'features/controlLayers/store/initialImageReducers'; import { inpaintMaskReducers } from 'features/controlLayers/store/inpaintMaskReducers'; import { ipAdaptersReducers } from 'features/controlLayers/store/ipAdaptersReducers'; import { layersReducers } from 'features/controlLayers/store/layersReducers'; @@ -30,6 +31,14 @@ const initialState: CanvasV2State = { ipAdapters: { entities: [] }, regions: { entities: [] }, loras: [], + initialImage: { + id: 'initial_image', + type: 'initial_image', + bbox: null, + bboxNeedsUpdate: false, + isEnabled: true, + imageObject: null, + }, inpaintMask: { id: 'inpaint_mask', type: 'inpaint_mask', @@ -141,6 +150,7 @@ export const canvasV2Slice = createSlice({ ...inpaintMaskReducers, ...sessionReducers, ...documentReducers, + ...initialImageReducers, entitySelected: (state, action: PayloadAction) => { state.selectedEntityIdentifier = action.payload; }, @@ -338,6 +348,11 @@ export const { sessionStagingCanceled, sessionNextStagedImageSelected, sessionPrevStagedImageSelected, + // Initial image + iiRecalled, + iiIsEnabledToggled, + iiReset, + iiImageChanged, } = canvasV2Slice.actions; export const selectCanvasV2Slice = (state: RootState) => state.canvasV2; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/documentReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/documentReducers.ts index 16fb34f7c07..a4649ca5414 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/documentReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/documentReducers.ts @@ -28,6 +28,11 @@ export const documentReducers = { if (!state.session.isActive) { state.bbox.rect.width = state.document.rect.width; state.bbox.rect.height = state.document.rect.height; + + if (state.initialImage.imageObject) { + state.initialImage.imageObject.width = state.document.rect.width; + state.initialImage.imageObject.height = state.document.rect.height; + } } }, documentHeightChanged: ( @@ -51,6 +56,11 @@ export const documentReducers = { if (!state.session.isActive) { state.bbox.rect.width = state.document.rect.width; state.bbox.rect.height = state.document.rect.height; + + if (state.initialImage.imageObject) { + state.initialImage.imageObject.width = state.document.rect.width; + state.initialImage.imageObject.height = state.document.rect.height; + } } }, documentAspectRatioLockToggled: (state) => { @@ -74,6 +84,11 @@ export const documentReducers = { if (!state.session.isActive) { state.bbox.rect.width = state.document.rect.width; state.bbox.rect.height = state.document.rect.height; + + if (state.initialImage.imageObject) { + state.initialImage.imageObject.width = state.document.rect.width; + state.initialImage.imageObject.height = state.document.rect.height; + } } }, documentDimensionsSwapped: (state) => { @@ -95,6 +110,11 @@ export const documentReducers = { if (!state.session.isActive) { state.bbox.rect.width = state.document.rect.width; state.bbox.rect.height = state.document.rect.height; + + if (state.initialImage.imageObject) { + state.initialImage.imageObject.width = state.document.rect.width; + state.initialImage.imageObject.height = state.document.rect.height; + } } }, documentSizeOptimized: (state) => { @@ -111,6 +131,11 @@ export const documentReducers = { if (!state.session.isActive) { state.bbox.rect.width = state.document.rect.width; state.bbox.rect.height = state.document.rect.height; + + if (state.initialImage.imageObject) { + state.initialImage.imageObject.width = state.document.rect.width; + state.initialImage.imageObject.height = state.document.rect.height; + } } }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/initialImageReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/initialImageReducers.ts new file mode 100644 index 00000000000..b30af45ab5b --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/initialImageReducers.ts @@ -0,0 +1,38 @@ +import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import { isEqual } from 'lodash-es'; +import type { ImageDTO } from 'services/api/types'; + +import type { CanvasV2State, InitialImageEntity } from './types'; +import { imageDTOToImageObject } from './types'; + +export const initialImageReducers = { + iiRecalled: (state, action: PayloadAction<{ data: InitialImageEntity }>) => { + const { data } = action.payload; + state.initialImage = data; + state.selectedEntityIdentifier = { type: 'initial_image', id: 'initial_image' }; + }, + iiIsEnabledToggled: (state) => { + if (!state.initialImage) { + return; + } + state.initialImage.isEnabled = !state.initialImage.isEnabled; + }, + iiReset: (state) => { + state.initialImage.imageObject = null; + }, + iiImageChanged: (state, action: PayloadAction<{ imageDTO: ImageDTO }>) => { + const { imageDTO } = action.payload; + if (!state.initialImage) { + return; + } + const newImageObject = imageDTOToImageObject('initial_image', 'initial_image_object', imageDTO); + if (isEqual(newImageObject, state.initialImage.imageObject)) { + return; + } + state.initialImage.bbox = null; + state.initialImage.bboxNeedsUpdate = true; + state.initialImage.isEnabled = true; + state.initialImage.imageObject = newImageObject; + state.selectedEntityIdentifier = { type: 'initial_image', id: 'initial_image' }; + }, +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index f215e1b194f..6a1ea536f79 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -668,6 +668,16 @@ const zInpaintMaskEntity = z.object({ }); export type InpaintMaskEntity = z.infer; +const zInitialImageEntity = z.object({ + id: z.literal('initial_image'), + type: z.literal('initial_image'), + isEnabled: z.boolean(), + bbox: zRect.nullable(), + bboxNeedsUpdate: z.boolean(), + imageObject: zImageObject.nullable(), +}); +export type InitialImageEntity = z.infer; + const zControlAdapterEntityBase = z.object({ id: zId, type: z.literal('control_adapter'), @@ -790,7 +800,13 @@ export type BoundingBoxScaleMethod = z.infer; export const isBoundingBoxScaleMethod = (v: unknown): v is BoundingBoxScaleMethod => zBoundingBoxScaleMethod.safeParse(v).success; -export type CanvasEntity = LayerEntity | ControlAdapterEntity | RegionEntity | InpaintMaskEntity | IPAdapterEntity; +export type CanvasEntity = + | LayerEntity + | ControlAdapterEntity + | RegionEntity + | InpaintMaskEntity + | IPAdapterEntity + | InitialImageEntity; export type CanvasEntityIdentifier = Pick; export type Size = { @@ -822,6 +838,7 @@ export type CanvasV2State = { ipAdapters: { entities: IPAdapterEntity[] }; regions: { entities: RegionEntity[] }; loras: LoRA[]; + initialImage: InitialImageEntity; tool: { selected: Tool; selectedBuffer: Tool | null; diff --git a/invokeai/frontend/web/src/features/dnd/types/index.ts b/invokeai/frontend/web/src/features/dnd/types/index.ts index 692454ea50a..b041546ec8a 100644 --- a/invokeai/frontend/web/src/features/dnd/types/index.ts +++ b/invokeai/frontend/web/src/features/dnd/types/index.ts @@ -66,6 +66,10 @@ type UpscaleInitialImageDropData = BaseDropData & { actionType: 'SET_UPSCALE_INITIAL_IMAGE'; }; +export type InitialImageDropData = BaseDropData & { + actionType: 'SET_INITIAL_IMAGE'; +}; + type NodesImageDropData = BaseDropData & { actionType: 'SET_NODES_IMAGE'; context: { @@ -101,7 +105,8 @@ export type TypesafeDroppableData = | RGIPAdapterImageDropData | SelectForCompareDropData | RasterLayerImageDropData - | UpscaleInitialImageDropData; + | UpscaleInitialImageDropData + | LayerImageDropData; type BaseDragData = { id: string; diff --git a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts index 128e5c5d501..80ea701727b 100644 --- a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts +++ b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts @@ -29,6 +29,8 @@ export const isValidDrop = (overData?: TypesafeDroppableData | null, activeData? return payloadType === 'IMAGE_DTO'; case 'SELECT_FOR_COMPARE': return payloadType === 'IMAGE_DTO'; + case 'SET_INITIAL_IMAGE': + return payloadType === 'IMAGE_DTO'; case 'ADD_TO_BOARD': { // If the board is the same, don't allow the drop diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts index 953e3505ef5..2bac462f12b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts @@ -17,8 +17,8 @@ export const addImageToImage = async ( ): Promise> => { denoise.denoising_start = denoising_start; - const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']); - const initialImage = await manager.getImageSourceImage({ bbox: cropBbox }); + const cropBbox = pick(bbox.rect, ['x', 'y', 'width', 'height']); + const initialImage = await manager.getInitialImage({ bbox: cropBbox }); if (!isEqual(scaledSize, originalSize)) { // Resize the initial image to the scaled size, denoise, then resize back to the original size diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts index 0b4520385f2..fcdd49b3ffd 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts @@ -21,8 +21,8 @@ export const addInpaint = async ( ): Promise> => { denoise.denoising_start = denoising_start; - const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']); - const initialImage = await manager.getImageSourceImage({ bbox: cropBbox }); + const cropBbox = pick(bbox.rect, ['x', 'y', 'width', 'height']); + const initialImage = await manager.getInitialImage({ bbox: cropBbox }); const maskImage = await manager.getInpaintMaskImage({ bbox: cropBbox }); if (!isEqual(scaledSize, originalSize)) { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index e5c774d9533..1dde41fb0cb 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts @@ -23,7 +23,7 @@ export const addOutpaint = async ( denoise.denoising_start = denoising_start; const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']); - const initialImage = await manager.getImageSourceImage({ bbox: cropBbox }); + const initialImage = await manager.getInitialImage({ bbox: cropBbox }); const maskImage = await manager.getInpaintMaskImage({ bbox: cropBbox }); const infill = getInfill(g, compositing); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index ab3fcef4939..e2b7a33cf5f 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -1,3 +1,4 @@ +import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; @@ -33,9 +34,11 @@ import { isNonRefinerMainModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; import { addRegions } from './addRegions'; +const log = logger('system'); export const buildSD1Graph = async (state: RootState, manager: CanvasManager): Promise => { const generationMode = manager.getGenerationMode(); + log.debug({ generationMode }, 'Building SD1/SD2 graph'); const { bbox, params } = state.canvasV2; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index c6bb11b9baf..9b4490660d8 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -1,3 +1,4 @@ +import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; @@ -32,9 +33,11 @@ import { isNonRefinerMainModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; import { addRegions } from './addRegions'; +const log = logger('system'); export const buildSDXLGraph = async (state: RootState, manager: CanvasManager): Promise => { const generationMode = manager.getGenerationMode(); + log.debug({ generationMode }, 'Building SDXL graph'); const { bbox, params } = state.canvasV2; From 061eeb809f8d5ed9015531475367df33e5bbabf5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 11 Jul 2024 20:37:00 +1000 Subject: [PATCH 198/678] feat(ui): img2img working --- .../addCommitStagingAreaImageListener.ts | 4 +- .../socketio/socketInvocationComplete.ts | 8 ++- .../src/common/hooks/useIsReadyToEnqueue.ts | 3 +- .../controlLayers/konva/CanvasImage.ts | 23 +++---- .../controlLayers/konva/CanvasInitialImage.ts | 29 +++------ .../controlLayers/konva/CanvasLayer.ts | 7 +-- .../controlLayers/konva/CanvasManager.ts | 61 +++++++++++-------- .../controlLayers/konva/CanvasPreview.ts | 8 ++- .../konva/CanvasProgressImage.ts | 18 ++++-- .../konva/CanvasProgressPreview.ts | 38 ++++++++++++ .../controlLayers/konva/CanvasStagingArea.ts | 57 +++-------------- .../src/features/controlLayers/konva/util.ts | 9 +++ .../util/graph/generation/addOutpaint.ts | 2 +- 13 files changed, 140 insertions(+), 127 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressPreview.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts index 3dbc159e760..a15a1efb140 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts @@ -4,8 +4,8 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware' import { layerAdded, layerImageAdded, - sessionStagingCanceled, sessionStagedImageAccepted, + sessionStagingCanceled, } from 'features/controlLayers/store/canvasV2Slice'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; @@ -67,7 +67,7 @@ export const addStagingListeners = (startAppListening: AppStartListening) => { const { id } = layer; - api.dispatch(layerImageAdded({ id, imageDTO, pos: { x: bbox.x - layer.x, y: bbox.y - layer.y } })); + api.dispatch(layerImageAdded({ id, imageDTO, pos: { x: bbox.rect.x - layer.x, y: bbox.rect.y - layer.y } })); }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts index 1b382c77df6..0ba7fa1668f 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts @@ -2,7 +2,7 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { deepClone } from 'common/util/deepClone'; import { parseify } from 'common/util/serialize'; -import { sessionImageStaged } from 'features/controlLayers/store/canvasV2Slice'; +import { $lastProgressEvent, sessionImageStaged } from 'features/controlLayers/store/canvasV2Slice'; import { boardIdSelected, galleryViewChanged, imageSelected, offsetChanged } from 'features/gallery/store/gallerySlice'; import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; import { zNodeStatus } from 'features/nodes/types/invocation'; @@ -44,9 +44,11 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi imageDTORequest.unsubscribe(); // handle tab-specific logic - if (data.origin === 'canvas') { - if (data.invocation_source_id === CANVAS_OUTPUT && canvasV2.session.isStaging) { + if (data.origin === 'canvas' && data.invocation_source_id === CANVAS_OUTPUT) { + if (canvasV2.session.isStaging) { dispatch(sessionImageStaged({ imageDTO })); + } else if (!canvasV2.session.isActive) { + $lastProgressEvent.set(null); } } else if (data.origin === 'workflows') { const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]); diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index eb9fc760145..289132c717c 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -24,6 +24,7 @@ const LAYER_TYPE_TO_TKEY: Record = { regional_guidance: 'controlLayers.regionalGuidance', layer: 'controlLayers.raster', inpaint_mask: 'controlLayers.inpaintMask', + initial_image: 'controlLayers.initialImage', }; const createSelector = (templates: Templates) => @@ -149,7 +150,7 @@ const createSelector = (templates: Templates) => // T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL) if (ca.adapterType === 't2i_adapter') { const multiple = model?.base === 'sdxl' ? 32 : 64; - if (bbox.width % multiple !== 0 || bbox.height % multiple !== 0) { + if (bbox.rect.width % multiple !== 0 || bbox.rect.height % multiple !== 0) { problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions', { multiple })); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts index d102869c3a2..9bc9f0778ae 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -1,9 +1,11 @@ import { FILTER_MAP } from 'features/controlLayers/konva/filters'; +import { loadImage } from 'features/controlLayers/konva/util'; import type { ImageObject } from 'features/controlLayers/store/types'; import { t } from 'i18next'; import Konva from 'konva'; import { getImageDTO as defaultGetImageDTO } from 'services/api/endpoints/images'; import type { ImageDTO } from 'services/api/types'; +import { assert } from 'tsafe'; export class CanvasImage { id: string; @@ -23,14 +25,14 @@ export class CanvasImage { constructor( imageObject: ImageObject, - options: { + options?: { getImageDTO?: (imageName: string) => Promise; onLoading?: () => void; onLoad?: (konvaImage: Konva.Image) => void; onError?: () => void; } ) { - const { getImageDTO, onLoading, onLoad, onError } = options; + const { getImageDTO, onLoading, onLoad, onError } = options ?? {}; const { id, width, height, x, y, filters } = imageObject; this.konvaImageGroup = new Konva.Group({ id, listening: false, x, y }); this.konvaPlaceholderGroup = new Konva.Group({ listening: false }); @@ -124,21 +126,10 @@ export class CanvasImage { async updateImageSource(imageName: string) { try { this.onLoading(); - const imageDTO = await this.getImageDTO(imageName); - if (!imageDTO) { - this.onError(); - return; - } - const imageEl = new Image(); - imageEl.onload = () => { - this.onLoad(imageName, imageEl); - }; - imageEl.onerror = () => { - this.onError(); - }; - imageEl.id = imageName; - imageEl.src = imageDTO.image_url; + assert(imageDTO !== null, 'imageDTO is null'); + const imageEl = await loadImage(imageDTO.image_url); + this.onLoad(imageName, imageEl); } catch { this.onError(); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts index e09fc428550..868e8a9761a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts @@ -41,30 +41,19 @@ export class CanvasInitialImage { return; } - const imageObject = this.initialImageState.imageObject; - - if (!imageObject) { - if (this.image) { - this.image.konvaImageGroup.visible(false); - } - } else if (!this.image) { - this.image = await new CanvasImage(imageObject, { - onLoad: () => { - this.updateGroup(); - }, - }); + if (!this.image) { + this.image = await new CanvasImage(this.initialImageState.imageObject, {}); this.objectsGroup.add(this.image.konvaImageGroup); - await this.image.updateImageSource(imageObject.image.name); + await this.image.update(this.initialImageState.imageObject, true); } else if (!this.image.isLoading && !this.image.isError) { - await this.image.update(imageObject); + await this.image.update(this.initialImageState.imageObject); } - this.updateGroup(); - } - - updateGroup() { - const visible = this.initialImageState ? this.initialImageState.isEnabled : false; - this.layer.visible(visible); + if (this.initialImageState && this.initialImageState.isEnabled && !this.image?.isLoading && !this.image?.isError) { + this.layer.visible(true); + } else { + this.layer.visible(false); + } } destroy(): void { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 5f6aafe4961..9b95f599531 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -180,14 +180,11 @@ export class CanvasLayer { assert(image instanceof CanvasImage || image === undefined); if (!image) { - image = await new CanvasImage(obj, { - onLoad: () => { - this.updateGroup(true); - }, - }); + image = await new CanvasImage(obj, {}); this.objects.set(image.id, image); this.objectsGroup.add(image.konvaImageGroup); await image.updateImageSource(obj.image.name); + this.updateGroup(true); } else { if (await image.update(obj, force)) { return true; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 17b5394602a..b4b347d3e55 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -2,6 +2,7 @@ import type { Store } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import { CanvasInitialImage } from 'features/controlLayers/konva/CanvasInitialImage'; +import { CanvasProgressPreview } from 'features/controlLayers/konva/CanvasProgressPreview'; import { getCompositeLayerImage, getControlAdapterImage, @@ -92,7 +93,8 @@ export class CanvasManager { new CanvasBbox(this), new CanvasTool(this), new CanvasDocumentSizeOverlay(this), - new CanvasStagingArea(this) + new CanvasStagingArea(this), + new CanvasProgressPreview(this) ); this.stage.add(this.preview.layer); @@ -111,7 +113,7 @@ export class CanvasManager { } async renderInitialImage() { - this.initialImage.render(this.stateApi.getInitialImageState()); + await this.initialImage.render(this.stateApi.getInitialImageState()); } async renderLayers() { @@ -135,7 +137,7 @@ export class CanvasManager { } } - renderRegions() { + async renderRegions() { const { entities } = this.stateApi.getRegionsState(); // Destroy the konva nodes for nonexistent entities @@ -153,16 +155,20 @@ export class CanvasManager { this.regions.set(adapter.id, adapter); this.stage.add(adapter.layer); } - adapter.render(entity); + await adapter.render(entity); } } - renderInpaintMask() { + async renderProgressPreview() { + await this.preview.progressPreview.render(this.stateApi.getLastProgressEvent()); + } + + async renderInpaintMask() { const inpaintMaskState = this.stateApi.getInpaintMaskState(); - this.inpaintMask.render(inpaintMaskState); + await this.inpaintMask.render(inpaintMaskState); } - renderControlAdapters() { + async renderControlAdapters() { const { entities } = this.stateApi.getControlAdaptersState(); for (const canvasControlAdapter of this.controlAdapters.values()) { @@ -179,7 +185,7 @@ export class CanvasManager { this.controlAdapters.set(adapter.id, adapter); this.stage.add(adapter.layer); } - adapter.render(entity); + await adapter.render(entity); } } @@ -222,7 +228,7 @@ export class CanvasManager { const state = this.stateApi.getState(); if (this.prevState === state && !this.isFirstRender) { - log.debug('No changes detected, skipping render'); + log.trace('No changes detected, skipping render'); return; } @@ -233,7 +239,7 @@ export class CanvasManager { state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id ) { log.debug('Rendering layers'); - this.renderLayers(); + await this.renderLayers(); } if ( @@ -243,8 +249,8 @@ export class CanvasManager { state.tool.selected !== this.prevState.tool.selected || state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id ) { - log.debug('Rendering intial image'); - this.renderInitialImage(); + log.debug('Rendering initial image'); + await this.renderInitialImage(); } if ( @@ -255,7 +261,7 @@ export class CanvasManager { state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id ) { log.debug('Rendering regions'); - this.renderRegions(); + await this.renderRegions(); } if ( @@ -266,7 +272,7 @@ export class CanvasManager { state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id ) { log.debug('Rendering inpaint mask'); - this.renderInpaintMask(); + await this.renderInpaintMask(); } if ( @@ -276,12 +282,12 @@ export class CanvasManager { state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id ) { log.debug('Rendering control adapters'); - this.renderControlAdapters(); + await this.renderControlAdapters(); } if (this.isFirstRender || state.document !== this.prevState.document) { log.debug('Rendering document bounds overlay'); - this.preview.documentSizeOverlay.render(); + await this.preview.documentSizeOverlay.render(); } if ( @@ -291,7 +297,7 @@ export class CanvasManager { state.session.isActive !== this.prevState.session.isActive ) { log.debug('Rendering generation bbox'); - this.preview.bbox.render(); + await this.preview.bbox.render(); } if ( @@ -306,7 +312,7 @@ export class CanvasManager { if (this.isFirstRender || state.session !== this.prevState.session) { log.debug('Rendering staging area'); - this.preview.stagingArea.render(); + await this.preview.stagingArea.render(); } if ( @@ -318,7 +324,7 @@ export class CanvasManager { state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id ) { log.debug('Arranging entities'); - this.arrangeEntities(); + await this.arrangeEntities(); } this.prevState = state; @@ -343,16 +349,21 @@ export class CanvasManager { const unsubscribeRenderer = this.store.subscribe(this.render); // When we this flag, we need to render the staging area - $shouldShowStagedImage.subscribe((shouldShowStagedImage, prevShouldShowStagedImage) => { - log.debug('Rendering staging area'); + $shouldShowStagedImage.subscribe(async (shouldShowStagedImage, prevShouldShowStagedImage) => { if (shouldShowStagedImage !== prevShouldShowStagedImage) { - this.preview.stagingArea.render(); + log.debug('Rendering staging area'); + await this.preview.stagingArea.render(); } }); - $lastProgressEvent.subscribe(() => { - log.debug('Rendering staging area'); - this.preview.stagingArea.render(); + $lastProgressEvent.subscribe(async (lastProgressEvent, prevLastProgressEvent) => { + if (lastProgressEvent !== prevLastProgressEvent) { + log.debug('Rendering progress image'); + await this.preview.progressPreview.render(lastProgressEvent); + if (this.stateApi.getSession().isActive) { + this.preview.stagingArea.render(); + } + } }); log.debug('First render of konva stage'); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts index f5f08429411..9634586560b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts @@ -1,3 +1,4 @@ +import type { CanvasProgressPreview } from 'features/controlLayers/konva/CanvasProgressPreview'; import Konva from 'konva'; import type { CanvasBbox } from './CanvasBbox'; @@ -11,12 +12,14 @@ export class CanvasPreview { bbox: CanvasBbox; documentSizeOverlay: CanvasDocumentSizeOverlay; stagingArea: CanvasStagingArea; + progressPreview: CanvasProgressPreview; constructor( bbox: CanvasBbox, tool: CanvasTool, documentSizeOverlay: CanvasDocumentSizeOverlay, - stagingArea: CanvasStagingArea + stagingArea: CanvasStagingArea, + progressPreview: CanvasProgressPreview ) { this.layer = new Konva.Layer({ listening: true, imageSmoothingEnabled: false }); @@ -31,5 +34,8 @@ export class CanvasPreview { this.tool = tool; this.layer.add(this.tool.group); + + this.progressPreview = progressPreview; + this.layer.add(this.progressPreview.group); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts index e56f88e1644..4e02a931a4d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts @@ -1,3 +1,4 @@ +import { loadImage } from 'features/controlLayers/konva/util'; import Konva from 'konva'; export class CanvasProgressImage { @@ -11,7 +12,6 @@ export class CanvasProgressImage { constructor(arg: { id: string }) { const { id } = arg; this.konvaImageGroup = new Konva.Group({ id, listening: false }); - this.id = id; this.progressImageId = null; this.konvaImage = null; @@ -27,8 +27,12 @@ export class CanvasProgressImage { width: number, height: number ) { - const imageEl = new Image(); - imageEl.onload = () => { + if (this.isLoading) { + return; + } + this.isLoading = true; + try { + const imageEl = await loadImage(dataURL); if (this.konvaImage) { this.konvaImage.setAttrs({ image: imageEl, @@ -49,9 +53,11 @@ export class CanvasProgressImage { }); this.konvaImageGroup.add(this.konvaImage); } - }; - imageEl.id = progressImageId; - imageEl.src = dataURL; + this.isLoading = false; + this.id = progressImageId; + } catch { + this.isError = true; + } } destroy() { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressPreview.ts new file mode 100644 index 00000000000..a37622da684 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressPreview.ts @@ -0,0 +1,38 @@ +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasProgressImage } from 'features/controlLayers/konva/CanvasProgressImage'; +import Konva from 'konva'; +import type { InvocationDenoiseProgressEvent } from 'services/events/types'; + +export class CanvasProgressPreview { + group: Konva.Group; + progressImage: CanvasProgressImage; + manager: CanvasManager; + + constructor(manager: CanvasManager) { + this.manager = manager; + this.group = new Konva.Group({ listening: false }); + this.progressImage = new CanvasProgressImage({ id: 'progress-image' }); + this.group.add(this.progressImage.konvaImageGroup); + } + + async render(lastProgressEvent: InvocationDenoiseProgressEvent | null) { + const bboxRect = this.manager.stateApi.getBbox().rect; + + if (lastProgressEvent) { + const { invocation, step, progress_image } = lastProgressEvent; + const { dataURL } = progress_image; + const { x, y, width, height } = bboxRect; + const progressImageId = `${invocation.id}_${step}`; + if ( + !this.progressImage.isLoading && + !this.progressImage.isError && + this.progressImage.progressImageId !== progressImageId + ) { + await this.progressImage.updateImageSource(progressImageId, dataURL, x, y, width, height); + this.progressImage.konvaImageGroup.visible(true); + } + } else { + this.progressImage.konvaImageGroup.visible(false); + } + } +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts index 8623c097529..fd6ff675be1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts @@ -1,13 +1,11 @@ import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasProgressImage } from 'features/controlLayers/konva/CanvasProgressImage'; import Konva from 'konva'; import type { ImageDTO } from 'services/api/types'; export class CanvasStagingArea { group: Konva.Group; image: CanvasImage | null; - progressImage: CanvasProgressImage | null; imageDTO: ImageDTO | null; manager: CanvasManager; @@ -15,35 +13,32 @@ export class CanvasStagingArea { this.manager = manager; this.group = new Konva.Group({ listening: false }); this.image = null; - this.progressImage = null; this.imageDTO = null; } async render() { - const stagingArea = this.manager.stateApi.getSession(); - const bbox = this.manager.stateApi.getBbox(); + const session = this.manager.stateApi.getSession(); + const bboxRect = this.manager.stateApi.getBbox().rect; const shouldShowStagedImage = this.manager.stateApi.getShouldShowStagedImage(); - const lastProgressEvent = this.manager.stateApi.getLastProgressEvent(); - this.imageDTO = stagingArea.stagedImages[stagingArea.selectedStagedImageIndex] ?? null; + this.imageDTO = session.stagedImages[session.selectedStagedImageIndex] ?? null; if (this.imageDTO) { if (this.image) { if (!this.image.isLoading && !this.image.isError && this.image.imageName !== this.imageDTO.image_name) { await this.image.updateImageSource(this.imageDTO.image_name); } - this.image.konvaImageGroup.x(bbox.x); - this.image.konvaImageGroup.y(bbox.y); + this.image.konvaImageGroup.x(bboxRect.x); + this.image.konvaImageGroup.y(bboxRect.y); this.image.konvaImageGroup.visible(shouldShowStagedImage); - this.progressImage?.konvaImageGroup.visible(false); } else { const { image_name, width, height } = this.imageDTO; this.image = new CanvasImage( { id: 'staging-area-image', type: 'image', - x: bbox.x, - y: bbox.y, + x: bboxRect.x, + y: bboxRect.y, width, height, filters: [], @@ -60,48 +55,16 @@ export class CanvasStagingArea { konvaImage.height(this.imageDTO.height); } this.manager.stateApi.resetLastProgressEvent(); + this.image?.konvaImageGroup.visible(shouldShowStagedImage); }, } ); this.group.add(this.image.konvaImageGroup); await this.image.updateImageSource(this.imageDTO.image_name); this.image.konvaImageGroup.visible(shouldShowStagedImage); - this.progressImage?.konvaImageGroup.visible(false); } - } - - if (stagingArea.isStaging && lastProgressEvent) { - const { invocation, step, progress_image } = lastProgressEvent; - const { dataURL } = progress_image; - const { x, y, width, height } = bbox; - const progressImageId = `${invocation.id}_${step}`; - if (this.progressImage) { - if ( - !this.progressImage.isLoading && - !this.progressImage.isError && - this.progressImage.progressImageId !== progressImageId - ) { - await this.progressImage.updateImageSource(progressImageId, dataURL, x, y, width, height); - this.image?.konvaImageGroup.visible(false); - this.progressImage.konvaImageGroup.visible(true); - } - } else { - this.progressImage = new CanvasProgressImage({ id: 'progress-image' }); - this.group.add(this.progressImage.konvaImageGroup); - await this.progressImage.updateImageSource(progressImageId, dataURL, x, y, width, height); - this.image?.konvaImageGroup.visible(false); - this.progressImage.konvaImageGroup.visible(true); - } - } - - if (!this.imageDTO && !lastProgressEvent) { - if (this.image) { - this.image.konvaImageGroup.visible(false); - } - if (this.progressImage) { - this.progressImage.konvaImageGroup.visible(false); - } - this.manager.stateApi.resetLastProgressEvent(); + } else { + this.image?.konvaImageGroup.visible(false); } } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index 103ef2811d3..f8ae0b3d93f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -538,3 +538,12 @@ export async function getCompositeLayerImage(arg: { manager.stateApi.onLayerImageCached(imageDTO); return imageDTO; } + +export function loadImage(src: string, imageEl?: HTMLImageElement): Promise { + return new Promise((resolve, reject) => { + const _imageEl = imageEl ?? new Image(); + _imageEl.onload = () => resolve(_imageEl); + _imageEl.onerror = (error) => reject(error); + _imageEl.src = src; + }); +} diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index 1dde41fb0cb..c65443f0f36 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts @@ -22,7 +22,7 @@ export const addOutpaint = async ( ): Promise> => { denoise.denoising_start = denoising_start; - const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']); + const cropBbox = pick(bbox.rect, ['x', 'y', 'width', 'height']); const initialImage = await manager.getInitialImage({ bbox: cropBbox }); const maskImage = await manager.getInpaintMaskImage({ bbox: cropBbox }); const infill = getInfill(g, compositing); From 28eb9b62a8f9bdfd61452c17adb11961ad0795da Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 15 Jul 2024 18:49:33 +1000 Subject: [PATCH 199/678] fix(ui): entity display list --- .../components/CanvasEntityList.tsx | 61 ++++++------------- .../ControlAdapter/CAEntityList.tsx | 31 ++++++++++ .../components/ControlLayersPanelContent.tsx | 6 -- .../components/IPAdapter/IPAEntityList.tsx | 31 ++++++++++ .../components/Layer/LayerEntityList.tsx | 31 ++++++++++ .../RegionalGuidance/RGEntityList.tsx | 31 ++++++++++ 6 files changed, 143 insertions(+), 48 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityList.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAEntityList.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerEntityList.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGEntityList.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx index 5df4b0a4c7b..d9d0255d143 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx @@ -1,53 +1,30 @@ /* eslint-disable i18next/no-literal-string */ import { Flex } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; -import { CA } from 'features/controlLayers/components/ControlAdapter/CA'; -import { IPA } from 'features/controlLayers/components/IPAdapter/IPA'; -import { Layer } from 'features/controlLayers/components/Layer/Layer'; -import { RG } from 'features/controlLayers/components/RegionalGuidance/RG'; -import { mapId } from 'features/controlLayers/konva/util'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { CAEntityList } from 'features/controlLayers/components/ControlAdapter/CAEntityList'; +import { InitialImage } from 'features/controlLayers/components/InitialImage/InitialImage'; +import { IM } from 'features/controlLayers/components/InpaintMask/IM'; +import { IPAEntityList } from 'features/controlLayers/components/IPAdapter/IPAEntityList'; +import { LayerEntityList } from 'features/controlLayers/components/Layer/LayerEntityList'; +import { RGEntityList } from 'features/controlLayers/components/RegionalGuidance/RGEntityList'; import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; - -const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { - const rgIds = canvasV2.regions.entities.map(mapId).reverse(); - const caIds = canvasV2.controlAdapters.entities.map(mapId).reverse(); - const ipaIds = canvasV2.ipAdapters.entities.map(mapId).reverse(); - const layerIds = canvasV2.layers.entities.map(mapId).reverse(); - const entityCount = rgIds.length + caIds.length + ipaIds.length + layerIds.length; - return { rgIds, caIds, ipaIds, layerIds, entityCount }; -}); export const CanvasEntityList = memo(() => { - const { t } = useTranslation(); - const { rgIds, caIds, ipaIds, layerIds, entityCount } = useAppSelector(selectEntityIds); - - if (entityCount > 0) { - return ( - - - {rgIds.map((id) => ( - - ))} - {caIds.map((id) => ( - - ))} - {ipaIds.map((id) => ( - - ))} - {layerIds.map((id) => ( - - ))} - - - ); - } + const isCanvasSessionActive = useAppSelector((s) => s.canvasV2.session.isActive); - return ; + return ( + + + {isCanvasSessionActive && } + + + + + {!isCanvasSessionActive && } + + + ); }); CanvasEntityList.displayName = 'CanvasEntityList'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityList.tsx new file mode 100644 index 00000000000..e18fa5e166f --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityList.tsx @@ -0,0 +1,31 @@ +/* eslint-disable i18next/no-literal-string */ +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppSelector } from 'app/store/storeHooks'; +import { CA } from 'features/controlLayers/components/ControlAdapter/CA'; +import { mapId } from 'features/controlLayers/konva/util'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { memo } from 'react'; + +const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { + return canvasV2.controlAdapters.entities.map(mapId).reverse(); +}); + +export const CAEntityList = memo(() => { + const caIds = useAppSelector(selectEntityIds); + + if (caIds.length === 0) { + return null; + } + + if (caIds.length > 0) { + return ( + <> + {caIds.map((id) => ( + + ))} + + ); + } +}); + +CAEntityList.displayName = 'CAEntityList'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index 94e706b68a0..f9f7c078113 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -1,24 +1,18 @@ /* eslint-disable i18next/no-literal-string */ import { Flex } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; import { AddLayerButton } from 'features/controlLayers/components/AddLayerButton'; import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList'; import { DeleteAllLayersButton } from 'features/controlLayers/components/DeleteAllLayersButton'; -import { InitialImage } from 'features/controlLayers/components/InitialImage/InitialImage'; -import { IM } from 'features/controlLayers/components/InpaintMask/IM'; import { memo } from 'react'; export const ControlLayersPanelContent = memo(() => { - const isCanvasSessionActive = useAppSelector((s) => s.canvasV2.session.isActive); return ( - {isCanvasSessionActive && } - {!isCanvasSessionActive && } ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAEntityList.tsx new file mode 100644 index 00000000000..d70847cf391 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAEntityList.tsx @@ -0,0 +1,31 @@ +/* eslint-disable i18next/no-literal-string */ +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppSelector } from 'app/store/storeHooks'; +import { IPA } from 'features/controlLayers/components/IPAdapter/IPA'; +import { mapId } from 'features/controlLayers/konva/util'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { memo } from 'react'; + +const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { + return canvasV2.ipAdapters.entities.map(mapId).reverse(); +}); + +export const IPAEntityList = memo(() => { + const ipaIds = useAppSelector(selectEntityIds); + + if (ipaIds.length === 0) { + return null; + } + + if (ipaIds.length > 0) { + return ( + <> + {ipaIds.map((id) => ( + + ))} + + ); + } +}); + +IPAEntityList.displayName = 'IPAEntityList'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerEntityList.tsx new file mode 100644 index 00000000000..89ab0d7aa10 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerEntityList.tsx @@ -0,0 +1,31 @@ +/* eslint-disable i18next/no-literal-string */ +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppSelector } from 'app/store/storeHooks'; +import { Layer } from 'features/controlLayers/components/Layer/Layer'; +import { mapId } from 'features/controlLayers/konva/util'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { memo } from 'react'; + +const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { + return canvasV2.layers.entities.map(mapId).reverse(); +}); + +export const LayerEntityList = memo(() => { + const layerIds = useAppSelector(selectEntityIds); + + if (layerIds.length === 0) { + return null; + } + + if (layerIds.length > 0) { + return ( + <> + {layerIds.map((id) => ( + + ))} + + ); + } +}); + +LayerEntityList.displayName = 'LayerEntityList'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGEntityList.tsx new file mode 100644 index 00000000000..1e9a68a3216 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGEntityList.tsx @@ -0,0 +1,31 @@ +/* eslint-disable i18next/no-literal-string */ +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppSelector } from 'app/store/storeHooks'; +import { RG } from 'features/controlLayers/components/RegionalGuidance/RG'; +import { mapId } from 'features/controlLayers/konva/util'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { memo } from 'react'; + +const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { + return canvasV2.regions.entities.map(mapId).reverse(); +}); + +export const RGEntityList = memo(() => { + const rgIds = useAppSelector(selectEntityIds); + + if (rgIds.length === 0) { + return null; + } + + if (rgIds.length > 0) { + return ( + <> + {rgIds.map((id) => ( + + ))} + + ); + } +}); + +RGEntityList.displayName = 'RGEntityList'; From 507acaa7c9f9320d3e36c7141419f7910b64969e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 15 Jul 2024 19:33:03 +1000 Subject: [PATCH 200/678] fix(ui): reset node executions states when loading workflow --- .../listenerMiddleware/listeners/workflowLoadRequested.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts index 2c0caa0ec93..ebce2f80b2b 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts @@ -1,6 +1,7 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { parseify } from 'common/util/serialize'; +import { $nodeExecutionStates } from 'features/nodes/hooks/useExecutionState'; import { workflowLoaded, workflowLoadRequested } from 'features/nodes/store/actions'; import { $templates } from 'features/nodes/store/nodesSlice'; import { $needsFit } from 'features/nodes/store/reactFlowInstance'; @@ -46,6 +47,7 @@ export const addWorkflowLoadRequestedListener = (startAppListening: AppStartList delete workflow.id; } + $nodeExecutionStates.set({}); dispatch(workflowLoaded(workflow)); if (!warnings.length) { toast({ From 5fef9bbceb144b530a7fdceeb6b368c1ce744c2f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 15 Jul 2024 19:35:15 +1000 Subject: [PATCH 201/678] fix(ui): reset initial image when resetting canvas --- .../web/src/features/controlLayers/store/canvasV2Slice.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 28781563fd5..77697caf9ee 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -1,5 +1,5 @@ import type { PayloadAction } from '@reduxjs/toolkit'; -import { createSlice } from '@reduxjs/toolkit'; +import { createAction, createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { deepClone } from 'common/util/deepClone'; import { bboxReducers } from 'features/controlLayers/store/bboxReducers'; @@ -172,6 +172,7 @@ export const canvasV2Slice = createSlice({ state.session = deepClone(initialState.session); state.tool = deepClone(initialState.tool); state.inpaintMask = deepClone(initialState.inpaintMask); + state.initialImage = deepClone(initialState.initialImage); }, }, }); @@ -386,3 +387,5 @@ export const canvasV2PersistConfig: PersistConfig = { migrate, persistDenylist: [], }; + +export const sessionRequested = createAction(`${canvasV2Slice.name}/sessionRequested`); From d35af4c048de2f736e9c3aecbe26281f5ca810b1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 15 Jul 2024 19:35:38 +1000 Subject: [PATCH 202/678] fix(ui): fix layer transparency calculation --- .../frontend/web/src/common/util/arrayBuffer.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/invokeai/frontend/web/src/common/util/arrayBuffer.ts b/invokeai/frontend/web/src/common/util/arrayBuffer.ts index c3b13dac265..f7ac9db03f8 100644 --- a/invokeai/frontend/web/src/common/util/arrayBuffer.ts +++ b/invokeai/frontend/web/src/common/util/arrayBuffer.ts @@ -3,7 +3,7 @@ export const getImageDataTransparency = (imageData: ImageData) => { let isPartiallyTransparent = false; const len = imageData.data.length; for (let i = 3; i < len; i += 4) { - if (imageData.data[i] === 255) { + if (imageData.data[i] !== 0) { isFullyTransparent = false; } else { isPartiallyTransparent = true; @@ -14,14 +14,3 @@ export const getImageDataTransparency = (imageData: ImageData) => { } return { isFullyTransparent, isPartiallyTransparent }; }; - -export const areAnyPixelsBlack = (pixels: Uint8ClampedArray) => { - const len = pixels.length; - const i = 0; - for (let i = 0; i < len; i) { - if (pixels[i++] === 0 && pixels[i++] === 0 && pixels[i++] === 0 && pixels[i++] === 255) { - return true; - } - } - return false; -}; From 17c9ccfdb01b2c05d6c67722b0d8e7965ac8e939 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 15 Jul 2024 19:36:11 +1000 Subject: [PATCH 203/678] feat(ui): convert initial image to layer when starting canvas session --- .../middleware/listenerMiddleware/index.ts | 2 ++ .../listeners/canvasSessionRequested.ts | 29 +++++++++++++++++++ .../components/NewSessionButton.tsx | 5 ++-- 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSessionRequested.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts index 29df0bf5422..79b90776b8c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -9,6 +9,7 @@ import { addBatchEnqueuedListener } from 'app/store/middleware/listenerMiddlewar import { addDeleteBoardAndImagesFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted'; import { addBoardIdSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/boardIdSelected'; import { addBulkDownloadListeners } from 'app/store/middleware/listenerMiddleware/listeners/bulkDownload'; +import { addCanvasSessionRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasSessionRequested'; import { addControlAdapterPreprocessor } from 'app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor'; import { addEnqueueRequestedLinear } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear'; import { addEnqueueRequestedNodes } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes'; @@ -89,6 +90,7 @@ addBatchEnqueuedListener(startAppListening); // addStagingAreaImageSavedListener(startAppListening); // addCommitStagingAreaImageListener(startAppListening); addStagingListeners(startAppListening); +addCanvasSessionRequestedListener(startAppListening); // Socket.IO addGeneratorProgressEventListener(startAppListening); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSessionRequested.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSessionRequested.ts new file mode 100644 index 00000000000..0e1c92dcffc --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSessionRequested.ts @@ -0,0 +1,29 @@ +import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +import { + layerAdded, + layerImageAdded, + sessionRequested, + sessionStarted, +} from 'features/controlLayers/store/canvasV2Slice'; +import { getImageDTO } from 'services/api/endpoints/images'; +import { assert } from 'tsafe'; + +export const addCanvasSessionRequestedListener = (startAppListening: AppStartListening) => { + startAppListening({ + actionCreator: sessionRequested, + effect: async (action, { getState, dispatch }) => { + const initialImageObject = getState().canvasV2.initialImage.imageObject; + if (initialImageObject) { + // We have an initial image that needs to be converted to a layer + dispatch(layerAdded()); + const newLayer = getState().canvasV2.layers.entities[0]; + assert(newLayer, 'Expected new layer to be created'); + const imageDTO = await getImageDTO(initialImageObject.image.name); + assert(imageDTO, 'Unable to fetch initial image DTO'); + dispatch(layerImageAdded({ id: newLayer.id, imageDTO })); + } + + dispatch(sessionStarted()); + }, + }); +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/NewSessionButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/NewSessionButton.tsx index 6befb0a59fe..a04ce3089b5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/NewSessionButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/NewSessionButton.tsx @@ -1,13 +1,12 @@ import { Button } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; -import { sessionStarted } from 'features/controlLayers/store/canvasV2Slice'; +import { sessionRequested } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; export const NewSessionButton = memo(() => { const dispatch = useAppDispatch(); - const onClick = useCallback(() => { - dispatch(sessionStarted()); + dispatch(sessionRequested()); }, [dispatch]); return ; From e50b4280f21ba6e69a00e3b4d81f89d9ed56f186 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 16 Jul 2024 17:03:55 +1000 Subject: [PATCH 204/678] feat(ui): rip out document size barely knew ye --- .../listeners/modelsLoaded.ts | 12 +- .../listeners/setDefaultSettings.ts | 8 +- .../components/CanvasResizer.tsx | 2 +- .../ControlAdapter/CAImagePreview.tsx | 10 +- .../components/HeadsUpDisplay.tsx | 2 - .../components/IPAdapter/IPAImagePreview.tsx | 10 +- .../InitialImage/InitialImagePreview.tsx | 10 +- .../konva/CanvasDocumentSizeOverlay.ts | 67 --------- .../controlLayers/konva/CanvasManager.ts | 13 +- .../controlLayers/konva/CanvasPreview.ts | 6 - .../controlLayers/konva/CanvasStateApi.ts | 3 - .../features/controlLayers/konva/events.ts | 15 +- .../controlLayers/store/bboxReducers.ts | 119 ++++++++++++++- .../controlLayers/store/canvasV2Slice.ts | 28 ++-- .../controlLayers/store/documentReducers.ts | 141 ------------------ .../controlLayers/store/paramsReducers.ts | 4 +- .../src/features/controlLayers/store/types.ts | 10 +- .../src/features/metadata/util/recallers.ts | 8 +- .../InfillAndScaling/ParamScaledHeight.tsx | 4 +- .../InfillAndScaling/ParamScaledWidth.tsx | 4 +- .../components/Core/ParamHeight.tsx | 6 +- .../parameters/components/Core/ParamWidth.tsx | 6 +- .../DocumentSize/AspectRatioIconPreview.tsx | 18 +-- .../DocumentSize/AspectRatioSelect.tsx | 6 +- .../DocumentSize/LockAspectRatioButton.tsx | 6 +- .../DocumentSize/SetOptimalSizeButton.tsx | 8 +- .../DocumentSize/SwapDimensionsButton.tsx | 4 +- .../ImageSettingsAccordion.tsx | 4 +- 28 files changed, 202 insertions(+), 332 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasDocumentSizeOverlay.ts delete mode 100644 invokeai/frontend/web/src/features/controlLayers/store/documentReducers.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts index a7b9f82d578..87967544a8c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts @@ -3,9 +3,9 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware' import type { AppDispatch, RootState } from 'app/store/store'; import type { JSONObject } from 'common/types'; import { + bboxHeightChanged, + bboxWidthChanged, caModelChanged, - documentHeightChanged, - documentWidthChanged, ipaModelChanged, loraDeleted, modelChanged, @@ -83,16 +83,16 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => { dispatch(modelChanged({ model: defaultModelInList, previousModel: currentModel })); const optimalDimension = getOptimalDimension(defaultModelInList); - if (getIsSizeOptimal(state.canvasV2.document.rect.width, state.canvasV2.document.rect.height, optimalDimension)) { + if (getIsSizeOptimal(state.canvasV2.bbox.rect.width, state.canvasV2.bbox.rect.height, optimalDimension)) { return; } const { width, height } = calculateNewSize( - state.canvasV2.document.aspectRatio.value, + state.canvasV2.bbox.aspectRatio.value, optimalDimension * optimalDimension ); - dispatch(documentWidthChanged({ width })); - dispatch(documentHeightChanged({ height })); + dispatch(bboxWidthChanged({ width })); + dispatch(bboxHeightChanged({ height })); return; } } diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts index f8ddadb4887..29828c911af 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts @@ -1,7 +1,7 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { - documentHeightChanged, - documentWidthChanged, + bboxHeightChanged, + bboxWidthChanged, setCfgRescaleMultiplier, setCfgScale, setScheduler, @@ -99,13 +99,13 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni const setSizeOptions = { updateAspectRatio: true, clamp: true }; if (width) { if (isParameterWidth(width)) { - dispatch(documentWidthChanged({ width, ...setSizeOptions })); + dispatch(bboxWidthChanged({ width, ...setSizeOptions })); } } if (height) { if (isParameterHeight(height)) { - dispatch(documentHeightChanged({ height, ...setSizeOptions })); + dispatch(bboxHeightChanged({ height, ...setSizeOptions })); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasResizer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasResizer.tsx index d5fc3852d2d..1d11a3d311c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasResizer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasResizer.tsx @@ -25,7 +25,7 @@ type ResizeDirection = | 'down-right'; export const CanvasResizer = memo(() => { - const document = useAppSelector((s) => s.canvasV2.document); + const bbox = useAppSelector((s) => s.canvasV2.bbox); const [resizeDirection, setResizeDirection] = useState('center-out'); const setDirUpLeft = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx index 5d6d253fbf4..b1c862bab18 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx @@ -3,7 +3,7 @@ import { skipToken } from '@reduxjs/toolkit/query'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; -import { documentHeightChanged, documentWidthChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import type { ControlAdapterEntity } from 'features/controlLayers/store/types'; import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types'; @@ -89,15 +89,15 @@ export const CAImagePreview = memo( if (shift) { const { width, height } = controlImage; - dispatch(documentWidthChanged({ width, ...options })); - dispatch(documentHeightChanged({ height, ...options })); + dispatch(bboxWidthChanged({ width, ...options })); + dispatch(bboxHeightChanged({ height, ...options })); } else { const { width, height } = calculateNewSize( controlImage.width / controlImage.height, optimalDimension * optimalDimension ); - dispatch(documentWidthChanged({ width, ...options })); - dispatch(documentHeightChanged({ height, ...options })); + dispatch(bboxWidthChanged({ width, ...options })); + dispatch(bboxHeightChanged({ height, ...options })); } }, [controlImage, dispatch, optimalDimension, shift]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx index 9de1876f60f..5f3bcab13cd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx @@ -20,12 +20,10 @@ export const HeadsUpDisplay = memo(() => { const lastMouseDownPos = useStore($lastMouseDownPos); const lastAddedPoint = useStore($lastAddedPoint); const bbox = useAppSelector((s) => s.canvasV2.bbox); - const document = useAppSelector((s) => s.canvasV2.document); return ( - { const options = { updateAspectRatio: true, clamp: true }; if (shift) { const { width, height } = imageDTO; - dispatch(documentWidthChanged({ width, ...options })); - dispatch(documentHeightChanged({ height, ...options })); + dispatch(bboxWidthChanged({ width, ...options })); + dispatch(bboxHeightChanged({ height, ...options })); } else { const { width, height } = calculateNewSize(imageDTO.width / imageDTO.height, optimalDimension * optimalDimension); - dispatch(documentWidthChanged({ width, ...options })); - dispatch(documentHeightChanged({ height, ...options })); + dispatch(bboxWidthChanged({ width, ...options })); + dispatch(bboxHeightChanged({ height, ...options })); } }, [imageDTO, dispatch, optimalDimension, shift]); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasDocumentSizeOverlay.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasDocumentSizeOverlay.ts deleted file mode 100644 index 00e51ab6cc3..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasDocumentSizeOverlay.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { getArbitraryBaseColor } from '@invoke-ai/ui-library'; -import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { DOCUMENT_FIT_PADDING_PX } from 'features/controlLayers/konva/constants'; -import Konva from 'konva'; - -export class CanvasDocumentSizeOverlay { - group: Konva.Group; - outerRect: Konva.Rect; - innerRect: Konva.Rect; - padding: number; - manager: CanvasManager; - - constructor(manager: CanvasManager, padding?: number) { - this.manager = manager; - this.padding = padding ?? DOCUMENT_FIT_PADDING_PX; - this.group = new Konva.Group({ id: 'document_overlay_group', listening: false }); - this.outerRect = new Konva.Rect({ - id: 'document_overlay_outer_rect', - listening: false, - fill: getArbitraryBaseColor(10), - opacity: 0.7, - }); - this.innerRect = new Konva.Rect({ - id: 'document_overlay_inner_rect', - listening: false, - fill: 'white', - globalCompositeOperation: 'destination-out', - }); - this.group.add(this.outerRect); - this.group.add(this.innerRect); - } - - render() { - const document = this.manager.stateApi.getDocument(); - this.group.zIndex(0); - - const x = this.manager.stage.x(); - const y = this.manager.stage.y(); - const width = this.manager.stage.width(); - const height = this.manager.stage.height(); - const scale = this.manager.stage.scaleX(); - - this.outerRect.setAttrs({ - offsetX: x / scale, - offsetY: y / scale, - width: width / scale, - height: height / scale, - }); - - this.innerRect.setAttrs(document.rect); - } - - fitToStage() { - const document = this.manager.stateApi.getDocument(); - - // Fit & center the document on the stage - const width = this.manager.stage.width(); - const height = this.manager.stage.height(); - const docWidthWithBuffer = document.rect.width + this.padding * 2; - const docHeightWithBuffer = document.rect.height + this.padding * 2; - const scale = Math.min(Math.min(width / docWidthWithBuffer, height / docHeightWithBuffer), 1); - const x = (width - docWidthWithBuffer * scale) / 2 + this.padding * scale; - const y = (height - docHeightWithBuffer * scale) / 2 + this.padding * scale; - this.manager.stage.setAttrs({ x, y, width, height, scaleX: scale, scaleY: scale }); - this.manager.stateApi.setStageAttrs({ x, y, width, height, scale }); - } -} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index b4b347d3e55..8c89e07c675 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -22,7 +22,6 @@ import { assert } from 'tsafe'; import { CanvasBackground } from './CanvasBackground'; import { CanvasBbox } from './CanvasBbox'; import { CanvasControlAdapter } from './CanvasControlAdapter'; -import { CanvasDocumentSizeOverlay } from './CanvasDocumentSizeOverlay'; import { CanvasInpaintMask } from './CanvasInpaintMask'; import { CanvasLayer } from './CanvasLayer'; import { CanvasPreview } from './CanvasPreview'; @@ -92,7 +91,6 @@ export class CanvasManager { this.preview = new CanvasPreview( new CanvasBbox(this), new CanvasTool(this), - new CanvasDocumentSizeOverlay(this), new CanvasStagingArea(this), new CanvasProgressPreview(this) ); @@ -221,7 +219,6 @@ export class CanvasManager { scale: this.stage.scaleX(), }); this.background.render(); - this.preview.documentSizeOverlay.render(); } render = async () => { @@ -245,7 +242,7 @@ export class CanvasManager { if ( this.isFirstRender || state.initialImage !== this.prevState.initialImage || - state.document !== this.prevState.document || + state.bbox.rect !== this.prevState.bbox.rect || state.tool.selected !== this.prevState.tool.selected || state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id ) { @@ -285,11 +282,6 @@ export class CanvasManager { await this.renderControlAdapters(); } - if (this.isFirstRender || state.document !== this.prevState.document) { - log.debug('Rendering document bounds overlay'); - await this.preview.documentSizeOverlay.render(); - } - if ( this.isFirstRender || state.bbox !== this.prevState.bbox || @@ -367,9 +359,6 @@ export class CanvasManager { }); log.debug('First render of konva stage'); - // On first render, the document should be fit to the stage. - this.preview.documentSizeOverlay.render(); - this.preview.documentSizeOverlay.fitToStage(); this.preview.tool.render(); this.render(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts index 9634586560b..3e64b8a28d7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts @@ -2,7 +2,6 @@ import type { CanvasProgressPreview } from 'features/controlLayers/konva/CanvasP import Konva from 'konva'; import type { CanvasBbox } from './CanvasBbox'; -import type { CanvasDocumentSizeOverlay } from './CanvasDocumentSizeOverlay'; import type { CanvasStagingArea } from './CanvasStagingArea'; import type { CanvasTool } from './CanvasTool'; @@ -10,22 +9,17 @@ export class CanvasPreview { layer: Konva.Layer; tool: CanvasTool; bbox: CanvasBbox; - documentSizeOverlay: CanvasDocumentSizeOverlay; stagingArea: CanvasStagingArea; progressPreview: CanvasProgressPreview; constructor( bbox: CanvasBbox, tool: CanvasTool, - documentSizeOverlay: CanvasDocumentSizeOverlay, stagingArea: CanvasStagingArea, progressPreview: CanvasProgressPreview ) { this.layer = new Konva.Layer({ listening: true, imageSmoothingEnabled: false }); - this.documentSizeOverlay = documentSizeOverlay; - this.layer.add(this.documentSizeOverlay.group); - this.stagingArea = stagingArea; this.layer.add(this.stagingArea.group); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index cac8dcce272..d40a51c3a6b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -207,9 +207,6 @@ export class CanvasStateApi { getBbox = () => { return this.getState().bbox; }; - getDocument = () => { - return this.getState().document; - }; getToolState = () => { return this.getState().tool; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 53a0218ab6e..daaf63c8e72 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -137,14 +137,14 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { function getClip(entity: RegionEntity | LayerEntity | InpaintMaskEntity) { const settings = getSettings(); - const bbox = getBbox(); + const bboxRect = getBbox().rect; if (settings.clipToBbox) { return { - x: bbox.x - entity.x, - y: bbox.y - entity.y, - width: bbox.width, - height: bbox.height, + x: bboxRect.x - entity.x, + y: bboxRect.y - entity.y, + width: bboxRect.width, + height: bboxRect.height, }; } else { return { @@ -486,7 +486,6 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { stage.position(newPos); setStageAttrs({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale }); manager.background.render(); - manager.preview.documentSizeOverlay.render(); } } manager.preview.tool.render(); @@ -502,7 +501,6 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { scale: stage.scaleX(), }); manager.background.render(); - manager.preview.documentSizeOverlay.render(); manager.preview.tool.render(); }); @@ -540,9 +538,8 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { } else if (e.key === 'r') { setLastCursorPos(null); setLastMouseDownPos(null); - manager.preview.documentSizeOverlay.fitToStage(); manager.background.render(); - manager.preview.documentSizeOverlay.render(); + // TODO(psyche): restore some kind of fit } manager.preview.tool.render(); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts index c01bdf5b1ad..202ec0dda24 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts @@ -1,12 +1,17 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import { deepClone } from 'common/util/deepClone'; +import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple'; import type { BoundingBoxScaleMethod, CanvasV2State, Size } from 'features/controlLayers/store/types'; import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; +import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; +import { ASPECT_RATIO_MAP, initialAspectRatioState } from 'features/parameters/components/DocumentSize/constants'; +import type { AspectRatioID } from 'features/parameters/components/DocumentSize/types'; import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; import type { IRect } from 'konva/lib/types'; import { pick } from 'lodash-es'; export const bboxReducers = { - scaledBboxChanged: (state, action: PayloadAction>) => { + bboxScaledSizeChanged: (state, action: PayloadAction>) => { state.layers.imageCache = null; state.bbox.scaledSize = { ...state.bbox.scaledSize, ...action.payload }; }, @@ -30,4 +35,116 @@ export const bboxReducers = { state.bbox.scaledSize = getScaledBoundingBoxDimensions(size, optimalDimension); } }, + bboxWidthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }>) => { + const { width, updateAspectRatio, clamp } = action.payload; + state.bbox.rect.width = clamp ? Math.max(roundDownToMultiple(width, 8), 64) : width; + + if (state.bbox.aspectRatio.isLocked) { + state.bbox.rect.height = roundToMultiple(state.bbox.rect.width / state.bbox.aspectRatio.value, 8); + } + + if (updateAspectRatio || !state.bbox.aspectRatio.isLocked) { + state.bbox.aspectRatio.value = state.bbox.rect.width / state.bbox.rect.height; + state.bbox.aspectRatio.id = 'Free'; + state.bbox.aspectRatio.isLocked = false; + } + + if (!state.session.isActive) { + if (state.initialImage.imageObject) { + state.initialImage.imageObject.width = state.bbox.rect.width; + state.initialImage.imageObject.height = state.bbox.rect.height; + } + } + }, + bboxHeightChanged: ( + state, + action: PayloadAction<{ height: number; updateAspectRatio?: boolean; clamp?: boolean }> + ) => { + const { height, updateAspectRatio, clamp } = action.payload; + + state.bbox.rect.height = clamp ? Math.max(roundDownToMultiple(height, 8), 64) : height; + + if (state.bbox.aspectRatio.isLocked) { + state.bbox.rect.width = roundToMultiple(state.bbox.rect.height * state.bbox.aspectRatio.value, 8); + } + + if (updateAspectRatio || !state.bbox.aspectRatio.isLocked) { + state.bbox.aspectRatio.value = state.bbox.rect.width / state.bbox.rect.height; + state.bbox.aspectRatio.id = 'Free'; + state.bbox.aspectRatio.isLocked = false; + } + + if (!state.session.isActive) { + if (state.initialImage.imageObject) { + state.initialImage.imageObject.width = state.bbox.rect.width; + state.initialImage.imageObject.height = state.bbox.rect.height; + } + } + }, + bboxAspectRatioLockToggled: (state) => { + state.bbox.aspectRatio.isLocked = !state.bbox.aspectRatio.isLocked; + }, + bboxAspectRatioIdChanged: (state, action: PayloadAction<{ id: AspectRatioID }>) => { + const { id } = action.payload; + state.bbox.aspectRatio.id = id; + if (id === 'Free') { + state.bbox.aspectRatio.isLocked = false; + } else { + state.bbox.aspectRatio.isLocked = true; + state.bbox.aspectRatio.value = ASPECT_RATIO_MAP[id].ratio; + const { width, height } = calculateNewSize( + state.bbox.aspectRatio.value, + state.bbox.rect.width * state.bbox.rect.height + ); + state.bbox.rect.width = width; + state.bbox.rect.height = height; + } + if (!state.session.isActive) { + if (state.initialImage.imageObject) { + state.initialImage.imageObject.width = state.bbox.rect.width; + state.initialImage.imageObject.height = state.bbox.rect.height; + } + } + }, + bboxDimensionsSwapped: (state) => { + state.bbox.aspectRatio.value = 1 / state.bbox.aspectRatio.value; + if (state.bbox.aspectRatio.id === 'Free') { + const newWidth = state.bbox.rect.height; + const newHeight = state.bbox.rect.width; + state.bbox.rect.width = newWidth; + state.bbox.rect.height = newHeight; + } else { + const { width, height } = calculateNewSize( + state.bbox.aspectRatio.value, + state.bbox.rect.width * state.bbox.rect.height + ); + state.bbox.rect.width = width; + state.bbox.rect.height = height; + state.bbox.aspectRatio.id = ASPECT_RATIO_MAP[state.bbox.aspectRatio.id].inverseID; + } + if (!state.session.isActive) { + if (state.initialImage.imageObject) { + state.initialImage.imageObject.width = state.bbox.rect.width; + state.initialImage.imageObject.height = state.bbox.rect.height; + } + } + }, + bboxSizeOptimized: (state) => { + const optimalDimension = getOptimalDimension(state.params.model); + if (state.bbox.aspectRatio.isLocked) { + const { width, height } = calculateNewSize(state.bbox.aspectRatio.value, optimalDimension ** 2); + state.bbox.rect.width = width; + state.bbox.rect.height = height; + } else { + state.bbox.aspectRatio = deepClone(initialAspectRatioState); + state.bbox.rect.width = optimalDimension; + state.bbox.rect.height = optimalDimension; + } + if (!state.session.isActive) { + if (state.initialImage.imageObject) { + state.initialImage.imageObject.width = state.bbox.rect.width; + state.initialImage.imageObject.height = state.bbox.rect.height; + } + } + }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 77697caf9ee..22accd05c11 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -5,7 +5,6 @@ import { deepClone } from 'common/util/deepClone'; import { bboxReducers } from 'features/controlLayers/store/bboxReducers'; import { compositingReducers } from 'features/controlLayers/store/compositingReducers'; import { controlAdaptersReducers } from 'features/controlLayers/store/controlAdaptersReducers'; -import { documentReducers } from 'features/controlLayers/store/documentReducers'; import { initialImageReducers } from 'features/controlLayers/store/initialImageReducers'; import { inpaintMaskReducers } from 'features/controlLayers/store/inpaintMaskReducers'; import { ipAdaptersReducers } from 'features/controlLayers/store/ipAdaptersReducers'; @@ -63,12 +62,9 @@ const initialState: CanvasV2State = { width: 50, }, }, - document: { - rect: { x: 0, y: 0, width: 512, height: 512 }, - aspectRatio: deepClone(initialAspectRatioState), - }, bbox: { rect: { x: 0, y: 0, width: 512, height: 512 }, + aspectRatio: deepClone(initialAspectRatioState), scaleMethod: 'auto', scaledSize: { width: 512, @@ -149,7 +145,6 @@ export const canvasV2Slice = createSlice({ ...bboxReducers, ...inpaintMaskReducers, ...sessionReducers, - ...documentReducers, ...initialImageReducers, entitySelected: (state, action: PayloadAction) => { state.selectedEntityIdentifier = action.payload; @@ -164,7 +159,6 @@ export const canvasV2Slice = createSlice({ canvasReset: (state) => { state.bbox = deepClone(initialState.bbox); state.controlAdapters = deepClone(initialState.controlAdapters); - state.document = deepClone(initialState.document); state.ipAdapters = deepClone(initialState.ipAdapters); state.layers = deepClone(initialState.layers); state.regions = deepClone(initialState.regions); @@ -178,7 +172,6 @@ export const canvasV2Slice = createSlice({ }); export const { - bboxChanged, brushWidthChanged, eraserWidthChanged, fillChanged, @@ -188,17 +181,18 @@ export const { maskOpacityChanged, entitySelected, allEntitiesDeleted, - scaledBboxChanged, - bboxScaleMethodChanged, clipToBboxChanged, canvasReset, - // document - documentWidthChanged, - documentHeightChanged, - documentAspectRatioLockToggled, - documentAspectRatioIdChanged, - documentDimensionsSwapped, - documentSizeOptimized, + // bbox + bboxChanged, + bboxScaledSizeChanged, + bboxScaleMethodChanged, + bboxWidthChanged, + bboxHeightChanged, + bboxAspectRatioLockToggled, + bboxAspectRatioIdChanged, + bboxDimensionsSwapped, + bboxSizeOptimized, // layers layerAdded, layerRecalled, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/documentReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/documentReducers.ts deleted file mode 100644 index a4649ca5414..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/store/documentReducers.ts +++ /dev/null @@ -1,141 +0,0 @@ -import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import { deepClone } from 'common/util/deepClone'; -import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple'; -import type { CanvasV2State } from 'features/controlLayers/store/types'; -import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; -import { ASPECT_RATIO_MAP, initialAspectRatioState } from 'features/parameters/components/DocumentSize/constants'; -import type { AspectRatioID } from 'features/parameters/components/DocumentSize/types'; -import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; - -export const documentReducers = { - documentWidthChanged: ( - state, - action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }> - ) => { - const { width, updateAspectRatio, clamp } = action.payload; - state.document.rect.width = clamp ? Math.max(roundDownToMultiple(width, 8), 64) : width; - - if (state.document.aspectRatio.isLocked) { - state.document.rect.height = roundToMultiple(state.document.rect.width / state.document.aspectRatio.value, 8); - } - - if (updateAspectRatio || !state.document.aspectRatio.isLocked) { - state.document.aspectRatio.value = state.document.rect.width / state.document.rect.height; - state.document.aspectRatio.id = 'Free'; - state.document.aspectRatio.isLocked = false; - } - - if (!state.session.isActive) { - state.bbox.rect.width = state.document.rect.width; - state.bbox.rect.height = state.document.rect.height; - - if (state.initialImage.imageObject) { - state.initialImage.imageObject.width = state.document.rect.width; - state.initialImage.imageObject.height = state.document.rect.height; - } - } - }, - documentHeightChanged: ( - state, - action: PayloadAction<{ height: number; updateAspectRatio?: boolean; clamp?: boolean }> - ) => { - const { height, updateAspectRatio, clamp } = action.payload; - - state.document.rect.height = clamp ? Math.max(roundDownToMultiple(height, 8), 64) : height; - - if (state.document.aspectRatio.isLocked) { - state.document.rect.width = roundToMultiple(state.document.rect.height * state.document.aspectRatio.value, 8); - } - - if (updateAspectRatio || !state.document.aspectRatio.isLocked) { - state.document.aspectRatio.value = state.document.rect.width / state.document.rect.height; - state.document.aspectRatio.id = 'Free'; - state.document.aspectRatio.isLocked = false; - } - - if (!state.session.isActive) { - state.bbox.rect.width = state.document.rect.width; - state.bbox.rect.height = state.document.rect.height; - - if (state.initialImage.imageObject) { - state.initialImage.imageObject.width = state.document.rect.width; - state.initialImage.imageObject.height = state.document.rect.height; - } - } - }, - documentAspectRatioLockToggled: (state) => { - state.document.aspectRatio.isLocked = !state.document.aspectRatio.isLocked; - }, - documentAspectRatioIdChanged: (state, action: PayloadAction<{ id: AspectRatioID }>) => { - const { id } = action.payload; - state.document.aspectRatio.id = id; - if (id === 'Free') { - state.document.aspectRatio.isLocked = false; - } else { - state.document.aspectRatio.isLocked = true; - state.document.aspectRatio.value = ASPECT_RATIO_MAP[id].ratio; - const { width, height } = calculateNewSize( - state.document.aspectRatio.value, - state.document.rect.width * state.document.rect.height - ); - state.document.rect.width = width; - state.document.rect.height = height; - } - if (!state.session.isActive) { - state.bbox.rect.width = state.document.rect.width; - state.bbox.rect.height = state.document.rect.height; - - if (state.initialImage.imageObject) { - state.initialImage.imageObject.width = state.document.rect.width; - state.initialImage.imageObject.height = state.document.rect.height; - } - } - }, - documentDimensionsSwapped: (state) => { - state.document.aspectRatio.value = 1 / state.document.aspectRatio.value; - if (state.document.aspectRatio.id === 'Free') { - const newWidth = state.document.rect.height; - const newHeight = state.document.rect.width; - state.document.rect.width = newWidth; - state.document.rect.height = newHeight; - } else { - const { width, height } = calculateNewSize( - state.document.aspectRatio.value, - state.document.rect.width * state.document.rect.height - ); - state.document.rect.width = width; - state.document.rect.height = height; - state.document.aspectRatio.id = ASPECT_RATIO_MAP[state.document.aspectRatio.id].inverseID; - } - if (!state.session.isActive) { - state.bbox.rect.width = state.document.rect.width; - state.bbox.rect.height = state.document.rect.height; - - if (state.initialImage.imageObject) { - state.initialImage.imageObject.width = state.document.rect.width; - state.initialImage.imageObject.height = state.document.rect.height; - } - } - }, - documentSizeOptimized: (state) => { - const optimalDimension = getOptimalDimension(state.params.model); - if (state.document.aspectRatio.isLocked) { - const { width, height } = calculateNewSize(state.document.aspectRatio.value, optimalDimension ** 2); - state.document.rect.width = width; - state.document.rect.height = height; - } else { - state.document.aspectRatio = deepClone(initialAspectRatioState); - state.document.rect.width = optimalDimension; - state.document.rect.height = optimalDimension; - } - if (!state.session.isActive) { - state.bbox.rect.width = state.document.rect.width; - state.bbox.rect.height = state.document.rect.height; - - if (state.initialImage.imageObject) { - state.initialImage.imageObject.width = state.document.rect.width; - state.initialImage.imageObject.height = state.document.rect.height; - } - } - }, -} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts index 6cf8fc68c3d..548833587ed 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts @@ -62,8 +62,8 @@ export const paramsReducers = { // Update the bbox size to match the new model's optimal size // TODO(psyche): Should we change the document size too? const optimalDimension = getOptimalDimension(model); - if (!getIsSizeOptimal(state.document.rect.width, state.document.rect.height, optimalDimension)) { - const bboxDims = calculateNewSize(state.document.aspectRatio.value, optimalDimension * optimalDimension); + if (!getIsSizeOptimal(state.bbox.rect.width, state.bbox.rect.height, optimalDimension)) { + const bboxDims = calculateNewSize(state.bbox.aspectRatio.value, optimalDimension * optimalDimension); state.bbox.rect.width = bboxDims.width; state.bbox.rect.height = bboxDims.height; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 6a1ea536f79..0a6f21c2e7e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -847,15 +847,6 @@ export type CanvasV2State = { eraser: { width: number }; fill: RgbaColor; }; - document: { - rect: { - x: number; - y: number; - width: ParameterWidth; - height: ParameterHeight; - }; - aspectRatio: AspectRatioState; - }; settings: { imageSmoothing: boolean; maskOpacity: number; @@ -872,6 +863,7 @@ export type CanvasV2State = { width: ParameterWidth; height: ParameterHeight; }; + aspectRatio: AspectRatioState; scaledSize: { width: ParameterWidth; height: ParameterHeight; diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index de9efe32ce3..94d19c57a89 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -11,9 +11,9 @@ import { getRGId, } from 'features/controlLayers/konva/naming'; import { + bboxHeightChanged, + bboxWidthChanged, caRecalled, - documentHeightChanged, - documentWidthChanged, ipaRecalled, layerAllDeleted, layerRecalled, @@ -115,11 +115,11 @@ const recallScheduler: MetadataRecallFunc = (scheduler) => { const setSizeOptions = { updateAspectRatio: true, clamp: true }; const recallWidth: MetadataRecallFunc = (width) => { - getStore().dispatch(documentWidthChanged({ width, ...setSizeOptions })); + getStore().dispatch(bboxWidthChanged({ width, ...setSizeOptions })); }; const recallHeight: MetadataRecallFunc = (height) => { - getStore().dispatch(documentHeightChanged({ height, ...setSizeOptions })); + getStore().dispatch(bboxHeightChanged({ height, ...setSizeOptions })); }; const recallSteps: MetadataRecallFunc = (steps) => { diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx index d9871bc78fc..5d35387fd87 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx @@ -1,6 +1,6 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { scaledBboxChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { bboxScaledSizeChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -20,7 +20,7 @@ const ParamScaledHeight = () => { const onChange = useCallback( (height: number) => { - dispatch(scaledBboxChanged({ height })); + dispatch(bboxScaledSizeChanged({ height })); }, [dispatch] ); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx index 6f5338c9ef0..7c92e2a7dc7 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx @@ -1,6 +1,6 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { scaledBboxChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { bboxScaledSizeChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -19,7 +19,7 @@ const ParamScaledWidth = () => { const fineStep = useAppSelector((s) => s.config.sd.scaledBoundingBoxWidth.fineStep); const onChange = useCallback( (width: number) => { - dispatch(scaledBboxChanged({ width })); + dispatch(bboxScaledSizeChanged({ width })); }, [dispatch] ); diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx index dbf8d9346a9..93fa1319b64 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { documentHeightChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { bboxHeightChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -10,7 +10,7 @@ export const ParamHeight = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const optimalDimension = useAppSelector(selectOptimalDimension); - const height = useAppSelector((s) => s.canvasV2.document.rect.height); + const height = useAppSelector((s) => s.canvasV2.bbox.rect.height); const sliderMin = useAppSelector((s) => s.config.sd.height.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.height.sliderMax); const numberInputMin = useAppSelector((s) => s.config.sd.height.numberInputMin); @@ -20,7 +20,7 @@ export const ParamHeight = memo(() => { const onChange = useCallback( (v: number) => { - dispatch(documentHeightChanged({ height: v })); + dispatch(bboxHeightChanged({ height: v })); }, [dispatch] ); diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamWidth.tsx index ddf4975fc95..66dc071d639 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamWidth.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamWidth.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { documentWidthChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { bboxWidthChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next'; export const ParamWidth = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const width = useAppSelector((s) => s.canvasV2.document.rect.width); + const width = useAppSelector((s) => s.canvasV2.bbox.rect.width); const optimalDimension = useAppSelector(selectOptimalDimension); const sliderMin = useAppSelector((s) => s.config.sd.width.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.width.sliderMax); @@ -20,7 +20,7 @@ export const ParamWidth = memo(() => { const onChange = useCallback( (v: number) => { - dispatch(documentWidthChanged({ width: v })); + dispatch(bboxWidthChanged({ width: v })); }, [dispatch] ); diff --git a/invokeai/frontend/web/src/features/parameters/components/DocumentSize/AspectRatioIconPreview.tsx b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/AspectRatioIconPreview.tsx index 9b8af7e16e7..df7c54c7a7c 100644 --- a/invokeai/frontend/web/src/features/parameters/components/DocumentSize/AspectRatioIconPreview.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/AspectRatioIconPreview.tsx @@ -16,13 +16,13 @@ import { } from './constants'; export const AspectRatioIconPreview = memo(() => { - const document = useAppSelector((s) => s.canvasV2.document); + const bbox = useAppSelector((s) => s.canvasV2.bbox); const containerRef = useRef(null); const containerSize = useSize(containerRef); const shouldShowIcon = useMemo( - () => document.aspectRatio.value < ICON_HIGH_CUTOFF && document.aspectRatio.value > ICON_LOW_CUTOFF, - [document.aspectRatio.value] + () => bbox.aspectRatio.value < ICON_HIGH_CUTOFF && bbox.aspectRatio.value > ICON_LOW_CUTOFF, + [bbox.aspectRatio.value] ); const { width, height } = useMemo(() => { @@ -30,19 +30,19 @@ export const AspectRatioIconPreview = memo(() => { return { width: 0, height: 0 }; } - let width = document.rect.width; - let height = document.rect.height; + let width = bbox.rect.width; + let height = bbox.rect.height; - if (document.rect.width > document.rect.height) { + if (bbox.rect.width > bbox.rect.height) { width = containerSize.width; - height = width / document.aspectRatio.value; + height = width / bbox.aspectRatio.value; } else { height = containerSize.height; - width = height * document.aspectRatio.value; + width = height * bbox.aspectRatio.value; } return { width, height }; - }, [containerSize, document.rect.width, document.rect.height, document.aspectRatio.value]); + }, [containerSize, bbox.rect.width, bbox.rect.height, bbox.aspectRatio.value]); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/DocumentSize/AspectRatioSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/AspectRatioSelect.tsx index 92256b3b3f2..f2c3fc66dea 100644 --- a/invokeai/frontend/web/src/features/parameters/components/DocumentSize/AspectRatioSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/AspectRatioSelect.tsx @@ -3,7 +3,7 @@ import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import type { SingleValue } from 'chakra-react-select'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { documentAspectRatioIdChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { bboxAspectRatioIdChanged } from 'features/controlLayers/store/canvasV2Slice'; import { ASPECT_RATIO_OPTIONS } from 'features/parameters/components/DocumentSize/constants'; import { isAspectRatioID } from 'features/parameters/components/DocumentSize/types'; import { memo, useCallback, useMemo } from 'react'; @@ -12,14 +12,14 @@ import { useTranslation } from 'react-i18next'; export const AspectRatioSelect = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const id = useAppSelector((s) => s.canvasV2.document.aspectRatio.id); + const id = useAppSelector((s) => s.canvasV2.bbox.aspectRatio.id); const onChange = useCallback( (v: SingleValue) => { if (!v || !isAspectRatioID(v.value)) { return; } - dispatch(documentAspectRatioIdChanged({ id: v.value })); + dispatch(bboxAspectRatioIdChanged({ id: v.value })); }, [dispatch] ); diff --git a/invokeai/frontend/web/src/features/parameters/components/DocumentSize/LockAspectRatioButton.tsx b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/LockAspectRatioButton.tsx index 1d239d201f1..47846ad8a03 100644 --- a/invokeai/frontend/web/src/features/parameters/components/DocumentSize/LockAspectRatioButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/LockAspectRatioButton.tsx @@ -1,6 +1,6 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { documentAspectRatioLockToggled } from 'features/controlLayers/store/canvasV2Slice'; +import { bboxAspectRatioLockToggled } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiLockSimpleFill, PiLockSimpleOpenBold } from 'react-icons/pi'; @@ -8,9 +8,9 @@ import { PiLockSimpleFill, PiLockSimpleOpenBold } from 'react-icons/pi'; export const LockAspectRatioButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const isLocked = useAppSelector((s) => s.canvasV2.document.aspectRatio.isLocked); + const isLocked = useAppSelector((s) => s.canvasV2.bbox.aspectRatio.isLocked); const onClick = useCallback(() => { - dispatch(documentAspectRatioLockToggled()); + dispatch(bboxAspectRatioLockToggled()); }, [dispatch]); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/DocumentSize/SetOptimalSizeButton.tsx b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/SetOptimalSizeButton.tsx index a5e88513f8a..377ae4d4b24 100644 --- a/invokeai/frontend/web/src/features/parameters/components/DocumentSize/SetOptimalSizeButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/SetOptimalSizeButton.tsx @@ -1,6 +1,6 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { documentSizeOptimized } from 'features/controlLayers/store/canvasV2Slice'; +import { bboxSizeOptimized } from 'features/controlLayers/store/canvasV2Slice'; import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { getIsSizeTooLarge, getIsSizeTooSmall } from 'features/parameters/util/optimalDimension'; import { memo, useCallback, useMemo } from 'react'; @@ -10,8 +10,8 @@ import { RiSparklingFill } from 'react-icons/ri'; export const SetOptimalSizeButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const width = useAppSelector((s) => s.canvasV2.document.rect.width); - const height = useAppSelector((s) => s.canvasV2.document.rect.height); + const width = useAppSelector((s) => s.canvasV2.bbox.rect.width); + const height = useAppSelector((s) => s.canvasV2.bbox.rect.height); const optimalDimension = useAppSelector(selectOptimalDimension); const isSizeTooSmall = useMemo( () => getIsSizeTooSmall(width, height, optimalDimension), @@ -22,7 +22,7 @@ export const SetOptimalSizeButton = memo(() => { [height, width, optimalDimension] ); const onClick = useCallback(() => { - dispatch(documentSizeOptimized()); + dispatch(bboxSizeOptimized()); }, [dispatch]); const tooltip = useMemo(() => { if (isSizeTooSmall) { diff --git a/invokeai/frontend/web/src/features/parameters/components/DocumentSize/SwapDimensionsButton.tsx b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/SwapDimensionsButton.tsx index bc57ca3f51e..f4f586c0f82 100644 --- a/invokeai/frontend/web/src/features/parameters/components/DocumentSize/SwapDimensionsButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/SwapDimensionsButton.tsx @@ -1,6 +1,6 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; -import { documentDimensionsSwapped } from 'features/controlLayers/store/canvasV2Slice'; +import { bboxDimensionsSwapped } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowsDownUpBold } from 'react-icons/pi'; @@ -9,7 +9,7 @@ export const SwapDimensionsButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const onClick = useCallback(() => { - dispatch(documentDimensionsSwapped()); + dispatch(bboxDimensionsSwapped()); }, [dispatch]); return ( Date: Tue, 16 Jul 2024 17:14:54 +1000 Subject: [PATCH 205/678] fix(ui): restore nodes output tracking --- .../socketio/socketInvocationComplete.ts | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts index 0ba7fa1668f..279f868d9ff 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts @@ -28,6 +28,19 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi ); const { result, invocation_source_id } = data; + + if (data.origin === 'workflows') { + const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]); + if (nes) { + nes.status = zNodeStatus.enum.COMPLETED; + if (nes.progress !== null) { + nes.progress = 1; + } + nes.outputs.push(result); + upsertExecutionState(nes.nodeId, nes); + } + } + // This complete event has an associated image output if (data.result.type === 'image_output' && !nodeTypeDenylist.includes(data.invocation.type)) { const { image_name } = data.result.image; @@ -50,16 +63,6 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi } else if (!canvasV2.session.isActive) { $lastProgressEvent.set(null); } - } else if (data.origin === 'workflows') { - const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]); - if (nes) { - nes.status = zNodeStatus.enum.COMPLETED; - if (nes.progress !== null) { - nes.progress = 1; - } - nes.outputs.push(result); - upsertExecutionState(nes.nodeId, nes); - } } if (!imageDTO.is_intermediate) { From fca9cacc4e6588855283b02b8ba2b43724fefe1d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 16 Jul 2024 18:33:33 +1000 Subject: [PATCH 206/678] feat(app): add CanvasV2MaskAndCropInvocation & CanvasV2MaskAndCropOutput This handles some masking and cropping that the canvas needs. --- invokeai/app/invocations/image.py | 53 ++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index a551f8df8a4..beaf31f8ecc 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -6,13 +6,19 @@ import numpy from PIL import Image, ImageChops, ImageFilter, ImageOps -from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation +from invokeai.app.invocations.baseinvocation import ( + BaseInvocation, + Classification, + invocation, + invocation_output, +) from invokeai.app.invocations.constants import IMAGE_MODES from invokeai.app.invocations.fields import ( ColorField, FieldDescriptions, ImageField, InputField, + OutputField, WithBoard, WithMetadata, ) @@ -1007,3 +1013,48 @@ def invoke(self, context: InvocationContext) -> ImageOutput: image_dto = context.images.save(image=mask, image_category=ImageCategory.MASK) return ImageOutput.build(image_dto) + + +@invocation_output("canvas_v2_mask_and_crop_output") +class CanvasV2MaskAndCropOutput(ImageOutput): + x: int = OutputField(description="The x coordinate of the image") + y: int = OutputField(description="The y coordinate of the image") + + +@invocation( + "canvas_v2_mask_and_crop", + title="Canvas V2 Mask and Crop", + tags=["image", "mask", "id"], + category="image", + version="1.0.0", +) +class CanvasV2MaskAndCropInvocation(BaseInvocation, WithMetadata, WithBoard): + """Apply a mask to an image""" + + image: ImageField = InputField(description="The image to apply the mask to") + mask: ImageField = InputField(description="The mask to apply") + invert: bool = InputField(default=False, description="Whether or not to invert the mask") + crop_visible: bool = InputField(default=False, description="Crop the image to the mask") + + def invoke(self, context: InvocationContext) -> CanvasV2MaskAndCropOutput: + image = context.images.get_pil(self.image.image_name) + mask = context.images.get_pil(self.mask.image_name) + + if self.invert: + mask = ImageOps.invert(mask) + + image.putalpha(mask) + bbox = image.getbbox() + + if self.crop_visible: + image = image.crop(bbox) + + image_dto = context.images.save(image=image) + + return CanvasV2MaskAndCropOutput( + image=ImageField(image_name=image_dto.image_name), + x=bbox[0], + y=bbox[1], + width=image.width, + height=image.height, + ) From df44fb9827bcb32afa252620b00eb06898d1305c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 16 Jul 2024 18:33:40 +1000 Subject: [PATCH 207/678] chore(ui): typegen --- .../frontend/web/src/services/api/schema.ts | 288 +++++++++++++++++- 1 file changed, 279 insertions(+), 9 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index a96f3716d99..97845b266e4 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -300,6 +300,50 @@ export type paths = { patch?: never; trace?: never; }; + "/api/v2/models/model_cache": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get maximum size of model manager RAM or VRAM cache. + * @description Return the current RAM or VRAM cache size setting (in GB). + */ + get: operations["get_cache_size"]; + /** + * Set maximum size of model manager RAM or VRAM cache, optionally writing new value out to invokeai.yaml config file. + * @description Set the current RAM or VRAM cache size setting (in GB). . + */ + put: operations["set_cache_size"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v2/models/stats": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get model manager RAM cache performance statistics. + * @description Return performance statistics on the model manager's RAM cache. Will return null if no models have been loaded. + */ + get: operations["get_stats"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/download_queue/": { parameters: { query?: never; @@ -2741,6 +2785,49 @@ export type components = { */ type: "infill_cv2"; }; + /** CacheStats */ + CacheStats: { + /** + * Hits + * @default 0 + */ + hits?: number; + /** + * Misses + * @default 0 + */ + misses?: number; + /** + * High Watermark + * @default 0 + */ + high_watermark?: number; + /** + * In Cache + * @default 0 + */ + in_cache?: number; + /** + * Cleared + * @default 0 + */ + cleared?: number; + /** + * Cache Size + * @default 0 + */ + cache_size?: number; + /** Loaded Model Sizes */ + loaded_model_sizes?: { + [key: string]: number; + }; + }; + /** + * CacheType + * @description Cache type - one of vram or ram. + * @enum {string} + */ + CacheType: "RAM" | "VRAM"; /** * Calculate Image Tiles Even Split * @description Calculate the coordinates and overlaps of tiles that cover a target image shape. @@ -3088,6 +3175,100 @@ export type components = { */ type: "canvas_paste_back"; }; + /** + * Canvas V2 Mask and Crop + * @description Apply a mask to an image + */ + CanvasV2MaskAndCropInvocation: { + /** + * @description The board to save the image to + * @default null + */ + board?: components["schemas"]["BoardField"] | null; + /** + * @description Optional metadata to be saved with the image + * @default null + */ + metadata?: components["schemas"]["MetadataField"] | null; + /** + * Id + * @description The id of this instance of an invocation. Must be unique among all instances of invocations. + */ + id: string; + /** + * Is Intermediate + * @description Whether or not this is an intermediate invocation. + * @default false + */ + is_intermediate?: boolean; + /** + * Use Cache + * @description Whether or not to use the cache + * @default true + */ + use_cache?: boolean; + /** + * @description The image to apply the mask to + * @default null + */ + image?: components["schemas"]["ImageField"]; + /** + * @description The mask to apply + * @default null + */ + mask?: components["schemas"]["ImageField"]; + /** + * Invert + * @description Whether or not to invert the mask + * @default false + */ + invert?: boolean; + /** + * Crop Visible + * @description Crop the image to the mask + * @default false + */ + crop_visible?: boolean; + /** + * type + * @default canvas_v2_mask_and_crop + * @constant + * @enum {string} + */ + type: "canvas_v2_mask_and_crop"; + }; + /** CanvasV2MaskAndCropOutput */ + CanvasV2MaskAndCropOutput: { + /** @description The output image */ + image: components["schemas"]["ImageField"]; + /** + * Width + * @description The width of the image in pixels + */ + width: number; + /** + * Height + * @description The height of the image in pixels + */ + height: number; + /** + * type + * @default canvas_v2_mask_and_crop_output + * @constant + * @enum {string} + */ + type: "canvas_v2_mask_and_crop_output"; + /** + * X + * @description The x coordinate of the image + */ + x: number; + /** + * Y + * @description The y coordinate of the image + */ + y: number; + }; /** * Center Pad or Crop Image * @description Pad or crop an image's sides from the center by specified pixels. Positive values are outside of the image. @@ -6032,7 +6213,7 @@ export type components = { type: "flux_text_encoder"; }; /** - * FLUX VAE Decode + * FLUX Latents to Image * @description Generates an image from latents. */ FluxVaeDecodeInvocation: { @@ -6082,7 +6263,7 @@ export type components = { type: "flux_vae_decode"; }; /** - * FLUX VAE Encode + * FLUX Image to Latents * @description Encodes an image into latents. */ FluxVaeEncodeInvocation: { @@ -6255,7 +6436,7 @@ export type components = { * @description The nodes in this graph */ nodes?: { - [key: string]: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"]; + [key: string]: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"]; }; /** * Edges @@ -6292,7 +6473,7 @@ export type components = { * @description The results of node executions */ results?: { - [key: string]: components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["BoundingBoxCollectionOutput"] | components["schemas"]["BoundingBoxOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["CLIPSkipInvocationOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["FluxConditioningOutput"] | components["schemas"]["FluxModelLoaderOutput"] | components["schemas"]["GradientMaskOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["LoRALoaderOutput"] | components["schemas"]["LoRASelectorOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["ModelIdentifierOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["SDXLLoRALoaderOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["String2Output"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["VAEOutput"]; + [key: string]: components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["BoundingBoxCollectionOutput"] | components["schemas"]["BoundingBoxOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["CLIPSkipInvocationOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["CanvasV2MaskAndCropOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["FluxConditioningOutput"] | components["schemas"]["FluxModelLoaderOutput"] | components["schemas"]["GradientMaskOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["LoRALoaderOutput"] | components["schemas"]["LoRASelectorOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["ModelIdentifierOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["SDXLLoRALoaderOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["String2Output"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["VAEOutput"]; }; /** * Errors @@ -8682,7 +8863,7 @@ export type components = { * Invocation * @description The ID of the invocation */ - invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"]; + invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"]; /** * Invocation Source Id * @description The ID of the prepared invocation's source node @@ -8692,7 +8873,7 @@ export type components = { * Result * @description The result of the invocation */ - result: components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["BoundingBoxCollectionOutput"] | components["schemas"]["BoundingBoxOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["CLIPSkipInvocationOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["FluxConditioningOutput"] | components["schemas"]["FluxModelLoaderOutput"] | components["schemas"]["GradientMaskOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["LoRALoaderOutput"] | components["schemas"]["LoRASelectorOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["ModelIdentifierOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["SDXLLoRALoaderOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["String2Output"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["VAEOutput"]; + result: components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["BoundingBoxCollectionOutput"] | components["schemas"]["BoundingBoxOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["CLIPSkipInvocationOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["CanvasV2MaskAndCropOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["FluxConditioningOutput"] | components["schemas"]["FluxModelLoaderOutput"] | components["schemas"]["GradientMaskOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["LoRALoaderOutput"] | components["schemas"]["LoRASelectorOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["ModelIdentifierOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["SDXLLoRALoaderOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["String2Output"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["VAEOutput"]; }; /** * InvocationDenoiseProgressEvent @@ -8733,7 +8914,7 @@ export type components = { * Invocation * @description The ID of the invocation */ - invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"]; + invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"]; /** * Invocation Source Id * @description The ID of the prepared invocation's source node @@ -8801,7 +8982,7 @@ export type components = { * Invocation * @description The ID of the invocation */ - invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"]; + invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"]; /** * Invocation Source Id * @description The ID of the prepared invocation's source node @@ -8847,6 +9028,7 @@ export type components = { calculate_image_tiles_min_overlap: components["schemas"]["CalculateImageTilesOutput"]; canny_image_processor: components["schemas"]["ImageOutput"]; canvas_paste_back: components["schemas"]["ImageOutput"]; + canvas_v2_mask_and_crop: components["schemas"]["CanvasV2MaskAndCropOutput"]; clip_skip: components["schemas"]["CLIPSkipInvocationOutput"]; collect: components["schemas"]["CollectInvocationOutput"]; color: components["schemas"]["ColorOutput"]; @@ -9026,7 +9208,7 @@ export type components = { * Invocation * @description The ID of the invocation */ - invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"]; + invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"]; /** * Invocation Source Id * @description The ID of the prepared invocation's source node @@ -16691,6 +16873,94 @@ export interface operations { }; }; }; + get_cache_size: { + parameters: { + query?: { + /** @description The cache type */ + cache_type?: components["schemas"]["CacheType"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + set_cache_size: { + parameters: { + query: { + /** @description The new value for the maximum cache size */ + value: number; + /** @description The cache type */ + cache_type?: components["schemas"]["CacheType"]; + /** @description Write new value out to invokeai.yaml */ + persist?: boolean; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_stats: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CacheStats"] | null; + }; + }; + }; + }; list_downloads: { parameters: { query?: never; From f480a89e7acca80e55321f5f43bf57e37229342a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 16 Jul 2024 18:33:50 +1000 Subject: [PATCH 208/678] feat(ui): use new canvas output node --- .../addCommitStagingAreaImageListener.ts | 25 ++++++++---- .../listeners/enqueueRequestedLinear.ts | 4 +- .../socketio/socketInvocationComplete.ts | 11 ++++-- .../StagingArea/StagingAreaToolbar.tsx | 30 +++++++-------- .../controlLayers/konva/CanvasStagingArea.ts | 38 ++++++++++--------- .../controlLayers/store/canvasV2Slice.ts | 6 ++- .../controlLayers/store/sessionReducers.ts | 27 ++++--------- .../src/features/controlLayers/store/types.ts | 7 +++- .../nodes/util/graph/generation/addInpaint.ts | 22 +++++------ .../util/graph/generation/addOutpaint.ts | 22 +++++------ .../util/graph/generation/buildSD1Graph.ts | 2 +- .../util/graph/generation/buildSDXLGraph.ts | 2 +- 12 files changed, 104 insertions(+), 92 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts index a15a1efb140..c6efd494e9d 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts @@ -1,11 +1,10 @@ -import { isAnyOf } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { layerAdded, layerImageAdded, - sessionStagedImageAccepted, - sessionStagingCanceled, + sessionStagingAreaImageAccepted, + sessionStagingAreaReset, } from 'features/controlLayers/store/canvasV2Slice'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; @@ -14,7 +13,7 @@ import { assert } from 'tsafe'; export const addStagingListeners = (startAppListening: AppStartListening) => { startAppListening({ - matcher: isAnyOf(sessionStagingCanceled, sessionStagedImageAccepted), + actionCreator: sessionStagingAreaReset, effect: async (_, { dispatch }) => { const log = logger('canvas'); @@ -47,10 +46,10 @@ export const addStagingListeners = (startAppListening: AppStartListening) => { }); startAppListening({ - actionCreator: sessionStagedImageAccepted, + actionCreator: sessionStagingAreaImageAccepted, effect: async (action, api) => { - const { imageDTO } = action.payload; - const { layers, selectedEntityIdentifier, bbox } = api.getState().canvasV2; + const { index } = action.payload; + const { layers, selectedEntityIdentifier } = api.getState().canvasV2; let layer = layers.entities.find((layer) => layer.id === selectedEntityIdentifier?.id); if (!layer) { @@ -63,11 +62,21 @@ export const addStagingListeners = (startAppListening: AppStartListening) => { layer = api.getState().canvasV2.layers.entities[0]; } + const stagedImage = api.getState().canvasV2.session.stagedImages[index]; + + assert(stagedImage, 'No staged image found to accept'); assert(layer, 'No layer found to stage image'); const { id } = layer; - api.dispatch(layerImageAdded({ id, imageDTO, pos: { x: bbox.rect.x - layer.x, y: bbox.rect.y - layer.y } })); + api.dispatch( + layerImageAdded({ + id, + imageDTO: stagedImage.imageDTO, + pos: { x: stagedImage.rect.x - layer.x, y: stagedImage.rect.y - layer.y }, + }) + ); + api.dispatch(sessionStagingAreaReset()); }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index 74425a4600b..6d53d3a84d3 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -1,7 +1,7 @@ import { enqueueRequested } from 'app/store/actions'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { getCanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { sessionStagingCanceled, sessionStartedStaging } from 'features/controlLayers/store/canvasV2Slice'; +import { sessionStagingAreaReset, sessionStartedStaging } from 'features/controlLayers/store/canvasV2Slice'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph'; import { buildSDXLGraph } from 'features/nodes/util/graph/generation/buildSDXLGraph'; @@ -49,7 +49,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) await req.unwrap(); } catch { if (didStartStaging && getState().canvasV2.session.isStaging) { - dispatch(sessionStagingCanceled()); + dispatch(sessionStagingAreaReset()); } } }, diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts index 279f868d9ff..0e3cffe22b4 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts @@ -6,7 +6,6 @@ import { $lastProgressEvent, sessionImageStaged } from 'features/controlLayers/s import { boardIdSelected, galleryViewChanged, imageSelected, offsetChanged } from 'features/gallery/store/gallerySlice'; import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; import { zNodeStatus } from 'features/nodes/types/invocation'; -import { CANVAS_OUTPUT } from 'features/nodes/util/graph/constants'; import { boardsApi } from 'services/api/endpoints/boards'; import { imagesApi } from 'services/api/endpoints/images'; import { getCategories, getListImagesUrl } from 'services/api/util'; @@ -42,7 +41,10 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi } // This complete event has an associated image output - if (data.result.type === 'image_output' && !nodeTypeDenylist.includes(data.invocation.type)) { + if ( + (data.result.type === 'image_output' || data.result.type === 'canvas_v2_mask_and_crop_output') && + !nodeTypeDenylist.includes(data.invocation.type) + ) { const { image_name } = data.result.image; const { gallery, canvasV2 } = getState(); @@ -57,9 +59,10 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi imageDTORequest.unsubscribe(); // handle tab-specific logic - if (data.origin === 'canvas' && data.invocation_source_id === CANVAS_OUTPUT) { + if (data.origin === 'canvas' && data.result.type === 'canvas_v2_mask_and_crop_output') { + const { x, y, width, height } = data.result; if (canvasV2.session.isStaging) { - dispatch(sessionImageStaged({ imageDTO })); + dispatch(sessionImageStaged({ imageDTO, rect: { x, y, width, height } })); } else if (!canvasV2.session.isActive) { $lastProgressEvent.set(null); } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx index 62b243157c5..ed2408cc7e7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx @@ -3,11 +3,11 @@ import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { $shouldShowStagedImage, - sessionStagingCanceled, - sessionStagedImageAccepted, - sessionStagedImageDiscarded, sessionNextStagedImageSelected, sessionPrevStagedImageSelected, + sessionStagedImageDiscarded, + sessionStagingAreaImageAccepted, + sessionStagingAreaReset, } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; @@ -40,7 +40,7 @@ export const StagingAreaToolbarContent = memo(() => { const stagingArea = useAppSelector((s) => s.canvasV2.session); const shouldShowStagedImage = useStore($shouldShowStagedImage); const images = useMemo(() => stagingArea.stagedImages, [stagingArea]); - const selectedImageDTO = useMemo(() => { + const selectedImage = useMemo(() => { return images[stagingArea.selectedStagedImageIndex] ?? null; }, [images, stagingArea.selectedStagedImageIndex]); @@ -57,25 +57,25 @@ export const StagingAreaToolbarContent = memo(() => { }, [dispatch]); const onAccept = useCallback(() => { - if (!selectedImageDTO) { + if (!selectedImage) { return; } - dispatch(sessionStagedImageAccepted({ imageDTO: selectedImageDTO })); - }, [dispatch, selectedImageDTO]); + dispatch(sessionStagingAreaImageAccepted({ index: stagingArea.selectedStagedImageIndex })); + }, [dispatch, selectedImage, stagingArea.selectedStagedImageIndex]); const onDiscardOne = useCallback(() => { - if (!selectedImageDTO) { + if (!selectedImage) { return; } if (images.length === 1) { - dispatch(sessionStagingCanceled()); + dispatch(sessionStagingAreaReset()); } else { - dispatch(sessionStagedImageDiscarded({ imageDTO: selectedImageDTO })); + dispatch(sessionStagedImageDiscarded({ index: stagingArea.selectedStagedImageIndex })); } - }, [dispatch, selectedImageDTO, images.length]); + }, [selectedImage, images.length, dispatch, stagingArea.selectedStagedImageIndex]); const onDiscardAll = useCallback(() => { - dispatch(sessionStagingCanceled()); + dispatch(sessionStagingAreaReset()); }, [dispatch]); const onToggleShouldShowStagedImage = useCallback(() => { @@ -145,7 +145,7 @@ export const StagingAreaToolbarContent = memo(() => { icon={} onClick={onAccept} colorScheme="invokeBlue" - isDisabled={!selectedImageDTO} + isDisabled={!selectedImage} /> { icon={} onClick={onSaveStagingImage} colorScheme="invokeBlue" - isDisabled={!selectedImageDTO || !selectedImageDTO.is_intermediate} + isDisabled={!selectedImage || !selectedImage.imageDTO.is_intermediate} /> { onClick={onDiscardOne} colorScheme="invokeBlue" fontSize={16} - isDisabled={!selectedImageDTO} + isDisabled={!selectedImage} /> { - if (this.imageDTO) { - konvaImage.width(this.imageDTO.width); - konvaImage.height(this.imageDTO.height); + if (this.selectedImage) { + konvaImage.width(this.selectedImage.rect.width); + konvaImage.height(this.selectedImage.rect.height); } this.manager.stateApi.resetLastProgressEvent(); this.image?.konvaImageGroup.visible(shouldShowStagedImage); @@ -60,7 +64,7 @@ export class CanvasStagingArea { } ); this.group.add(this.image.konvaImageGroup); - await this.image.updateImageSource(this.imageDTO.image_name); + await this.image.updateImageSource(this.selectedImage.imageDTO.image_name); this.image.konvaImageGroup.visible(shouldShowStagedImage); } } else { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 22accd05c11..b96fa09da0c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -339,8 +339,7 @@ export const { sessionStartedStaging, sessionImageStaged, sessionStagedImageDiscarded, - sessionStagedImageAccepted, - sessionStagingCanceled, + sessionStagingAreaReset, sessionNextStagedImageSelected, sessionPrevStagedImageSelected, // Initial image @@ -383,3 +382,6 @@ export const canvasV2PersistConfig: PersistConfig = { }; export const sessionRequested = createAction(`${canvasV2Slice.name}/sessionRequested`); +export const sessionStagingAreaImageAccepted = createAction<{ index: number }>( + `${canvasV2Slice.name}/sessionStagingAreaImageAccepted` +); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts index 77337938510..8b6d240f094 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts @@ -1,6 +1,5 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import type { CanvasV2State } from 'features/controlLayers/store/types'; -import type { ImageDTO } from 'services/api/types'; +import type { CanvasV2State, StagingAreaImage } from 'features/controlLayers/store/types'; export const sessionReducers = { sessionStarted: (state) => { @@ -15,9 +14,9 @@ export const sessionReducers = { state.tool.selectedBuffer = state.tool.selected; state.tool.selected = 'view'; }, - sessionImageStaged: (state, action: PayloadAction<{ imageDTO: ImageDTO }>) => { - const { imageDTO } = action.payload; - state.session.stagedImages.push(imageDTO); + sessionImageStaged: (state, action: PayloadAction) => { + const { imageDTO, rect } = action.payload; + state.session.stagedImages.push({ imageDTO, rect }); state.session.selectedStagedImageIndex = state.session.stagedImages.length - 1; }, sessionNextStagedImageSelected: (state) => { @@ -29,9 +28,9 @@ export const sessionReducers = { (state.session.selectedStagedImageIndex - 1 + state.session.stagedImages.length) % state.session.stagedImages.length; }, - sessionStagedImageDiscarded: (state, action: PayloadAction<{ imageDTO: ImageDTO }>) => { - const { imageDTO } = action.payload; - state.session.stagedImages = state.session.stagedImages.filter((image) => image.image_name !== imageDTO.image_name); + sessionStagedImageDiscarded: (state, action: PayloadAction<{ index: number }>) => { + const { index } = action.payload; + state.session.stagedImages = state.session.stagedImages.splice(index, 1); state.session.selectedStagedImageIndex = Math.min( state.session.selectedStagedImageIndex, state.session.stagedImages.length - 1 @@ -40,17 +39,7 @@ export const sessionReducers = { state.session.isStaging = false; } }, - sessionStagedImageAccepted: (state, _: PayloadAction<{ imageDTO: ImageDTO }>) => { - // When we finish staging, reset the tool back to the previous selection. - state.session.isStaging = false; - state.session.stagedImages = []; - state.session.selectedStagedImageIndex = 0; - if (state.tool.selectedBuffer) { - state.tool.selected = state.tool.selectedBuffer; - state.tool.selectedBuffer = null; - } - }, - sessionStagingCanceled: (state) => { + sessionStagingAreaReset: (state) => { state.session.isStaging = false; state.session.stagedImages = []; state.session.selectedStagedImageIndex = 0; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 0a6f21c2e7e..bafc43727c7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -826,6 +826,11 @@ export type LoRA = { weight: number; }; +export type StagingAreaImage = { + imageDTO: ImageDTO; + rect: Rect; +}; + export type CanvasV2State = { _version: 3; selectedEntityIdentifier: CanvasEntityIdentifier | null; @@ -913,7 +918,7 @@ export type CanvasV2State = { session: { isActive: boolean; isStaging: boolean; - stagedImages: ImageDTO[]; + stagedImages: StagingAreaImage[]; selectedStagedImageIndex: number; }; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts index fcdd49b3ffd..ce001e44a52 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts @@ -18,7 +18,7 @@ export const addInpaint = async ( compositing: CanvasV2State['compositing'], denoising_start: number, vaePrecision: ParameterPrecision -): Promise> => { +): Promise> => { denoise.denoising_start = denoising_start; const cropBbox = pick(bbox.rect, ['x', 'y', 'width', 'height']); @@ -64,10 +64,10 @@ export const addInpaint = async ( fp32: vaePrecision === 'fp32', }); const canvasPasteBack = g.addNode({ - id: 'canvas_paste_back', - type: 'canvas_paste_back', - mask_blur: compositing.maskBlur, - source_image: { image_name: initialImage.image_name }, + id: 'canvas_v2_mask_and_crop', + type: 'canvas_v2_mask_and_crop', + invert: true, + crop_visible: true, }); // Resize initial image and mask to scaled size, feed into to gradient mask @@ -88,7 +88,7 @@ export const addInpaint = async ( g.addEdge(createGradientMask, 'expanded_mask_area', resizeMaskToOriginalSize, 'image'); // Finally, paste the generated masked image back onto the original image - g.addEdge(resizeImageToOriginalSize, 'image', canvasPasteBack, 'target_image'); + g.addEdge(resizeImageToOriginalSize, 'image', canvasPasteBack, 'image'); g.addEdge(resizeMaskToOriginalSize, 'image', canvasPasteBack, 'mask'); return canvasPasteBack; @@ -111,10 +111,10 @@ export const addInpaint = async ( image: { image_name: initialImage.image_name }, }); const canvasPasteBack = g.addNode({ - id: 'canvas_paste_back', - type: 'canvas_paste_back', - mask_blur: compositing.maskBlur, - source_image: { image_name: initialImage.image_name }, + id: 'canvas_v2_mask_and_crop', + type: 'canvas_v2_mask_and_crop', + invert: true, + crop_visible: true, }); g.addEdge(alphaToMask, 'image', createGradientMask, 'mask'); g.addEdge(i2l, 'latents', denoise, 'latents'); @@ -124,7 +124,7 @@ export const addInpaint = async ( g.addEdge(createGradientMask, 'denoise_mask', denoise, 'denoise_mask'); g.addEdge(createGradientMask, 'expanded_mask_area', canvasPasteBack, 'mask'); - g.addEdge(l2i, 'image', canvasPasteBack, 'target_image'); + g.addEdge(l2i, 'image', canvasPasteBack, 'image'); return canvasPasteBack; } diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index c65443f0f36..8d8caae6d6a 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts @@ -19,7 +19,7 @@ export const addOutpaint = async ( compositing: CanvasV2State['compositing'], denoising_start: number, vaePrecision: ParameterPrecision -): Promise> => { +): Promise> => { denoise.denoising_start = denoising_start; const cropBbox = pick(bbox.rect, ['x', 'y', 'width', 'height']); @@ -99,10 +99,10 @@ export const addOutpaint = async ( ...originalSize, }); const canvasPasteBack = g.addNode({ - id: 'canvas_paste_back', - type: 'canvas_paste_back', - mask_blur: compositing.maskBlur, - source_image: { image_name: initialImage.image_name }, + id: 'canvas_v2_mask_and_crop', + type: 'canvas_v2_mask_and_crop', + invert: true, + crop_visible: true, }); // Resize initial image and mask to scaled size, feed into to gradient mask @@ -112,7 +112,7 @@ export const addOutpaint = async ( g.addEdge(createGradientMask, 'expanded_mask_area', resizeOutputMaskToOriginalSize, 'image'); // Finally, paste the generated masked image back onto the original image - g.addEdge(resizeOutputImageToOriginalSize, 'image', canvasPasteBack, 'target_image'); + g.addEdge(resizeOutputImageToOriginalSize, 'image', canvasPasteBack, 'image'); g.addEdge(resizeOutputMaskToOriginalSize, 'image', canvasPasteBack, 'mask'); return canvasPasteBack; @@ -145,9 +145,10 @@ export const addOutpaint = async ( image: { image_name: initialImage.image_name }, }); const canvasPasteBack = g.addNode({ - id: 'canvas_paste_back', - type: 'canvas_paste_back', - mask_blur: compositing.maskBlur, + id: 'canvas_v2_mask_and_crop', + type: 'canvas_v2_mask_and_crop', + invert: true, + crop_visible: true, }); g.addEdge(maskAlphaToMask, 'image', maskCombine, 'mask1'); g.addEdge(initialImageAlphaToMask, 'image', maskCombine, 'mask2'); @@ -159,8 +160,7 @@ export const addOutpaint = async ( g.addEdge(modelLoader, 'unet', createGradientMask, 'unet'); g.addEdge(createGradientMask, 'denoise_mask', denoise, 'denoise_mask'); g.addEdge(createGradientMask, 'expanded_mask_area', canvasPasteBack, 'mask'); - g.addEdge(infill, 'image', canvasPasteBack, 'source_image'); - g.addEdge(l2i, 'image', canvasPasteBack, 'target_image'); + g.addEdge(l2i, 'image', canvasPasteBack, 'image'); return canvasPasteBack; } diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index e2b7a33cf5f..7f99ce6debf 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -122,7 +122,7 @@ export const buildSD1Graph = async (state: RootState, manager: CanvasManager): P }) : null; - let canvasOutput: Invocation<'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_paste_back'> = l2i; + let canvasOutput: Invocation<'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_v2_mask_and_crop'> = l2i; g.addEdge(modelLoader, 'unet', denoise, 'unet'); g.addEdge(modelLoader, 'clip', clipSkip, 'clip'); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index 9b4490660d8..34868e86025 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -121,7 +121,7 @@ export const buildSDXLGraph = async (state: RootState, manager: CanvasManager): }) : null; - let canvasOutput: Invocation<'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_paste_back'> = l2i; + let canvasOutput: Invocation<'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_v2_mask_and_crop'> = l2i; g.addEdge(modelLoader, 'unet', denoise, 'unet'); g.addEdge(modelLoader, 'clip', posCond, 'clip'); From 9e89ddf2f12040c59f356338ac52e7205e90c957 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 16 Jul 2024 21:31:43 +1000 Subject: [PATCH 209/678] feat(app): update CanvasV2MaskAndCropInvocation --- invokeai/app/invocations/image.py | 33 ++++++++++++++++--------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index beaf31f8ecc..340dc32f96e 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -1017,8 +1017,8 @@ def invoke(self, context: InvocationContext) -> ImageOutput: @invocation_output("canvas_v2_mask_and_crop_output") class CanvasV2MaskAndCropOutput(ImageOutput): - x: int = OutputField(description="The x coordinate of the image") - y: int = OutputField(description="The y coordinate of the image") + offset_x: int = OutputField(description="The x offset of the image, after cropping") + offset_y: int = OutputField(description="The y offset of the image, after cropping") @invocation( @@ -1029,32 +1029,33 @@ class CanvasV2MaskAndCropOutput(ImageOutput): version="1.0.0", ) class CanvasV2MaskAndCropInvocation(BaseInvocation, WithMetadata, WithBoard): - """Apply a mask to an image""" + """Handles Canvas V2 image output masking and cropping""" image: ImageField = InputField(description="The image to apply the mask to") mask: ImageField = InputField(description="The mask to apply") - invert: bool = InputField(default=False, description="Whether or not to invert the mask") - crop_visible: bool = InputField(default=False, description="Crop the image to the mask") + mask_blur: int = InputField(default=0, ge=0, description="The amount to blur the mask by") + + def _prepare_mask(self, mask: Image.Image) -> Image.Image: + mask_array = numpy.array(mask) + kernel = numpy.ones((self.mask_blur, self.mask_blur), numpy.uint8) + dilated_mask_array = cv2.erode(mask_array, kernel, iterations=3) + dilated_mask = Image.fromarray(dilated_mask_array) + if self.mask_blur > 0: + mask = dilated_mask.filter(ImageFilter.GaussianBlur(self.mask_blur)) + return ImageOps.invert(mask.convert("L")) def invoke(self, context: InvocationContext) -> CanvasV2MaskAndCropOutput: image = context.images.get_pil(self.image.image_name) - mask = context.images.get_pil(self.mask.image_name) - - if self.invert: - mask = ImageOps.invert(mask) - + mask = self._prepare_mask(context.images.get_pil(self.mask.image_name)) image.putalpha(mask) bbox = image.getbbox() - - if self.crop_visible: - image = image.crop(bbox) - + image = image.crop(bbox) image_dto = context.images.save(image=image) return CanvasV2MaskAndCropOutput( image=ImageField(image_name=image_dto.image_name), - x=bbox[0], - y=bbox[1], + offset_x=bbox[0], + offset_y=bbox[1], width=image.width, height=image.height, ) From 8794c51e4247e096e7eaaa913cdad679a95b81c7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 16 Jul 2024 21:31:49 +1000 Subject: [PATCH 210/678] chore(ui): typegen --- .../frontend/web/src/services/api/schema.ts | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index 97845b266e4..cebfdda5b5b 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -3177,7 +3177,7 @@ export type components = { }; /** * Canvas V2 Mask and Crop - * @description Apply a mask to an image + * @description Handles Canvas V2 image output masking and cropping */ CanvasV2MaskAndCropInvocation: { /** @@ -3218,17 +3218,11 @@ export type components = { */ mask?: components["schemas"]["ImageField"]; /** - * Invert - * @description Whether or not to invert the mask - * @default false - */ - invert?: boolean; - /** - * Crop Visible - * @description Crop the image to the mask - * @default false + * Mask Blur + * @description The amount to blur the mask by + * @default 0 */ - crop_visible?: boolean; + mask_blur?: number; /** * type * @default canvas_v2_mask_and_crop @@ -3259,15 +3253,15 @@ export type components = { */ type: "canvas_v2_mask_and_crop_output"; /** - * X - * @description The x coordinate of the image + * Offset X + * @description The x offset of the image, after cropping */ - x: number; + offset_x: number; /** - * Y - * @description The y coordinate of the image + * Offset Y + * @description The y offset of the image, after cropping */ - y: number; + offset_y: number; }; /** * Center Pad or Crop Image From 781ef806def3af4801589f06d7bb25018b24b364 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 16 Jul 2024 21:31:57 +1000 Subject: [PATCH 211/678] feat(ui): update staging handling to work w/ cropped mask --- .../addCommitStagingAreaImageListener.ts | 33 ++-------- .../socketio/socketInvocationComplete.ts | 20 ++++-- .../controlLayers/konva/CanvasStagingArea.ts | 64 ++++++++----------- .../controlLayers/store/canvasV2Slice.ts | 10 +++ .../controlLayers/store/layersReducers.ts | 28 ++++++++ .../controlLayers/store/paramsReducers.ts | 1 - .../controlLayers/store/sessionReducers.ts | 6 +- .../src/features/controlLayers/store/types.ts | 3 +- .../nodes/util/graph/generation/addInpaint.ts | 6 +- .../util/graph/generation/addNSFWChecker.ts | 2 +- .../util/graph/generation/addOutpaint.ts | 6 +- .../util/graph/generation/addWatermarker.ts | 2 +- 12 files changed, 97 insertions(+), 84 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts index c6efd494e9d..f1501b9533c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts @@ -1,8 +1,7 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { - layerAdded, - layerImageAdded, + layerAddedFromStagingArea, sessionStagingAreaImageAccepted, sessionStagingAreaReset, } from 'features/controlLayers/store/canvasV2Slice'; @@ -49,33 +48,13 @@ export const addStagingListeners = (startAppListening: AppStartListening) => { actionCreator: sessionStagingAreaImageAccepted, effect: async (action, api) => { const { index } = action.payload; - const { layers, selectedEntityIdentifier } = api.getState().canvasV2; - let layer = layers.entities.find((layer) => layer.id === selectedEntityIdentifier?.id); + const state = api.getState(); + const stagingAreaImage = state.canvasV2.session.stagedImages[index]; - if (!layer) { - layer = layers.entities[0]; - } - - if (!layer) { - // We need to create a new layer to add the accepted image - api.dispatch(layerAdded()); - layer = api.getState().canvasV2.layers.entities[0]; - } - - const stagedImage = api.getState().canvasV2.session.stagedImages[index]; - - assert(stagedImage, 'No staged image found to accept'); - assert(layer, 'No layer found to stage image'); - - const { id } = layer; + assert(stagingAreaImage, 'No staged image found to accept'); + const { x, y } = state.canvasV2.bbox.rect; - api.dispatch( - layerImageAdded({ - id, - imageDTO: stagedImage.imageDTO, - pos: { x: stagedImage.rect.x - layer.x, y: stagedImage.rect.y - layer.y }, - }) - ); + api.dispatch(layerAddedFromStagingArea({ stagingAreaImage, pos: { x, y } })); api.dispatch(sessionStagingAreaReset()); }, }); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts index 0e3cffe22b4..d8dcfb76b16 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts @@ -59,12 +59,20 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi imageDTORequest.unsubscribe(); // handle tab-specific logic - if (data.origin === 'canvas' && data.result.type === 'canvas_v2_mask_and_crop_output') { - const { x, y, width, height } = data.result; - if (canvasV2.session.isStaging) { - dispatch(sessionImageStaged({ imageDTO, rect: { x, y, width, height } })); - } else if (!canvasV2.session.isActive) { - $lastProgressEvent.set(null); + if (data.origin === 'canvas' && data.invocation_source_id === 'canvas_output') { + if (data.result.type === 'canvas_v2_mask_and_crop_output') { + const { offset_x, offset_y } = data.result; + if (canvasV2.session.isStaging) { + dispatch(sessionImageStaged({ stagingAreaImage: { imageDTO, offsetX: offset_x, offsetY: offset_y } })); + } else if (!canvasV2.session.isActive) { + $lastProgressEvent.set(null); + } + } else if (data.result.type === 'image_output') { + if (canvasV2.session.isStaging) { + dispatch(sessionImageStaged({ stagingAreaImage: { imageDTO, offsetX: 0, offsetY: 0 } })); + } else if (!canvasV2.session.isActive) { + $lastProgressEvent.set(null); + } } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts index e6e5d72b135..4bee9409a52 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts @@ -18,55 +18,47 @@ export class CanvasStagingArea { async render() { const session = this.manager.stateApi.getSession(); + const bboxRect = this.manager.stateApi.getBbox().rect; const shouldShowStagedImage = this.manager.stateApi.getShouldShowStagedImage(); this.selectedImage = session.stagedImages[session.selectedStagedImageIndex] ?? null; if (this.selectedImage) { + const { imageDTO, offsetX, offsetY } = this.selectedImage; if (this.image) { - if ( - !this.image.isLoading && - !this.image.isError && - this.image.imageName !== this.selectedImage.imageDTO.image_name - ) { - await this.image.updateImageSource(this.selectedImage.imageDTO.image_name); + if (!this.image.isLoading && !this.image.isError && this.image.imageName !== imageDTO.image_name) { + this.image.konvaImageGroup.visible(false); + this.image.konvaImage?.width(imageDTO.width); + this.image.konvaImage?.height(imageDTO.height); + this.image.konvaImageGroup.x(bboxRect.x + offsetX); + this.image.konvaImageGroup.y(bboxRect.y + offsetY); + await this.image.updateImageSource(imageDTO.image_name); } - this.image.konvaImageGroup.x(this.selectedImage.rect.x); - this.image.konvaImageGroup.y(this.selectedImage.rect.y); - this.image.konvaImageGroup.visible(shouldShowStagedImage); } else { - const { image_name } = this.selectedImage.imageDTO; - const { x, y, width, height } = this.selectedImage.rect; - this.image = new CanvasImage( - { - id: 'staging-area-image', - type: 'image', - x, - y, + const { image_name, width, height } = imageDTO; + this.image = new CanvasImage({ + id: 'staging-area-image', + type: 'image', + x: 0, + y: 0, + width, + height, + filters: [], + image: { + name: image_name, width, height, - filters: [], - image: { - name: image_name, - width, - height, - }, }, - { - onLoad: (konvaImage) => { - if (this.selectedImage) { - konvaImage.width(this.selectedImage.rect.width); - konvaImage.height(this.selectedImage.rect.height); - } - this.manager.stateApi.resetLastProgressEvent(); - this.image?.konvaImageGroup.visible(shouldShowStagedImage); - }, - } - ); + }); this.group.add(this.image.konvaImageGroup); - await this.image.updateImageSource(this.selectedImage.imageDTO.image_name); - this.image.konvaImageGroup.visible(shouldShowStagedImage); + this.image.konvaImage?.width(imageDTO.width); + this.image.konvaImage?.height(imageDTO.height); + this.image.konvaImageGroup.x(bboxRect.x + offsetX); + this.image.konvaImageGroup.y(bboxRect.y + offsetY); + await this.image.updateImageSource(imageDTO.image_name); } + this.manager.stateApi.resetLastProgressEvent(); + this.image.konvaImageGroup.visible(shouldShowStagedImage); } else { this.image?.konvaImageGroup.visible(false); } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index b96fa09da0c..f5c106457bb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -15,7 +15,10 @@ import { regionsReducers } from 'features/controlLayers/store/regionsReducers'; import { sessionReducers } from 'features/controlLayers/store/sessionReducers'; import { settingsReducers } from 'features/controlLayers/store/settingsReducers'; import { toolReducers } from 'features/controlLayers/store/toolReducers'; +import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; import { initialAspectRatioState } from 'features/parameters/components/DocumentSize/constants'; +import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; +import { pick } from 'lodash-es'; import { atom } from 'nanostores'; import type { InvocationDenoiseProgressEvent } from 'services/events/types'; @@ -158,6 +161,12 @@ export const canvasV2Slice = createSlice({ }, canvasReset: (state) => { state.bbox = deepClone(initialState.bbox); + const optimalDimension = getOptimalDimension(state.params.model); + state.bbox.rect.width = optimalDimension; + state.bbox.rect.height = optimalDimension; + const size = pick(state.bbox.rect, 'width', 'height'); + state.bbox.scaledSize = getScaledBoundingBoxDimensions(size, optimalDimension); + state.controlAdapters = deepClone(initialState.controlAdapters); state.ipAdapters = deepClone(initialState.ipAdapters); state.layers = deepClone(initialState.layers); @@ -195,6 +204,7 @@ export const { bboxSizeOptimized, // layers layerAdded, + layerAddedFromStagingArea, layerRecalled, layerDeleted, layerReset, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index d63f7e9c6b8..d98740258eb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -11,8 +11,10 @@ import type { EraserLine, ImageObjectAddedArg, LayerEntity, + Position, RectShape, ScaleChangedArg, + StagingAreaImage, } from './types'; import { imageDTOToImageObject, imageDTOToImageWithDims } from './types'; @@ -43,6 +45,32 @@ export const layersReducers = { }, prepare: () => ({ payload: { id: uuidv4() } }), }, + layerAddedFromStagingArea: { + reducer: ( + state, + action: PayloadAction<{ id: string; objectId: string; stagingAreaImage: StagingAreaImage; pos: Position }> + ) => { + const { id, objectId, stagingAreaImage, pos } = action.payload; + const { imageDTO, offsetX, offsetY } = stagingAreaImage; + const imageObject = imageDTOToImageObject(id, objectId, imageDTO); + state.layers.entities.push({ + id, + type: 'layer', + isEnabled: true, + bbox: null, + bboxNeedsUpdate: false, + objects: [imageObject], + opacity: 1, + x: pos.x + offsetX, + y: pos.y + offsetY, + }); + state.selectedEntityIdentifier = { type: 'layer', id }; + state.layers.imageCache = null; + }, + prepare: (payload: { stagingAreaImage: StagingAreaImage; pos: Position }) => ({ + payload: { ...payload, id: uuidv4(), objectId: uuidv4() }, + }), + }, layerRecalled: (state, action: PayloadAction<{ data: LayerEntity }>) => { const { data } = action.payload; state.layers.entities.push(data); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts index 548833587ed..5e8a2b60ae6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts @@ -60,7 +60,6 @@ export const paramsReducers = { } // Update the bbox size to match the new model's optimal size - // TODO(psyche): Should we change the document size too? const optimalDimension = getOptimalDimension(model); if (!getIsSizeOptimal(state.bbox.rect.width, state.bbox.rect.height, optimalDimension)) { const bboxDims = calculateNewSize(state.bbox.aspectRatio.value, optimalDimension * optimalDimension); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts index 8b6d240f094..03236aeaa22 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts @@ -14,9 +14,9 @@ export const sessionReducers = { state.tool.selectedBuffer = state.tool.selected; state.tool.selected = 'view'; }, - sessionImageStaged: (state, action: PayloadAction) => { - const { imageDTO, rect } = action.payload; - state.session.stagedImages.push({ imageDTO, rect }); + sessionImageStaged: (state, action: PayloadAction<{ stagingAreaImage: StagingAreaImage }>) => { + const { stagingAreaImage } = action.payload; + state.session.stagedImages.push(stagingAreaImage); state.session.selectedStagedImageIndex = state.session.stagedImages.length - 1; }, sessionNextStagedImageSelected: (state) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index bafc43727c7..f4972f2d1dc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -828,7 +828,8 @@ export type LoRA = { export type StagingAreaImage = { imageDTO: ImageDTO; - rect: Rect; + offsetX: number; + offsetY: number; }; export type CanvasV2State = { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts index ce001e44a52..ceeed6caa4d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts @@ -66,8 +66,7 @@ export const addInpaint = async ( const canvasPasteBack = g.addNode({ id: 'canvas_v2_mask_and_crop', type: 'canvas_v2_mask_and_crop', - invert: true, - crop_visible: true, + mask_blur: compositing.maskBlur, }); // Resize initial image and mask to scaled size, feed into to gradient mask @@ -113,8 +112,7 @@ export const addInpaint = async ( const canvasPasteBack = g.addNode({ id: 'canvas_v2_mask_and_crop', type: 'canvas_v2_mask_and_crop', - invert: true, - crop_visible: true, + mask_blur: compositing.maskBlur, }); g.addEdge(alphaToMask, 'image', createGradientMask, 'mask'); g.addEdge(i2l, 'latents', denoise, 'latents'); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addNSFWChecker.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addNSFWChecker.ts index 5a3bb741f51..ec9a809a9f3 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addNSFWChecker.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addNSFWChecker.ts @@ -10,7 +10,7 @@ import type { Invocation } from 'services/api/types'; */ export const addNSFWChecker = ( g: Graph, - imageOutput: Invocation<'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_paste_back'> + imageOutput: Invocation<'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_v2_mask_and_crop'> ): Invocation<'img_nsfw'> => { const nsfw = g.addNode({ id: NSFW_CHECKER, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index 8d8caae6d6a..fd0dd8b1930 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts @@ -101,8 +101,7 @@ export const addOutpaint = async ( const canvasPasteBack = g.addNode({ id: 'canvas_v2_mask_and_crop', type: 'canvas_v2_mask_and_crop', - invert: true, - crop_visible: true, + mask_blur: compositing.maskBlur, }); // Resize initial image and mask to scaled size, feed into to gradient mask @@ -147,8 +146,7 @@ export const addOutpaint = async ( const canvasPasteBack = g.addNode({ id: 'canvas_v2_mask_and_crop', type: 'canvas_v2_mask_and_crop', - invert: true, - crop_visible: true, + mask_blur: compositing.maskBlur, }); g.addEdge(maskAlphaToMask, 'image', maskCombine, 'mask1'); g.addEdge(initialImageAlphaToMask, 'image', maskCombine, 'mask2'); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addWatermarker.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addWatermarker.ts index 9cd197a38cb..b0f0f14008a 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addWatermarker.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addWatermarker.ts @@ -10,7 +10,7 @@ import type { Invocation } from 'services/api/types'; */ export const addWatermarker = ( g: Graph, - imageOutput: Invocation<'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_paste_back'> + imageOutput: Invocation<'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_v2_mask_and_crop'> ): Invocation<'img_watermark'> => { const watermark = g.addNode({ id: WATERMARKER, From 2a699678636b13d62d29e1bc26375fb2445b16ae Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 16 Jul 2024 22:16:16 +1000 Subject: [PATCH 212/678] feat(ui): de-jank staging area and progress images --- .../addCommitStagingAreaImageListener.ts | 4 + .../konva/CanvasControlAdapter.ts | 7 +- .../controlLayers/konva/CanvasImage.ts | 79 ++++++------------- .../controlLayers/konva/CanvasManager.ts | 3 - .../konva/CanvasProgressPreview.ts | 3 +- .../controlLayers/konva/CanvasStagingArea.ts | 17 ++-- 6 files changed, 38 insertions(+), 75 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts index f1501b9533c..85fbac41ec4 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts @@ -1,6 +1,7 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { + $lastProgressEvent, layerAddedFromStagingArea, sessionStagingAreaImageAccepted, sessionStagingAreaReset, @@ -25,6 +26,9 @@ export const addStagingListeners = (startAppListening: AppStartListening) => { ); const { canceled } = await req.unwrap(); req.reset(); + + $lastProgressEvent.set(null); + if (canceled > 0) { log.debug(`Canceled ${canceled} canvas batches`); toast({ diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts index a69f4f3283a..8b4208a6a6c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts @@ -76,11 +76,8 @@ export class CanvasControlAdapter { didDraw = true; } } else if (!this.image) { - this.image = await new CanvasImage(imageObject, { - onLoad: () => { - this.updateGroup(true); - }, - }); + this.image = await new CanvasImage(imageObject); + this.updateGroup(true); this.objectsGroup.add(this.image.konvaImageGroup); await this.image.updateImageSource(imageObject.image.name); } else if (!this.image.isLoading && !this.image.isError) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts index 9bc9f0778ae..cb38a7136e1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -3,8 +3,7 @@ import { loadImage } from 'features/controlLayers/konva/util'; import type { ImageObject } from 'features/controlLayers/store/types'; import { t } from 'i18next'; import Konva from 'konva'; -import { getImageDTO as defaultGetImageDTO } from 'services/api/endpoints/images'; -import type { ImageDTO } from 'services/api/types'; +import { getImageDTO } from 'services/api/endpoints/images'; import { assert } from 'tsafe'; export class CanvasImage { @@ -17,23 +16,10 @@ export class CanvasImage { konvaImage: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately isLoading: boolean; isError: boolean; - getImageDTO: (imageName: string) => Promise; - onLoading: () => void; - onLoad: (imageName: string, imageEl: HTMLImageElement) => void; - onError: () => void; lastImageObject: ImageObject; - constructor( - imageObject: ImageObject, - options?: { - getImageDTO?: (imageName: string) => Promise; - onLoading?: () => void; - onLoad?: (konvaImage: Konva.Image) => void; - onError?: () => void; - } - ) { - const { getImageDTO, onLoading, onLoad, onError } = options ?? {}; - const { id, width, height, x, y, filters } = imageObject; + constructor(imageObject: ImageObject) { + const { id, width, height, x, y } = imageObject; this.konvaImageGroup = new Konva.Group({ id, listening: false, x, y }); this.konvaPlaceholderGroup = new Konva.Group({ listening: false }); this.konvaPlaceholderRect = new Konva.Rect({ @@ -64,19 +50,23 @@ export class CanvasImage { this.konvaImage = null; this.isLoading = false; this.isError = false; - this.getImageDTO = getImageDTO ?? defaultGetImageDTO; - this.onLoading = function () { + this.lastImageObject = imageObject; + } + + async updateImageSource(imageName: string) { + try { this.isLoading = true; + this.konvaImageGroup.visible(true); + if (!this.konvaImage) { this.konvaPlaceholderGroup.visible(true); this.konvaPlaceholderText.text(t('common.loadingImage', 'Loading Image')); } - this.konvaImageGroup.visible(true); - if (onLoading) { - onLoading(); - } - }; - this.onLoad = function (imageName: string, imageEl: HTMLImageElement) { + + const imageDTO = await getImageDTO(imageName); + assert(imageDTO !== null, 'imageDTO is null'); + const imageEl = await loadImage(imageDTO.image_url); + if (this.konvaImage) { this.konvaImage.setAttrs({ image: imageEl, @@ -86,52 +76,31 @@ export class CanvasImage { id: this.id, listening: false, image: imageEl, - width, - height, + width: this.lastImageObject.width, + height: this.lastImageObject.height, }); this.konvaImageGroup.add(this.konvaImage); } - if (filters.length > 0) { + + if (this.lastImageObject.filters.length > 0) { this.konvaImage.cache(); - this.konvaImage.filters(filters.map((f) => FILTER_MAP[f])); + this.konvaImage.filters(this.lastImageObject.filters.map((f) => FILTER_MAP[f])); } else { this.konvaImage.clearCache(); this.konvaImage.filters([]); } + this.imageName = imageName; this.isLoading = false; this.isError = false; this.konvaPlaceholderGroup.visible(false); - this.konvaImageGroup.visible(true); - - if (onLoad) { - onLoad(this.konvaImage); - } - }; - this.onError = function () { + } catch { + this.konvaImage?.visible(false); this.imageName = null; this.isLoading = false; this.isError = true; - this.konvaPlaceholderGroup.visible(true); this.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load')); - this.konvaImageGroup.visible(true); - - if (onError) { - onError(); - } - }; - this.lastImageObject = imageObject; - } - - async updateImageSource(imageName: string) { - try { - this.onLoading(); - const imageDTO = await this.getImageDTO(imageName); - assert(imageDTO !== null, 'imageDTO is null'); - const imageEl = await loadImage(imageDTO.image_url); - this.onLoad(imageName, imageEl); - } catch { - this.onError(); + this.konvaPlaceholderGroup.visible(true); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 8c89e07c675..8d2d5b198cd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -352,9 +352,6 @@ export class CanvasManager { if (lastProgressEvent !== prevLastProgressEvent) { log.debug('Rendering progress image'); await this.preview.progressPreview.render(lastProgressEvent); - if (this.stateApi.getSession().isActive) { - this.preview.stagingArea.render(); - } } }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressPreview.ts index a37622da684..f2e814cefa4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressPreview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressPreview.ts @@ -17,8 +17,9 @@ export class CanvasProgressPreview { async render(lastProgressEvent: InvocationDenoiseProgressEvent | null) { const bboxRect = this.manager.stateApi.getBbox().rect; + const session = this.manager.stateApi.getSession(); - if (lastProgressEvent) { + if (lastProgressEvent && session.isStaging) { const { invocation, step, progress_image } = lastProgressEvent; const { dataURL } = progress_image; const { x, y, width, height } = bboxRect; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts index 4bee9409a52..eada1fadcbe 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts @@ -25,16 +25,8 @@ export class CanvasStagingArea { if (this.selectedImage) { const { imageDTO, offsetX, offsetY } = this.selectedImage; - if (this.image) { - if (!this.image.isLoading && !this.image.isError && this.image.imageName !== imageDTO.image_name) { - this.image.konvaImageGroup.visible(false); - this.image.konvaImage?.width(imageDTO.width); - this.image.konvaImage?.height(imageDTO.height); - this.image.konvaImageGroup.x(bboxRect.x + offsetX); - this.image.konvaImageGroup.y(bboxRect.y + offsetY); - await this.image.updateImageSource(imageDTO.image_name); - } - } else { + + if (!this.image) { const { image_name, width, height } = imageDTO; this.image = new CanvasImage({ id: 'staging-area-image', @@ -51,13 +43,16 @@ export class CanvasStagingArea { }, }); this.group.add(this.image.konvaImageGroup); + } + + if (!this.image.isLoading && !this.image.isError && this.image.imageName !== imageDTO.image_name) { this.image.konvaImage?.width(imageDTO.width); this.image.konvaImage?.height(imageDTO.height); this.image.konvaImageGroup.x(bboxRect.x + offsetX); this.image.konvaImageGroup.y(bboxRect.y + offsetY); await this.image.updateImageSource(imageDTO.image_name); + this.manager.stateApi.resetLastProgressEvent(); } - this.manager.stateApi.resetLastProgressEvent(); this.image.konvaImageGroup.visible(shouldShowStagedImage); } else { this.image?.konvaImageGroup.visible(false); From c98c5f13f7b1aebe62b7876d58bfd2ec208a1ff5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 17 Jul 2024 10:26:04 +1000 Subject: [PATCH 213/678] feat(invocation): reduce canvas v2 mask & crop mask dilation --- invokeai/app/invocations/image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index 340dc32f96e..a80cddbe9ba 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -1038,7 +1038,7 @@ class CanvasV2MaskAndCropInvocation(BaseInvocation, WithMetadata, WithBoard): def _prepare_mask(self, mask: Image.Image) -> Image.Image: mask_array = numpy.array(mask) kernel = numpy.ones((self.mask_blur, self.mask_blur), numpy.uint8) - dilated_mask_array = cv2.erode(mask_array, kernel, iterations=3) + dilated_mask_array = cv2.erode(mask_array, kernel) dilated_mask = Image.fromarray(dilated_mask_array) if self.mask_blur > 0: mask = dilated_mask.filter(ImageFilter.GaussianBlur(self.mask_blur)) From dda53292bf4bd362a5cf0ffad0ed4ac8b1bae8e4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 17 Jul 2024 10:37:03 +1000 Subject: [PATCH 214/678] fix(ui): layer rendering when starting as disabled --- .../controlLayers/konva/CanvasLayer.ts | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 9b95f599531..49442876f4c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -180,11 +180,11 @@ export class CanvasLayer { assert(image instanceof CanvasImage || image === undefined); if (!image) { - image = await new CanvasImage(obj, {}); + image = await new CanvasImage(obj); this.objects.set(image.id, image); this.objectsGroup.add(image.konvaImageGroup); await image.updateImageSource(obj.image.name); - this.updateGroup(true); + return true; } else { if (await image.update(obj, force)) { return true; @@ -196,8 +196,12 @@ export class CanvasLayer { } updateGroup(didDraw: boolean) { - this.layer.visible(this.layerState.isEnabled); + if (!this.layerState.isEnabled) { + this.layer.visible(false); + return; + } + this.layer.visible(true); this.group.opacity(this.layerState.opacity); const isSelected = this.manager.stateApi.getIsSelected(this.id); const selectedTool = this.manager.stateApi.getToolState().selected; @@ -209,10 +213,7 @@ export class CanvasLayer { if (this.group.isCached()) { this.group.clearCache(); } - return; - } - - if (isSelected && selectedTool === 'move') { + } else if (isSelected && selectedTool === 'move') { // When the layer is selected and being moved, we should always cache it. // We should update the cache if we drew to the layer. if (!this.group.isCached() || didDraw) { @@ -222,10 +223,7 @@ export class CanvasLayer { this.layer.listening(true); this.transformer.nodes([this.group]); this.transformer.forceUpdate(); - return; - } - - if (isSelected && selectedTool !== 'move') { + } else if (isSelected && selectedTool !== 'move') { // If the layer is selected but not using the move tool, we don't want the layer to be listening. this.layer.listening(false); // The transformer also does not need to be active. @@ -243,10 +241,7 @@ export class CanvasLayer { this.group.cache(); } } - return; - } - - if (!isSelected) { + } else if (!isSelected) { // Unselected layers should not be listening this.layer.listening(false); // The transformer also does not need to be active. @@ -255,8 +250,6 @@ export class CanvasLayer { if (!this.group.isCached() || didDraw) { this.group.cache(); } - - return; } } } From 23979bdbeec01e0ce6fe74512fb98eadb72d4cf2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 17 Jul 2024 10:40:23 +1000 Subject: [PATCH 215/678] tidy(ui): hide layer settings by default --- .../src/features/controlLayers/components/InpaintMask/IM.tsx | 2 +- .../web/src/features/controlLayers/components/Layer/Layer.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx index 3c0fb3b75b6..544fece75bb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx @@ -11,7 +11,7 @@ export const IM = memo(() => { const dispatch = useAppDispatch(); const selectedBorderColor = useAppSelector((s) => rgbColorToString(s.canvasV2.inpaintMask.fill)); const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === 'inpaint_mask'); - const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); + const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: false }); const onSelect = useCallback(() => { dispatch(entitySelected({ id: 'inpaint_mask', type: 'inpaint_mask' })); }, [dispatch]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx index 9c1b315b804..5fda4e72b45 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx @@ -15,7 +15,7 @@ type Props = { export const Layer = memo(({ id }: Props) => { const dispatch = useAppDispatch(); const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id); - const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); + const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: false }); const onSelect = useCallback(() => { dispatch(entitySelected({ id, type: 'layer' })); }, [dispatch, id]); From 34ccd5aa86f8d1644e03ecaec6e62587c61bf349 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 17 Jul 2024 10:43:38 +1000 Subject: [PATCH 216/678] feat(ui): rename types size and position to dimensions and coordinate --- .../features/controlLayers/konva/events.ts | 12 +++++------ .../controlLayers/store/bboxReducers.ts | 4 ++-- .../controlLayers/store/canvasV2Slice.ts | 8 ++++---- .../controlLayers/store/layersReducers.ts | 6 +++--- .../src/features/controlLayers/store/types.ts | 20 ++++++++++--------- .../util/getScaledBoundingBoxDimensions.ts | 4 ++-- .../ImageViewer/ImageComparison.tsx | 4 ++-- .../ImageViewer/ImageComparisonHover.tsx | 6 +++--- .../ImageViewer/ImageComparisonSlider.tsx | 6 +++--- .../gallery/components/ImageViewer/common.ts | 14 ++++++------- .../util/graph/generation/addImageToImage.ts | 6 +++--- .../nodes/util/graph/generation/addInpaint.ts | 6 +++--- .../util/graph/generation/addOutpaint.ts | 6 +++--- .../util/graph/generation/addTextToImage.ts | 6 +++--- 14 files changed, 55 insertions(+), 53 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index daaf63c8e72..b6869c43189 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -4,7 +4,7 @@ import type { CanvasV2State, InpaintMaskEntity, LayerEntity, - Position, + Coordinate, RegionEntity, Tool, } from 'features/controlLayers/store/types'; @@ -45,10 +45,10 @@ const calculateNewBrushSize = (brushSize: number, delta: number) => { }; const getNextPoint = ( - currentPos: Position, + currentPos: Coordinate, toolState: CanvasV2State['tool'], - lastAddedPoint: Position | null -): Position | null => { + lastAddedPoint: Coordinate | null +): Coordinate | null => { // Continue the last line const minSpacingPx = toolState.selected === 'brush' @@ -65,7 +65,7 @@ const getNextPoint = ( return currentPos; }; -const getLastPointOfLine = (points: number[]): Position | null => { +const getLastPointOfLine = (points: number[]): Coordinate | null => { if (points.length < 2) { return null; } @@ -80,7 +80,7 @@ const getLastPointOfLine = (points: number[]): Position | null => { const getLastPointOfLastLineOfEntity = ( entity: LayerEntity | RegionEntity | InpaintMaskEntity, tool: Tool -): Position | null => { +): Coordinate | null => { const lastObject = entity.objects[entity.objects.length - 1]; if (!lastObject) { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts index 202ec0dda24..a1c276f4ae5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts @@ -1,7 +1,7 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { deepClone } from 'common/util/deepClone'; import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple'; -import type { BoundingBoxScaleMethod, CanvasV2State, Size } from 'features/controlLayers/store/types'; +import type { BoundingBoxScaleMethod, CanvasV2State, Dimensions } from 'features/controlLayers/store/types'; import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; import { ASPECT_RATIO_MAP, initialAspectRatioState } from 'features/parameters/components/DocumentSize/constants'; @@ -11,7 +11,7 @@ import type { IRect } from 'konva/lib/types'; import { pick } from 'lodash-es'; export const bboxReducers = { - bboxScaledSizeChanged: (state, action: PayloadAction>) => { + bboxScaledSizeChanged: (state, action: PayloadAction>) => { state.layers.imageCache = null; state.bbox.scaledSize = { ...state.bbox.scaledSize, ...action.payload }; }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index f5c106457bb..621d1fda826 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -22,7 +22,7 @@ import { pick } from 'lodash-es'; import { atom } from 'nanostores'; import type { InvocationDenoiseProgressEvent } from 'services/events/types'; -import type { CanvasEntityIdentifier, CanvasV2State, Position, StageAttrs } from './types'; +import type { CanvasEntityIdentifier, CanvasV2State, Coordinate, StageAttrs } from './types'; import { RGBA_RED } from './types'; const initialState: CanvasV2State = { @@ -379,9 +379,9 @@ export const $shouldShowStagedImage = atom(true); export const $lastProgressEvent = atom(null); export const $isDrawing = atom(false); export const $isMouseDown = atom(false); -export const $lastAddedPoint = atom(null); -export const $lastMouseDownPos = atom(null); -export const $lastCursorPos = atom(null); +export const $lastAddedPoint = atom(null); +export const $lastMouseDownPos = atom(null); +export const $lastCursorPos = atom(null); export const $spaceKey = atom(false); export const canvasV2PersistConfig: PersistConfig = { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index d98740258eb..52c7cf08496 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -11,7 +11,7 @@ import type { EraserLine, ImageObjectAddedArg, LayerEntity, - Position, + Coordinate, RectShape, ScaleChangedArg, StagingAreaImage, @@ -48,7 +48,7 @@ export const layersReducers = { layerAddedFromStagingArea: { reducer: ( state, - action: PayloadAction<{ id: string; objectId: string; stagingAreaImage: StagingAreaImage; pos: Position }> + action: PayloadAction<{ id: string; objectId: string; stagingAreaImage: StagingAreaImage; pos: Coordinate }> ) => { const { id, objectId, stagingAreaImage, pos } = action.payload; const { imageDTO, offsetX, offsetY } = stagingAreaImage; @@ -67,7 +67,7 @@ export const layersReducers = { state.selectedEntityIdentifier = { type: 'layer', id }; state.layers.imageCache = null; }, - prepare: (payload: { stagingAreaImage: StagingAreaImage; pos: Position }) => ({ + prepare: (payload: { stagingAreaImage: StagingAreaImage; pos: Coordinate }) => ({ payload: { ...payload, id: uuidv4(), objectId: uuidv4() }, }), }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index f4972f2d1dc..836c04459ce 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -809,15 +809,17 @@ export type CanvasEntity = | InitialImageEntity; export type CanvasEntityIdentifier = Pick; -export type Size = { - width: number; - height: number; -}; +const zDimensions = z.object({ + width: z.number().int().positive(), + height: z.number().int().positive(), +}); +export type Dimensions = z.infer; -export type Position = { - x: number; - y: number; -}; +const zCoordinate = z.object({ + x: z.number(), + y: z.number(), +}); +export type Coordinate = z.infer; export type LoRA = { id: string; @@ -937,7 +939,7 @@ export type EraserLineAddedArg = { export type BrushLineAddedArg = EraserLineAddedArg & { color: RgbaColor }; export type PointAddedToLineArg = { id: string; point: [number, number] }; export type RectShapeAddedArg = { id: string; rect: IRect; color: RgbaColor }; -export type ImageObjectAddedArg = { id: string; imageDTO: ImageDTO; pos?: Position }; +export type ImageObjectAddedArg = { id: string; imageDTO: ImageDTO; pos?: Coordinate }; //#region Type guards export const isLine = (obj: RenderableObject): obj is BrushLine | EraserLine => { diff --git a/invokeai/frontend/web/src/features/controlLayers/util/getScaledBoundingBoxDimensions.ts b/invokeai/frontend/web/src/features/controlLayers/util/getScaledBoundingBoxDimensions.ts index e35e11e226c..d98d03f33ec 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/getScaledBoundingBoxDimensions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/getScaledBoundingBoxDimensions.ts @@ -1,6 +1,6 @@ import { roundToMultiple } from 'common/util/roundDownToMultiple'; import { CANVAS_GRID_SIZE_FINE } from 'features/controlLayers/konva/constants'; -import type { Size } from 'features/controlLayers/store/types'; +import type { Dimensions } from 'features/controlLayers/store/types'; /** * Scales the bounding box dimensions to the optimal dimension. The optimal dimensions should be the trained dimension @@ -8,7 +8,7 @@ import type { Size } from 'features/controlLayers/store/types'; * @param dimensions The un-scaled bbox dimensions * @param optimalDimension The optimal dimension to scale the bbox to */ -export const getScaledBoundingBoxDimensions = (dimensions: Size, optimalDimension: number): Size => { +export const getScaledBoundingBoxDimensions = (dimensions: Dimensions, optimalDimension: number): Dimensions => { const { width, height } = dimensions; const scaledDimensions = { width, height }; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparison.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparison.tsx index 3e1583cf6e3..ff97a5a687a 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparison.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparison.tsx @@ -1,6 +1,6 @@ import { useAppSelector } from 'app/store/storeHooks'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; -import type { Size } from 'features/controlLayers/store/types'; +import type { Dimensions } from 'features/controlLayers/store/types'; import { selectComparisonImages } from 'features/gallery/components/ImageViewer/common'; import { ImageComparisonHover } from 'features/gallery/components/ImageViewer/ImageComparisonHover'; import { ImageComparisonSideBySide } from 'features/gallery/components/ImageViewer/ImageComparisonSideBySide'; @@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next'; import { PiImagesBold } from 'react-icons/pi'; type Props = { - containerDims: Size; + containerDims: Dimensions; }; export const ImageComparison = memo(({ containerDims }: Props) => { diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonHover.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonHover.tsx index 83afa376cdb..11f9da928be 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonHover.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonHover.tsx @@ -3,7 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { useBoolean } from 'common/hooks/useBoolean'; import { preventDefault } from 'common/util/stopPropagation'; import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants'; -import type { Size } from 'features/controlLayers/store/types'; +import type { Dimensions } from 'features/controlLayers/store/types'; import { ImageComparisonLabel } from 'features/gallery/components/ImageViewer/ImageComparisonLabel'; import { memo, useMemo, useRef } from 'react'; @@ -14,11 +14,11 @@ export const ImageComparisonHover = memo(({ firstImage, secondImage, containerDi const comparisonFit = useAppSelector((s) => s.gallery.comparisonFit); const imageContainerRef = useRef(null); const mouseOver = useBoolean(false); - const fittedDims = useMemo( + const fittedDims = useMemo( () => fitDimsToContainer(containerDims, firstImage), [containerDims, firstImage] ); - const compareImageDims = useMemo( + const compareImageDims = useMemo( () => getSecondImageDims(comparisonFit, fittedDims, firstImage, secondImage), [comparisonFit, fittedDims, firstImage, secondImage] ); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx index 2a9da095168..00b25b1b326 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx @@ -2,7 +2,7 @@ import { Box, Flex, Icon, Image } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { preventDefault } from 'common/util/stopPropagation'; import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants'; -import type { Size } from 'features/controlLayers/store/types'; +import type { Dimensions } from 'features/controlLayers/store/types'; import { ImageComparisonLabel } from 'features/gallery/components/ImageViewer/ImageComparisonLabel'; import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { PiCaretLeftBold, PiCaretRightBold } from 'react-icons/pi'; @@ -31,12 +31,12 @@ export const ImageComparisonSlider = memo(({ firstImage, secondImage, containerD const rafRef = useRef(null); const lastMoveTimeRef = useRef(0); - const fittedDims = useMemo( + const fittedDims = useMemo( () => fitDimsToContainer(containerDims, firstImage), [containerDims, firstImage] ); - const compareImageDims = useMemo( + const compareImageDims = useMemo( () => getSecondImageDims(comparisonFit, fittedDims, firstImage, secondImage), [comparisonFit, fittedDims, firstImage, secondImage] ); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/common.ts b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/common.ts index 7c58ad2aff5..ac3d7b172b4 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/common.ts +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/common.ts @@ -1,5 +1,5 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import type { Size } from 'features/controlLayers/store/types'; +import type { Dimensions } from 'features/controlLayers/store/types'; import { selectGallerySlice } from 'features/gallery/store/gallerySlice'; import type { ComparisonFit } from 'features/gallery/store/types'; import type { ImageDTO } from 'services/api/types'; @@ -9,10 +9,10 @@ export const DROP_SHADOW = 'drop-shadow(0px 0px 4px rgb(0, 0, 0)) drop-shadow(0p export type ComparisonProps = { firstImage: ImageDTO; secondImage: ImageDTO; - containerDims: Size; + containerDims: Dimensions; }; -export const fitDimsToContainer = (containerDims: Size, imageDims: Size): Size => { +export const fitDimsToContainer = (containerDims: Dimensions, imageDims: Dimensions): Dimensions => { // Fall back to the image's dimensions if the container has no dimensions if (containerDims.width === 0 || containerDims.height === 0) { return { width: imageDims.width, height: imageDims.height }; @@ -46,10 +46,10 @@ export const fitDimsToContainer = (containerDims: Size, imageDims: Size): Size = */ export const getSecondImageDims = ( comparisonFit: ComparisonFit, - fittedDims: Size, - firstImageDims: Size, - secondImageDims: Size -): Size => { + fittedDims: Dimensions, + firstImageDims: Dimensions, + secondImageDims: Dimensions +): Dimensions => { const width = comparisonFit === 'fill' ? fittedDims.width : (fittedDims.width * secondImageDims.width) / firstImageDims.width; const height = diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts index 2bac462f12b..730077b304b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts @@ -1,5 +1,5 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { CanvasV2State, Size } from 'features/controlLayers/store/types'; +import type { CanvasV2State, Dimensions } from 'features/controlLayers/store/types'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { isEqual, pick } from 'lodash-es'; import type { Invocation } from 'services/api/types'; @@ -10,8 +10,8 @@ export const addImageToImage = async ( l2i: Invocation<'l2i'>, denoise: Invocation<'denoise_latents'>, vaeSource: Invocation<'main_model_loader' | 'sdxl_model_loader' | 'seamless' | 'vae_loader'>, - originalSize: Size, - scaledSize: Size, + originalSize: Dimensions, + scaledSize: Dimensions, bbox: CanvasV2State['bbox'], denoising_start: number ): Promise> => { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts index ceeed6caa4d..24c10a6ea3c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts @@ -1,5 +1,5 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { CanvasV2State, Size } from 'features/controlLayers/store/types'; +import type { CanvasV2State, Dimensions } from 'features/controlLayers/store/types'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import type { ParameterPrecision } from 'features/parameters/types/parameterSchemas'; import { isEqual, pick } from 'lodash-es'; @@ -12,8 +12,8 @@ export const addInpaint = async ( denoise: Invocation<'denoise_latents'>, vaeSource: Invocation<'main_model_loader' | 'sdxl_model_loader' | 'seamless' | 'vae_loader'>, modelLoader: Invocation<'main_model_loader' | 'sdxl_model_loader'>, - originalSize: Size, - scaledSize: Size, + originalSize: Dimensions, + scaledSize: Dimensions, bbox: CanvasV2State['bbox'], compositing: CanvasV2State['compositing'], denoising_start: number, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index fd0dd8b1930..6a7c5ec6f88 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts @@ -1,5 +1,5 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { CanvasV2State, Size } from 'features/controlLayers/store/types'; +import type { CanvasV2State, Dimensions } from 'features/controlLayers/store/types'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { getInfill } from 'features/nodes/util/graph/graphBuilderUtils'; import type { ParameterPrecision } from 'features/parameters/types/parameterSchemas'; @@ -13,8 +13,8 @@ export const addOutpaint = async ( denoise: Invocation<'denoise_latents'>, vaeSource: Invocation<'main_model_loader' | 'sdxl_model_loader' | 'seamless' | 'vae_loader'>, modelLoader: Invocation<'main_model_loader' | 'sdxl_model_loader'>, - originalSize: Size, - scaledSize: Size, + originalSize: Dimensions, + scaledSize: Dimensions, bbox: CanvasV2State['bbox'], compositing: CanvasV2State['compositing'], denoising_start: number, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addTextToImage.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addTextToImage.ts index 6a80c325faa..bc11f76be2b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addTextToImage.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addTextToImage.ts @@ -1,4 +1,4 @@ -import type { Size } from 'features/controlLayers/store/types'; +import type { Dimensions } from 'features/controlLayers/store/types'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { isEqual } from 'lodash-es'; import type { Invocation } from 'services/api/types'; @@ -6,8 +6,8 @@ import type { Invocation } from 'services/api/types'; export const addTextToImage = ( g: Graph, l2i: Invocation<'l2i'>, - originalSize: Size, - scaledSize: Size + originalSize: Dimensions, + scaledSize: Dimensions ): Invocation<'img_resize' | 'l2i'> => { if (!isEqual(scaledSize, originalSize)) { // We need to resize the output image back to the original size From 6632727d006fa677e906b2c8f45d6facfb0133f6 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 17 Jul 2024 10:50:20 +1000 Subject: [PATCH 217/678] fix(ui): remove weird rtkq hook wrapper I do not understand why I did that initially but it doesn't work with TS. --- invokeai/frontend/web/src/services/api/endpoints/images.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/endpoints/images.ts b/invokeai/frontend/web/src/services/api/endpoints/images.ts index 9ba144ecd8e..43b6347b5fa 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/images.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/images.ts @@ -549,6 +549,7 @@ export const imagesApi = api.injectEndpoints({ export const { useGetIntermediatesCountQuery, useListImagesQuery, + useGetImageDTOQuery, useGetImageMetadataQuery, useGetImageWorkflowQuery, useLazyGetImageWorkflowQuery, @@ -566,10 +567,6 @@ export const { useBulkDownloadImagesMutation, } = imagesApi; -export const useGetImageDTOQuery = (...args: Parameters) => { - return imagesApi.useGetImageDTOQuery(...args); -}; - /** * Imperative RTKQ helper to fetch an ImageDTO. * @param image_name The name of the image to fetch From 9105c02681219851376449ed25bc21a5d7d3cac3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 17 Jul 2024 11:08:19 +1000 Subject: [PATCH 218/678] feat(ui): use `position` and `dimensions` instead of separate x,y,width,height attrs --- .../addCommitStagingAreaImageListener.ts | 2 +- .../components/HeadsUpDisplay.tsx | 4 +- .../konva/CanvasControlAdapter.ts | 11 +-- .../controlLayers/konva/CanvasInitialImage.ts | 2 +- .../controlLayers/konva/CanvasInpaintMask.ts | 11 +-- .../controlLayers/konva/CanvasLayer.ts | 8 +-- .../controlLayers/konva/CanvasManager.ts | 6 +- .../controlLayers/konva/CanvasRegion.ts | 11 +-- .../controlLayers/konva/CanvasStateApi.ts | 4 +- .../features/controlLayers/konva/events.ts | 70 ++++++++++--------- .../controlLayers/store/canvasV2Slice.ts | 12 ++-- .../store/controlAdaptersReducers.ts | 16 ++--- .../store/inpaintMaskReducers.ts | 13 ++-- .../controlLayers/store/layersReducers.ts | 30 ++++---- .../controlLayers/store/regionsReducers.ts | 16 ++--- .../src/features/controlLayers/store/types.ts | 44 ++++++------ 16 files changed, 130 insertions(+), 130 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts index 85fbac41ec4..dcd3d7f70c0 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts @@ -58,7 +58,7 @@ export const addStagingListeners = (startAppListening: AppStartListening) => { assert(stagingAreaImage, 'No staged image found to accept'); const { x, y } = state.canvasV2.bbox.rect; - api.dispatch(layerAddedFromStagingArea({ stagingAreaImage, pos: { x, y } })); + api.dispatch(layerAddedFromStagingArea({ stagingAreaImage, position: { x, y } })); api.dispatch(sessionStagingAreaReset()); }, }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx index 5f3bcab13cd..abf799c8934 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx @@ -24,10 +24,10 @@ export const HeadsUpDisplay = memo(() => { return ( - + diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts index 8b4208a6a6c..4c29b17eb0c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts @@ -42,12 +42,15 @@ export class CanvasControlAdapter { }); this.transformer.on('transformend', () => { this.manager.stateApi.onScaleChanged( - { id: this.id, scale: this.group.scaleX(), x: this.group.x(), y: this.group.y() }, + { id: this.id, scale: this.group.scaleX(), position: { x: this.group.x(), y: this.group.y() } }, 'control_adapter' ); }); this.transformer.on('dragend', () => { - this.manager.stateApi.onPosChanged({ id: this.id, x: this.group.x(), y: this.group.y() }, 'control_adapter'); + this.manager.stateApi.onPosChanged( + { id: this.id, position: { x: this.group.x(), y: this.group.y() } }, + 'control_adapter' + ); }); this.layer.add(this.transformer); @@ -60,8 +63,8 @@ export class CanvasControlAdapter { // Update the layer's position and listening state this.group.setAttrs({ - x: controlAdapterState.x, - y: controlAdapterState.y, + x: controlAdapterState.position.x, + y: controlAdapterState.position.y, scaleX: 1, scaleY: 1, }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts index 868e8a9761a..b3aaf24ef23 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts @@ -42,7 +42,7 @@ export class CanvasInitialImage { } if (!this.image) { - this.image = await new CanvasImage(this.initialImageState.imageObject, {}); + this.image = await new CanvasImage(this.initialImageState.imageObject); this.objectsGroup.add(this.image.konvaImageGroup); await this.image.update(this.initialImageState.imageObject, true); } else if (!this.image.isLoading && !this.image.isError) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts index b30dae3f5d2..c470b87dd0c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts @@ -46,12 +46,15 @@ export class CanvasInpaintMask { }); this.transformer.on('transformend', () => { this.manager.stateApi.onScaleChanged( - { id: this.id, scale: this.group.scaleX(), x: this.group.x(), y: this.group.y() }, + { id: this.id, scale: this.group.scaleX(), position: { x: this.group.x(), y: this.group.y() } }, 'inpaint_mask' ); }); this.transformer.on('dragend', () => { - this.manager.stateApi.onPosChanged({ id: this.id, x: this.group.x(), y: this.group.y() }, 'inpaint_mask'); + this.manager.stateApi.onPosChanged( + { id: this.id, position: { x: this.group.x(), y: this.group.y() } }, + 'inpaint_mask' + ); }); this.layer.add(this.transformer); @@ -103,8 +106,8 @@ export class CanvasInpaintMask { // Update the layer's position and listening state this.group.setAttrs({ - x: inpaintMaskState.x, - y: inpaintMaskState.y, + x: inpaintMaskState.position.x, + y: inpaintMaskState.position.y, scaleX: 1, scaleY: 1, }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 49442876f4c..d6cb942a18d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -48,12 +48,12 @@ export class CanvasLayer { }); this.transformer.on('transformend', () => { this.manager.stateApi.onScaleChanged( - { id: this.id, scale: this.group.scaleX(), x: this.group.x(), y: this.group.y() }, + { id: this.id, scale: this.group.scaleX(), position: { x: this.group.x(), y: this.group.y() } }, 'layer' ); }); this.transformer.on('dragend', () => { - this.manager.stateApi.onPosChanged({ id: this.id, x: this.group.x(), y: this.group.y() }, 'layer'); + this.manager.stateApi.onPosChanged({ id: this.id, position: { x: this.group.x(), y: this.group.y() } }, 'layer'); }); this.layer.add(this.transformer); @@ -99,8 +99,8 @@ export class CanvasLayer { // Update the layer's position and listening state this.group.setAttrs({ - x: layerState.x, - y: layerState.y, + x: layerState.position.x, + y: layerState.position.y, scaleX: 1, scaleY: 1, }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 8d2d5b198cd..81c05076015 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -212,10 +212,8 @@ export class CanvasManager { this.stage.width(this.container.offsetWidth); this.stage.height(this.container.offsetHeight); this.stateApi.setStageAttrs({ - x: this.stage.x(), - y: this.stage.y(), - width: this.stage.width(), - height: this.stage.height(), + position: { x: this.stage.x(), y: this.stage.y() }, + dimensions: { width: this.stage.width(), height: this.stage.height() }, scale: this.stage.scaleX(), }); this.background.render(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts index 1a331e0827d..2ecbf959782 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts @@ -47,12 +47,15 @@ export class CanvasRegion { }); this.transformer.on('transformend', () => { this.manager.stateApi.onScaleChanged( - { id: this.id, scale: this.group.scaleX(), x: this.group.x(), y: this.group.y() }, + { id: this.id, scale: this.group.scaleX(), position: { x: this.group.x(), y: this.group.y() } }, 'regional_guidance' ); }); this.transformer.on('dragend', () => { - this.manager.stateApi.onPosChanged({ id: this.id, x: this.group.x(), y: this.group.y() }, 'regional_guidance'); + this.manager.stateApi.onPosChanged( + { id: this.id, position: { x: this.group.x(), y: this.group.y() } }, + 'regional_guidance' + ); }); this.layer.add(this.transformer); @@ -104,8 +107,8 @@ export class CanvasRegion { // Update the layer's position and listening state this.group.setAttrs({ - x: regionState.x, - y: regionState.y, + x: regionState.position.x, + y: regionState.position.y, scaleX: 1, scaleY: 1, }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index d40a51c3a6b..162789bd2e5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -47,7 +47,7 @@ import type { BrushLine, CanvasEntity, EraserLine, - PosChangedArg, + PositionChangedArg, RectShape, ScaleChangedArg, Tool, @@ -70,7 +70,7 @@ export class CanvasStateApi { return this.store.getState().canvasV2; }; - onPosChanged = (arg: PosChangedArg, entityType: CanvasEntity['type']) => { + onPosChanged = (arg: PositionChangedArg, entityType: CanvasEntity['type']) => { log.debug('onPosChanged'); if (entityType === 'layer') { this.store.dispatch(layerTranslated(arg)); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index b6869c43189..a04242695f2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -2,9 +2,9 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getScaledCursorPosition } from 'features/controlLayers/konva/util'; import type { CanvasV2State, + Coordinate, InpaintMaskEntity, LayerEntity, - Coordinate, RegionEntity, Tool, } from 'features/controlLayers/store/types'; @@ -141,15 +141,15 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (settings.clipToBbox) { return { - x: bboxRect.x - entity.x, - y: bboxRect.y - entity.y, + x: bboxRect.x - entity.position.x, + y: bboxRect.y - entity.position.y, width: bboxRect.width, height: bboxRect.height, }; } else { return { - x: -stage.x() / stage.scaleX() - entity.x, - y: -stage.y() / stage.scaleY() - entity.y, + x: -stage.x() / stage.scaleX() - entity.position.x, + y: -stage.y() / stage.scaleY() - entity.position.y, width: stage.width() / stage.scaleX(), height: stage.height() / stage.scaleY(), }; @@ -194,8 +194,8 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { // The last point of the last line is already normalized to the entity's coordinates lastLinePoint.x, lastLinePoint.y, - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, + pos.x - selectedEntity.position.x, + pos.y - selectedEntity.position.y, ], strokeWidth: toolState.brush.width, color: getCurrentFill(), @@ -208,7 +208,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.setDrawingBuffer({ id: getBrushLineId(selectedEntityAdapter.id, uuidv4()), type: 'brush_line', - points: [pos.x - selectedEntity.x, pos.y - selectedEntity.y], + points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y], strokeWidth: toolState.brush.width, color: getCurrentFill(), clip: getClip(selectedEntity), @@ -231,8 +231,8 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { // The last point of the last line is already normalized to the entity's coordinates lastLinePoint.x, lastLinePoint.y, - pos.x - selectedEntity.x, - pos.y - selectedEntity.y, + pos.x - selectedEntity.position.x, + pos.y - selectedEntity.position.y, ], strokeWidth: toolState.eraser.width, clip: getClip(selectedEntity), @@ -244,7 +244,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.setDrawingBuffer({ id: getEraserLineId(selectedEntityAdapter.id, uuidv4()), type: 'eraser_line', - points: [pos.x - selectedEntity.x, pos.y - selectedEntity.y], + points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y], strokeWidth: toolState.eraser.width, clip: getClip(selectedEntity), }); @@ -259,8 +259,8 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.setDrawingBuffer({ id: getRectShapeId(selectedEntityAdapter.id, uuidv4()), type: 'rect_shape', - x: pos.x - selectedEntity.x, - y: pos.y - selectedEntity.y, + x: pos.x - selectedEntity.position.x, + y: pos.y - selectedEntity.position.y, width: 0, height: 0, color: getCurrentFill(), @@ -342,7 +342,10 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (drawingBuffer?.type === 'brush_line') { const nextPoint = getNextPoint(pos, toolState, getLastPointOfLine(drawingBuffer.points)); if (nextPoint) { - drawingBuffer.points.push(nextPoint.x - selectedEntity.x, nextPoint.y - selectedEntity.y); + drawingBuffer.points.push( + nextPoint.x - selectedEntity.position.x, + nextPoint.y - selectedEntity.position.y + ); await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); setLastAddedPoint(nextPoint); } @@ -356,7 +359,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.setDrawingBuffer({ id: getBrushLineId(selectedEntityAdapter.id, uuidv4()), type: 'brush_line', - points: [pos.x - selectedEntity.x, pos.y - selectedEntity.y], + points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y], strokeWidth: toolState.brush.width, color: getCurrentFill(), clip: getClip(selectedEntity), @@ -371,7 +374,10 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (drawingBuffer.type === 'eraser_line') { const nextPoint = getNextPoint(pos, toolState, getLastPointOfLine(drawingBuffer.points)); if (nextPoint) { - drawingBuffer.points.push(nextPoint.x - selectedEntity.x, nextPoint.y - selectedEntity.y); + drawingBuffer.points.push( + nextPoint.x - selectedEntity.position.x, + nextPoint.y - selectedEntity.position.y + ); await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); setLastAddedPoint(nextPoint); } @@ -385,7 +391,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.setDrawingBuffer({ id: getEraserLineId(selectedEntityAdapter.id, uuidv4()), type: 'eraser_line', - points: [pos.x - selectedEntity.x, pos.y - selectedEntity.y], + points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y], strokeWidth: toolState.eraser.width, clip: getClip(selectedEntity), }); @@ -397,8 +403,8 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); if (drawingBuffer) { if (drawingBuffer.type === 'rect_shape') { - drawingBuffer.width = pos.x - selectedEntity.x - drawingBuffer.x; - drawingBuffer.height = pos.y - selectedEntity.y - drawingBuffer.y; + drawingBuffer.width = pos.x - selectedEntity.position.x - drawingBuffer.x; + drawingBuffer.height = pos.y - selectedEntity.position.y - drawingBuffer.y; await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); } else { await selectedEntityAdapter.setDrawingBuffer(null); @@ -429,16 +435,16 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { ) { const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); if (toolState.selected === 'brush' && drawingBuffer?.type === 'brush_line') { - drawingBuffer.points.push(pos.x - selectedEntity.x, pos.y - selectedEntity.y); + drawingBuffer.points.push(pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y); await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); selectedEntityAdapter.finalizeDrawingBuffer(); } else if (toolState.selected === 'eraser' && drawingBuffer?.type === 'eraser_line') { - drawingBuffer.points.push(pos.x - selectedEntity.x, pos.y - selectedEntity.y); + drawingBuffer.points.push(pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y); await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); selectedEntityAdapter.finalizeDrawingBuffer(); } else if (toolState.selected === 'rect' && drawingBuffer?.type === 'rect_shape') { - drawingBuffer.width = pos.x - selectedEntity.x - drawingBuffer.x; - drawingBuffer.height = pos.y - selectedEntity.y - drawingBuffer.y; + drawingBuffer.width = pos.x - selectedEntity.position.x - drawingBuffer.x; + drawingBuffer.height = pos.y - selectedEntity.position.y - drawingBuffer.y; await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); selectedEntityAdapter.finalizeDrawingBuffer(); } @@ -484,7 +490,11 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { stage.scaleX(newScale); stage.scaleY(newScale); stage.position(newPos); - setStageAttrs({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale }); + setStageAttrs({ + position: newPos, + dimensions: { width: stage.width(), height: stage.height() }, + scale: newScale, + }); manager.background.render(); } } @@ -494,10 +504,8 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { //#region dragmove stage.on('dragmove', () => { setStageAttrs({ - x: Math.floor(stage.x()), - y: Math.floor(stage.y()), - width: stage.width(), - height: stage.height(), + position: { x: Math.floor(stage.x()), y: Math.floor(stage.y()) }, + dimensions: { width: stage.width(), height: stage.height() }, scale: stage.scaleX(), }); manager.background.render(); @@ -508,10 +516,8 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { stage.on('dragend', () => { // Stage position should always be an integer, else we get fractional pixels which are blurry setStageAttrs({ - x: Math.floor(stage.x()), - y: Math.floor(stage.y()), - width: stage.width(), - height: stage.height(), + position: { x: Math.floor(stage.x()), y: Math.floor(stage.y()) }, + dimensions: { width: stage.width(), height: stage.height() }, scale: stage.scaleX(), }); manager.preview.tool.render(); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 621d1fda826..30e673cdc38 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -50,8 +50,10 @@ const initialState: CanvasV2State = { imageCache: null, isEnabled: true, objects: [], - x: 0, - y: 0, + position: { + x: 0, + y: 0, + }, }, tool: { selected: 'view', @@ -369,10 +371,8 @@ const migrate = (state: any): any => { // Ephemeral state that does not need to be in redux export const $isPreviewVisible = atom(true); export const $stageAttrs = atom({ - x: 0, - y: 0, - width: 0, - height: 0, + position: { x: 0, y: 0 }, + dimensions: { width: 0, height: 0 }, scale: 0, }); export const $shouldShowStagedImage = atom(true); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts index e82b7aa2acd..0283f4097cd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts @@ -14,6 +14,7 @@ import type { ControlNetConfig, ControlNetData, Filter, + PositionChangedArg, ProcessorConfig, ScaleChangedArg, T2IAdapterConfig, @@ -35,8 +36,7 @@ export const controlAdaptersReducers = { state.controlAdapters.entities.push({ id, type: 'control_adapter', - x: 0, - y: 0, + position: { x: 0, y: 0 }, bbox: null, bboxNeedsUpdate: false, isEnabled: true, @@ -64,17 +64,16 @@ export const controlAdaptersReducers = { } ca.isEnabled = !ca.isEnabled; }, - caTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => { - const { id, x, y } = action.payload; + caTranslated: (state, action: PayloadAction) => { + const { id, position } = action.payload; const ca = selectCA(state, id); if (!ca) { return; } - ca.x = x; - ca.y = y; + ca.position = position; }, caScaled: (state, action: PayloadAction) => { - const { id, scale, x, y } = action.payload; + const { id, scale, position } = action.payload; const ca = selectCA(state, id); if (!ca) { return; @@ -92,8 +91,7 @@ export const controlAdaptersReducers = { ca.processedImageObject.height *= scale; ca.processedImageObject.width *= scale; } - ca.x = x; - ca.y = y; + ca.position = position; ca.bboxNeedsUpdate = true; state.layers.imageCache = null; }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts index 0e4c6bfc407..e49552cf2ac 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts @@ -2,6 +2,7 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import type { BrushLine, CanvasV2State, + Coordinate, EraserLine, InpaintMaskEntity, RectShape, @@ -28,13 +29,12 @@ export const inpaintMaskReducers = { imIsEnabledToggled: (state) => { state.inpaintMask.isEnabled = !state.inpaintMask.isEnabled; }, - imTranslated: (state, action: PayloadAction<{ x: number; y: number }>) => { - const { x, y } = action.payload; - state.inpaintMask.x = x; - state.inpaintMask.y = y; + imTranslated: (state, action: PayloadAction<{ position: Coordinate }>) => { + const { position } = action.payload; + state.inpaintMask.position = position; }, imScaled: (state, action: PayloadAction) => { - const { scale, x, y } = action.payload; + const { scale, position } = action.payload; for (const obj of state.inpaintMask.objects) { if (obj.type === 'brush_line') { obj.points = obj.points.map((point) => point * scale); @@ -49,8 +49,7 @@ export const inpaintMaskReducers = { obj.width *= scale; } } - state.inpaintMask.x = x; - state.inpaintMask.y = y; + state.inpaintMask.position = position; state.inpaintMask.bboxNeedsUpdate = true; state.inpaintMask.imageCache = null; }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index 52c7cf08496..0869db42867 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -8,10 +8,11 @@ import { v4 as uuidv4 } from 'uuid'; import type { BrushLine, CanvasV2State, + Coordinate, EraserLine, ImageObjectAddedArg, LayerEntity, - Coordinate, + PositionChangedArg, RectShape, ScaleChangedArg, StagingAreaImage, @@ -37,8 +38,7 @@ export const layersReducers = { bboxNeedsUpdate: false, objects: [], opacity: 1, - x: 0, - y: 0, + position: { x: 0, y: 0 }, }); state.selectedEntityIdentifier = { type: 'layer', id }; state.layers.imageCache = null; @@ -48,9 +48,9 @@ export const layersReducers = { layerAddedFromStagingArea: { reducer: ( state, - action: PayloadAction<{ id: string; objectId: string; stagingAreaImage: StagingAreaImage; pos: Coordinate }> + action: PayloadAction<{ id: string; objectId: string; stagingAreaImage: StagingAreaImage; position: Coordinate }> ) => { - const { id, objectId, stagingAreaImage, pos } = action.payload; + const { id, objectId, stagingAreaImage, position } = action.payload; const { imageDTO, offsetX, offsetY } = stagingAreaImage; const imageObject = imageDTOToImageObject(id, objectId, imageDTO); state.layers.entities.push({ @@ -61,13 +61,12 @@ export const layersReducers = { bboxNeedsUpdate: false, objects: [imageObject], opacity: 1, - x: pos.x + offsetX, - y: pos.y + offsetY, + position: { x: position.x + offsetX, y: position.y + offsetY }, }); state.selectedEntityIdentifier = { type: 'layer', id }; state.layers.imageCache = null; }, - prepare: (payload: { stagingAreaImage: StagingAreaImage; pos: Coordinate }) => ({ + prepare: (payload: { stagingAreaImage: StagingAreaImage; position: Coordinate }) => ({ payload: { ...payload, id: uuidv4(), objectId: uuidv4() }, }), }, @@ -86,14 +85,13 @@ export const layersReducers = { layer.isEnabled = !layer.isEnabled; state.layers.imageCache = null; }, - layerTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => { - const { id, x, y } = action.payload; + layerTranslated: (state, action: PayloadAction) => { + const { id, position } = action.payload; const layer = selectLayer(state, id); if (!layer) { return; } - layer.x = x; - layer.y = y; + layer.position = position; state.layers.imageCache = null; }, layerBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { @@ -121,8 +119,7 @@ export const layersReducers = { layer.bbox = null; layer.bboxNeedsUpdate = false; state.layers.imageCache = null; - layer.x = 0; - layer.y = 0; + layer.position = { x: 0, y: 0 }; }, layerDeleted: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -212,7 +209,7 @@ export const layersReducers = { state.layers.imageCache = null; }, layerScaled: (state, action: PayloadAction) => { - const { id, scale, x, y } = action.payload; + const { id, scale, position } = action.payload; const layer = selectLayer(state, id); if (!layer) { return; @@ -236,8 +233,7 @@ export const layersReducers = { obj.width *= scale; } } - layer.x = x; - layer.y = y; + layer.position = position; layer.bboxNeedsUpdate = true; state.layers.imageCache = null; }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index 1975d65681d..5a93dcd8d43 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -6,6 +6,7 @@ import type { CLIPVisionModelV2, EraserLine, IPMethodV2, + PositionChangedArg, RectShape, ScaleChangedArg, } from 'features/controlLayers/store/types'; @@ -61,8 +62,7 @@ export const regionsReducers = { bboxNeedsUpdate: false, objects: [], fill: getRGMaskFill(state), - x: 0, - y: 0, + position: { x: 0, y: 0 }, autoNegative: 'invert', positivePrompt: '', negativePrompt: null, @@ -97,16 +97,15 @@ export const regionsReducers = { rg.isEnabled = !rg.isEnabled; } }, - rgTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => { - const { id, x, y } = action.payload; + rgTranslated: (state, action: PayloadAction) => { + const { id, position } = action.payload; const rg = selectRG(state, id); if (rg) { - rg.x = x; - rg.y = y; + rg.position = position; } }, rgScaled: (state, action: PayloadAction) => { - const { id, scale, x, y } = action.payload; + const { id, scale, position } = action.payload; const rg = selectRG(state, id); if (!rg) { return; @@ -125,8 +124,7 @@ export const regionsReducers = { obj.width *= scale; } } - rg.x = x; - rg.y = y; + rg.position = position; rg.bboxNeedsUpdate = true; state.layers.imageCache = null; }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 836c04459ce..be4c3e1e9d1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -506,6 +506,18 @@ export const RGBA_RED: RgbaColor = { r: 255, g: 0, b: 0, a: 1 }; const zOpacity = z.number().gte(0).lte(1); +const zDimensions = z.object({ + width: z.number().int().positive(), + height: z.number().int().positive(), +}); +export type Dimensions = z.infer; + +const zCoordinate = z.object({ + x: z.number(), + y: z.number(), +}); +export type Coordinate = z.infer; + const zRect = z.object({ x: z.number(), y: z.number(), @@ -566,8 +578,7 @@ export const zLayerEntity = z.object({ id: zId, type: z.literal('layer'), isEnabled: z.boolean(), - x: z.number(), - y: z.number(), + position: zCoordinate, bbox: zRect.nullable(), bboxNeedsUpdate: z.boolean(), opacity: zOpacity, @@ -631,8 +642,7 @@ export const zRegionEntity = z.object({ id: zId, type: z.literal('regional_guidance'), isEnabled: z.boolean(), - x: z.number(), - y: z.number(), + position: zCoordinate, bbox: zRect.nullable(), bboxNeedsUpdate: z.boolean(), objects: z.array(zMaskObject), @@ -658,8 +668,7 @@ const zInpaintMaskEntity = z.object({ id: z.literal('inpaint_mask'), type: z.literal('inpaint_mask'), isEnabled: z.boolean(), - x: z.number(), - y: z.number(), + position: zCoordinate, bbox: zRect.nullable(), bboxNeedsUpdate: z.boolean(), objects: z.array(zMaskObject), @@ -682,8 +691,7 @@ const zControlAdapterEntityBase = z.object({ id: zId, type: z.literal('control_adapter'), isEnabled: z.boolean(), - x: z.number(), - y: z.number(), + position: zCoordinate, bbox: zRect.nullable(), bboxNeedsUpdate: z.boolean(), opacity: zOpacity, @@ -809,18 +817,6 @@ export type CanvasEntity = | InitialImageEntity; export type CanvasEntityIdentifier = Pick; -const zDimensions = z.object({ - width: z.number().int().positive(), - height: z.number().int().positive(), -}); -export type Dimensions = z.infer; - -const zCoordinate = z.object({ - x: z.number(), - y: z.number(), -}); -export type Coordinate = z.infer; - export type LoRA = { id: string; isEnabled: boolean; @@ -926,9 +922,9 @@ export type CanvasV2State = { }; }; -export type StageAttrs = { x: number; y: number; width: number; height: number; scale: number }; -export type PosChangedArg = { id: string; x: number; y: number }; -export type ScaleChangedArg = { id: string; scale: number; x: number; y: number }; +export type StageAttrs = { position: Coordinate; dimensions: Dimensions; scale: number }; +export type PositionChangedArg = { id: string; position: Coordinate }; +export type ScaleChangedArg = { id: string; scale: number; position: Coordinate }; export type BboxChangedArg = { id: string; bbox: Rect | null }; export type EraserLineAddedArg = { id: string; @@ -939,7 +935,7 @@ export type EraserLineAddedArg = { export type BrushLineAddedArg = EraserLineAddedArg & { color: RgbaColor }; export type PointAddedToLineArg = { id: string; point: [number, number] }; export type RectShapeAddedArg = { id: string; rect: IRect; color: RgbaColor }; -export type ImageObjectAddedArg = { id: string; imageDTO: ImageDTO; pos?: Coordinate }; +export type ImageObjectAddedArg = { id: string; imageDTO: ImageDTO; position?: Coordinate }; //#region Type guards export const isLine = (obj: RenderableObject): obj is BrushLine | EraserLine => { From aa3986e9f2cd73a0c41472a23aa3a7acedf92fda Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 17 Jul 2024 12:21:27 +1000 Subject: [PATCH 219/678] fix(ui): do not await creating new canvas image If you await this, it causes a race condition where multiple images are created. --- .../src/features/controlLayers/konva/CanvasControlAdapter.ts | 2 +- .../web/src/features/controlLayers/konva/CanvasInitialImage.ts | 2 +- .../web/src/features/controlLayers/konva/CanvasLayer.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts index 4c29b17eb0c..9e701c42758 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts @@ -79,7 +79,7 @@ export class CanvasControlAdapter { didDraw = true; } } else if (!this.image) { - this.image = await new CanvasImage(imageObject); + this.image = new CanvasImage(imageObject); this.updateGroup(true); this.objectsGroup.add(this.image.konvaImageGroup); await this.image.updateImageSource(imageObject.image.name); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts index b3aaf24ef23..7bf8bf77b80 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts @@ -42,7 +42,7 @@ export class CanvasInitialImage { } if (!this.image) { - this.image = await new CanvasImage(this.initialImageState.imageObject); + this.image = new CanvasImage(this.initialImageState.imageObject); this.objectsGroup.add(this.image.konvaImageGroup); await this.image.update(this.initialImageState.imageObject, true); } else if (!this.image.isLoading && !this.image.isError) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index d6cb942a18d..8f9a227a248 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -180,7 +180,7 @@ export class CanvasLayer { assert(image instanceof CanvasImage || image === undefined); if (!image) { - image = await new CanvasImage(obj); + image = new CanvasImage(obj); this.objects.set(image.id, image); this.objectsGroup.add(image.konvaImageGroup); await image.updateImageSource(obj.image.name); From 5fa10a3f8e1b6f7fc33004100d3124064a36265d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 17 Jul 2024 12:48:33 +1000 Subject: [PATCH 220/678] feat(ui): add names to all konva objects Makes troubleshooting much simpler --- .../controlLayers/konva/CanvasBackground.ts | 5 ++- .../controlLayers/konva/CanvasBbox.ts | 9 +++++- .../controlLayers/konva/CanvasBrushLine.ts | 6 ++++ .../konva/CanvasControlAdapter.ts | 18 +++++++---- .../controlLayers/konva/CanvasEraserLine.ts | 6 ++++ .../controlLayers/konva/CanvasImage.ts | 15 +++++++-- .../controlLayers/konva/CanvasInpaintMask.ts | 23 ++++++++----- .../controlLayers/konva/CanvasLayer.ts | 27 ++++++++-------- .../konva/CanvasProgressImage.ts | 8 +++-- .../konva/CanvasProgressPreview.ts | 5 ++- .../controlLayers/konva/CanvasRect.ts | 4 +++ .../controlLayers/konva/CanvasRegion.ts | 27 +++++++++------- .../controlLayers/konva/CanvasTool.ts | 32 +++++++++++++++---- 13 files changed, 130 insertions(+), 55 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts index 5bf87510d82..4ead021eb6f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts @@ -30,12 +30,15 @@ const getGridSpacing = (scale: number): number => { }; export class CanvasBackground { + static BASE_NAME = 'background'; + static LAYER_NAME = `${CanvasBackground.BASE_NAME}_layer`; + layer: Konva.Layer; manager: CanvasManager; constructor(manager: CanvasManager) { this.manager = manager; - this.layer = new Konva.Layer({ listening: false }); + this.layer = new Konva.Layer({ name: CanvasBackground.LAYER_NAME, listening: false }); } render() { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts index f5bc028a1bd..cfa44e8bf6b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts @@ -6,6 +6,11 @@ import { atom } from 'nanostores'; import { assert } from 'tsafe'; export class CanvasBbox { + static BASE_NAME = 'bbox'; + static GROUP_NAME = `${CanvasBbox.BASE_NAME}_group`; + static RECT_NAME = `${CanvasBbox.BASE_NAME}_rect`; + static TRANSFORMER_NAME = `${CanvasBbox.BASE_NAME}_transformer`; + group: Konva.Group; rect: Konva.Rect; transformer: Konva.Transformer; @@ -33,8 +38,9 @@ export class CanvasBbox { // Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully // transparent rect for this purpose. - this.group = new Konva.Group({ id: 'bbox_group', listening: false }); + this.group = new Konva.Group({ name: CanvasBbox.GROUP_NAME, listening: false }); this.rect = new Konva.Rect({ + name: CanvasBbox.RECT_NAME, listening: false, strokeEnabled: false, draggable: true, @@ -55,6 +61,7 @@ export class CanvasBbox { }); this.transformer = new Konva.Transformer({ + name: CanvasBbox.TRANSFORMER_NAME, borderDash: [5, 5], borderStroke: 'rgba(212,216,234,1)', borderEnabled: true, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts index e607e705ea1..e9cc950ef11 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts @@ -3,6 +3,10 @@ import type { BrushLine } from 'features/controlLayers/store/types'; import Konva from 'konva'; export class CanvasBrushLine { + static NAME_PREFIX = 'brush-line'; + static GROUP_NAME = `${CanvasBrushLine.NAME_PREFIX}_group`; + static LINE_NAME = `${CanvasBrushLine.NAME_PREFIX}_line`; + id: string; konvaLineGroup: Konva.Group; konvaLine: Konva.Line; @@ -12,10 +16,12 @@ export class CanvasBrushLine { const { id, strokeWidth, clip, color, points } = brushLine; this.id = id; this.konvaLineGroup = new Konva.Group({ + name: CanvasBrushLine.GROUP_NAME, clip, listening: false, }); this.konvaLine = new Konva.Line({ + name: CanvasBrushLine.LINE_NAME, id, listening: false, shadowForStrokeEnabled: false, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts index 9e701c42758..82d20d897f1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts @@ -1,11 +1,17 @@ import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { getObjectGroupId } from 'features/controlLayers/konva/naming'; import { type ControlAdapterEntity, isDrawingTool } from 'features/controlLayers/store/types'; import Konva from 'konva'; -import { v4 as uuidv4 } from 'uuid'; export class CanvasControlAdapter { + static NAME_PREFIX = 'control-adapter'; + static LAYER_NAME = `${CanvasControlAdapter.NAME_PREFIX}_layer`; + static TRANSFORMER_NAME = `${CanvasControlAdapter.NAME_PREFIX}_transformer`; + static GROUP_NAME = `${CanvasControlAdapter.NAME_PREFIX}_group`; + static OBJECT_GROUP_NAME = `${CanvasControlAdapter.NAME_PREFIX}_object-group`; + + private controlAdapterState: ControlAdapterEntity; + id: string; manager: CanvasManager; layer: Konva.Layer; @@ -13,26 +19,26 @@ export class CanvasControlAdapter { objectsGroup: Konva.Group; image: CanvasImage | null; transformer: Konva.Transformer; - private controlAdapterState: ControlAdapterEntity; constructor(controlAdapterState: ControlAdapterEntity, manager: CanvasManager) { const { id } = controlAdapterState; this.id = id; this.manager = manager; this.layer = new Konva.Layer({ - id, + name: CanvasControlAdapter.LAYER_NAME, imageSmoothingEnabled: false, listening: false, }); this.group = new Konva.Group({ - id: getObjectGroupId(this.layer.id(), uuidv4()), + name: CanvasControlAdapter.GROUP_NAME, listening: false, }); - this.objectsGroup = new Konva.Group({ listening: false }); + this.objectsGroup = new Konva.Group({ name: CanvasControlAdapter.GROUP_NAME, listening: false }); this.group.add(this.objectsGroup); this.layer.add(this.group); this.transformer = new Konva.Transformer({ + name: CanvasControlAdapter.TRANSFORMER_NAME, shouldOverdrawWholeArea: true, draggable: true, dragDistance: 0, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts index ead3b4e0906..b572feea049 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts @@ -4,6 +4,10 @@ import { RGBA_RED } from 'features/controlLayers/store/types'; import Konva from 'konva'; export class CanvasEraserLine { + static NAME_PREFIX = 'eraser-line'; + static GROUP_NAME = `${CanvasEraserLine.NAME_PREFIX}_group`; + static LINE_NAME = `${CanvasEraserLine.NAME_PREFIX}_line`; + id: string; konvaLineGroup: Konva.Group; konvaLine: Konva.Line; @@ -13,10 +17,12 @@ export class CanvasEraserLine { const { id, strokeWidth, clip, points } = eraserLine; this.id = id; this.konvaLineGroup = new Konva.Group({ + name: CanvasEraserLine.GROUP_NAME, clip, listening: false, }); this.konvaLine = new Konva.Line({ + name: CanvasEraserLine.LINE_NAME, id, listening: false, shadowForStrokeEnabled: false, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts index cb38a7136e1..89243e99af5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -7,6 +7,13 @@ import { getImageDTO } from 'services/api/endpoints/images'; import { assert } from 'tsafe'; export class CanvasImage { + static NAME_PREFIX = 'canvas-image'; + static GROUP_NAME = `${CanvasImage.NAME_PREFIX}_group`; + static IMAGE_NAME = `${CanvasImage.NAME_PREFIX}_image`; + static PLACEHOLDER_GROUP_NAME = `${CanvasImage.NAME_PREFIX}_placeholder-group`; + static PLACEHOLDER_RECT_NAME = `${CanvasImage.NAME_PREFIX}_placeholder-rect`; + static PLACEHOLDER_TEXT_NAME = `${CanvasImage.NAME_PREFIX}_placeholder-text`; + id: string; konvaImageGroup: Konva.Group; konvaPlaceholderGroup: Konva.Group; @@ -20,15 +27,17 @@ export class CanvasImage { constructor(imageObject: ImageObject) { const { id, width, height, x, y } = imageObject; - this.konvaImageGroup = new Konva.Group({ id, listening: false, x, y }); - this.konvaPlaceholderGroup = new Konva.Group({ listening: false }); + this.konvaImageGroup = new Konva.Group({ name: CanvasImage.GROUP_NAME, listening: false, x, y }); + this.konvaPlaceholderGroup = new Konva.Group({ name: CanvasImage.PLACEHOLDER_GROUP_NAME, listening: false }); this.konvaPlaceholderRect = new Konva.Rect({ + name: CanvasImage.PLACEHOLDER_RECT_NAME, fill: 'hsl(220 12% 45% / 1)', // 'base.500' width, height, listening: false, }); this.konvaPlaceholderText = new Konva.Text({ + name: CanvasImage.PLACEHOLDER_TEXT_NAME, fill: 'hsl(220 12% 10% / 1)', // 'base.900' width, height, @@ -73,7 +82,7 @@ export class CanvasImage { }); } else { this.konvaImage = new Konva.Image({ - id: this.id, + name: CanvasImage.IMAGE_NAME, listening: false, image: imageEl, width: this.lastImageObject.width, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts index c470b87dd0c..58264eb5a0b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts @@ -4,15 +4,23 @@ import { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine' import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox'; -import { getObjectGroupId } from 'features/controlLayers/konva/naming'; import { mapId } from 'features/controlLayers/konva/util'; import type { BrushLine, EraserLine, InpaintMaskEntity, RectShape } from 'features/controlLayers/store/types'; import { isDrawingTool, RGBA_RED } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { assert } from 'tsafe'; -import { v4 as uuidv4 } from 'uuid'; export class CanvasInpaintMask { + static NAME_PREFIX = 'inpaint-mask'; + static LAYER_NAME = `${CanvasInpaintMask.NAME_PREFIX}_layer`; + static TRANSFORMER_NAME = `${CanvasInpaintMask.NAME_PREFIX}_transformer`; + static GROUP_NAME = `${CanvasInpaintMask.NAME_PREFIX}_group`; + static OBJECT_GROUP_NAME = `${CanvasInpaintMask.NAME_PREFIX}_object-group`; + static COMPOSITING_RECT_NAME = `${CanvasInpaintMask.NAME_PREFIX}_compositing-rect`; + + private drawingBuffer: BrushLine | EraserLine | RectShape | null; + private inpaintMaskState: InpaintMaskEntity; + id = 'inpaint_mask'; manager: CanvasManager; layer: Konva.Layer; @@ -21,22 +29,21 @@ export class CanvasInpaintMask { compositingRect: Konva.Rect; transformer: Konva.Transformer; objects: Map; - private drawingBuffer: BrushLine | EraserLine | RectShape | null; - private inpaintMaskState: InpaintMaskEntity; constructor(entity: InpaintMaskEntity, manager: CanvasManager) { this.manager = manager; - this.layer = new Konva.Layer({ id: this.id }); + this.layer = new Konva.Layer({ name: CanvasInpaintMask.LAYER_NAME }); this.group = new Konva.Group({ - id: getObjectGroupId(this.layer.id(), uuidv4()), + name: CanvasInpaintMask.GROUP_NAME, listening: false, }); - this.objectsGroup = new Konva.Group({ listening: false }); + this.objectsGroup = new Konva.Group({ name: CanvasInpaintMask.OBJECT_GROUP_NAME, listening: false }); this.group.add(this.objectsGroup); this.layer.add(this.group); this.transformer = new Konva.Transformer({ + name: CanvasInpaintMask.TRANSFORMER_NAME, shouldOverdrawWholeArea: true, draggable: true, dragDistance: 0, @@ -58,7 +65,7 @@ export class CanvasInpaintMask { }); this.layer.add(this.transformer); - this.compositingRect = new Konva.Rect({ listening: false }); + this.compositingRect = new Konva.Rect({ name: CanvasInpaintMask.COMPOSITING_RECT_NAME, listening: false }); this.group.add(this.compositingRect); this.objects = new Map(); this.drawingBuffer = null; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 8f9a227a248..0539641b507 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -3,15 +3,22 @@ import { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine' import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; -import { getObjectGroupId } from 'features/controlLayers/konva/naming'; import { mapId } from 'features/controlLayers/konva/util'; import type { BrushLine, EraserLine, LayerEntity, RectShape } from 'features/controlLayers/store/types'; import { isDrawingTool } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { assert } from 'tsafe'; -import { v4 as uuidv4 } from 'uuid'; export class CanvasLayer { + static NAME_PREFIX = 'layer'; + static LAYER_NAME = `${CanvasLayer.NAME_PREFIX}_layer`; + static TRANSFORMER_NAME = `${CanvasLayer.NAME_PREFIX}_transformer`; + static GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_group`; + static OBJECT_GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_object-group`; + + private drawingBuffer: BrushLine | EraserLine | RectShape | null; + private layerState: LayerEntity; + id: string; manager: CanvasManager; layer: Konva.Layer; @@ -19,26 +26,18 @@ export class CanvasLayer { objectsGroup: Konva.Group; transformer: Konva.Transformer; objects: Map; - private drawingBuffer: BrushLine | EraserLine | RectShape | null; - private layerState: LayerEntity; constructor(entity: LayerEntity, manager: CanvasManager) { this.id = entity.id; this.manager = manager; - this.layer = new Konva.Layer({ - id: entity.id, - listening: false, - }); - - this.group = new Konva.Group({ - id: getObjectGroupId(this.layer.id(), uuidv4()), - listening: false, - }); - this.objectsGroup = new Konva.Group({ listening: false }); + this.layer = new Konva.Layer({ name: CanvasLayer.LAYER_NAME, listening: false }); + this.group = new Konva.Group({ name: CanvasLayer.GROUP_NAME, listening: false }); + this.objectsGroup = new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }); this.group.add(this.objectsGroup); this.layer.add(this.group); this.transformer = new Konva.Transformer({ + name: CanvasLayer.TRANSFORMER_NAME, shouldOverdrawWholeArea: true, draggable: true, dragDistance: 0, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts index 4e02a931a4d..ac98c457029 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts @@ -2,6 +2,10 @@ import { loadImage } from 'features/controlLayers/konva/util'; import Konva from 'konva'; export class CanvasProgressImage { + static NAME_PREFIX = 'progress-image'; + static GROUP_NAME = `${CanvasProgressImage.NAME_PREFIX}_group`; + static IMAGE_NAME = `${CanvasProgressImage.NAME_PREFIX}_image`; + id: string; progressImageId: string | null; konvaImageGroup: Konva.Group; @@ -11,7 +15,7 @@ export class CanvasProgressImage { constructor(arg: { id: string }) { const { id } = arg; - this.konvaImageGroup = new Konva.Group({ id, listening: false }); + this.konvaImageGroup = new Konva.Group({ name: CanvasProgressImage.GROUP_NAME, listening: false }); this.id = id; this.progressImageId = null; this.konvaImage = null; @@ -43,7 +47,7 @@ export class CanvasProgressImage { }); } else { this.konvaImage = new Konva.Image({ - id: this.id, + name: CanvasProgressImage.IMAGE_NAME, listening: false, image: imageEl, x, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressPreview.ts index f2e814cefa4..bcf42ea438f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressPreview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressPreview.ts @@ -4,13 +4,16 @@ import Konva from 'konva'; import type { InvocationDenoiseProgressEvent } from 'services/events/types'; export class CanvasProgressPreview { + static NAME_PREFIX = 'progress-preview'; + static GROUP_NAME = `${CanvasProgressPreview.NAME_PREFIX}_group`; + group: Konva.Group; progressImage: CanvasProgressImage; manager: CanvasManager; constructor(manager: CanvasManager) { this.manager = manager; - this.group = new Konva.Group({ listening: false }); + this.group = new Konva.Group({ name: CanvasProgressPreview.GROUP_NAME, listening: false }); this.progressImage = new CanvasProgressImage({ id: 'progress-image' }); this.group.add(this.progressImage.konvaImageGroup); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts index a5a8eea8b92..32c755d0f59 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts @@ -3,6 +3,9 @@ import type { RectShape } from 'features/controlLayers/store/types'; import Konva from 'konva'; export class CanvasRect { + static NAME_PREFIX = 'canvas-rect'; + static RECT_NAME = `${CanvasRect.NAME_PREFIX}_rect`; + id: string; konvaRect: Konva.Rect; lastRectShape: RectShape; @@ -11,6 +14,7 @@ export class CanvasRect { const { id, x, y, width, height } = rectShape; this.id = id; const konvaRect = new Konva.Rect({ + name: CanvasRect.RECT_NAME, id, x, y, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts index 2ecbf959782..980675ee143 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts @@ -4,15 +4,23 @@ import { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine' import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox'; -import { getObjectGroupId } from 'features/controlLayers/konva/naming'; import { mapId } from 'features/controlLayers/konva/util'; import type { BrushLine, EraserLine, RectShape, RegionEntity } from 'features/controlLayers/store/types'; import { isDrawingTool, RGBA_RED } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { assert } from 'tsafe'; -import { v4 as uuidv4 } from 'uuid'; export class CanvasRegion { + static NAME_PREFIX = 'region'; + static LAYER_NAME = `${CanvasRegion.NAME_PREFIX}_layer`; + static TRANSFORMER_NAME = `${CanvasRegion.NAME_PREFIX}_transformer`; + static GROUP_NAME = `${CanvasRegion.NAME_PREFIX}_group`; + static OBJECT_GROUP_NAME = `${CanvasRegion.NAME_PREFIX}_object-group`; + static COMPOSITING_RECT_NAME = `${CanvasRegion.NAME_PREFIX}_compositing-rect`; + + private drawingBuffer: BrushLine | EraserLine | RectShape | null; + private regionState: RegionEntity; + id: string; manager: CanvasManager; layer: Konva.Layer; @@ -21,23 +29,18 @@ export class CanvasRegion { compositingRect: Konva.Rect; transformer: Konva.Transformer; objects: Map; - private drawingBuffer: BrushLine | EraserLine | RectShape | null; - private regionState: RegionEntity; constructor(entity: RegionEntity, manager: CanvasManager) { this.id = entity.id; this.manager = manager; - this.layer = new Konva.Layer({ id: entity.id }); - - this.group = new Konva.Group({ - id: getObjectGroupId(this.layer.id(), uuidv4()), - listening: false, - }); - this.objectsGroup = new Konva.Group({ listening: false }); + this.layer = new Konva.Layer({ name: CanvasRegion.LAYER_NAME, listening: false }); + this.group = new Konva.Group({ name: CanvasRegion.GROUP_NAME, listening: false }); + this.objectsGroup = new Konva.Group({ name: CanvasRegion.OBJECT_GROUP_NAME, listening: false }); this.group.add(this.objectsGroup); this.layer.add(this.group); this.transformer = new Konva.Transformer({ + name: CanvasRegion.TRANSFORMER_NAME, shouldOverdrawWholeArea: true, draggable: true, dragDistance: 0, @@ -59,7 +62,7 @@ export class CanvasRegion { }); this.layer.add(this.transformer); - this.compositingRect = new Konva.Rect({ listening: false }); + this.compositingRect = new Konva.Rect({ name: CanvasRegion.COMPOSITING_RECT_NAME, listening: false }); this.group.add(this.compositingRect); this.objects = new Map(); this.drawingBuffer = null; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts index 76145f954b7..93b4633379b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts @@ -8,6 +8,22 @@ import { import Konva from 'konva'; export class CanvasTool { + static NAME_PREFIX = 'tool'; + + static GROUP_NAME = `${CanvasTool.NAME_PREFIX}_group`; + + static BRUSH_NAME_PREFIX = `${CanvasTool.NAME_PREFIX}_brush`; + static BRUSH_GROUP_NAME = `${CanvasTool.BRUSH_NAME_PREFIX}_group`; + static BRUSH_FILL_CIRCLE_NAME = `${CanvasTool.BRUSH_NAME_PREFIX}_fill-circle`; + static BRUSH_INNER_BORDER_CIRCLE_NAME = `${CanvasTool.BRUSH_NAME_PREFIX}_inner-border-circle`; + static BRUSH_OUTER_BORDER_CIRCLE_NAME = `${CanvasTool.BRUSH_NAME_PREFIX}_outer-border-circle`; + + static ERASER_NAME_PREFIX = `${CanvasTool.NAME_PREFIX}_eraser`; + static ERASER_GROUP_NAME = `${CanvasTool.ERASER_NAME_PREFIX}_group`; + static ERASER_FILL_CIRCLE_NAME = `${CanvasTool.ERASER_NAME_PREFIX}_fill-circle`; + static ERASER_INNER_BORDER_CIRCLE_NAME = `${CanvasTool.ERASER_NAME_PREFIX}_inner-border-circle`; + static ERASER_OUTER_BORDER_CIRCLE_NAME = `${CanvasTool.ERASER_NAME_PREFIX}_outer-border-circle`; + manager: CanvasManager; group: Konva.Group; brush: { @@ -22,29 +38,28 @@ export class CanvasTool { innerBorderCircle: Konva.Circle; outerBorderCircle: Konva.Circle; }; - // rect: { - // group: Konva.Group; - // fillRect: Konva.Rect; - // }; constructor(manager: CanvasManager) { this.manager = manager; - this.group = new Konva.Group(); + this.group = new Konva.Group({ name: CanvasTool.GROUP_NAME }); // Create the brush preview group & circles this.brush = { - group: new Konva.Group(), + group: new Konva.Group({ name: CanvasTool.BRUSH_GROUP_NAME }), fillCircle: new Konva.Circle({ + name: CanvasTool.BRUSH_FILL_CIRCLE_NAME, listening: false, strokeEnabled: false, }), innerBorderCircle: new Konva.Circle({ + name: CanvasTool.BRUSH_INNER_BORDER_CIRCLE_NAME, listening: false, stroke: BRUSH_BORDER_INNER_COLOR, strokeWidth: BRUSH_ERASER_BORDER_WIDTH, strokeEnabled: true, }), outerBorderCircle: new Konva.Circle({ + name: CanvasTool.BRUSH_OUTER_BORDER_CIRCLE_NAME, listening: false, stroke: BRUSH_BORDER_OUTER_COLOR, strokeWidth: BRUSH_ERASER_BORDER_WIDTH, @@ -57,20 +72,23 @@ export class CanvasTool { this.group.add(this.brush.group); this.eraser = { - group: new Konva.Group(), + group: new Konva.Group({ name: CanvasTool.ERASER_GROUP_NAME }), fillCircle: new Konva.Circle({ + name: CanvasTool.ERASER_FILL_CIRCLE_NAME, listening: false, strokeEnabled: false, fill: 'white', globalCompositeOperation: 'destination-out', }), innerBorderCircle: new Konva.Circle({ + name: CanvasTool.ERASER_INNER_BORDER_CIRCLE_NAME, listening: false, stroke: BRUSH_BORDER_INNER_COLOR, strokeWidth: BRUSH_ERASER_BORDER_WIDTH, strokeEnabled: true, }), outerBorderCircle: new Konva.Circle({ + name: CanvasTool.ERASER_OUTER_BORDER_CIRCLE_NAME, listening: false, stroke: BRUSH_BORDER_OUTER_COLOR, strokeWidth: BRUSH_ERASER_BORDER_WIDTH, From 517ad7e77c7de39970763e2b08004f647354dc38 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:18:38 +1000 Subject: [PATCH 221/678] tidy(ui): update canvas classes, organise location of konva nodes --- .../controlLayers/konva/CanvasBackground.ts | 15 +- .../controlLayers/konva/CanvasBbox.ts | 178 ++++++++--------- .../controlLayers/konva/CanvasBrushLine.ts | 52 ++--- .../konva/CanvasControlAdapter.ts | 123 ++++++------ .../controlLayers/konva/CanvasEraserLine.ts | 52 ++--- .../controlLayers/konva/CanvasImage.ts | 115 +++++------ .../controlLayers/konva/CanvasInitialImage.ts | 50 ++--- .../controlLayers/konva/CanvasInpaintMask.ts | 123 ++++++------ .../controlLayers/konva/CanvasLayer.ts | 117 +++++++----- .../controlLayers/konva/CanvasManager.ts | 24 +-- .../controlLayers/konva/CanvasPreview.ts | 8 +- .../konva/CanvasProgressImage.ts | 22 ++- .../konva/CanvasProgressPreview.ts | 26 +-- .../controlLayers/konva/CanvasRect.ts | 35 ++-- .../controlLayers/konva/CanvasRegion.ts | 121 ++++++------ .../controlLayers/konva/CanvasStagingArea.ts | 22 ++- .../controlLayers/konva/CanvasTool.ts | 180 +++++++++--------- .../src/features/controlLayers/konva/util.ts | 16 +- 18 files changed, 678 insertions(+), 601 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts index 4ead021eb6f..c5b78ea7bb0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts @@ -33,16 +33,19 @@ export class CanvasBackground { static BASE_NAME = 'background'; static LAYER_NAME = `${CanvasBackground.BASE_NAME}_layer`; - layer: Konva.Layer; + konva: { + layer: Konva.Layer; + }; + manager: CanvasManager; constructor(manager: CanvasManager) { this.manager = manager; - this.layer = new Konva.Layer({ name: CanvasBackground.LAYER_NAME, listening: false }); + this.konva = { layer: new Konva.Layer({ name: CanvasBackground.LAYER_NAME, listening: false }) }; } render() { - this.layer.zIndex(0); + this.konva.layer.zIndex(0); const scale = this.manager.stage.scaleX(); const gridSpacing = getGridSpacing(scale); const x = this.manager.stage.x(); @@ -86,11 +89,11 @@ export class CanvasBackground { let _x = 0; let _y = 0; - this.layer.destroyChildren(); + this.konva.layer.destroyChildren(); for (let i = 0; i < xSteps; i++) { _x = gridFullRect.x1 + i * gridSpacing; - this.layer.add( + this.konva.layer.add( new Konva.Line({ x: _x, y: gridFullRect.y1, @@ -103,7 +106,7 @@ export class CanvasBackground { } for (let i = 0; i < ySteps; i++) { _y = gridFullRect.y1 + i * gridSpacing; - this.layer.add( + this.konva.layer.add( new Konva.Line({ x: gridFullRect.x1, y: _y, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts index cfa44e8bf6b..78b47e44205 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts @@ -11,11 +11,10 @@ export class CanvasBbox { static RECT_NAME = `${CanvasBbox.BASE_NAME}_rect`; static TRANSFORMER_NAME = `${CanvasBbox.BASE_NAME}_transformer`; - group: Konva.Group; - rect: Konva.Rect; - transformer: Konva.Transformer; manager: CanvasManager; + konva: { group: Konva.Group; rect: Konva.Rect; transformer: Konva.Transformer }; + ALL_ANCHORS: string[] = [ 'top-left', 'top-center', @@ -36,88 +35,89 @@ export class CanvasBbox { const bbox = this.manager.stateApi.getBbox(); const $aspectRatioBuffer = atom(bbox.rect.width / bbox.rect.height); - // Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully - // transparent rect for this purpose. - this.group = new Konva.Group({ name: CanvasBbox.GROUP_NAME, listening: false }); - this.rect = new Konva.Rect({ - name: CanvasBbox.RECT_NAME, - listening: false, - strokeEnabled: false, - draggable: true, - ...this.manager.stateApi.getBbox(), - }); - this.rect.on('dragmove', () => { + this.konva = { + group: new Konva.Group({ name: CanvasBbox.GROUP_NAME, listening: false }), + // Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully + // transparent rect for this purpose. + rect: new Konva.Rect({ + name: CanvasBbox.RECT_NAME, + listening: false, + strokeEnabled: false, + draggable: true, + ...this.manager.stateApi.getBbox(), + }), + transformer: new Konva.Transformer({ + name: CanvasBbox.TRANSFORMER_NAME, + borderDash: [5, 5], + borderStroke: 'rgba(212,216,234,1)', + borderEnabled: true, + rotateEnabled: false, + keepRatio: false, + ignoreStroke: true, + listening: false, + flipEnabled: false, + anchorFill: 'rgba(212,216,234,1)', + anchorStroke: 'rgb(42,42,42)', + anchorSize: 12, + anchorCornerRadius: 3, + shiftBehavior: 'none', // we will implement our own shift behavior + centeredScaling: false, + anchorStyleFunc: (anchor) => { + // Make the x/y resize anchors little bars + if (anchor.hasName('top-center') || anchor.hasName('bottom-center')) { + anchor.height(8); + anchor.offsetY(4); + anchor.width(30); + anchor.offsetX(15); + } + if (anchor.hasName('middle-left') || anchor.hasName('middle-right')) { + anchor.height(30); + anchor.offsetY(15); + anchor.width(8); + anchor.offsetX(4); + } + }, + anchorDragBoundFunc: (_oldAbsPos, newAbsPos) => { + // This function works with absolute position - that is, a position in "physical" pixels on the screen, as opposed + // to konva's internal coordinate system. + const stage = this.konva.transformer.getStage(); + assert(stage, 'Stage must exist'); + + // We need to snap the anchors to the grid. If the user is holding ctrl/meta, we use the finer 8px grid. + const gridSize = this.manager.stateApi.getCtrlKey() || this.manager.stateApi.getMetaKey() ? 8 : 64; + // Because we are working in absolute coordinates, we need to scale the grid size by the stage scale. + const scaledGridSize = gridSize * stage.scaleX(); + // To snap the anchor to the grid, we need to calculate an offset from the stage's absolute position. + const stageAbsPos = stage.getAbsolutePosition(); + // The offset is the remainder of the stage's absolute position divided by the scaled grid size. + const offsetX = stageAbsPos.x % scaledGridSize; + const offsetY = stageAbsPos.y % scaledGridSize; + // Finally, calculate the position by rounding to the grid and adding the offset. + return { + x: roundToMultiple(newAbsPos.x, scaledGridSize) + offsetX, + y: roundToMultiple(newAbsPos.y, scaledGridSize) + offsetY, + }; + }, + }), + }; + this.konva.rect.on('dragmove', () => { const gridSize = this.manager.stateApi.getCtrlKey() || this.manager.stateApi.getMetaKey() ? 8 : 64; const bbox = this.manager.stateApi.getBbox(); const bboxRect: Rect = { ...bbox.rect, - x: roundToMultiple(this.rect.x(), gridSize), - y: roundToMultiple(this.rect.y(), gridSize), + x: roundToMultiple(this.konva.rect.x(), gridSize), + y: roundToMultiple(this.konva.rect.y(), gridSize), }; - this.rect.setAttrs(bboxRect); + this.konva.rect.setAttrs(bboxRect); if (bbox.rect.x !== bboxRect.x || bbox.rect.y !== bboxRect.y) { this.manager.stateApi.onBboxTransformed(bboxRect); } }); - this.transformer = new Konva.Transformer({ - name: CanvasBbox.TRANSFORMER_NAME, - borderDash: [5, 5], - borderStroke: 'rgba(212,216,234,1)', - borderEnabled: true, - rotateEnabled: false, - keepRatio: false, - ignoreStroke: true, - listening: false, - flipEnabled: false, - anchorFill: 'rgba(212,216,234,1)', - anchorStroke: 'rgb(42,42,42)', - anchorSize: 12, - anchorCornerRadius: 3, - shiftBehavior: 'none', // we will implement our own shift behavior - centeredScaling: false, - anchorStyleFunc: (anchor) => { - // Make the x/y resize anchors little bars - if (anchor.hasName('top-center') || anchor.hasName('bottom-center')) { - anchor.height(8); - anchor.offsetY(4); - anchor.width(30); - anchor.offsetX(15); - } - if (anchor.hasName('middle-left') || anchor.hasName('middle-right')) { - anchor.height(30); - anchor.offsetY(15); - anchor.width(8); - anchor.offsetX(4); - } - }, - anchorDragBoundFunc: (_oldAbsPos, newAbsPos) => { - // This function works with absolute position - that is, a position in "physical" pixels on the screen, as opposed - // to konva's internal coordinate system. - const stage = this.transformer.getStage(); - assert(stage, 'Stage must exist'); - - // We need to snap the anchors to the grid. If the user is holding ctrl/meta, we use the finer 8px grid. - const gridSize = this.manager.stateApi.getCtrlKey() || this.manager.stateApi.getMetaKey() ? 8 : 64; - // Because we are working in absolute coordinates, we need to scale the grid size by the stage scale. - const scaledGridSize = gridSize * stage.scaleX(); - // To snap the anchor to the grid, we need to calculate an offset from the stage's absolute position. - const stageAbsPos = stage.getAbsolutePosition(); - // The offset is the remainder of the stage's absolute position divided by the scaled grid size. - const offsetX = stageAbsPos.x % scaledGridSize; - const offsetY = stageAbsPos.y % scaledGridSize; - // Finally, calculate the position by rounding to the grid and adding the offset. - return { - x: roundToMultiple(newAbsPos.x, scaledGridSize) + offsetX, - y: roundToMultiple(newAbsPos.y, scaledGridSize) + offsetY, - }; - }, - }); - - this.transformer.on('transform', () => { + this.konva.transformer.on('transform', () => { // In the transform callback, we calculate the bbox's new dims and pos and update the konva object. // Some special handling is needed depending on the anchor being dragged. - const anchor = this.transformer.getActiveAnchor(); + const anchor = this.konva.transformer.getActiveAnchor(); if (!anchor) { // Pretty sure we should always have an anchor here? return; @@ -140,14 +140,14 @@ export class CanvasBbox { } // The coords should be correct per the anchorDragBoundFunc. - let x = this.rect.x(); - let y = this.rect.y(); + let x = this.konva.rect.x(); + let y = this.konva.rect.y(); // Konva transforms by scaling the dims, not directly changing width and height. At this point, the width and height // *have not changed*, only the scale has changed. To get the final height, we need to scale the dims and then snap // them to the grid. - let width = roundToMultipleMin(this.rect.width() * this.rect.scaleX(), gridSize); - let height = roundToMultipleMin(this.rect.height() * this.rect.scaleY(), gridSize); + let width = roundToMultipleMin(this.konva.rect.width() * this.konva.rect.scaleX(), gridSize); + let height = roundToMultipleMin(this.konva.rect.height() * this.konva.rect.scaleY(), gridSize); // If shift is held and we are resizing from a corner, retain aspect ratio - needs special handling. We skip this // if alt/opt is held - this requires math too big for my brain. @@ -187,7 +187,7 @@ export class CanvasBbox { // Update the bboxRect's attrs directly with the new transform, and reset its scale to 1. // TODO(psyche): In `renderBboxPreview()` we also call setAttrs, need to do it twice to ensure it renders correctly. // Gotta be a way to avoid setting it twice... - this.rect.setAttrs({ ...bboxRect, scaleX: 1, scaleY: 1 }); + this.konva.rect.setAttrs({ ...bboxRect, scaleX: 1, scaleY: 1 }); // Update the bbox in internal state. this.manager.stateApi.onBboxTransformed(bboxRect); @@ -199,16 +199,16 @@ export class CanvasBbox { } }); - this.transformer.on('transformend', () => { + this.konva.transformer.on('transformend', () => { // Always update the aspect ratio buffer when the transform ends, so if the next transform starts with shift held, // we have the correct aspect ratio to start from. - $aspectRatioBuffer.set(this.rect.width() / this.rect.height()); + $aspectRatioBuffer.set(this.konva.rect.width() / this.konva.rect.height()); }); // The transformer will always be transforming the dummy rect - this.transformer.nodes([this.rect]); - this.group.add(this.rect); - this.group.add(this.transformer); + this.konva.transformer.nodes([this.konva.rect]); + this.konva.group.add(this.konva.rect); + this.konva.group.add(this.konva.transformer); } render() { @@ -217,14 +217,14 @@ export class CanvasBbox { const toolState = this.manager.stateApi.getToolState(); if (!session.isActive) { - this.group.listening(false); - this.group.visible(false); + this.konva.group.listening(false); + this.konva.group.visible(false); return; } - this.group.visible(true); - this.group.listening(toolState.selected === 'bbox'); - this.rect.setAttrs({ + this.konva.group.visible(true); + this.konva.group.listening(toolState.selected === 'bbox'); + this.konva.rect.setAttrs({ x: bbox.rect.x, y: bbox.rect.y, width: bbox.rect.width, @@ -233,7 +233,7 @@ export class CanvasBbox { scaleY: 1, listening: toolState.selected === 'bbox', }); - this.transformer.setAttrs({ + this.konva.transformer.setAttrs({ listening: toolState.selected === 'bbox', enabledAnchors: toolState.selected === 'bbox' ? this.ALL_ANCHORS : this.NO_ANCHORS, }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts index e9cc950ef11..b5ac4ff1e16 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts @@ -8,40 +8,44 @@ export class CanvasBrushLine { static LINE_NAME = `${CanvasBrushLine.NAME_PREFIX}_line`; id: string; - konvaLineGroup: Konva.Group; - konvaLine: Konva.Line; + konva: { + group: Konva.Group; + line: Konva.Line; + }; lastBrushLine: BrushLine; constructor(brushLine: BrushLine) { const { id, strokeWidth, clip, color, points } = brushLine; this.id = id; - this.konvaLineGroup = new Konva.Group({ - name: CanvasBrushLine.GROUP_NAME, - clip, - listening: false, - }); - this.konvaLine = new Konva.Line({ - name: CanvasBrushLine.LINE_NAME, - id, - listening: false, - shadowForStrokeEnabled: false, - strokeWidth, - tension: 0, - lineCap: 'round', - lineJoin: 'round', - globalCompositeOperation: 'source-over', - stroke: rgbaColorToString(color), - // A line with only one point will not be rendered, so we duplicate the points to make it visible - points: points.length === 2 ? [...points, ...points] : points, - }); - this.konvaLineGroup.add(this.konvaLine); + this.konva = { + group: new Konva.Group({ + name: CanvasBrushLine.GROUP_NAME, + clip, + listening: false, + }), + line: new Konva.Line({ + name: CanvasBrushLine.LINE_NAME, + id, + listening: false, + shadowForStrokeEnabled: false, + strokeWidth, + tension: 0, + lineCap: 'round', + lineJoin: 'round', + globalCompositeOperation: 'source-over', + stroke: rgbaColorToString(color), + // A line with only one point will not be rendered, so we duplicate the points to make it visible + points: points.length === 2 ? [...points, ...points] : points, + }), + }; + this.konva.group.add(this.konva.line); this.lastBrushLine = brushLine; } update(brushLine: BrushLine, force?: boolean): boolean { if (this.lastBrushLine !== brushLine || force) { const { points, color, clip, strokeWidth } = brushLine; - this.konvaLine.setAttrs({ + this.konva.line.setAttrs({ // A line with only one point will not be rendered, so we duplicate the points to make it visible points: points.length === 2 ? [...points, ...points] : points, stroke: rgbaColorToString(color), @@ -56,6 +60,6 @@ export class CanvasBrushLine { } destroy() { - this.konvaLineGroup.destroy(); + this.konva.group.destroy(); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts index 82d20d897f1..58772e8ff1f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts @@ -14,51 +14,60 @@ export class CanvasControlAdapter { id: string; manager: CanvasManager; - layer: Konva.Layer; - group: Konva.Group; - objectsGroup: Konva.Group; + + konva: { + layer: Konva.Layer; + group: Konva.Group; + objectGroup: Konva.Group; + transformer: Konva.Transformer; + }; + image: CanvasImage | null; - transformer: Konva.Transformer; constructor(controlAdapterState: ControlAdapterEntity, manager: CanvasManager) { const { id } = controlAdapterState; this.id = id; this.manager = manager; - this.layer = new Konva.Layer({ - name: CanvasControlAdapter.LAYER_NAME, - imageSmoothingEnabled: false, - listening: false, - }); - this.group = new Konva.Group({ - name: CanvasControlAdapter.GROUP_NAME, - listening: false, - }); - this.objectsGroup = new Konva.Group({ name: CanvasControlAdapter.GROUP_NAME, listening: false }); - this.group.add(this.objectsGroup); - this.layer.add(this.group); - - this.transformer = new Konva.Transformer({ - name: CanvasControlAdapter.TRANSFORMER_NAME, - shouldOverdrawWholeArea: true, - draggable: true, - dragDistance: 0, - enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], - rotateEnabled: false, - flipEnabled: false, - }); - this.transformer.on('transformend', () => { + this.konva = { + layer: new Konva.Layer({ + name: CanvasControlAdapter.LAYER_NAME, + imageSmoothingEnabled: false, + listening: false, + }), + group: new Konva.Group({ + name: CanvasControlAdapter.GROUP_NAME, + listening: false, + }), + objectGroup: new Konva.Group({ name: CanvasControlAdapter.GROUP_NAME, listening: false }), + transformer: new Konva.Transformer({ + name: CanvasControlAdapter.TRANSFORMER_NAME, + shouldOverdrawWholeArea: true, + draggable: true, + dragDistance: 0, + enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], + rotateEnabled: false, + flipEnabled: false, + }), + }; + this.konva.transformer.on('transformend', () => { this.manager.stateApi.onScaleChanged( - { id: this.id, scale: this.group.scaleX(), position: { x: this.group.x(), y: this.group.y() } }, + { + id: this.id, + scale: this.konva.group.scaleX(), + position: { x: this.konva.group.x(), y: this.konva.group.y() }, + }, 'control_adapter' ); }); - this.transformer.on('dragend', () => { + this.konva.transformer.on('dragend', () => { this.manager.stateApi.onPosChanged( - { id: this.id, position: { x: this.group.x(), y: this.group.y() } }, + { id: this.id, position: { x: this.konva.group.x(), y: this.konva.group.y() } }, 'control_adapter' ); }); - this.layer.add(this.transformer); + this.konva.group.add(this.konva.objectGroup); + this.konva.layer.add(this.konva.group); + this.konva.layer.add(this.konva.transformer); this.image = null; this.controlAdapterState = controlAdapterState; @@ -68,7 +77,7 @@ export class CanvasControlAdapter { this.controlAdapterState = controlAdapterState; // Update the layer's position and listening state - this.group.setAttrs({ + this.konva.group.setAttrs({ x: controlAdapterState.position.x, y: controlAdapterState.position.y, scaleX: 1, @@ -81,13 +90,13 @@ export class CanvasControlAdapter { if (!imageObject) { if (this.image) { - this.image.konvaImageGroup.visible(false); + this.image.konva.group.visible(false); didDraw = true; } } else if (!this.image) { this.image = new CanvasImage(imageObject); this.updateGroup(true); - this.objectsGroup.add(this.image.konvaImageGroup); + this.konva.objectGroup.add(this.image.konva.group); await this.image.updateImageSource(imageObject.image.name); } else if (!this.image.isLoading && !this.image.isError) { if (await this.image.update(imageObject)) { @@ -99,18 +108,18 @@ export class CanvasControlAdapter { } updateGroup(didDraw: boolean) { - this.layer.visible(this.controlAdapterState.isEnabled); + this.konva.layer.visible(this.controlAdapterState.isEnabled); - this.group.opacity(this.controlAdapterState.opacity); + this.konva.group.opacity(this.controlAdapterState.opacity); const isSelected = this.manager.stateApi.getIsSelected(this.id); const selectedTool = this.manager.stateApi.getToolState().selected; - if (!this.image?.konvaImage) { + if (!this.image?.image) { // If the layer is totally empty, reset the cache and bail out. - this.layer.listening(false); - this.transformer.nodes([]); - if (this.group.isCached()) { - this.group.clearCache(); + this.konva.layer.listening(false); + this.konva.transformer.nodes([]); + if (this.konva.group.isCached()) { + this.konva.group.clearCache(); } return; } @@ -118,32 +127,32 @@ export class CanvasControlAdapter { if (isSelected && selectedTool === 'move') { // When the layer is selected and being moved, we should always cache it. // We should update the cache if we drew to the layer. - if (!this.group.isCached() || didDraw) { - this.group.cache(); + if (!this.konva.group.isCached() || didDraw) { + this.konva.group.cache(); } // Activate the transformer - this.layer.listening(true); - this.transformer.nodes([this.group]); - this.transformer.forceUpdate(); + this.konva.layer.listening(true); + this.konva.transformer.nodes([this.konva.group]); + this.konva.transformer.forceUpdate(); return; } if (isSelected && selectedTool !== 'move') { // If the layer is selected but not using the move tool, we don't want the layer to be listening. - this.layer.listening(false); + this.konva.layer.listening(false); // The transformer also does not need to be active. - this.transformer.nodes([]); + this.konva.transformer.nodes([]); if (isDrawingTool(selectedTool)) { // We are using a drawing tool (brush, eraser, rect). These tools change the layer's rendered appearance, so we // should never be cached. - if (this.group.isCached()) { - this.group.clearCache(); + if (this.konva.group.isCached()) { + this.konva.group.clearCache(); } } else { // We are using a non-drawing tool (move, view, bbox), so we should cache the layer. // We should update the cache if we drew to the layer. - if (!this.group.isCached() || didDraw) { - this.group.cache(); + if (!this.konva.group.isCached() || didDraw) { + this.konva.group.cache(); } } return; @@ -151,12 +160,12 @@ export class CanvasControlAdapter { if (!isSelected) { // Unselected layers should not be listening - this.layer.listening(false); + this.konva.layer.listening(false); // The transformer also does not need to be active. - this.transformer.nodes([]); + this.konva.transformer.nodes([]); // Update the layer's cache if it's not already cached or we drew to it. - if (!this.group.isCached() || didDraw) { - this.group.cache(); + if (!this.konva.group.isCached() || didDraw) { + this.konva.group.cache(); } return; @@ -164,6 +173,6 @@ export class CanvasControlAdapter { } destroy(): void { - this.layer.destroy(); + this.konva.layer.destroy(); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts index b572feea049..b10b9d64fd3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts @@ -9,40 +9,44 @@ export class CanvasEraserLine { static LINE_NAME = `${CanvasEraserLine.NAME_PREFIX}_line`; id: string; - konvaLineGroup: Konva.Group; - konvaLine: Konva.Line; + konva: { + group: Konva.Group; + line: Konva.Line; + }; lastEraserLine: EraserLine; constructor(eraserLine: EraserLine) { const { id, strokeWidth, clip, points } = eraserLine; this.id = id; - this.konvaLineGroup = new Konva.Group({ - name: CanvasEraserLine.GROUP_NAME, - clip, - listening: false, - }); - this.konvaLine = new Konva.Line({ - name: CanvasEraserLine.LINE_NAME, - id, - listening: false, - shadowForStrokeEnabled: false, - strokeWidth, - tension: 0, - lineCap: 'round', - lineJoin: 'round', - globalCompositeOperation: 'destination-out', - stroke: rgbaColorToString(RGBA_RED), - // A line with only one point will not be rendered, so we duplicate the points to make it visible - points: points.length === 2 ? [...points, ...points] : points, - }); - this.konvaLineGroup.add(this.konvaLine); + this.konva = { + group: new Konva.Group({ + name: CanvasEraserLine.GROUP_NAME, + clip, + listening: false, + }), + line: new Konva.Line({ + name: CanvasEraserLine.LINE_NAME, + id, + listening: false, + shadowForStrokeEnabled: false, + strokeWidth, + tension: 0, + lineCap: 'round', + lineJoin: 'round', + globalCompositeOperation: 'destination-out', + stroke: rgbaColorToString(RGBA_RED), + // A line with only one point will not be rendered, so we duplicate the points to make it visible + points: points.length === 2 ? [...points, ...points] : points, + }), + }; + this.konva.group.add(this.konva.line); this.lastEraserLine = eraserLine; } update(eraserLine: EraserLine, force?: boolean): boolean { if (this.lastEraserLine !== eraserLine || force) { const { points, clip, strokeWidth } = eraserLine; - this.konvaLine.setAttrs({ + this.konva.line.setAttrs({ // A line with only one point will not be rendered, so we duplicate the points to make it visible points: points.length === 2 ? [...points, ...points] : points, clip, @@ -56,6 +60,6 @@ export class CanvasEraserLine { } destroy() { - this.konvaLineGroup.destroy(); + this.konva.group.destroy(); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts index 89243e99af5..b941ea96b77 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -15,48 +15,51 @@ export class CanvasImage { static PLACEHOLDER_TEXT_NAME = `${CanvasImage.NAME_PREFIX}_placeholder-text`; id: string; - konvaImageGroup: Konva.Group; - konvaPlaceholderGroup: Konva.Group; - konvaPlaceholderRect: Konva.Rect; - konvaPlaceholderText: Konva.Text; + konva: { + group: Konva.Group; + placeholder: { group: Konva.Group; rect: Konva.Rect; text: Konva.Text }; + }; imageName: string | null; - konvaImage: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately + image: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately isLoading: boolean; isError: boolean; lastImageObject: ImageObject; constructor(imageObject: ImageObject) { const { id, width, height, x, y } = imageObject; - this.konvaImageGroup = new Konva.Group({ name: CanvasImage.GROUP_NAME, listening: false, x, y }); - this.konvaPlaceholderGroup = new Konva.Group({ name: CanvasImage.PLACEHOLDER_GROUP_NAME, listening: false }); - this.konvaPlaceholderRect = new Konva.Rect({ - name: CanvasImage.PLACEHOLDER_RECT_NAME, - fill: 'hsl(220 12% 45% / 1)', // 'base.500' - width, - height, - listening: false, - }); - this.konvaPlaceholderText = new Konva.Text({ - name: CanvasImage.PLACEHOLDER_TEXT_NAME, - fill: 'hsl(220 12% 10% / 1)', // 'base.900' - width, - height, - align: 'center', - verticalAlign: 'middle', - fontFamily: '"Inter Variable", sans-serif', - fontSize: width / 16, - fontStyle: '600', - text: t('common.loadingImage', 'Loading Image'), - listening: false, - }); - - this.konvaPlaceholderGroup.add(this.konvaPlaceholderRect); - this.konvaPlaceholderGroup.add(this.konvaPlaceholderText); - this.konvaImageGroup.add(this.konvaPlaceholderGroup); + this.konva = { + group: new Konva.Group({ name: CanvasImage.GROUP_NAME, listening: false, x, y }), + placeholder: { + group: new Konva.Group({ name: CanvasImage.PLACEHOLDER_GROUP_NAME, listening: false }), + rect: new Konva.Rect({ + name: CanvasImage.PLACEHOLDER_RECT_NAME, + fill: 'hsl(220 12% 45% / 1)', // 'base.500' + width, + height, + listening: false, + }), + text: new Konva.Text({ + name: CanvasImage.PLACEHOLDER_TEXT_NAME, + fill: 'hsl(220 12% 10% / 1)', // 'base.900' + width, + height, + align: 'center', + verticalAlign: 'middle', + fontFamily: '"Inter Variable", sans-serif', + fontSize: width / 16, + fontStyle: '600', + text: t('common.loadingImage', 'Loading Image'), + listening: false, + }), + }, + }; + this.konva.placeholder.group.add(this.konva.placeholder.rect); + this.konva.placeholder.group.add(this.konva.placeholder.text); + this.konva.group.add(this.konva.placeholder.group); this.id = id; this.imageName = null; - this.konvaImage = null; + this.image = null; this.isLoading = false; this.isError = false; this.lastImageObject = imageObject; @@ -65,51 +68,51 @@ export class CanvasImage { async updateImageSource(imageName: string) { try { this.isLoading = true; - this.konvaImageGroup.visible(true); + this.konva.group.visible(true); - if (!this.konvaImage) { - this.konvaPlaceholderGroup.visible(true); - this.konvaPlaceholderText.text(t('common.loadingImage', 'Loading Image')); + if (!this.image) { + this.konva.placeholder.group.visible(true); + this.konva.placeholder.text.text(t('common.loadingImage', 'Loading Image')); } const imageDTO = await getImageDTO(imageName); assert(imageDTO !== null, 'imageDTO is null'); const imageEl = await loadImage(imageDTO.image_url); - if (this.konvaImage) { - this.konvaImage.setAttrs({ + if (this.image) { + this.image.setAttrs({ image: imageEl, }); } else { - this.konvaImage = new Konva.Image({ + this.image = new Konva.Image({ name: CanvasImage.IMAGE_NAME, listening: false, image: imageEl, width: this.lastImageObject.width, height: this.lastImageObject.height, }); - this.konvaImageGroup.add(this.konvaImage); + this.konva.group.add(this.image); } if (this.lastImageObject.filters.length > 0) { - this.konvaImage.cache(); - this.konvaImage.filters(this.lastImageObject.filters.map((f) => FILTER_MAP[f])); + this.image.cache(); + this.image.filters(this.lastImageObject.filters.map((f) => FILTER_MAP[f])); } else { - this.konvaImage.clearCache(); - this.konvaImage.filters([]); + this.image.clearCache(); + this.image.filters([]); } this.imageName = imageName; this.isLoading = false; this.isError = false; - this.konvaPlaceholderGroup.visible(false); + this.konva.placeholder.group.visible(false); } catch { - this.konvaImage?.visible(false); + this.image?.visible(false); this.imageName = null; this.isLoading = false; this.isError = true; - this.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load')); - this.konvaPlaceholderGroup.visible(true); + this.konva.placeholder.text.text(t('common.imageFailedToLoad', 'Image Failed to Load')); + this.konva.placeholder.group.visible(true); } } @@ -119,16 +122,16 @@ export class CanvasImage { if (this.lastImageObject.image.name !== image.name || force) { await this.updateImageSource(image.name); } - this.konvaImage?.setAttrs({ x, y, width, height }); + this.image?.setAttrs({ x, y, width, height }); if (filters.length > 0) { - this.konvaImage?.cache(); - this.konvaImage?.filters(filters.map((f) => FILTER_MAP[f])); + this.image?.cache(); + this.image?.filters(filters.map((f) => FILTER_MAP[f])); } else { - this.konvaImage?.clearCache(); - this.konvaImage?.filters([]); + this.image?.clearCache(); + this.image?.filters([]); } - this.konvaPlaceholderRect.setAttrs({ width, height }); - this.konvaPlaceholderText.setAttrs({ width, height, fontSize: width / 16 }); + this.konva.placeholder.rect.setAttrs({ width, height }); + this.konva.placeholder.text.setAttrs({ width, height, fontSize: width / 16 }); this.lastImageObject = imageObject; return true; } else { @@ -137,6 +140,6 @@ export class CanvasImage { } destroy() { - this.konvaImageGroup.destroy(); + this.konva.group.destroy(); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts index 7bf8bf77b80..589fbaec855 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts @@ -1,33 +1,37 @@ import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { getObjectGroupId } from 'features/controlLayers/konva/naming'; import type { InitialImageEntity } from 'features/controlLayers/store/types'; import Konva from 'konva'; -import { v4 as uuidv4 } from 'uuid'; export class CanvasInitialImage { + static NAME_PREFIX = 'initial-image'; + static LAYER_NAME = `${CanvasInitialImage.NAME_PREFIX}_layer`; + static GROUP_NAME = `${CanvasInitialImage.NAME_PREFIX}_group`; + static OBJECT_GROUP_NAME = `${CanvasInitialImage.NAME_PREFIX}_object-group`; + id = 'initial_image'; + + private initialImageState: InitialImageEntity; + manager: CanvasManager; - layer: Konva.Layer; - group: Konva.Group; - objectsGroup: Konva.Group; + + konva: { + layer: Konva.Layer; + group: Konva.Group; + objectGroup: Konva.Group; + }; + image: CanvasImage | null; - private initialImageState: InitialImageEntity; constructor(initialImageState: InitialImageEntity, manager: CanvasManager) { this.manager = manager; - this.layer = new Konva.Layer({ - id: this.id, - imageSmoothingEnabled: true, - listening: false, - }); - this.group = new Konva.Group({ - id: getObjectGroupId(this.layer.id(), uuidv4()), - listening: false, - }); - this.objectsGroup = new Konva.Group({ listening: false }); - this.group.add(this.objectsGroup); - this.layer.add(this.group); + this.konva = { + layer: new Konva.Layer({ name: CanvasInitialImage.LAYER_NAME, imageSmoothingEnabled: true, listening: false }), + group: new Konva.Group({ name: CanvasInitialImage.GROUP_NAME, listening: false }), + objectGroup: new Konva.Group({ name: CanvasInitialImage.OBJECT_GROUP_NAME, listening: false }), + }; + this.konva.group.add(this.konva.objectGroup); + this.konva.layer.add(this.konva.group); this.image = null; this.initialImageState = initialImageState; @@ -37,26 +41,26 @@ export class CanvasInitialImage { this.initialImageState = initialImageState; if (!this.initialImageState.imageObject) { - this.layer.visible(false); + this.konva.layer.visible(false); return; } if (!this.image) { this.image = new CanvasImage(this.initialImageState.imageObject); - this.objectsGroup.add(this.image.konvaImageGroup); + this.konva.objectGroup.add(this.image.konva.group); await this.image.update(this.initialImageState.imageObject, true); } else if (!this.image.isLoading && !this.image.isError) { await this.image.update(this.initialImageState.imageObject); } if (this.initialImageState && this.initialImageState.isEnabled && !this.image?.isLoading && !this.image?.isError) { - this.layer.visible(true); + this.konva.layer.visible(true); } else { - this.layer.visible(false); + this.konva.layer.visible(false); } } destroy(): void { - this.layer.destroy(); + this.konva.layer.destroy(); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts index 58264eb5a0b..a529db3b941 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts @@ -23,57 +23,64 @@ export class CanvasInpaintMask { id = 'inpaint_mask'; manager: CanvasManager; - layer: Konva.Layer; - group: Konva.Group; - objectsGroup: Konva.Group; - compositingRect: Konva.Rect; - transformer: Konva.Transformer; + + konva: { + layer: Konva.Layer; + group: Konva.Group; + objectGroup: Konva.Group; + transformer: Konva.Transformer; + compositingRect: Konva.Rect; + }; objects: Map; constructor(entity: InpaintMaskEntity, manager: CanvasManager) { this.manager = manager; - this.layer = new Konva.Layer({ name: CanvasInpaintMask.LAYER_NAME }); - this.group = new Konva.Group({ - name: CanvasInpaintMask.GROUP_NAME, - listening: false, - }); - this.objectsGroup = new Konva.Group({ name: CanvasInpaintMask.OBJECT_GROUP_NAME, listening: false }); - this.group.add(this.objectsGroup); - this.layer.add(this.group); - - this.transformer = new Konva.Transformer({ - name: CanvasInpaintMask.TRANSFORMER_NAME, - shouldOverdrawWholeArea: true, - draggable: true, - dragDistance: 0, - enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], - rotateEnabled: false, - flipEnabled: false, - }); - this.transformer.on('transformend', () => { + this.konva = { + layer: new Konva.Layer({ name: CanvasInpaintMask.LAYER_NAME }), + group: new Konva.Group({ name: CanvasInpaintMask.GROUP_NAME, listening: false }), + objectGroup: new Konva.Group({ name: CanvasInpaintMask.OBJECT_GROUP_NAME, listening: false }), + transformer: new Konva.Transformer({ + name: CanvasInpaintMask.TRANSFORMER_NAME, + shouldOverdrawWholeArea: true, + draggable: true, + dragDistance: 0, + enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], + rotateEnabled: false, + flipEnabled: false, + }), + compositingRect: new Konva.Rect({ name: CanvasInpaintMask.COMPOSITING_RECT_NAME, listening: false }), + }; + + this.konva.group.add(this.konva.objectGroup); + this.konva.layer.add(this.konva.group); + + this.konva.transformer.on('transformend', () => { this.manager.stateApi.onScaleChanged( - { id: this.id, scale: this.group.scaleX(), position: { x: this.group.x(), y: this.group.y() } }, + { + id: this.id, + scale: this.konva.group.scaleX(), + position: { x: this.konva.group.x(), y: this.konva.group.y() }, + }, 'inpaint_mask' ); }); - this.transformer.on('dragend', () => { + this.konva.transformer.on('dragend', () => { this.manager.stateApi.onPosChanged( - { id: this.id, position: { x: this.group.x(), y: this.group.y() } }, + { id: this.id, position: { x: this.konva.group.x(), y: this.konva.group.y() } }, 'inpaint_mask' ); }); - this.layer.add(this.transformer); + this.konva.layer.add(this.konva.transformer); - this.compositingRect = new Konva.Rect({ name: CanvasInpaintMask.COMPOSITING_RECT_NAME, listening: false }); - this.group.add(this.compositingRect); + this.konva.group.add(this.konva.compositingRect); this.objects = new Map(); this.drawingBuffer = null; this.inpaintMaskState = entity; } destroy(): void { - this.layer.destroy(); + this.konva.layer.destroy(); } getDrawingBuffer() { @@ -112,7 +119,7 @@ export class CanvasInpaintMask { this.inpaintMaskState = inpaintMaskState; // Update the layer's position and listening state - this.group.setAttrs({ + this.konva.group.setAttrs({ x: inpaintMaskState.position.x, y: inpaintMaskState.position.y, scaleX: 1, @@ -154,7 +161,7 @@ export class CanvasInpaintMask { if (!brushLine) { brushLine = new CanvasBrushLine(obj); this.objects.set(brushLine.id, brushLine); - this.objectsGroup.add(brushLine.konvaLineGroup); + this.konva.objectGroup.add(brushLine.konva.group); return true; } else { if (brushLine.update(obj, force)) { @@ -168,7 +175,7 @@ export class CanvasInpaintMask { if (!eraserLine) { eraserLine = new CanvasEraserLine(obj); this.objects.set(eraserLine.id, eraserLine); - this.objectsGroup.add(eraserLine.konvaLineGroup); + this.konva.objectGroup.add(eraserLine.konva.group); return true; } else { if (eraserLine.update(obj, force)) { @@ -182,7 +189,7 @@ export class CanvasInpaintMask { if (!rect) { rect = new CanvasRect(obj); this.objects.set(rect.id, rect); - this.objectsGroup.add(rect.konvaRect); + this.konva.objectGroup.add(rect.konva.group); return true; } else { if (rect.update(obj, force)) { @@ -195,19 +202,19 @@ export class CanvasInpaintMask { } updateGroup(didDraw: boolean) { - this.layer.visible(this.inpaintMaskState.isEnabled); + this.konva.layer.visible(this.inpaintMaskState.isEnabled); // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work - this.group.opacity(1); + this.konva.group.opacity(1); if (didDraw) { // Convert the color to a string, stripping the alpha - the object group will handle opacity. const rgbColor = rgbColorToString(this.inpaintMaskState.fill); const maskOpacity = this.manager.stateApi.getMaskOpacity(); - this.compositingRect.setAttrs({ + this.konva.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 - ...getNodeBboxFast(this.objectsGroup), + ...getNodeBboxFast(this.konva.objectGroup), fill: rgbColor, opacity: maskOpacity, // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) @@ -223,10 +230,10 @@ export class CanvasInpaintMask { if (this.objects.size === 0) { // If the layer is totally empty, reset the cache and bail out. - this.layer.listening(false); - this.transformer.nodes([]); - if (this.group.isCached()) { - this.group.clearCache(); + this.konva.layer.listening(false); + this.konva.transformer.nodes([]); + if (this.konva.group.isCached()) { + this.konva.group.clearCache(); } return; } @@ -234,32 +241,32 @@ export class CanvasInpaintMask { if (isSelected && selectedTool === 'move') { // When the layer is selected and being moved, we should always cache it. // We should update the cache if we drew to the layer. - if (!this.group.isCached() || didDraw) { - this.group.cache(); + if (!this.konva.group.isCached() || didDraw) { + this.konva.group.cache(); } // Activate the transformer - this.layer.listening(true); - this.transformer.nodes([this.group]); - this.transformer.forceUpdate(); + this.konva.layer.listening(true); + this.konva.transformer.nodes([this.konva.group]); + this.konva.transformer.forceUpdate(); return; } if (isSelected && selectedTool !== 'move') { // If the layer is selected but not using the move tool, we don't want the layer to be listening. - this.layer.listening(false); + this.konva.layer.listening(false); // The transformer also does not need to be active. - this.transformer.nodes([]); + this.konva.transformer.nodes([]); if (isDrawingTool(selectedTool)) { // We are using a drawing tool (brush, eraser, rect). These tools change the layer's rendered appearance, so we // should never be cached. - if (this.group.isCached()) { - this.group.clearCache(); + if (this.konva.group.isCached()) { + this.konva.group.clearCache(); } } else { // We are using a non-drawing tool (move, view, bbox), so we should cache the layer. // We should update the cache if we drew to the layer. - if (!this.group.isCached() || didDraw) { - this.group.cache(); + if (!this.konva.group.isCached() || didDraw) { + this.konva.group.cache(); } } return; @@ -267,12 +274,12 @@ export class CanvasInpaintMask { if (!isSelected) { // Unselected layers should not be listening - this.layer.listening(false); + this.konva.layer.listening(false); // The transformer also does not need to be active. - this.transformer.nodes([]); + this.konva.transformer.nodes([]); // Update the layer's cache if it's not already cached or we drew to it. - if (!this.group.isCached() || didDraw) { - this.group.cache(); + if (!this.konva.group.isCached() || didDraw) { + this.konva.group.cache(); } return; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 0539641b507..7951c9d0411 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -21,40 +21,53 @@ export class CanvasLayer { id: string; manager: CanvasManager; - layer: Konva.Layer; - group: Konva.Group; - objectsGroup: Konva.Group; - transformer: Konva.Transformer; + + konva: { + layer: Konva.Layer; + group: Konva.Group; + objectGroup: Konva.Group; + transformer: Konva.Transformer; + }; objects: Map; constructor(entity: LayerEntity, manager: CanvasManager) { this.id = entity.id; this.manager = manager; - this.layer = new Konva.Layer({ name: CanvasLayer.LAYER_NAME, listening: false }); - this.group = new Konva.Group({ name: CanvasLayer.GROUP_NAME, listening: false }); - this.objectsGroup = new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }); - this.group.add(this.objectsGroup); - this.layer.add(this.group); - - this.transformer = new Konva.Transformer({ - name: CanvasLayer.TRANSFORMER_NAME, - shouldOverdrawWholeArea: true, - draggable: true, - dragDistance: 0, - enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], - rotateEnabled: false, - flipEnabled: false, - }); - this.transformer.on('transformend', () => { + this.konva = { + layer: new Konva.Layer({ name: CanvasLayer.LAYER_NAME, listening: false }), + group: new Konva.Group({ name: CanvasLayer.GROUP_NAME, listening: false }), + objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }), + transformer: new Konva.Transformer({ + name: CanvasLayer.TRANSFORMER_NAME, + shouldOverdrawWholeArea: true, + draggable: true, + dragDistance: 0, + enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], + rotateEnabled: false, + flipEnabled: false, + }), + }; + + this.konva.group.add(this.konva.objectGroup); + this.konva.layer.add(this.konva.group); + + this.konva.transformer.on('transformend', () => { this.manager.stateApi.onScaleChanged( - { id: this.id, scale: this.group.scaleX(), position: { x: this.group.x(), y: this.group.y() } }, + { + id: this.id, + scale: this.konva.group.scaleX(), + position: { x: this.konva.group.x(), y: this.konva.group.y() }, + }, 'layer' ); }); - this.transformer.on('dragend', () => { - this.manager.stateApi.onPosChanged({ id: this.id, position: { x: this.group.x(), y: this.group.y() } }, 'layer'); + this.konva.transformer.on('dragend', () => { + this.manager.stateApi.onPosChanged( + { id: this.id, position: { x: this.konva.group.x(), y: this.konva.group.y() } }, + 'layer' + ); }); - this.layer.add(this.transformer); + this.konva.layer.add(this.konva.transformer); this.objects = new Map(); this.drawingBuffer = null; @@ -62,7 +75,7 @@ export class CanvasLayer { } destroy(): void { - this.layer.destroy(); + this.konva.layer.destroy(); } getDrawingBuffer() { @@ -97,7 +110,7 @@ export class CanvasLayer { this.layerState = layerState; // Update the layer's position and listening state - this.group.setAttrs({ + this.konva.group.setAttrs({ x: layerState.position.x, y: layerState.position.y, scaleX: 1, @@ -139,7 +152,7 @@ export class CanvasLayer { if (!brushLine) { brushLine = new CanvasBrushLine(obj); this.objects.set(brushLine.id, brushLine); - this.objectsGroup.add(brushLine.konvaLineGroup); + this.konva.objectGroup.add(brushLine.konva.group); return true; } else { if (brushLine.update(obj, force)) { @@ -153,7 +166,7 @@ export class CanvasLayer { if (!eraserLine) { eraserLine = new CanvasEraserLine(obj); this.objects.set(eraserLine.id, eraserLine); - this.objectsGroup.add(eraserLine.konvaLineGroup); + this.konva.objectGroup.add(eraserLine.konva.group); return true; } else { if (eraserLine.update(obj, force)) { @@ -167,7 +180,7 @@ export class CanvasLayer { if (!rect) { rect = new CanvasRect(obj); this.objects.set(rect.id, rect); - this.objectsGroup.add(rect.konvaRect); + this.konva.objectGroup.add(rect.konva.group); return true; } else { if (rect.update(obj, force)) { @@ -181,7 +194,7 @@ export class CanvasLayer { if (!image) { image = new CanvasImage(obj); this.objects.set(image.id, image); - this.objectsGroup.add(image.konvaImageGroup); + this.konva.objectGroup.add(image.konva.group); await image.updateImageSource(obj.image.name); return true; } else { @@ -196,58 +209,58 @@ export class CanvasLayer { updateGroup(didDraw: boolean) { if (!this.layerState.isEnabled) { - this.layer.visible(false); + this.konva.layer.visible(false); return; } - this.layer.visible(true); - this.group.opacity(this.layerState.opacity); + this.konva.layer.visible(true); + this.konva.group.opacity(this.layerState.opacity); const isSelected = this.manager.stateApi.getIsSelected(this.id); const selectedTool = this.manager.stateApi.getToolState().selected; if (this.objects.size === 0) { // If the layer is totally empty, reset the cache and bail out. - this.layer.listening(false); - this.transformer.nodes([]); - if (this.group.isCached()) { - this.group.clearCache(); + this.konva.layer.listening(false); + this.konva.transformer.nodes([]); + if (this.konva.group.isCached()) { + this.konva.group.clearCache(); } } else if (isSelected && selectedTool === 'move') { // When the layer is selected and being moved, we should always cache it. // We should update the cache if we drew to the layer. - if (!this.group.isCached() || didDraw) { - this.group.cache(); + if (!this.konva.group.isCached() || didDraw) { + this.konva.group.cache(); } // Activate the transformer - this.layer.listening(true); - this.transformer.nodes([this.group]); - this.transformer.forceUpdate(); + this.konva.layer.listening(true); + this.konva.transformer.nodes([this.konva.group]); + this.konva.transformer.forceUpdate(); } else if (isSelected && selectedTool !== 'move') { // If the layer is selected but not using the move tool, we don't want the layer to be listening. - this.layer.listening(false); + this.konva.layer.listening(false); // The transformer also does not need to be active. - this.transformer.nodes([]); + this.konva.transformer.nodes([]); if (isDrawingTool(selectedTool)) { // We are using a drawing tool (brush, eraser, rect). These tools change the layer's rendered appearance, so we // should never be cached. - if (this.group.isCached()) { - this.group.clearCache(); + if (this.konva.group.isCached()) { + this.konva.group.clearCache(); } } else { // We are using a non-drawing tool (move, view, bbox), so we should cache the layer. // We should update the cache if we drew to the layer. - if (!this.group.isCached() || didDraw) { - this.group.cache(); + if (!this.konva.group.isCached() || didDraw) { + this.konva.group.cache(); } } } else if (!isSelected) { // Unselected layers should not be listening - this.layer.listening(false); + this.konva.layer.listening(false); // The transformer also does not need to be active. - this.transformer.nodes([]); + this.konva.transformer.nodes([]); // Update the layer's cache if it's not already cached or we drew to it. - if (!this.group.isCached() || didDraw) { - this.group.cache(); + if (!this.konva.group.isCached() || didDraw) { + this.konva.group.cache(); } } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 81c05076015..0ff1ba6a16a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -97,17 +97,17 @@ export class CanvasManager { this.stage.add(this.preview.layer); this.background = new CanvasBackground(this); - this.stage.add(this.background.layer); + this.stage.add(this.background.konva.layer); this.inpaintMask = new CanvasInpaintMask(this.stateApi.getInpaintMaskState(), this); - this.stage.add(this.inpaintMask.layer); + this.stage.add(this.inpaintMask.konva.layer); this.layers = new Map(); this.regions = new Map(); this.controlAdapters = new Map(); this.initialImage = new CanvasInitialImage(this.stateApi.getInitialImageState(), this); - this.stage.add(this.initialImage.layer); + this.stage.add(this.initialImage.konva.layer); } async renderInitialImage() { @@ -129,7 +129,7 @@ export class CanvasManager { if (!adapter) { adapter = new CanvasLayer(entity, this); this.layers.set(adapter.id, adapter); - this.stage.add(adapter.layer); + this.stage.add(adapter.konva.layer); } await adapter.render(entity); } @@ -151,7 +151,7 @@ export class CanvasManager { if (!adapter) { adapter = new CanvasRegion(entity, this); this.regions.set(adapter.id, adapter); - this.stage.add(adapter.layer); + this.stage.add(adapter.konva.layer); } await adapter.render(entity); } @@ -181,7 +181,7 @@ export class CanvasManager { if (!adapter) { adapter = new CanvasControlAdapter(entity, this); this.controlAdapters.set(adapter.id, adapter); - this.stage.add(adapter.layer); + this.stage.add(adapter.konva.layer); } await adapter.render(entity); } @@ -193,18 +193,18 @@ export class CanvasManager { const controlAdapters = getControlAdaptersState().entities; const regions = getRegionsState().entities; let zIndex = 0; - this.background.layer.zIndex(++zIndex); - this.initialImage.layer.zIndex(++zIndex); + this.background.konva.layer.zIndex(++zIndex); + this.initialImage.konva.layer.zIndex(++zIndex); for (const layer of layers) { - this.layers.get(layer.id)?.layer.zIndex(++zIndex); + this.layers.get(layer.id)?.konva.layer.zIndex(++zIndex); } for (const ca of controlAdapters) { - this.controlAdapters.get(ca.id)?.layer.zIndex(++zIndex); + this.controlAdapters.get(ca.id)?.konva.layer.zIndex(++zIndex); } for (const rg of regions) { - this.regions.get(rg.id)?.layer.zIndex(++zIndex); + this.regions.get(rg.id)?.konva.layer.zIndex(++zIndex); } - this.inpaintMask.layer.zIndex(++zIndex); + this.inpaintMask.konva.layer.zIndex(++zIndex); this.preview.layer.zIndex(++zIndex); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts index 3e64b8a28d7..882a01e7afb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts @@ -21,15 +21,15 @@ export class CanvasPreview { this.layer = new Konva.Layer({ listening: true, imageSmoothingEnabled: false }); this.stagingArea = stagingArea; - this.layer.add(this.stagingArea.group); + this.layer.add(this.stagingArea.konva.group); this.bbox = bbox; - this.layer.add(this.bbox.group); + this.layer.add(this.bbox.konva.group); this.tool = tool; - this.layer.add(this.tool.group); + this.layer.add(this.tool.konva.group); this.progressPreview = progressPreview; - this.layer.add(this.progressPreview.group); + this.layer.add(this.progressPreview.konva.group); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts index ac98c457029..edda6c26f59 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts @@ -8,17 +8,21 @@ export class CanvasProgressImage { id: string; progressImageId: string | null; - konvaImageGroup: Konva.Group; - konvaImage: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately + konva: { + group: Konva.Group; + image: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately + }; isLoading: boolean; isError: boolean; constructor(arg: { id: string }) { const { id } = arg; - this.konvaImageGroup = new Konva.Group({ name: CanvasProgressImage.GROUP_NAME, listening: false }); + this.konva = { + group: new Konva.Group({ name: CanvasProgressImage.GROUP_NAME, listening: false }), + image: null, + }; this.id = id; this.progressImageId = null; - this.konvaImage = null; this.isLoading = false; this.isError = false; } @@ -37,8 +41,8 @@ export class CanvasProgressImage { this.isLoading = true; try { const imageEl = await loadImage(dataURL); - if (this.konvaImage) { - this.konvaImage.setAttrs({ + if (this.konva.image) { + this.konva.image.setAttrs({ image: imageEl, x, y, @@ -46,7 +50,7 @@ export class CanvasProgressImage { height, }); } else { - this.konvaImage = new Konva.Image({ + this.konva.image = new Konva.Image({ name: CanvasProgressImage.IMAGE_NAME, listening: false, image: imageEl, @@ -55,7 +59,7 @@ export class CanvasProgressImage { width, height, }); - this.konvaImageGroup.add(this.konvaImage); + this.konva.group.add(this.konva.image); } this.isLoading = false; this.id = progressImageId; @@ -65,6 +69,6 @@ export class CanvasProgressImage { } destroy() { - this.konvaImageGroup.destroy(); + this.konva.group.destroy(); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressPreview.ts index bcf42ea438f..95c7910b527 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressPreview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressPreview.ts @@ -7,15 +7,19 @@ export class CanvasProgressPreview { static NAME_PREFIX = 'progress-preview'; static GROUP_NAME = `${CanvasProgressPreview.NAME_PREFIX}_group`; - group: Konva.Group; - progressImage: CanvasProgressImage; + konva: { + group: Konva.Group; + progressImage: CanvasProgressImage; + }; manager: CanvasManager; constructor(manager: CanvasManager) { this.manager = manager; - this.group = new Konva.Group({ name: CanvasProgressPreview.GROUP_NAME, listening: false }); - this.progressImage = new CanvasProgressImage({ id: 'progress-image' }); - this.group.add(this.progressImage.konvaImageGroup); + this.konva = { + group: new Konva.Group({ name: CanvasProgressPreview.GROUP_NAME, listening: false }), + progressImage: new CanvasProgressImage({ id: 'progress-image' }), + }; + this.konva.group.add(this.konva.progressImage.konva.group); } async render(lastProgressEvent: InvocationDenoiseProgressEvent | null) { @@ -28,15 +32,15 @@ export class CanvasProgressPreview { const { x, y, width, height } = bboxRect; const progressImageId = `${invocation.id}_${step}`; if ( - !this.progressImage.isLoading && - !this.progressImage.isError && - this.progressImage.progressImageId !== progressImageId + !this.konva.progressImage.isLoading && + !this.konva.progressImage.isError && + this.konva.progressImage.progressImageId !== progressImageId ) { - await this.progressImage.updateImageSource(progressImageId, dataURL, x, y, width, height); - this.progressImage.konvaImageGroup.visible(true); + await this.konva.progressImage.updateImageSource(progressImageId, dataURL, x, y, width, height); + this.konva.progressImage.konva.group.visible(true); } } else { - this.progressImage.konvaImageGroup.visible(false); + this.konva.progressImage.konva.group.visible(false); } } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts index 32c755d0f59..62733091596 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts @@ -4,33 +4,40 @@ import Konva from 'konva'; export class CanvasRect { static NAME_PREFIX = 'canvas-rect'; + static GROUP_NAME = `${CanvasRect.NAME_PREFIX}_group`; static RECT_NAME = `${CanvasRect.NAME_PREFIX}_rect`; id: string; - konvaRect: Konva.Rect; + konva: { + group: Konva.Group; + rect: Konva.Rect; + }; lastRectShape: RectShape; constructor(rectShape: RectShape) { const { id, x, y, width, height } = rectShape; this.id = id; - const konvaRect = new Konva.Rect({ - name: CanvasRect.RECT_NAME, - id, - x, - y, - width, - height, - listening: false, - fill: rgbaColorToString(rectShape.color), - }); - this.konvaRect = konvaRect; + this.konva = { + group: new Konva.Group({ name: CanvasRect.GROUP_NAME, listening: false }), + rect: new Konva.Rect({ + name: CanvasRect.RECT_NAME, + id, + x, + y, + width, + height, + listening: false, + fill: rgbaColorToString(rectShape.color), + }), + }; + this.konva.group.add(this.konva.rect); this.lastRectShape = rectShape; } update(rectShape: RectShape, force?: boolean): boolean { if (this.lastRectShape !== rectShape || force) { const { x, y, width, height, color } = rectShape; - this.konvaRect.setAttrs({ + this.konva.rect.setAttrs({ x, y, width, @@ -45,6 +52,6 @@ export class CanvasRect { } destroy() { - this.konvaRect.destroy(); + this.konva.group.destroy(); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts index 980675ee143..86603973ece 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts @@ -23,54 +23,63 @@ export class CanvasRegion { id: string; manager: CanvasManager; - layer: Konva.Layer; - group: Konva.Group; - objectsGroup: Konva.Group; - compositingRect: Konva.Rect; - transformer: Konva.Transformer; + + konva: { + layer: Konva.Layer; + group: Konva.Group; + objectGroup: Konva.Group; + compositingRect: Konva.Rect; + transformer: Konva.Transformer; + }; + objects: Map; constructor(entity: RegionEntity, manager: CanvasManager) { this.id = entity.id; this.manager = manager; - this.layer = new Konva.Layer({ name: CanvasRegion.LAYER_NAME, listening: false }); - this.group = new Konva.Group({ name: CanvasRegion.GROUP_NAME, listening: false }); - this.objectsGroup = new Konva.Group({ name: CanvasRegion.OBJECT_GROUP_NAME, listening: false }); - this.group.add(this.objectsGroup); - this.layer.add(this.group); - - this.transformer = new Konva.Transformer({ - name: CanvasRegion.TRANSFORMER_NAME, - shouldOverdrawWholeArea: true, - draggable: true, - dragDistance: 0, - enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], - rotateEnabled: false, - flipEnabled: false, - }); - this.transformer.on('transformend', () => { + + this.konva = { + layer: new Konva.Layer({ name: CanvasRegion.LAYER_NAME, listening: false }), + group: new Konva.Group({ name: CanvasRegion.GROUP_NAME, listening: false }), + objectGroup: new Konva.Group({ name: CanvasRegion.OBJECT_GROUP_NAME, listening: false }), + transformer: new Konva.Transformer({ + name: CanvasRegion.TRANSFORMER_NAME, + shouldOverdrawWholeArea: true, + draggable: true, + dragDistance: 0, + enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], + rotateEnabled: false, + flipEnabled: false, + }), + compositingRect: new Konva.Rect({ name: CanvasRegion.COMPOSITING_RECT_NAME, listening: false }), + }; + this.konva.group.add(this.konva.objectGroup); + this.konva.layer.add(this.konva.group); + this.konva.transformer.on('transformend', () => { this.manager.stateApi.onScaleChanged( - { id: this.id, scale: this.group.scaleX(), position: { x: this.group.x(), y: this.group.y() } }, + { + id: this.id, + scale: this.konva.group.scaleX(), + position: { x: this.konva.group.x(), y: this.konva.group.y() }, + }, 'regional_guidance' ); }); - this.transformer.on('dragend', () => { + this.konva.transformer.on('dragend', () => { this.manager.stateApi.onPosChanged( - { id: this.id, position: { x: this.group.x(), y: this.group.y() } }, + { id: this.id, position: { x: this.konva.group.x(), y: this.konva.group.y() } }, 'regional_guidance' ); }); - this.layer.add(this.transformer); - - this.compositingRect = new Konva.Rect({ name: CanvasRegion.COMPOSITING_RECT_NAME, listening: false }); - this.group.add(this.compositingRect); + this.konva.layer.add(this.konva.transformer); + this.konva.group.add(this.konva.compositingRect); this.objects = new Map(); this.drawingBuffer = null; this.regionState = entity; } destroy(): void { - this.layer.destroy(); + this.konva.layer.destroy(); } getDrawingBuffer() { @@ -109,7 +118,7 @@ export class CanvasRegion { this.regionState = regionState; // Update the layer's position and listening state - this.group.setAttrs({ + this.konva.group.setAttrs({ x: regionState.position.x, y: regionState.position.y, scaleX: 1, @@ -151,7 +160,7 @@ export class CanvasRegion { if (!brushLine) { brushLine = new CanvasBrushLine(obj); this.objects.set(brushLine.id, brushLine); - this.objectsGroup.add(brushLine.konvaLineGroup); + this.konva.objectGroup.add(brushLine.konva.group); return true; } else { if (brushLine.update(obj, force)) { @@ -165,7 +174,7 @@ export class CanvasRegion { if (!eraserLine) { eraserLine = new CanvasEraserLine(obj); this.objects.set(eraserLine.id, eraserLine); - this.objectsGroup.add(eraserLine.konvaLineGroup); + this.konva.objectGroup.add(eraserLine.konva.group); return true; } else { if (eraserLine.update(obj, force)) { @@ -179,7 +188,7 @@ export class CanvasRegion { if (!rect) { rect = new CanvasRect(obj); this.objects.set(rect.id, rect); - this.objectsGroup.add(rect.konvaRect); + this.konva.objectGroup.add(rect.konva.group); return true; } else { if (rect.update(obj, force)) { @@ -192,18 +201,18 @@ export class CanvasRegion { } updateGroup(didDraw: boolean) { - this.layer.visible(this.regionState.isEnabled); + this.konva.layer.visible(this.regionState.isEnabled); // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work - this.group.opacity(1); + this.konva.group.opacity(1); if (didDraw) { // Convert the color to a string, stripping the alpha - the object group will handle opacity. const rgbColor = rgbColorToString(this.regionState.fill); const maskOpacity = this.manager.stateApi.getMaskOpacity(); - this.compositingRect.setAttrs({ + this.konva.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 - ...getNodeBboxFast(this.objectsGroup), + ...getNodeBboxFast(this.konva.objectGroup), fill: rgbColor, opacity: maskOpacity, // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) @@ -218,10 +227,10 @@ export class CanvasRegion { if (this.objects.size === 0) { // If the layer is totally empty, reset the cache and bail out. - this.layer.listening(false); - this.transformer.nodes([]); - if (this.group.isCached()) { - this.group.clearCache(); + this.konva.layer.listening(false); + this.konva.transformer.nodes([]); + if (this.konva.group.isCached()) { + this.konva.group.clearCache(); } return; } @@ -229,32 +238,32 @@ export class CanvasRegion { if (isSelected && selectedTool === 'move') { // When the layer is selected and being moved, we should always cache it. // We should update the cache if we drew to the layer. - if (!this.group.isCached() || didDraw) { - this.group.cache(); + if (!this.konva.group.isCached() || didDraw) { + this.konva.group.cache(); } // Activate the transformer - this.layer.listening(true); - this.transformer.nodes([this.group]); - this.transformer.forceUpdate(); + this.konva.layer.listening(true); + this.konva.transformer.nodes([this.konva.group]); + this.konva.transformer.forceUpdate(); return; } if (isSelected && selectedTool !== 'move') { // If the layer is selected but not using the move tool, we don't want the layer to be listening. - this.layer.listening(false); + this.konva.layer.listening(false); // The transformer also does not need to be active. - this.transformer.nodes([]); + this.konva.transformer.nodes([]); if (isDrawingTool(selectedTool)) { // We are using a drawing tool (brush, eraser, rect). These tools change the layer's rendered appearance, so we // should never be cached. - if (this.group.isCached()) { - this.group.clearCache(); + if (this.konva.group.isCached()) { + this.konva.group.clearCache(); } } else { // We are using a non-drawing tool (move, view, bbox), so we should cache the layer. // We should update the cache if we drew to the layer. - if (!this.group.isCached() || didDraw) { - this.group.cache(); + if (!this.konva.group.isCached() || didDraw) { + this.konva.group.cache(); } } return; @@ -262,12 +271,12 @@ export class CanvasRegion { if (!isSelected) { // Unselected layers should not be listening - this.layer.listening(false); + this.konva.layer.listening(false); // The transformer also does not need to be active. - this.transformer.nodes([]); + this.konva.transformer.nodes([]); // Update the layer's cache if it's not already cached or we drew to it. - if (!this.group.isCached() || didDraw) { - this.group.cache(); + if (!this.konva.group.isCached() || didDraw) { + this.konva.group.cache(); } return; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts index eada1fadcbe..cd4ae82e326 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts @@ -4,14 +4,18 @@ import type { StagingAreaImage } from 'features/controlLayers/store/types'; import Konva from 'konva'; export class CanvasStagingArea { - group: Konva.Group; + static NAME_PREFIX = 'staging-area'; + static GROUP_NAME = `${CanvasStagingArea.NAME_PREFIX}_group`; + + konva: { group: Konva.Group }; + image: CanvasImage | null; selectedImage: StagingAreaImage | null; manager: CanvasManager; constructor(manager: CanvasManager) { this.manager = manager; - this.group = new Konva.Group({ listening: false }); + this.konva = { group: new Konva.Group({ name: CanvasStagingArea.GROUP_NAME, listening: false }) }; this.image = null; this.selectedImage = null; } @@ -42,20 +46,20 @@ export class CanvasStagingArea { height, }, }); - this.group.add(this.image.konvaImageGroup); + this.konva.group.add(this.image.konva.group); } if (!this.image.isLoading && !this.image.isError && this.image.imageName !== imageDTO.image_name) { - this.image.konvaImage?.width(imageDTO.width); - this.image.konvaImage?.height(imageDTO.height); - this.image.konvaImageGroup.x(bboxRect.x + offsetX); - this.image.konvaImageGroup.y(bboxRect.y + offsetY); + this.image.image?.width(imageDTO.width); + this.image.image?.height(imageDTO.height); + this.image.konva.group.x(bboxRect.x + offsetX); + this.image.konva.group.y(bboxRect.y + offsetY); await this.image.updateImageSource(imageDTO.image_name); this.manager.stateApi.resetLastProgressEvent(); } - this.image.konvaImageGroup.visible(shouldShowStagedImage); + this.image.konva.group.visible(shouldShowStagedImage); } else { - this.image?.konvaImageGroup.visible(false); + this.image?.konva.group.visible(false); } } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts index 93b4633379b..41291ee80bd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts @@ -25,80 +25,82 @@ export class CanvasTool { static ERASER_OUTER_BORDER_CIRCLE_NAME = `${CanvasTool.ERASER_NAME_PREFIX}_outer-border-circle`; manager: CanvasManager; - group: Konva.Group; - brush: { + konva: { group: Konva.Group; - fillCircle: Konva.Circle; - innerBorderCircle: Konva.Circle; - outerBorderCircle: Konva.Circle; - }; - eraser: { - group: Konva.Group; - fillCircle: Konva.Circle; - innerBorderCircle: Konva.Circle; - outerBorderCircle: Konva.Circle; + brush: { + group: Konva.Group; + fillCircle: Konva.Circle; + innerBorderCircle: Konva.Circle; + outerBorderCircle: Konva.Circle; + }; + eraser: { + group: Konva.Group; + fillCircle: Konva.Circle; + innerBorderCircle: Konva.Circle; + outerBorderCircle: Konva.Circle; + }; }; constructor(manager: CanvasManager) { this.manager = manager; - this.group = new Konva.Group({ name: CanvasTool.GROUP_NAME }); - - // Create the brush preview group & circles - this.brush = { - group: new Konva.Group({ name: CanvasTool.BRUSH_GROUP_NAME }), - fillCircle: new Konva.Circle({ - name: CanvasTool.BRUSH_FILL_CIRCLE_NAME, - listening: false, - strokeEnabled: false, - }), - innerBorderCircle: new Konva.Circle({ - name: CanvasTool.BRUSH_INNER_BORDER_CIRCLE_NAME, - listening: false, - stroke: BRUSH_BORDER_INNER_COLOR, - strokeWidth: BRUSH_ERASER_BORDER_WIDTH, - strokeEnabled: true, - }), - outerBorderCircle: new Konva.Circle({ - name: CanvasTool.BRUSH_OUTER_BORDER_CIRCLE_NAME, - listening: false, - stroke: BRUSH_BORDER_OUTER_COLOR, - strokeWidth: BRUSH_ERASER_BORDER_WIDTH, - strokeEnabled: true, - }), - }; - this.brush.group.add(this.brush.fillCircle); - this.brush.group.add(this.brush.innerBorderCircle); - this.brush.group.add(this.brush.outerBorderCircle); - this.group.add(this.brush.group); - - this.eraser = { - group: new Konva.Group({ name: CanvasTool.ERASER_GROUP_NAME }), - fillCircle: new Konva.Circle({ - name: CanvasTool.ERASER_FILL_CIRCLE_NAME, - listening: false, - strokeEnabled: false, - fill: 'white', - globalCompositeOperation: 'destination-out', - }), - innerBorderCircle: new Konva.Circle({ - name: CanvasTool.ERASER_INNER_BORDER_CIRCLE_NAME, - listening: false, - stroke: BRUSH_BORDER_INNER_COLOR, - strokeWidth: BRUSH_ERASER_BORDER_WIDTH, - strokeEnabled: true, - }), - outerBorderCircle: new Konva.Circle({ - name: CanvasTool.ERASER_OUTER_BORDER_CIRCLE_NAME, - listening: false, - stroke: BRUSH_BORDER_OUTER_COLOR, - strokeWidth: BRUSH_ERASER_BORDER_WIDTH, - strokeEnabled: true, - }), + this.konva = { + group: new Konva.Group({ name: CanvasTool.GROUP_NAME }), + brush: { + group: new Konva.Group({ name: CanvasTool.BRUSH_GROUP_NAME }), + fillCircle: new Konva.Circle({ + name: CanvasTool.BRUSH_FILL_CIRCLE_NAME, + listening: false, + strokeEnabled: false, + }), + innerBorderCircle: new Konva.Circle({ + name: CanvasTool.BRUSH_INNER_BORDER_CIRCLE_NAME, + listening: false, + stroke: BRUSH_BORDER_INNER_COLOR, + strokeWidth: BRUSH_ERASER_BORDER_WIDTH, + strokeEnabled: true, + }), + outerBorderCircle: new Konva.Circle({ + name: CanvasTool.BRUSH_OUTER_BORDER_CIRCLE_NAME, + listening: false, + stroke: BRUSH_BORDER_OUTER_COLOR, + strokeWidth: BRUSH_ERASER_BORDER_WIDTH, + strokeEnabled: true, + }), + }, + eraser: { + group: new Konva.Group({ name: CanvasTool.ERASER_GROUP_NAME }), + fillCircle: new Konva.Circle({ + name: CanvasTool.ERASER_FILL_CIRCLE_NAME, + listening: false, + strokeEnabled: false, + fill: 'white', + globalCompositeOperation: 'destination-out', + }), + innerBorderCircle: new Konva.Circle({ + name: CanvasTool.ERASER_INNER_BORDER_CIRCLE_NAME, + listening: false, + stroke: BRUSH_BORDER_INNER_COLOR, + strokeWidth: BRUSH_ERASER_BORDER_WIDTH, + strokeEnabled: true, + }), + outerBorderCircle: new Konva.Circle({ + name: CanvasTool.ERASER_OUTER_BORDER_CIRCLE_NAME, + listening: false, + stroke: BRUSH_BORDER_OUTER_COLOR, + strokeWidth: BRUSH_ERASER_BORDER_WIDTH, + strokeEnabled: true, + }), + }, }; - this.eraser.group.add(this.eraser.fillCircle); - this.eraser.group.add(this.eraser.innerBorderCircle); - this.eraser.group.add(this.eraser.outerBorderCircle); - this.group.add(this.eraser.group); + this.konva.brush.group.add(this.konva.brush.fillCircle); + this.konva.brush.group.add(this.konva.brush.innerBorderCircle); + this.konva.brush.group.add(this.konva.brush.outerBorderCircle); + this.konva.group.add(this.konva.brush.group); + + this.konva.eraser.group.add(this.konva.eraser.fillCircle); + this.konva.eraser.group.add(this.konva.eraser.innerBorderCircle); + this.konva.eraser.group.add(this.konva.eraser.outerBorderCircle); + this.konva.group.add(this.konva.eraser.group); // // Create the rect preview - this is a rectangle drawn from the last mouse down position to the current cursor position // this.rect = { @@ -110,7 +112,7 @@ export class CanvasTool { // }), // }; // this.rect.group.add(this.rect.fillRect); - // this.group.add(this.rect.group); + // this.konva.group.add(this.rect.group); } scaleTool = () => { @@ -118,15 +120,15 @@ export class CanvasTool { const scale = this.manager.stage.scaleX(); const brushRadius = toolState.brush.width / 2; - this.brush.innerBorderCircle.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale); - this.brush.outerBorderCircle.setAttrs({ + this.konva.brush.innerBorderCircle.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale); + this.konva.brush.outerBorderCircle.setAttrs({ strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, radius: brushRadius + BRUSH_ERASER_BORDER_WIDTH / scale, }); const eraserRadius = toolState.eraser.width / 2; - this.eraser.innerBorderCircle.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale); - this.eraser.outerBorderCircle.setAttrs({ + this.konva.eraser.innerBorderCircle.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale); + this.konva.eraser.outerBorderCircle.setAttrs({ strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, radius: eraserRadius + BRUSH_ERASER_BORDER_WIDTH / scale, }); @@ -175,16 +177,16 @@ export class CanvasTool { if (!cursorPos || renderedEntityCount === 0 || !isDrawableEntity) { // We can bail early if the mouse isn't over the stage or there are no layers - this.group.visible(false); + this.konva.group.visible(false); } else { - this.group.visible(true); + this.konva.group.visible(true); // No need to render the brush preview if the cursor position or color is missing if (cursorPos && tool === 'brush') { const scale = stage.scaleX(); // Update the fill circle const radius = toolState.brush.width / 2; - this.brush.fillCircle.setAttrs({ + this.konva.brush.fillCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius, @@ -192,10 +194,10 @@ export class CanvasTool { }); // Update the inner border of the brush preview - this.brush.innerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius }); + this.konva.brush.innerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius }); // Update the outer border of the brush preview - this.brush.outerBorderCircle.setAttrs({ + this.konva.brush.outerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, @@ -203,14 +205,14 @@ export class CanvasTool { this.scaleTool(); - this.brush.group.visible(true); - this.eraser.group.visible(false); + this.konva.brush.group.visible(true); + this.konva.eraser.group.visible(false); // this.rect.group.visible(false); } else if (cursorPos && tool === 'eraser') { const scale = stage.scaleX(); // Update the fill circle const radius = toolState.eraser.width / 2; - this.eraser.fillCircle.setAttrs({ + this.konva.eraser.fillCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius, @@ -218,10 +220,10 @@ export class CanvasTool { }); // Update the inner border of the eraser preview - this.eraser.innerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius }); + this.konva.eraser.innerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius }); // Update the outer border of the eraser preview - this.eraser.outerBorderCircle.setAttrs({ + this.konva.eraser.outerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, @@ -229,8 +231,8 @@ export class CanvasTool { this.scaleTool(); - this.brush.group.visible(false); - this.eraser.group.visible(true); + this.konva.brush.group.visible(false); + this.konva.eraser.group.visible(true); // this.rect.group.visible(false); // } else if (cursorPos && lastMouseDownPos && tool === 'rect') { // this.rect.fillRect.setAttrs({ @@ -241,12 +243,12 @@ export class CanvasTool { // fill: rgbaColorToString(currentFill), // visible: true, // }); - // this.brush.group.visible(false); - // this.eraser.group.visible(false); + // this.konva.brush.group.visible(false); + // this.konva.eraser.group.visible(false); // this.rect.group.visible(true); } else { - this.brush.group.visible(false); - this.eraser.group.visible(false); + this.konva.brush.group.visible(false); + this.konva.eraser.group.visible(false); // this.rect.group.visible(false); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index f8ae0b3d93f..26037add09d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -269,8 +269,8 @@ export const previewBlob = async (blob: Blob, label?: string) => { export function getInpaintMaskLayerClone(arg: { manager: CanvasManager }): Konva.Layer { const { manager } = arg; - const layerClone = manager.inpaintMask.layer.clone(); - const objectGroupClone = manager.inpaintMask.group.clone(); + const layerClone = manager.inpaintMask.konva.layer.clone(); + const objectGroupClone = manager.inpaintMask.konva.group.clone(); layerClone.destroyChildren(); layerClone.add(objectGroupClone); @@ -287,8 +287,8 @@ export function getRegionMaskLayerClone(arg: { manager: CanvasManager; id: strin const canvasRegion = manager.regions.get(id); assert(canvasRegion, `Canvas region with id ${id} not found`); - const layerClone = canvasRegion.layer.clone(); - const objectGroupClone = canvasRegion.group.clone(); + const layerClone = canvasRegion.konva.layer.clone(); + const objectGroupClone = canvasRegion.konva.group.clone(); layerClone.destroyChildren(); layerClone.add(objectGroupClone); @@ -305,8 +305,8 @@ export function getControlAdapterLayerClone(arg: { manager: CanvasManager; id: s const controlAdapter = manager.controlAdapters.get(id); assert(controlAdapter, `Canvas region with id ${id} not found`); - const controlAdapterClone = controlAdapter.layer.clone(); - const objectGroupClone = controlAdapter.group.clone(); + const controlAdapterClone = controlAdapter.konva.layer.clone(); + const objectGroupClone = controlAdapter.konva.group.clone(); controlAdapterClone.destroyChildren(); controlAdapterClone.add(objectGroupClone); @@ -322,8 +322,8 @@ export function getInitialImageLayerClone(arg: { manager: CanvasManager }): Konv const initialImage = manager.initialImage; - const initialImageClone = initialImage.layer.clone(); - const objectGroupClone = initialImage.group.clone(); + const initialImageClone = initialImage.konva.layer.clone(); + const objectGroupClone = initialImage.konva.group.clone(); initialImageClone.destroyChildren(); initialImageClone.add(objectGroupClone); From 39db3be151505f531961df0fdd41af13ce70e6e3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:22:20 +1000 Subject: [PATCH 222/678] tidy(ui): CanvasBackground --- .../controlLayers/konva/CanvasBackground.ts | 63 +++++++++---------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts index c5b78ea7bb0..c75e8703b43 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts @@ -2,43 +2,18 @@ import { getArbitraryBaseColor } from '@invoke-ai/ui-library'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import Konva from 'konva'; -const baseGridLineColor = getArbitraryBaseColor(27); -const fineGridLineColor = getArbitraryBaseColor(18); - -/** - * Gets the grid spacing. The value depends on the stage scale - at higher scales, the grid spacing is smaller. - * @param scale The stage scale - * @returns The grid spacing based on the stage scale - */ -const getGridSpacing = (scale: number): number => { - if (scale >= 2) { - return 8; - } - if (scale >= 1 && scale < 2) { - return 16; - } - if (scale >= 0.5 && scale < 1) { - return 32; - } - if (scale >= 0.25 && scale < 0.5) { - return 64; - } - if (scale >= 0.125 && scale < 0.25) { - return 128; - } - return 256; -}; - export class CanvasBackground { static BASE_NAME = 'background'; static LAYER_NAME = `${CanvasBackground.BASE_NAME}_layer`; + static GRID_LINE_COLOR_COARSE = getArbitraryBaseColor(27); + static GRID_LINE_COLOR_FINE = getArbitraryBaseColor(18); + + manager: CanvasManager; konva: { layer: Konva.Layer; }; - manager: CanvasManager; - constructor(manager: CanvasManager) { this.manager = manager; this.konva = { layer: new Konva.Layer({ name: CanvasBackground.LAYER_NAME, listening: false }) }; @@ -47,7 +22,7 @@ export class CanvasBackground { render() { this.konva.layer.zIndex(0); const scale = this.manager.stage.scaleX(); - const gridSpacing = getGridSpacing(scale); + const gridSpacing = CanvasBackground.getGridSpacing(scale); const x = this.manager.stage.x(); const y = this.manager.stage.y(); const width = this.manager.stage.width(); @@ -98,7 +73,7 @@ export class CanvasBackground { x: _x, y: gridFullRect.y1, points: [0, 0, 0, ySize], - stroke: _x % 64 ? fineGridLineColor : baseGridLineColor, + stroke: _x % 64 ? CanvasBackground.GRID_LINE_COLOR_FINE : CanvasBackground.GRID_LINE_COLOR_COARSE, strokeWidth, listening: false, }) @@ -111,11 +86,35 @@ export class CanvasBackground { x: gridFullRect.x1, y: _y, points: [0, 0, xSize, 0], - stroke: _y % 64 ? fineGridLineColor : baseGridLineColor, + stroke: _y % 64 ? CanvasBackground.GRID_LINE_COLOR_FINE : CanvasBackground.GRID_LINE_COLOR_COARSE, strokeWidth, listening: false, }) ); } } + + /** + * Gets the grid spacing. The value depends on the stage scale - at higher scales, the grid spacing is smaller. + * @param scale The stage scale + * @returns The grid spacing based on the stage scale + */ + static getGridSpacing = (scale: number): number => { + if (scale >= 2) { + return 8; + } + if (scale >= 1 && scale < 2) { + return 16; + } + if (scale >= 0.5 && scale < 1) { + return 32; + } + if (scale >= 0.25 && scale < 0.5) { + return 64; + } + if (scale >= 0.125 && scale < 0.25) { + return 128; + } + return 256; + }; } From f1b0130389825ebbe5acfe43dac8e18d41e812d1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:30:17 +1000 Subject: [PATCH 223/678] tidy(ui): CanvasBbox --- .../controlLayers/konva/CanvasBbox.ts | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts index 78b47e44205..d60896ae9d4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts @@ -10,12 +10,7 @@ export class CanvasBbox { static GROUP_NAME = `${CanvasBbox.BASE_NAME}_group`; static RECT_NAME = `${CanvasBbox.BASE_NAME}_rect`; static TRANSFORMER_NAME = `${CanvasBbox.BASE_NAME}_transformer`; - - manager: CanvasManager; - - konva: { group: Konva.Group; rect: Konva.Rect; transformer: Konva.Transformer }; - - ALL_ANCHORS: string[] = [ + static ALL_ANCHORS: string[] = [ 'top-left', 'top-center', 'top-right', @@ -25,8 +20,16 @@ export class CanvasBbox { 'bottom-center', 'bottom-right', ]; - CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right']; - NO_ANCHORS: string[] = []; + static CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right']; + static NO_ANCHORS: string[] = []; + + manager: CanvasManager; + + konva: { + group: Konva.Group; + rect: Konva.Rect; + transformer: Konva.Transformer; + }; constructor(manager: CanvasManager) { this.manager = manager; @@ -44,7 +47,10 @@ export class CanvasBbox { listening: false, strokeEnabled: false, draggable: true, - ...this.manager.stateApi.getBbox(), + x: bbox.rect.x, + y: bbox.rect.y, + width: bbox.rect.width, + height: bbox.rect.height, }), transformer: new Konva.Transformer({ name: CanvasBbox.TRANSFORMER_NAME, @@ -151,7 +157,7 @@ export class CanvasBbox { // If shift is held and we are resizing from a corner, retain aspect ratio - needs special handling. We skip this // if alt/opt is held - this requires math too big for my brain. - if (shift && this.CORNER_ANCHORS.includes(anchor) && !alt) { + if (shift && CanvasBbox.CORNER_ANCHORS.includes(anchor) && !alt) { // Fit the bbox to the last aspect ratio let fittedWidth = Math.sqrt(width * height * $aspectRatioBuffer.get()); let fittedHeight = fittedWidth / $aspectRatioBuffer.get(); @@ -235,7 +241,7 @@ export class CanvasBbox { }); this.konva.transformer.setAttrs({ listening: toolState.selected === 'bbox', - enabledAnchors: toolState.selected === 'bbox' ? this.ALL_ANCHORS : this.NO_ANCHORS, + enabledAnchors: toolState.selected === 'bbox' ? CanvasBbox.ALL_ANCHORS : CanvasBbox.NO_ANCHORS, }); } } From 496cf3da4f0bf7f8de389ea881054b52230192d4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:32:42 +1000 Subject: [PATCH 224/678] tidy(ui): CanvasBrushLine --- .../controlLayers/konva/CanvasBrushLine.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts index b5ac4ff1e16..84c824022fa 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts @@ -7,15 +7,17 @@ export class CanvasBrushLine { static GROUP_NAME = `${CanvasBrushLine.NAME_PREFIX}_group`; static LINE_NAME = `${CanvasBrushLine.NAME_PREFIX}_line`; + private state: BrushLine; + id: string; konva: { group: Konva.Group; line: Konva.Line; }; - lastBrushLine: BrushLine; - constructor(brushLine: BrushLine) { - const { id, strokeWidth, clip, color, points } = brushLine; + constructor(state: BrushLine) { + this.state = state; + const { id, strokeWidth, clip, color, points } = this.state; this.id = id; this.konva = { group: new Konva.Group({ @@ -39,12 +41,12 @@ export class CanvasBrushLine { }), }; this.konva.group.add(this.konva.line); - this.lastBrushLine = brushLine; + this.state = state; } - update(brushLine: BrushLine, force?: boolean): boolean { - if (this.lastBrushLine !== brushLine || force) { - const { points, color, clip, strokeWidth } = brushLine; + update(state: BrushLine, force?: boolean): boolean { + if (this.state !== state || force) { + const { points, color, clip, strokeWidth } = state; this.konva.line.setAttrs({ // A line with only one point will not be rendered, so we duplicate the points to make it visible points: points.length === 2 ? [...points, ...points] : points, @@ -52,7 +54,7 @@ export class CanvasBrushLine { clip, strokeWidth, }); - this.lastBrushLine = brushLine; + this.state = state; return true; } else { return false; From b346b25a7b5f72160a11214c6059e50aa30ae5a7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:33:12 +1000 Subject: [PATCH 225/678] tidy(ui): CanvasControlAdapter --- .../konva/CanvasControlAdapter.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts index 58772e8ff1f..a16d0a158cc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts @@ -10,7 +10,7 @@ export class CanvasControlAdapter { static GROUP_NAME = `${CanvasControlAdapter.NAME_PREFIX}_group`; static OBJECT_GROUP_NAME = `${CanvasControlAdapter.NAME_PREFIX}_object-group`; - private controlAdapterState: ControlAdapterEntity; + private state: ControlAdapterEntity; id: string; manager: CanvasManager; @@ -24,8 +24,8 @@ export class CanvasControlAdapter { image: CanvasImage | null; - constructor(controlAdapterState: ControlAdapterEntity, manager: CanvasManager) { - const { id } = controlAdapterState; + constructor(state: ControlAdapterEntity, manager: CanvasManager) { + const { id } = state; this.id = id; this.manager = manager; this.konva = { @@ -70,21 +70,21 @@ export class CanvasControlAdapter { this.konva.layer.add(this.konva.transformer); this.image = null; - this.controlAdapterState = controlAdapterState; + this.state = state; } - async render(controlAdapterState: ControlAdapterEntity) { - this.controlAdapterState = controlAdapterState; + async render(state: ControlAdapterEntity) { + this.state = state; // Update the layer's position and listening state this.konva.group.setAttrs({ - x: controlAdapterState.position.x, - y: controlAdapterState.position.y, + x: state.position.x, + y: state.position.y, scaleX: 1, scaleY: 1, }); - const imageObject = controlAdapterState.processedImageObject ?? controlAdapterState.imageObject; + const imageObject = state.processedImageObject ?? state.imageObject; let didDraw = false; @@ -108,9 +108,9 @@ export class CanvasControlAdapter { } updateGroup(didDraw: boolean) { - this.konva.layer.visible(this.controlAdapterState.isEnabled); + this.konva.layer.visible(this.state.isEnabled); - this.konva.group.opacity(this.controlAdapterState.opacity); + this.konva.group.opacity(this.state.opacity); const isSelected = this.manager.stateApi.getIsSelected(this.id); const selectedTool = this.manager.stateApi.getToolState().selected; From 247ca97fbd0937392dda7c85d70b4a563f6b04a2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:33:58 +1000 Subject: [PATCH 226/678] tidy(ui): CanvasEraserLine --- .../controlLayers/konva/CanvasEraserLine.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts index b10b9d64fd3..f1d93fe9a95 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts @@ -8,15 +8,16 @@ export class CanvasEraserLine { static GROUP_NAME = `${CanvasEraserLine.NAME_PREFIX}_group`; static LINE_NAME = `${CanvasEraserLine.NAME_PREFIX}_line`; + private state: EraserLine; + id: string; konva: { group: Konva.Group; line: Konva.Line; }; - lastEraserLine: EraserLine; - constructor(eraserLine: EraserLine) { - const { id, strokeWidth, clip, points } = eraserLine; + constructor(state: EraserLine) { + const { id, strokeWidth, clip, points } = state; this.id = id; this.konva = { group: new Konva.Group({ @@ -40,19 +41,19 @@ export class CanvasEraserLine { }), }; this.konva.group.add(this.konva.line); - this.lastEraserLine = eraserLine; + this.state = state; } - update(eraserLine: EraserLine, force?: boolean): boolean { - if (this.lastEraserLine !== eraserLine || force) { - const { points, clip, strokeWidth } = eraserLine; + update(state: EraserLine, force?: boolean): boolean { + if (this.state !== state || force) { + const { points, clip, strokeWidth } = state; this.konva.line.setAttrs({ // A line with only one point will not be rendered, so we duplicate the points to make it visible points: points.length === 2 ? [...points, ...points] : points, clip, strokeWidth, }); - this.lastEraserLine = eraserLine; + this.state = state; return true; } else { return false; From 22ca3db870aaf377f33afaaab9ca9189cbd5f4aa Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:34:36 +1000 Subject: [PATCH 227/678] tidy(ui): CanvasImage --- .../controlLayers/konva/CanvasImage.ts | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts index b941ea96b77..1dc29e63721 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -14,6 +14,8 @@ export class CanvasImage { static PLACEHOLDER_RECT_NAME = `${CanvasImage.NAME_PREFIX}_placeholder-rect`; static PLACEHOLDER_TEXT_NAME = `${CanvasImage.NAME_PREFIX}_placeholder-text`; + private state: ImageObject; + id: string; konva: { group: Konva.Group; @@ -23,10 +25,9 @@ export class CanvasImage { image: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately isLoading: boolean; isError: boolean; - lastImageObject: ImageObject; - constructor(imageObject: ImageObject) { - const { id, width, height, x, y } = imageObject; + constructor(state: ImageObject) { + const { id, width, height, x, y } = state; this.konva = { group: new Konva.Group({ name: CanvasImage.GROUP_NAME, listening: false, x, y }), placeholder: { @@ -62,7 +63,7 @@ export class CanvasImage { this.image = null; this.isLoading = false; this.isError = false; - this.lastImageObject = imageObject; + this.state = state; } async updateImageSource(imageName: string) { @@ -88,15 +89,15 @@ export class CanvasImage { name: CanvasImage.IMAGE_NAME, listening: false, image: imageEl, - width: this.lastImageObject.width, - height: this.lastImageObject.height, + width: this.state.width, + height: this.state.height, }); this.konva.group.add(this.image); } - if (this.lastImageObject.filters.length > 0) { + if (this.state.filters.length > 0) { this.image.cache(); - this.image.filters(this.lastImageObject.filters.map((f) => FILTER_MAP[f])); + this.image.filters(this.state.filters.map((f) => FILTER_MAP[f])); } else { this.image.clearCache(); this.image.filters([]); @@ -116,10 +117,10 @@ export class CanvasImage { } } - async update(imageObject: ImageObject, force?: boolean): Promise { - if (this.lastImageObject !== imageObject || force) { - const { width, height, x, y, image, filters } = imageObject; - if (this.lastImageObject.image.name !== image.name || force) { + async update(state: ImageObject, force?: boolean): Promise { + if (this.state !== state || force) { + const { width, height, x, y, image, filters } = state; + if (this.state.image.name !== image.name || force) { await this.updateImageSource(image.name); } this.image?.setAttrs({ x, y, width, height }); @@ -132,7 +133,7 @@ export class CanvasImage { } this.konva.placeholder.rect.setAttrs({ width, height }); this.konva.placeholder.text.setAttrs({ width, height, fontSize: width / 16 }); - this.lastImageObject = imageObject; + this.state = state; return true; } else { return false; From 2b34a5c646086ebe98e35b71939638494dbde853 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:35:02 +1000 Subject: [PATCH 228/678] tidy(ui): CanvasInitialImage --- .../controlLayers/konva/CanvasInitialImage.ts | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts index 589fbaec855..37fe66c96b9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts @@ -9,10 +9,9 @@ export class CanvasInitialImage { static GROUP_NAME = `${CanvasInitialImage.NAME_PREFIX}_group`; static OBJECT_GROUP_NAME = `${CanvasInitialImage.NAME_PREFIX}_object-group`; - id = 'initial_image'; - - private initialImageState: InitialImageEntity; + private state: InitialImageEntity; + id = 'initial_image'; manager: CanvasManager; konva: { @@ -23,7 +22,7 @@ export class CanvasInitialImage { image: CanvasImage | null; - constructor(initialImageState: InitialImageEntity, manager: CanvasManager) { + constructor(state: InitialImageEntity, manager: CanvasManager) { this.manager = manager; this.konva = { layer: new Konva.Layer({ name: CanvasInitialImage.LAYER_NAME, imageSmoothingEnabled: true, listening: false }), @@ -34,26 +33,26 @@ export class CanvasInitialImage { this.konva.layer.add(this.konva.group); this.image = null; - this.initialImageState = initialImageState; + this.state = state; } - async render(initialImageState: InitialImageEntity) { - this.initialImageState = initialImageState; + async render(state: InitialImageEntity) { + this.state = state; - if (!this.initialImageState.imageObject) { + if (!this.state.imageObject) { this.konva.layer.visible(false); return; } if (!this.image) { - this.image = new CanvasImage(this.initialImageState.imageObject); + this.image = new CanvasImage(this.state.imageObject); this.konva.objectGroup.add(this.image.konva.group); - await this.image.update(this.initialImageState.imageObject, true); + await this.image.update(this.state.imageObject, true); } else if (!this.image.isLoading && !this.image.isError) { - await this.image.update(this.initialImageState.imageObject); + await this.image.update(this.state.imageObject); } - if (this.initialImageState && this.initialImageState.isEnabled && !this.image?.isLoading && !this.image?.isError) { + if (this.state && this.state.isEnabled && !this.image?.isLoading && !this.image?.isError) { this.konva.layer.visible(true); } else { this.konva.layer.visible(false); From f8af1e9014f51b57d85a7fde5cb5b7c8e0fcd79c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:35:45 +1000 Subject: [PATCH 229/678] tidy(ui): CanvasInpaintMask --- .../controlLayers/konva/CanvasInpaintMask.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts index a529db3b941..a2197f29327 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts @@ -19,7 +19,7 @@ export class CanvasInpaintMask { static COMPOSITING_RECT_NAME = `${CanvasInpaintMask.NAME_PREFIX}_compositing-rect`; private drawingBuffer: BrushLine | EraserLine | RectShape | null; - private inpaintMaskState: InpaintMaskEntity; + private state: InpaintMaskEntity; id = 'inpaint_mask'; manager: CanvasManager; @@ -33,7 +33,7 @@ export class CanvasInpaintMask { }; objects: Map; - constructor(entity: InpaintMaskEntity, manager: CanvasManager) { + constructor(state: InpaintMaskEntity, manager: CanvasManager) { this.manager = manager; this.konva = { @@ -76,7 +76,7 @@ export class CanvasInpaintMask { this.konva.group.add(this.konva.compositingRect); this.objects = new Map(); this.drawingBuffer = null; - this.inpaintMaskState = entity; + this.state = state; } destroy(): void { @@ -115,20 +115,20 @@ export class CanvasInpaintMask { this.setDrawingBuffer(null); } - async render(inpaintMaskState: InpaintMaskEntity) { - this.inpaintMaskState = inpaintMaskState; + async render(state: InpaintMaskEntity) { + this.state = state; // Update the layer's position and listening state this.konva.group.setAttrs({ - x: inpaintMaskState.position.x, - y: inpaintMaskState.position.y, + x: state.position.x, + y: state.position.y, scaleX: 1, scaleY: 1, }); let didDraw = false; - const objectIds = inpaintMaskState.objects.map(mapId); + const objectIds = state.objects.map(mapId); // Destroy any objects that are no longer in state for (const object of this.objects.values()) { if (!objectIds.includes(object.id)) { @@ -138,7 +138,7 @@ export class CanvasInpaintMask { } } - for (const obj of inpaintMaskState.objects) { + for (const obj of state.objects) { if (await this.renderObject(obj)) { didDraw = true; } @@ -202,14 +202,14 @@ export class CanvasInpaintMask { } updateGroup(didDraw: boolean) { - this.konva.layer.visible(this.inpaintMaskState.isEnabled); + this.konva.layer.visible(this.state.isEnabled); // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work this.konva.group.opacity(1); if (didDraw) { // Convert the color to a string, stripping the alpha - the object group will handle opacity. - const rgbColor = rgbColorToString(this.inpaintMaskState.fill); + const rgbColor = rgbColorToString(this.state.fill); const maskOpacity = this.manager.stateApi.getMaskOpacity(); this.konva.compositingRect.setAttrs({ From 2d01086a3eb7478d9d76b17649819cfe620bfc17 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:36:13 +1000 Subject: [PATCH 230/678] tidy(ui): CanvasLayer --- .../controlLayers/konva/CanvasLayer.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 7951c9d0411..ba5e7f31a55 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -17,7 +17,7 @@ export class CanvasLayer { static OBJECT_GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_object-group`; private drawingBuffer: BrushLine | EraserLine | RectShape | null; - private layerState: LayerEntity; + private state: LayerEntity; id: string; manager: CanvasManager; @@ -30,8 +30,8 @@ export class CanvasLayer { }; objects: Map; - constructor(entity: LayerEntity, manager: CanvasManager) { - this.id = entity.id; + constructor(state: LayerEntity, manager: CanvasManager) { + this.id = state.id; this.manager = manager; this.konva = { layer: new Konva.Layer({ name: CanvasLayer.LAYER_NAME, listening: false }), @@ -71,7 +71,7 @@ export class CanvasLayer { this.objects = new Map(); this.drawingBuffer = null; - this.layerState = entity; + this.state = state; } destroy(): void { @@ -106,20 +106,20 @@ export class CanvasLayer { this.setDrawingBuffer(null); } - async render(layerState: LayerEntity) { - this.layerState = layerState; + async render(state: LayerEntity) { + this.state = state; // Update the layer's position and listening state this.konva.group.setAttrs({ - x: layerState.position.x, - y: layerState.position.y, + x: state.position.x, + y: state.position.y, scaleX: 1, scaleY: 1, }); let didDraw = false; - const objectIds = layerState.objects.map(mapId); + const objectIds = state.objects.map(mapId); // Destroy any objects that are no longer in state for (const object of this.objects.values()) { if (!objectIds.includes(object.id) && object.id !== this.drawingBuffer?.id) { @@ -129,7 +129,7 @@ export class CanvasLayer { } } - for (const obj of layerState.objects) { + for (const obj of state.objects) { if (await this.renderObject(obj)) { didDraw = true; } @@ -208,13 +208,13 @@ export class CanvasLayer { } updateGroup(didDraw: boolean) { - if (!this.layerState.isEnabled) { + if (!this.state.isEnabled) { this.konva.layer.visible(false); return; } this.konva.layer.visible(true); - this.konva.group.opacity(this.layerState.opacity); + this.konva.group.opacity(this.state.opacity); const isSelected = this.manager.stateApi.getIsSelected(this.id); const selectedTool = this.manager.stateApi.getToolState().selected; From 25fb1bb8377a3729f5a199c348308bf3e19b6e3b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:37:31 +1000 Subject: [PATCH 231/678] tidy(ui): CanvasRect --- .../controlLayers/konva/CanvasRect.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts index 62733091596..2e5bbb0f799 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts @@ -7,15 +7,16 @@ export class CanvasRect { static GROUP_NAME = `${CanvasRect.NAME_PREFIX}_group`; static RECT_NAME = `${CanvasRect.NAME_PREFIX}_rect`; + private state: RectShape; + id: string; konva: { group: Konva.Group; rect: Konva.Rect; }; - lastRectShape: RectShape; - constructor(rectShape: RectShape) { - const { id, x, y, width, height } = rectShape; + constructor(state: RectShape) { + const { id, x, y, width, height, color } = state; this.id = id; this.konva = { group: new Konva.Group({ name: CanvasRect.GROUP_NAME, listening: false }), @@ -27,16 +28,16 @@ export class CanvasRect { width, height, listening: false, - fill: rgbaColorToString(rectShape.color), + fill: rgbaColorToString(color), }), }; this.konva.group.add(this.konva.rect); - this.lastRectShape = rectShape; + this.state = state; } - update(rectShape: RectShape, force?: boolean): boolean { - if (this.lastRectShape !== rectShape || force) { - const { x, y, width, height, color } = rectShape; + update(state: RectShape, force?: boolean): boolean { + if (this.state !== state || force) { + const { x, y, width, height, color } = state; this.konva.rect.setAttrs({ x, y, @@ -44,7 +45,7 @@ export class CanvasRect { height, fill: rgbaColorToString(color), }); - this.lastRectShape = rectShape; + this.state = state; return true; } else { return false; From 9c809ba147e6bf21045abef9e86c7264f9a95dc3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:37:57 +1000 Subject: [PATCH 232/678] tidy(ui): CanvasRegion --- .../controlLayers/konva/CanvasRegion.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts index 86603973ece..1e3fa446fbc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts @@ -19,7 +19,7 @@ export class CanvasRegion { static COMPOSITING_RECT_NAME = `${CanvasRegion.NAME_PREFIX}_compositing-rect`; private drawingBuffer: BrushLine | EraserLine | RectShape | null; - private regionState: RegionEntity; + private state: RegionEntity; id: string; manager: CanvasManager; @@ -34,8 +34,8 @@ export class CanvasRegion { objects: Map; - constructor(entity: RegionEntity, manager: CanvasManager) { - this.id = entity.id; + constructor(state: RegionEntity, manager: CanvasManager) { + this.id = state.id; this.manager = manager; this.konva = { @@ -75,7 +75,7 @@ export class CanvasRegion { this.konva.group.add(this.konva.compositingRect); this.objects = new Map(); this.drawingBuffer = null; - this.regionState = entity; + this.state = state; } destroy(): void { @@ -114,20 +114,20 @@ export class CanvasRegion { this.setDrawingBuffer(null); } - async render(regionState: RegionEntity) { - this.regionState = regionState; + async render(state: RegionEntity) { + this.state = state; // Update the layer's position and listening state this.konva.group.setAttrs({ - x: regionState.position.x, - y: regionState.position.y, + x: state.position.x, + y: state.position.y, scaleX: 1, scaleY: 1, }); let didDraw = false; - const objectIds = regionState.objects.map(mapId); + const objectIds = state.objects.map(mapId); // Destroy any objects that are no longer in state for (const object of this.objects.values()) { if (!objectIds.includes(object.id)) { @@ -137,7 +137,7 @@ export class CanvasRegion { } } - for (const obj of regionState.objects) { + for (const obj of state.objects) { if (await this.renderObject(obj)) { didDraw = true; } @@ -201,14 +201,14 @@ export class CanvasRegion { } updateGroup(didDraw: boolean) { - this.konva.layer.visible(this.regionState.isEnabled); + this.konva.layer.visible(this.state.isEnabled); // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work this.konva.group.opacity(1); if (didDraw) { // Convert the color to a string, stripping the alpha - the object group will handle opacity. - const rgbColor = rgbColorToString(this.regionState.fill); + const rgbColor = rgbColorToString(this.state.fill); const maskOpacity = this.manager.stateApi.getMaskOpacity(); this.konva.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 From 7c3d2f55785c06885ecab25b8d7470daba3487f5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 17 Jul 2024 15:14:10 +1000 Subject: [PATCH 233/678] feat(ui): canvas entity list headers --- invokeai/frontend/web/public/locales/en.json | 4 ++++ .../components/ControlAdapter/CAEntityList.tsx | 15 ++++++++++++++- .../components/IPAdapter/IPAEntityList.tsx | 11 +++++++++++ .../components/Layer/LayerEntityList.tsx | 12 +++++++++++- .../components/RegionalGuidance/RGEntityList.tsx | 15 ++++++++++++++- 5 files changed, 54 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 0b8bb968705..7cf98dac0ad 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1673,6 +1673,10 @@ "raster": "Raster", "rasterLayer": "$t(controlLayers.raster) $t(unifiedCanvas.layer)", "opacity": "Opacity", + "regionalGuidance_withCount": "Regional Guidance ({{count}})", + "controlAdapters_withCount": "Control Adapters ({{count}})", + "layers_withCount": "Raster Layers ({{count}})", + "ipAdapters_withCount": "IP Adapters ({{count}})", "globalControlAdapter": "Global $t(controlnet.controlAdapter_one)", "globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)", "globalIPAdapter": "Global $t(common.ipAdapter)", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityList.tsx index e18fa5e166f..01c1310b751 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityList.tsx @@ -1,16 +1,22 @@ -/* eslint-disable i18next/no-literal-string */ +import { Text } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { CA } from 'features/controlLayers/components/ControlAdapter/CA'; import { mapId } from 'features/controlLayers/konva/util'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { return canvasV2.controlAdapters.entities.map(mapId).reverse(); }); export const CAEntityList = memo(() => { + const { t } = useTranslation(); + const isTypeSelected = useAppSelector((s) => + Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'control_adapter') + ); + const caIds = useAppSelector(selectEntityIds); if (caIds.length === 0) { @@ -20,6 +26,13 @@ export const CAEntityList = memo(() => { if (caIds.length > 0) { return ( <> + + {t('controlLayers.controlAdapters_withCount', { count: caIds.length })} + {caIds.map((id) => ( ))} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAEntityList.tsx index d70847cf391..01f1eda0e51 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAEntityList.tsx @@ -1,16 +1,20 @@ /* eslint-disable i18next/no-literal-string */ +import { Text } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { IPA } from 'features/controlLayers/components/IPAdapter/IPA'; import { mapId } from 'features/controlLayers/konva/util'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { return canvasV2.ipAdapters.entities.map(mapId).reverse(); }); export const IPAEntityList = memo(() => { + const { t } = useTranslation(); + const isTypeSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'ip_adapter')); const ipaIds = useAppSelector(selectEntityIds); if (ipaIds.length === 0) { @@ -20,6 +24,13 @@ export const IPAEntityList = memo(() => { if (ipaIds.length > 0) { return ( <> + + {t('controlLayers.ipAdapters_withCount', { count: ipaIds.length })} + {ipaIds.map((id) => ( ))} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerEntityList.tsx index 89ab0d7aa10..92f3174b7ad 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerEntityList.tsx @@ -1,16 +1,19 @@ -/* eslint-disable i18next/no-literal-string */ +import { Text } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { Layer } from 'features/controlLayers/components/Layer/Layer'; import { mapId } from 'features/controlLayers/konva/util'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { return canvasV2.layers.entities.map(mapId).reverse(); }); export const LayerEntityList = memo(() => { + const { t } = useTranslation(); + const isTypeSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'layer')); const layerIds = useAppSelector(selectEntityIds); if (layerIds.length === 0) { @@ -20,6 +23,13 @@ export const LayerEntityList = memo(() => { if (layerIds.length > 0) { return ( <> + + {t('controlLayers.layers_withCount', { count: layerIds.length })} + {layerIds.map((id) => ( ))} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGEntityList.tsx index 1e9a68a3216..6b1b38ac89e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGEntityList.tsx @@ -1,16 +1,22 @@ -/* eslint-disable i18next/no-literal-string */ +import { Text } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { RG } from 'features/controlLayers/components/RegionalGuidance/RG'; import { mapId } from 'features/controlLayers/konva/util'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { return canvasV2.regions.entities.map(mapId).reverse(); }); export const RGEntityList = memo(() => { + const { t } = useTranslation(); + const isTypeSelected = useAppSelector((s) => + Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'regional_guidance') + ); + const rgIds = useAppSelector(selectEntityIds); if (rgIds.length === 0) { @@ -20,6 +26,13 @@ export const RGEntityList = memo(() => { if (rgIds.length > 0) { return ( <> + + {t('controlLayers.regionalGuidance_withCount', { count: rgIds.length })} + {rgIds.map((id) => ( ))} From 92931b0d4d92e07daecf96bb5ea4d469d3cbc715 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 17 Jul 2024 16:22:03 +1000 Subject: [PATCH 234/678] feat(ui): tweaked entity & group selection styles --- .../components/ControlAdapter/CA.tsx | 2 +- .../ControlAdapter/CAEntityHeader.tsx | 5 +++-- .../components/ControlAdapter/CAEntityList.tsx | 18 ++++++------------ .../controlLayers/components/IPAdapter/IPA.tsx | 2 +- .../components/IPAdapter/IPAEntityList.tsx | 15 ++++++--------- .../components/IPAdapter/IPAHeader.tsx | 5 +++-- .../components/InpaintMask/IM.tsx | 2 +- .../components/InpaintMask/IMHeader.tsx | 5 +++-- .../controlLayers/components/Layer/Layer.tsx | 2 +- .../components/Layer/LayerEntityList.tsx | 15 ++++++--------- .../components/Layer/LayerHeader.tsx | 5 +++-- .../components/RegionalGuidance/RG.tsx | 2 +- .../RegionalGuidance/RGEntityList.tsx | 18 ++++++------------ .../components/RegionalGuidance/RGHeader.tsx | 5 +++-- .../common/CanvasEntityContainer.tsx | 16 +++++----------- .../common/CanvasEntityGroupTitle.tsx | 17 +++++++++++++++++ .../components/common/CanvasEntityTitle.tsx | 5 +++-- 17 files changed, 69 insertions(+), 70 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupTitle.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx index f58e37b275d..9bb6dff7919 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx @@ -20,7 +20,7 @@ export const CA = memo(({ id }: Props) => { return ( - + {isOpen && } ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityHeader.tsx index fe49d9953b9..2eef6f5cb67 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityHeader.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityHeader.tsx @@ -13,10 +13,11 @@ import { useTranslation } from 'react-i18next'; type Props = { id: string; + isSelected: boolean; onToggleVisibility: () => void; }; -export const CAHeader = memo(({ id, onToggleVisibility }: Props) => { +export const CAHeader = memo(({ id, isSelected, onToggleVisibility }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const isEnabled = useAppSelector((s) => selectCAOrThrow(s.canvasV2, id).isEnabled); @@ -30,7 +31,7 @@ export const CAHeader = memo(({ id, onToggleVisibility }: Props) => { return ( - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityList.tsx index 01c1310b751..bc6d5bbe559 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityList.tsx @@ -1,6 +1,6 @@ -import { Text } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; +import { CanvasEntityGroupTitle } from 'features/controlLayers/components/common/CanvasEntityGroupTitle'; import { CA } from 'features/controlLayers/components/ControlAdapter/CA'; import { mapId } from 'features/controlLayers/konva/util'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; @@ -13,10 +13,7 @@ const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) = export const CAEntityList = memo(() => { const { t } = useTranslation(); - const isTypeSelected = useAppSelector((s) => - Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'control_adapter') - ); - + const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'control_adapter')); const caIds = useAppSelector(selectEntityIds); if (caIds.length === 0) { @@ -26,13 +23,10 @@ export const CAEntityList = memo(() => { if (caIds.length > 0) { return ( <> - - {t('controlLayers.controlAdapters_withCount', { count: caIds.length })} - + {caIds.map((id) => ( ))} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx index 69e88c9d8c4..8281cadc32c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx @@ -20,7 +20,7 @@ export const IPA = memo(({ id }: Props) => { return ( - + {isOpen && } ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAEntityList.tsx index 01f1eda0e51..5e04bc61627 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAEntityList.tsx @@ -1,7 +1,7 @@ /* eslint-disable i18next/no-literal-string */ -import { Text } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; +import { CanvasEntityGroupTitle } from 'features/controlLayers/components/common/CanvasEntityGroupTitle'; import { IPA } from 'features/controlLayers/components/IPAdapter/IPA'; import { mapId } from 'features/controlLayers/konva/util'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; @@ -14,7 +14,7 @@ const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) = export const IPAEntityList = memo(() => { const { t } = useTranslation(); - const isTypeSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'ip_adapter')); + const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'ip_adapter')); const ipaIds = useAppSelector(selectEntityIds); if (ipaIds.length === 0) { @@ -24,13 +24,10 @@ export const IPAEntityList = memo(() => { if (ipaIds.length > 0) { return ( <> - - {t('controlLayers.ipAdapters_withCount', { count: ipaIds.length })} - + {ipaIds.map((id) => ( ))} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeader.tsx index ec56c81b911..81e63929e8e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeader.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeader.tsx @@ -11,10 +11,11 @@ import { useTranslation } from 'react-i18next'; type Props = { id: string; + isSelected: boolean; onToggleVisibility: () => void; }; -export const IPAHeader = memo(({ id, onToggleVisibility }: Props) => { +export const IPAHeader = memo(({ id, isSelected, onToggleVisibility }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const isEnabled = useAppSelector((s) => selectIPAOrThrow(s.canvasV2, id).isEnabled); @@ -28,7 +29,7 @@ export const IPAHeader = memo(({ id, onToggleVisibility }: Props) => { return ( - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx index 544fece75bb..55a332270d8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx @@ -17,7 +17,7 @@ export const IM = memo(() => { }, [dispatch]); return ( - + {isOpen && } ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMHeader.tsx index 1102e056f3f..888c5b1eb36 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMHeader.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMHeader.tsx @@ -11,10 +11,11 @@ import { useTranslation } from 'react-i18next'; import { IMMaskFillColorPicker } from './IMMaskFillColorPicker'; type Props = { + isSelected: boolean; onToggleVisibility: () => void; }; -export const IMHeader = memo(({ onToggleVisibility }: Props) => { +export const IMHeader = memo(({ isSelected, onToggleVisibility }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const isEnabled = useAppSelector((s) => s.canvasV2.inpaintMask.isEnabled); @@ -25,7 +26,7 @@ export const IMHeader = memo(({ onToggleVisibility }: Props) => { return ( - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx index 5fda4e72b45..1e2b6fdf1d0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx @@ -26,7 +26,7 @@ export const Layer = memo(({ id }: Props) => { return ( - + {isOpen && } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerEntityList.tsx index 92f3174b7ad..1dae90ba0b6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerEntityList.tsx @@ -1,6 +1,6 @@ -import { Text } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; +import { CanvasEntityGroupTitle } from 'features/controlLayers/components/common/CanvasEntityGroupTitle'; import { Layer } from 'features/controlLayers/components/Layer/Layer'; import { mapId } from 'features/controlLayers/konva/util'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; @@ -13,7 +13,7 @@ const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) = export const LayerEntityList = memo(() => { const { t } = useTranslation(); - const isTypeSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'layer')); + const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'layer')); const layerIds = useAppSelector(selectEntityIds); if (layerIds.length === 0) { @@ -23,13 +23,10 @@ export const LayerEntityList = memo(() => { if (layerIds.length > 0) { return ( <> - - {t('controlLayers.layers_withCount', { count: layerIds.length })} - + {layerIds.map((id) => ( ))} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx index 53a8d0dbfa4..b53ca7f2753 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx @@ -14,10 +14,11 @@ import { LayerOpacity } from './LayerOpacity'; type Props = { id: string; + isSelected: boolean; onToggleVisibility: () => void; }; -export const LayerHeader = memo(({ id, onToggleVisibility }: Props) => { +export const LayerHeader = memo(({ id, isSelected, onToggleVisibility }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const isEnabled = useAppSelector((s) => selectLayerOrThrow(s.canvasV2, id).isEnabled); @@ -35,7 +36,7 @@ export const LayerHeader = memo(({ id, onToggleVisibility }: Props) => { return ( - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx index 12297ab5a1f..fe923169cb6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx @@ -22,7 +22,7 @@ export const RG = memo(({ id }: Props) => { }, [dispatch, id]); return ( - + {isOpen && } ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGEntityList.tsx index 6b1b38ac89e..a00fea3e0a0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGEntityList.tsx @@ -1,6 +1,6 @@ -import { Text } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; +import { CanvasEntityGroupTitle } from 'features/controlLayers/components/common/CanvasEntityGroupTitle'; import { RG } from 'features/controlLayers/components/RegionalGuidance/RG'; import { mapId } from 'features/controlLayers/konva/util'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; @@ -13,10 +13,7 @@ const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) = export const RGEntityList = memo(() => { const { t } = useTranslation(); - const isTypeSelected = useAppSelector((s) => - Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'regional_guidance') - ); - + const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'regional_guidance')); const rgIds = useAppSelector(selectEntityIds); if (rgIds.length === 0) { @@ -26,13 +23,10 @@ export const RGEntityList = memo(() => { if (rgIds.length > 0) { return ( <> - - {t('controlLayers.regionalGuidance_withCount', { count: rgIds.length })} - + {rgIds.map((id) => ( ))} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGHeader.tsx index ec4cc36b664..866913a08cc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGHeader.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGHeader.tsx @@ -15,10 +15,11 @@ import { RGSettingsPopover } from './RGSettingsPopover'; type Props = { id: string; + isSelected: boolean; onToggleVisibility: () => void; }; -export const RGHeader = memo(({ id, onToggleVisibility }: Props) => { +export const RGHeader = memo(({ id, isSelected, onToggleVisibility }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const isEnabled = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).isEnabled); @@ -33,7 +34,7 @@ export const RGHeader = memo(({ id, onToggleVisibility }: Props) => { return ( - + {autoNegative === 'invert' && ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx index 7c0fd050904..9093086fbdc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx @@ -1,7 +1,7 @@ import type { ChakraProps } from '@invoke-ai/ui-library'; import { Flex } from '@invoke-ai/ui-library'; import type { PropsWithChildren } from 'react'; -import { memo, useCallback, useMemo } from 'react'; +import { memo, useCallback } from 'react'; type Props = PropsWithChildren<{ isSelected: boolean; @@ -9,13 +9,8 @@ type Props = PropsWithChildren<{ selectedBorderColor?: ChakraProps['bg']; }>; -export const CanvasEntityContainer = memo(({ isSelected, onSelect, selectedBorderColor, children }: Props) => { - const borderColor = useMemo(() => { - if (isSelected) { - return selectedBorderColor ?? 'base.400'; - } - return 'base.800'; - }, [isSelected, selectedBorderColor]); +export const CanvasEntityContainer = memo((props: Props) => { + const { isSelected, onSelect, selectedBorderColor = 'base.400', children } = props; const _onSelect = useCallback(() => { if (isSelected) { return; @@ -28,11 +23,10 @@ export const CanvasEntityContainer = memo(({ isSelected, onSelect, selectedBorde position="relative" // necessary for drop overlay flexDir="column" w="full" - bg="base.850" + bg={isSelected ? 'base.800' : 'base.850'} onClick={_onSelect} borderInlineStartWidth={5} - borderColor={borderColor} - opacity={isSelected ? 1 : 0.6} + borderColor={isSelected ? selectedBorderColor : 'base.800'} borderRadius="base" > {children} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupTitle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupTitle.tsx new file mode 100644 index 00000000000..69fcefe8c3d --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupTitle.tsx @@ -0,0 +1,17 @@ +import { Text } from '@invoke-ai/ui-library'; +import { memo } from 'react'; + +type Props = { + title: string; + isSelected: boolean; +}; + +export const CanvasEntityGroupTitle = memo(({ title, isSelected }: Props) => { + return ( + + {title} + + ); +}); + +CanvasEntityGroupTitle.displayName = 'CanvasEntityGroupTitle'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitle.tsx index ce72f2f0025..1fc3d6e9f03 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitle.tsx @@ -3,11 +3,12 @@ import { memo } from 'react'; type Props = { title: string; + isSelected: boolean; }; -export const CanvasEntityTitle = memo(({ title }: Props) => { +export const CanvasEntityTitle = memo(({ title, isSelected }: Props) => { return ( - + {title} ); From 35c941c540b32e20f2380a0d4217ddd690857b49 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 18 Jul 2024 19:21:40 +1000 Subject: [PATCH 235/678] feat(ui): layer bbox calc in worker --- .../frontend/web/src/app/logging/logger.ts | 3 +- .../components/ControlLayersToolbar.tsx | 11 +- .../controlLayers/konva/CanvasInpaintMask.ts | 6 +- .../controlLayers/konva/CanvasLayer.ts | 96 ++++++++++++- .../controlLayers/konva/CanvasManager.ts | 63 +++++++++ .../controlLayers/konva/entityBbox.ts | 4 +- .../features/controlLayers/konva/events.ts | 1 + .../src/features/controlLayers/konva/util.ts | 19 +++ .../features/controlLayers/konva/worker.ts | 131 ++++++++++++++++++ .../controlLayers/store/layersReducers.ts | 2 +- invokeai/frontend/web/tsconfig.json | 2 +- 11 files changed, 324 insertions(+), 14 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/worker.ts diff --git a/invokeai/frontend/web/src/app/logging/logger.ts b/invokeai/frontend/web/src/app/logging/logger.ts index 3af19af2efd..3d35681dc44 100644 --- a/invokeai/frontend/web/src/app/logging/logger.ts +++ b/invokeai/frontend/web/src/app/logging/logger.ts @@ -29,7 +29,8 @@ export type LoggerNamespace = | 'dnd' | 'controlLayers' | 'metadata' - | 'konva'; + | 'konva' + | 'worker'; export const logger = (namespace: LoggerNamespace) => $logger.get().child({ namespace }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index e3cc5cb5fac..7386461c27a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -1,4 +1,5 @@ /* eslint-disable i18next/no-literal-string */ +import { Button } from '@chakra-ui/react'; import { Flex } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { BrushWidth } from 'features/controlLayers/components/BrushWidth'; @@ -9,12 +10,19 @@ import { NewSessionButton } from 'features/controlLayers/components/NewSessionBu import { ResetCanvasButton } from 'features/controlLayers/components/ResetCanvasButton'; import { ToolChooser } from 'features/controlLayers/components/ToolChooser'; import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup'; +import { getCanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton'; import { ViewerToggleMenu } from 'features/gallery/components/ImageViewer/ViewerToggleMenu'; -import { memo } from 'react'; +import { memo, useCallback } from 'react'; export const ControlLayersToolbar = memo(() => { const tool = useAppSelector((s) => s.canvasV2.tool.selected); + const bbox = useCallback(() => { + const manager = getCanvasManager(); + for (const l of manager.layers.values()) { + l.getBbox(); + } + }, []); return ( @@ -27,6 +35,7 @@ export const ControlLayersToolbar = memo(() => { {tool === 'brush' && } {tool === 'eraser' && } + diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts index a2197f29327..e03133b78cb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts @@ -242,7 +242,7 @@ export class CanvasInpaintMask { // When the layer is selected and being moved, we should always cache it. // We should update the cache if we drew to the layer. if (!this.konva.group.isCached() || didDraw) { - this.konva.group.cache(); + // this.konva.group.cache(); } // Activate the transformer this.konva.layer.listening(true); @@ -266,7 +266,7 @@ export class CanvasInpaintMask { // We are using a non-drawing tool (move, view, bbox), so we should cache the layer. // We should update the cache if we drew to the layer. if (!this.konva.group.isCached() || didDraw) { - this.konva.group.cache(); + // this.konva.group.cache(); } } return; @@ -279,7 +279,7 @@ export class CanvasInpaintMask { this.konva.transformer.nodes([]); // Update the layer's cache if it's not already cached or we drew to it. if (!this.konva.group.isCached() || didDraw) { - this.konva.group.cache(); + // this.konva.group.cache(); } return; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index ba5e7f31a55..b9ced230e5c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -4,9 +4,10 @@ import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; import { mapId } from 'features/controlLayers/konva/util'; -import type { BrushLine, EraserLine, LayerEntity, RectShape } from 'features/controlLayers/store/types'; +import type { BrushLine, EraserLine, LayerEntity, Rect, RectShape } from 'features/controlLayers/store/types'; import { isDrawingTool } from 'features/controlLayers/store/types'; import Konva from 'konva'; +import { debounce } from 'lodash-es'; import { assert } from 'tsafe'; export class CanvasLayer { @@ -24,18 +25,26 @@ export class CanvasLayer { konva: { layer: Konva.Layer; + bbox: Konva.Rect; group: Konva.Group; objectGroup: Konva.Group; transformer: Konva.Transformer; }; objects: Map; + bbox: Rect | null; + + getBbox = debounce(this._getBbox, 300); constructor(state: LayerEntity, manager: CanvasManager) { this.id = state.id; this.manager = manager; this.konva = { layer: new Konva.Layer({ name: CanvasLayer.LAYER_NAME, listening: false }), - group: new Konva.Group({ name: CanvasLayer.GROUP_NAME, listening: false }), + group: new Konva.Group({ name: CanvasLayer.GROUP_NAME, listening: true }), + bbox: new Konva.Rect({ + listening: true, + stroke: 'hsl(200deg 76% 59%)', // invokeBlue.400 + }), objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }), transformer: new Konva.Transformer({ name: CanvasLayer.TRANSFORMER_NAME, @@ -49,6 +58,7 @@ export class CanvasLayer { }; this.konva.group.add(this.konva.objectGroup); + this.konva.group.add(this.konva.bbox); this.konva.layer.add(this.konva.group); this.konva.transformer.on('transformend', () => { @@ -72,6 +82,7 @@ export class CanvasLayer { this.objects = new Map(); this.drawingBuffer = null; this.state = state; + this.bbox = null; } destroy(): void { @@ -213,6 +224,10 @@ export class CanvasLayer { return; } + if (didDraw) { + this.getBbox(); + } + this.konva.layer.visible(true); this.konva.group.opacity(this.state.opacity); const isSelected = this.manager.stateApi.getIsSelected(this.id); @@ -229,7 +244,7 @@ export class CanvasLayer { // When the layer is selected and being moved, we should always cache it. // We should update the cache if we drew to the layer. if (!this.konva.group.isCached() || didDraw) { - this.konva.group.cache(); + // this.konva.group.cache(); } // Activate the transformer this.konva.layer.listening(true); @@ -250,7 +265,7 @@ export class CanvasLayer { // We are using a non-drawing tool (move, view, bbox), so we should cache the layer. // We should update the cache if we drew to the layer. if (!this.konva.group.isCached() || didDraw) { - this.konva.group.cache(); + // this.konva.group.cache(); } } } else if (!isSelected) { @@ -260,8 +275,79 @@ export class CanvasLayer { this.konva.transformer.nodes([]); // Update the layer's cache if it's not already cached or we drew to it. if (!this.konva.group.isCached() || didDraw) { - this.konva.group.cache(); + // this.konva.group.cache(); + } + } + } + + renderBbox() { + if (!this.bbox) { + this.konva.bbox.visible(false); + return; + } + this.konva.bbox.visible(true); + this.konva.bbox.strokeWidth(1 / this.manager.stage.scaleX()); + this.konva.bbox.setAttrs(this.bbox); + } + + private _getBbox() { + let needsPixelBbox = false; + const rect = this.konva.objectGroup.getClientRect({ skipTransform: true }); + // console.log('rect', rect); + // If there are no eraser strokes, we can use the client rect directly + for (const obj of this.objects.values()) { + if (obj instanceof CanvasEraserLine) { + needsPixelBbox = true; + break; } } + + if (!needsPixelBbox) { + if (rect.width === 0 || rect.height === 0) { + this.bbox = null; + } else { + this.bbox = rect; + } + this.renderBbox(); + return; + } + + // We have eraser strokes - we must calculate the bbox using pixel data + + // const a = window.performance.now(); + const clone = this.konva.objectGroup.clone(); + // const b = window.performance.now(); + // console.log('cloned layer', b - a); + // const c = window.performance.now(); + const canvas = clone.toCanvas(); + // const d = window.performance.now(); + // console.log('got canvas', d - c); + const ctx = canvas.getContext('2d'); + if (!ctx) { + return; + } + const imageData = ctx.getImageData(0, 0, rect.width, rect.height); + // const e = window.performance.now(); + // console.log('got image data', e - d); + this.manager.requestBbox( + { buffer: imageData.data.buffer, width: imageData.width, height: imageData.height }, + (extents) => { + // console.log('extents', extents); + if (extents) { + this.bbox = { + x: extents.minX + rect.x - Math.floor(this.konva.layer.x()), + y: extents.minY + rect.y - Math.floor(this.konva.layer.y()), + width: extents.maxX - extents.minX, + height: extents.maxY - extents.minY, + }; + } else { + this.bbox = null; + } + this.renderBbox(); + clone.destroy(); + // console.log('bbox', this.bbox); + } + ); + // console.log('transferred message', window.performance.now() - e); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 0ff1ba6a16a..7c56b21a7e8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -11,6 +11,7 @@ import { getInpaintMaskImage, getRegionMaskImage, } from 'features/controlLayers/konva/util'; +import type { Extents, ExtentsResult, GetBboxTask, WorkerLogMessage } from 'features/controlLayers/konva/worker'; import { $lastProgressEvent, $shouldShowStagedImage } from 'features/controlLayers/store/canvasV2Slice'; import type { CanvasV2State, GenerationMode } from 'features/controlLayers/store/types'; import type Konva from 'konva'; @@ -33,6 +34,24 @@ import { setStageEventHandlers } from './events'; const log = logger('canvas'); +// type Extents = { +// minX: number; +// minY: number; +// maxX: number; +// maxY: number; +// }; +// type GetBboxTask = { +// id: string; +// type: 'get_bbox'; +// data: { imageData: ImageData }; +// }; + +// type GetBboxResult = { +// id: string; +// type: 'get_bbox'; +// data: { extents: Extents | null }; +// }; + type Util = { getImageDTO: (imageName: string) => Promise; uploadImage: ( @@ -65,9 +84,12 @@ export class CanvasManager { stateApi: CanvasStateApi; preview: CanvasPreview; background: CanvasBackground; + private store: Store; private isFirstRender: boolean; private prevState: CanvasV2State; + private worker: Worker; + private tasks: Map void }>; constructor( stage: Konva.Stage, @@ -108,6 +130,41 @@ export class CanvasManager { this.initialImage = new CanvasInitialImage(this.stateApi.getInitialImageState(), this); this.stage.add(this.initialImage.konva.layer); + + this.worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module', name: 'worker' }); + this.tasks = new Map(); + this.worker.onmessage = (event: MessageEvent) => { + const { type, data } = event.data; + if (type === 'log') { + if (data.ctx) { + log[data.level](data.ctx, data.message); + } else { + log[data.level](data.message); + } + } else if (type === 'extents') { + const task = this.tasks.get(data.id); + if (!task) { + return; + } + task.onComplete(data.extents); + } + }; + this.worker.onerror = (event) => { + log.error({ message: event.message }, 'Worker error'); + }; + this.worker.onmessageerror = () => { + log.error('Worker message error'); + }; + } + + requestBbox(data: Omit, onComplete: (extents: Extents | null) => void) { + const id = crypto.randomUUID(); + const task: GetBboxTask = { + type: 'get_bbox', + data: { ...data, id }, + }; + this.tasks.set(id, { task, onComplete }); + this.worker.postMessage(task, [data.buffer]); } async renderInitialImage() { @@ -187,6 +244,12 @@ export class CanvasManager { } } + renderBboxes() { + for (const layer of this.layers.values()) { + layer.renderBbox(); + } + } + arrangeEntities() { const { getLayersState, getControlAdaptersState, getRegionsState } = this.stateApi; const layers = getLayersState().entities; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts index e68131c386c..6a55ae4f1b7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts @@ -47,7 +47,7 @@ const GET_CLIENT_RECT_CONFIG = { skipTransform: true }; * @param imageData The ImageData object to get the bounding box of. * @returns The minimum and maximum x and y values of the image's bounding box, or null if the image has no pixels. */ -const getImageDataBbox = (imageData: ImageData): Extents | null => { +export const getImageDataBbox = (imageData: ImageData): Extents | null => { const { data, width, height } = imageData; let minX = width; let minY = height; @@ -77,7 +77,7 @@ const getImageDataBbox = (imageData: ImageData): Extents | null => { } } - return isEmpty ? null : { minX, minY, maxX, maxY }; + return isEmpty ? null : { minX, minY, maxX: maxX + 1, maxY: maxY + 1 }; }; /** diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index a04242695f2..e49f6d0b1c4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -496,6 +496,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { scale: newScale, }); manager.background.render(); + manager.renderBboxes(); } } manager.preview.tool.render(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index 26037add09d..ba6064d53bb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -142,6 +142,25 @@ export function imageDataToDataURL(imageData: ImageData): string { return canvas.toDataURL(); } +export function imageDataToBlob(imageData: ImageData): Promise { + const w = imageData.width; + const h = imageData.height; + const canvas = document.createElement('canvas'); + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext('2d'); + + if (!ctx) { + return Promise.resolve(null); + } + + ctx.putImageData(imageData, 0, 0); + + return new Promise((resolve) => { + canvas.toBlob(resolve); + }); +} + /** * Download a Blob as a file */ diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/worker.ts b/invokeai/frontend/web/src/features/controlLayers/konva/worker.ts new file mode 100644 index 00000000000..3c7efb38fdb --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/worker.ts @@ -0,0 +1,131 @@ +import type { LogLevel } from 'app/logging/logger'; +import type { JsonObject } from 'roarr/dist/types'; + +export type Extents = { + minX: number; + minY: number; + maxX: number; + maxY: number; +}; + +/** + * Get the bounding box of an image. + * @param buffer The ArrayBuffer of the image to get the bounding box of. + * @param width The width of the image. + * @param height The height of the image. + * @returns The minimum and maximum x and y values of the image's bounding box, or null if the image has no pixels. + */ +const getImageDataBboxArrayBuffer = (buffer: ArrayBuffer, width: number, height: number): Extents | null => { + let minX = width; + let minY = height; + let maxX = -1; + let maxY = -1; + let alpha = 0; + let isEmpty = true; + const arr = new Uint8ClampedArray(buffer); + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + alpha = arr[(y * width + x) * 4 + 3] ?? 0; + if (alpha > 0) { + isEmpty = false; + if (x < minX) { + minX = x; + } + if (x > maxX) { + maxX = x; + } + if (y < minY) { + minY = y; + } + if (y > maxY) { + maxY = y; + } + } + } + } + + return isEmpty ? null : { minX, minY, maxX: maxX + 1, maxY: maxY + 1 }; +}; + +export type GetBboxTask = { + type: 'get_bbox'; + data: { id: string; buffer: ArrayBuffer; width: number; height: number }; +}; + +type TaskWithTimestamps> = T & { started: number | null; finished: number | null }; + +export type ExtentsResult = { + type: 'extents'; + data: { id: string; extents: Extents | null }; +}; + +export type WorkerLogMessage = { + type: 'log'; + data: { level: LogLevel; message: string; ctx?: JsonObject }; +}; + +// A single worker is used to process tasks in a queue +const queue: TaskWithTimestamps[] = []; +let currentTask: TaskWithTimestamps | null = null; + +function postLogMessage(level: LogLevel, message: string, ctx?: JsonObject) { + const data: WorkerLogMessage = { + type: 'log', + data: { level, message, ctx }, + }; + self.postMessage(data); +} + +function processNextTask() { + // Grab the next task + const task = queue.shift(); + if (!task) { + // Queue empty - we can clear the current task to allow the worker to resume the queue when another task is posted + currentTask = null; + return; + } + + postLogMessage('debug', 'Processing task', { type: task.type, id: task.data.id }); + task.started = performance.now(); + + // Set the current task so we don't process another one + currentTask = task; + + // Process the task + if (task.type === 'get_bbox') { + const { buffer, width, height, id } = task.data; + const extents = getImageDataBboxArrayBuffer(buffer, width, height); + const result: ExtentsResult = { + type: 'extents', + data: { id, extents }, + }; + task.finished = performance.now(); + postLogMessage('debug', 'Task complete', { + type: task.type, + id: task.data.id, + started: task.started, + finished: task.finished, + durationMs: task.finished - task.started, + }); + self.postMessage(result); + } else { + postLogMessage('error', 'Unknown task type', { type: task.type }); + } + + // Repeat + processNextTask(); +} + +self.onmessage = (event: MessageEvent>) => { + const task = event.data; + + postLogMessage('debug', 'Received task', { type: task.type, id: task.data.id }); + // Add the task to the queue + queue.push({ ...event.data, started: null, finished: null }); + + // If we are not currently processing a task, process the next one + if (!currentTask) { + processNextTask(); + } +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index 0869db42867..6e29c191835 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -58,7 +58,7 @@ export const layersReducers = { type: 'layer', isEnabled: true, bbox: null, - bboxNeedsUpdate: false, + bboxNeedsUpdate: true, objects: [imageObject], opacity: 1, position: { x: position.x + offsetX, y: position.y + offsetY }, diff --git a/invokeai/frontend/web/tsconfig.json b/invokeai/frontend/web/tsconfig.json index b1e4ebfc0b3..67d709940ff 100644 --- a/invokeai/frontend/web/tsconfig.json +++ b/invokeai/frontend/web/tsconfig.json @@ -5,7 +5,7 @@ "lib": ["DOM", "DOM.Iterable", "ESNext"], "allowJs": false, "skipLibCheck": true, - "esModuleInterop": false, + "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, From 5c39935e886ec36cbf9a1fd4f4d72f6ae158f2e5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 18 Jul 2024 21:04:38 +1000 Subject: [PATCH 236/678] feat(ui): move tool now only moves --- .../components/StageComponent.tsx | 4 +- .../controlLayers/konva/CanvasLayer.ts | 81 +++++++++++++------ .../controlLayers/konva/CanvasManager.ts | 5 +- .../controlLayers/konva/CanvasStateApi.ts | 5 ++ .../src/features/controlLayers/konva/util.ts | 9 ++- .../src/features/controlLayers/store/types.ts | 2 +- 6 files changed, 78 insertions(+), 28 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index 1db97cce034..31072b0fa19 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -10,6 +10,8 @@ import { v4 as uuidv4 } from 'uuid'; const log = logger('canvas'); +const showHud = false; + // This will log warnings when layers > 5 - maybe use `import.meta.env.MODE === 'development'` instead? Konva.showWarnings = false; @@ -83,7 +85,7 @@ export const StageComponent = memo(({ asPreview = false }: Props) => { /> {!asPreview && ( - + {showHud && } )} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index b9ced230e5c..df8392c4f56 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -16,6 +16,7 @@ export class CanvasLayer { static TRANSFORMER_NAME = `${CanvasLayer.NAME_PREFIX}_transformer`; static GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_group`; static OBJECT_GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_object-group`; + static BBOX_NAME = `${CanvasLayer.NAME_PREFIX}_bbox`; private drawingBuffer: BrushLine | EraserLine | RectShape | null; private state: LayerEntity; @@ -39,21 +40,26 @@ export class CanvasLayer { this.id = state.id; this.manager = manager; this.konva = { - layer: new Konva.Layer({ name: CanvasLayer.LAYER_NAME, listening: false }), - group: new Konva.Group({ name: CanvasLayer.GROUP_NAME, listening: true }), + layer: new Konva.Layer({ id: this.id, name: CanvasLayer.LAYER_NAME, listening: false }), + group: new Konva.Group({ name: CanvasLayer.GROUP_NAME, listening: false, draggable: true }), bbox: new Konva.Rect({ - listening: true, + listening: false, + name: CanvasLayer.BBOX_NAME, stroke: 'hsl(200deg 76% 59%)', // invokeBlue.400 + fill: '', + perfectDrawEnabled: false, + strokeHitEnabled: false, }), objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }), transformer: new Konva.Transformer({ name: CanvasLayer.TRANSFORMER_NAME, shouldOverdrawWholeArea: true, - draggable: true, + draggable: false, dragDistance: 0, enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], rotateEnabled: false, flipEnabled: false, + listening: false, }), }; @@ -71,7 +77,7 @@ export class CanvasLayer { 'layer' ); }); - this.konva.transformer.on('dragend', () => { + this.konva.group.on('dragend', () => { this.manager.stateApi.onPosChanged( { id: this.id, position: { x: this.konva.group.x(), y: this.konva.group.y() } }, 'layer' @@ -152,6 +158,7 @@ export class CanvasLayer { } } + this.renderBbox(); this.updateGroup(didDraw); } @@ -225,7 +232,12 @@ export class CanvasLayer { } if (didDraw) { - this.getBbox(); + if (this.objects.size > 0) { + this.getBbox(); + } else { + this.bbox = null; + this.renderBbox(); + } } this.konva.layer.visible(true); @@ -233,26 +245,42 @@ export class CanvasLayer { const isSelected = this.manager.stateApi.getIsSelected(this.id); const selectedTool = this.manager.stateApi.getToolState().selected; + const transformerListening = selectedTool === 'transform' && isSelected; + const bboxListening = selectedTool === 'move' && isSelected; + + this.konva.layer.listening(transformerListening || bboxListening); + this.konva.transformer.listening(transformerListening); + this.konva.group.listening(bboxListening); + this.konva.bbox.listening(bboxListening); + if (this.objects.size === 0) { // If the layer is totally empty, reset the cache and bail out. - this.konva.layer.listening(false); this.konva.transformer.nodes([]); if (this.konva.group.isCached()) { this.konva.group.clearCache(); } - } else if (isSelected && selectedTool === 'move') { + } else if (isSelected && selectedTool === 'transform') { // When the layer is selected and being moved, we should always cache it. // We should update the cache if we drew to the layer. if (!this.konva.group.isCached() || didDraw) { // this.konva.group.cache(); } // Activate the transformer - this.konva.layer.listening(true); this.konva.transformer.nodes([this.konva.group]); this.konva.transformer.forceUpdate(); - } else if (isSelected && selectedTool !== 'move') { + this.konva.transformer.visible(true); + } else if (selectedTool === 'move') { + // When the layer is selected and being moved, we should always cache it. + // We should update the cache if we drew to the layer. + if (!this.konva.group.isCached() || didDraw) { + // this.konva.group.cache(); + } + // Activate the transformer + this.konva.transformer.nodes([]); + this.konva.transformer.forceUpdate(); + this.konva.transformer.visible(false); + } else if (isSelected) { // If the layer is selected but not using the move tool, we don't want the layer to be listening. - this.konva.layer.listening(false); // The transformer also does not need to be active. this.konva.transformer.nodes([]); if (isDrawingTool(selectedTool)) { @@ -270,7 +298,6 @@ export class CanvasLayer { } } else if (!isSelected) { // Unselected layers should not be listening - this.konva.layer.listening(false); // The transformer also does not need to be active. this.konva.transformer.nodes([]); // Update the layer's cache if it's not already cached or we drew to it. @@ -281,16 +308,23 @@ export class CanvasLayer { } renderBbox() { - if (!this.bbox) { - this.konva.bbox.visible(false); - return; - } - this.konva.bbox.visible(true); - this.konva.bbox.strokeWidth(1 / this.manager.stage.scaleX()); - this.konva.bbox.setAttrs(this.bbox); + const isSelected = this.manager.stateApi.getIsSelected(this.id); + const selectedTool = this.manager.stateApi.getToolState().selected; + + this.konva.bbox.setAttrs({ + ...this.bbox, + strokeWidth: 1 / this.manager.stage.scaleX(), + visible: this.bbox !== null && selectedTool === 'move' && isSelected, + }); } private _getBbox() { + if (this.objects.size === 0) { + this.bbox = null; + this.renderBbox(); + return; + } + let needsPixelBbox = false; const rect = this.konva.objectGroup.getClientRect({ skipTransform: true }); // console.log('rect', rect); @@ -334,11 +368,12 @@ export class CanvasLayer { (extents) => { // console.log('extents', extents); if (extents) { + const { minX, minY, maxX, maxY } = extents; this.bbox = { - x: extents.minX + rect.x - Math.floor(this.konva.layer.x()), - y: extents.minY + rect.y - Math.floor(this.konva.layer.y()), - width: extents.maxX - extents.minX, - height: extents.maxY - extents.minY, + x: minX + rect.x - Math.floor(this.konva.layer.x()), + y: minY + rect.y - Math.floor(this.konva.layer.y()), + width: maxX - minX, + height: maxY - minY, }; } else { this.bbox = null; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 7c56b21a7e8..a294f715069 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -33,6 +33,7 @@ import { CanvasTool } from './CanvasTool'; import { setStageEventHandlers } from './events'; const log = logger('canvas'); +const workerLog = logger('worker'); // type Extents = { // minX: number; @@ -137,9 +138,9 @@ export class CanvasManager { const { type, data } = event.data; if (type === 'log') { if (data.ctx) { - log[data.level](data.ctx, data.message); + workerLog[data.level](data.ctx, data.message); } else { - log[data.level](data.message); + workerLog[data.level](data.message); } } else if (type === 'extents') { const task = this.tasks.get(data.id); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index 162789bd2e5..f16fd45ec5b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -17,6 +17,7 @@ import { caBboxChanged, caScaled, caTranslated, + entitySelected, eraserWidthChanged, imBboxChanged, imBrushLineAdded, @@ -136,6 +137,10 @@ export class CanvasStateApi { this.store.dispatch(imRectShapeAdded(arg)); } }; + onEntitySelected = (arg: { id: string; type: CanvasEntity['type'] }) => { + log.debug('Entity selected'); + this.store.dispatch(entitySelected(arg)); + }; onBboxTransformed = (bbox: IRect) => { log.debug('Generation bbox transformed'); this.store.dispatch(bboxChanged(bbox)); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index ba6064d53bb..fc764afe04a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -1,4 +1,5 @@ import { getImageDataTransparency } from 'common/util/arrayBuffer'; +import { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { GenerationMode, Rect, RgbaColor } from 'features/controlLayers/store/types'; import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers'; @@ -366,7 +367,7 @@ export function getCompositeLayerStageClone(arg: { manager: CanvasManager }): Ko stageClone.y(0); const validLayers = layersState.entities.filter(isValidLayer); - + console.log(validLayers); // Konva bug (?) - when iterating over the array returned from `stage.getLayers()`, if you destroy a layer, the array // is mutated in-place and the next iteration will skip the next layer. To avoid this, we first collect the layers // to delete in a separate array and then destroy them. @@ -376,7 +377,10 @@ export function getCompositeLayerStageClone(arg: { manager: CanvasManager }): Ko for (const konvaLayer of stageClone.getLayers()) { const layer = validLayers.find((l) => l.id === konvaLayer.id()); if (!layer) { + console.log('deleting', konvaLayer); toDelete.push(konvaLayer); + } else { + konvaLayer.findOne(`.${CanvasLayer.GROUP_NAME}`)?.findOne(`.${CanvasLayer.BBOX_NAME}`)?.destroy(); } } @@ -395,6 +399,9 @@ export function getGenerationMode(arg: { manager: CanvasManager }): GenerationMo const inpaintMaskTransparency = getImageDataTransparency(inpaintMaskImageData); const compositeLayer = getCompositeLayerStageClone(arg); const compositeLayerImageData = konvaNodeToImageData(compositeLayer, { x, y, width, height }); + imageDataToBlob(compositeLayerImageData).then((blob) => { + previewBlob(blob, 'composite layer'); + }); const compositeLayerTransparency = getImageDataTransparency(compositeLayerImageData); if (compositeLayerTransparency.isPartiallyTransparent) { if (compositeLayerTransparency.isFullyTransparent) { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index be4c3e1e9d1..eaa2e32c647 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -464,7 +464,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }, }; -const zTool = z.enum(['brush', 'eraser', 'move', 'rect', 'view', 'bbox']); +const zTool = z.enum(['brush', 'eraser', 'move', 'rect', 'view', 'bbox', 'transform']); export type Tool = z.infer; export function isDrawingTool(tool: Tool): tool is 'brush' | 'eraser' | 'rect' { return tool === 'brush' || tool === 'eraser' || tool === 'rect'; From 42612a4f92493e1e00ddce81cc803f66c3c91842 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 19 Jul 2024 13:16:02 +1000 Subject: [PATCH 237/678] fix(ui): move tool fixes, add transform tool --- .../controlLayers/components/ToolChooser.tsx | 2 + .../components/TransformToolButton.tsx | 35 +++++ .../controlLayers/konva/CanvasLayer.ts | 129 ++++++++++++------ 3 files changed, 126 insertions(+), 40 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx index 706d51b74cb..b9b6c8ca845 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx @@ -5,6 +5,7 @@ import { BrushToolButton } from 'features/controlLayers/components/BrushToolButt import { EraserToolButton } from 'features/controlLayers/components/EraserToolButton'; import { MoveToolButton } from 'features/controlLayers/components/MoveToolButton'; import { RectToolButton } from 'features/controlLayers/components/RectToolButton'; +import { TransformToolButton } from 'features/controlLayers/components/TransformToolButton'; import { ViewToolButton } from 'features/controlLayers/components/ViewToolButton'; import { useCanvasDeleteLayerHotkey } from 'features/controlLayers/hooks/useCanvasDeleteLayerHotkey'; import { useCanvasResetLayerHotkey } from 'features/controlLayers/hooks/useCanvasResetLayerHotkey'; @@ -21,6 +22,7 @@ export const ToolChooser: React.FC = () => { + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx new file mode 100644 index 00000000000..e8ac2e2577e --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx @@ -0,0 +1,35 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; +import { PiResizeBold } from 'react-icons/pi'; + +export const TransformToolButton = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'transform'); + const isDisabled = useAppSelector( + (s) => s.canvasV2.selectedEntityIdentifier === null || s.canvasV2.session.isStaging + ); + + const onClick = useCallback(() => { + dispatch(toolChanged('transform')); + }, [dispatch]); + + useHotkeys(['ctrl+t', 'meta+t'], onClick, { enabled: !isDisabled }, [isDisabled, onClick]); + + return ( + } + variant={isSelected ? 'solid' : 'outline'} + onClick={onClick} + isDisabled={isDisabled} + /> + ); +}); + +TransformToolButton.displayName = 'TransformToolButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index df8392c4f56..e3073df65b9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -14,6 +14,7 @@ export class CanvasLayer { static NAME_PREFIX = 'layer'; static LAYER_NAME = `${CanvasLayer.NAME_PREFIX}_layer`; static TRANSFORMER_NAME = `${CanvasLayer.NAME_PREFIX}_transformer`; + static INTERACTION_RECT_NAME = `${CanvasLayer.NAME_PREFIX}_interaction-rect`; static GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_group`; static OBJECT_GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_object-group`; static BBOX_NAME = `${CanvasLayer.NAME_PREFIX}_bbox`; @@ -26,13 +27,15 @@ export class CanvasLayer { konva: { layer: Konva.Layer; - bbox: Konva.Rect; group: Konva.Group; + bbox: Konva.Rect; + objectGroup: Konva.Group; transformer: Konva.Transformer; + interactionRect: Konva.Rect; }; objects: Map; - bbox: Rect | null; + bbox: Rect; getBbox = debounce(this._getBbox, 300); @@ -41,38 +44,63 @@ export class CanvasLayer { this.manager = manager; this.konva = { layer: new Konva.Layer({ id: this.id, name: CanvasLayer.LAYER_NAME, listening: false }), - group: new Konva.Group({ name: CanvasLayer.GROUP_NAME, listening: false, draggable: true }), + group: new Konva.Group({ name: CanvasLayer.GROUP_NAME, listening: true, draggable: true }), bbox: new Konva.Rect({ listening: false, + draggable: false, name: CanvasLayer.BBOX_NAME, stroke: 'hsl(200deg 76% 59%)', // invokeBlue.400 - fill: '', perfectDrawEnabled: false, strokeHitEnabled: false, }), objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }), transformer: new Konva.Transformer({ name: CanvasLayer.TRANSFORMER_NAME, - shouldOverdrawWholeArea: true, draggable: false, - dragDistance: 0, enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], rotateEnabled: false, flipEnabled: false, listening: false, }), + interactionRect: new Konva.Rect({ + name: CanvasLayer.INTERACTION_RECT_NAME, + listening: false, + draggable: false, + fill: 'rgba(255,0,0,0.5)', + }), }; + this.konva.layer.add(this.konva.group); + this.konva.layer.add(this.konva.transformer); this.konva.group.add(this.konva.objectGroup); + this.konva.group.add(this.konva.interactionRect); this.konva.group.add(this.konva.bbox); - this.konva.layer.add(this.konva.group); + this.konva.transformer.on('transform', () => { + console.log(this.konva.interactionRect.position()); + this.konva.objectGroup.setAttrs({ + scaleX: this.konva.interactionRect.scaleX(), + scaleY: this.konva.interactionRect.scaleY(), + // rotation: this.konva.interactionRect.rotation(), + x: this.konva.interactionRect.x(), + t: this.konva.interactionRect.y(), + }); + }); this.konva.transformer.on('transformend', () => { + console.log(this.bbox); + this.bbox = { + x: this.konva.interactionRect.x(), + y: this.konva.interactionRect.y(), + width: this.konva.interactionRect.width() * this.konva.interactionRect.scaleX(), + height: this.konva.interactionRect.height() * this.konva.interactionRect.scaleY(), + }; + console.log(this.bbox); + this.renderBbox(); this.manager.stateApi.onScaleChanged( { id: this.id, - scale: this.konva.group.scaleX(), - position: { x: this.konva.group.x(), y: this.konva.group.y() }, + scale: this.konva.objectGroup.scaleX(), + position: { x: this.konva.objectGroup.x(), y: this.konva.objectGroup.y() }, }, 'layer' ); @@ -83,12 +111,15 @@ export class CanvasLayer { 'layer' ); }); - this.konva.layer.add(this.konva.transformer); this.objects = new Map(); this.drawingBuffer = null; this.state = state; - this.bbox = null; + this.bbox = CanvasLayer.DEFAULT_BBOX_RECT; + } + + private static get DEFAULT_BBOX_RECT() { + return { x: 0, y: 0, width: 0, height: 0 }; } destroy(): void { @@ -235,48 +266,50 @@ export class CanvasLayer { if (this.objects.size > 0) { this.getBbox(); } else { - this.bbox = null; + this.bbox = CanvasLayer.DEFAULT_BBOX_RECT; this.renderBbox(); } } this.konva.layer.visible(true); - this.konva.group.opacity(this.state.opacity); + this.konva.objectGroup.opacity(this.state.opacity); const isSelected = this.manager.stateApi.getIsSelected(this.id); const selectedTool = this.manager.stateApi.getToolState().selected; - const transformerListening = selectedTool === 'transform' && isSelected; - const bboxListening = selectedTool === 'move' && isSelected; + const isTransforming = selectedTool === 'transform' && isSelected; + const isMoving = selectedTool === 'move' && isSelected; - this.konva.layer.listening(transformerListening || bboxListening); - this.konva.transformer.listening(transformerListening); - this.konva.group.listening(bboxListening); - this.konva.bbox.listening(bboxListening); + this.konva.layer.listening(isTransforming || isMoving); + this.konva.transformer.listening(isTransforming); + this.konva.bbox.visible(isMoving); + this.konva.interactionRect.listening(isMoving); if (this.objects.size === 0) { // If the layer is totally empty, reset the cache and bail out. this.konva.transformer.nodes([]); - if (this.konva.group.isCached()) { - this.konva.group.clearCache(); + if (this.konva.objectGroup.isCached()) { + this.konva.objectGroup.clearCache(); } } else if (isSelected && selectedTool === 'transform') { // When the layer is selected and being moved, we should always cache it. // We should update the cache if we drew to the layer. - if (!this.konva.group.isCached() || didDraw) { - // this.konva.group.cache(); + if (!this.konva.objectGroup.isCached() || didDraw) { + // this.konva.objectGroup.cache(); } // Activate the transformer - this.konva.transformer.nodes([this.konva.group]); + this.konva.transformer.nodes([this.konva.interactionRect]); + this.konva.transformer.enabledAnchors(['top-left', 'top-right', 'bottom-left', 'bottom-right']); this.konva.transformer.forceUpdate(); this.konva.transformer.visible(true); } else if (selectedTool === 'move') { // When the layer is selected and being moved, we should always cache it. // We should update the cache if we drew to the layer. - if (!this.konva.group.isCached() || didDraw) { - // this.konva.group.cache(); + if (!this.konva.objectGroup.isCached() || didDraw) { + // this.konva.objectGroup.cache(); } // Activate the transformer - this.konva.transformer.nodes([]); + this.konva.transformer.nodes([this.konva.interactionRect]); + this.konva.transformer.enabledAnchors([]); this.konva.transformer.forceUpdate(); this.konva.transformer.visible(false); } else if (isSelected) { @@ -286,14 +319,14 @@ export class CanvasLayer { if (isDrawingTool(selectedTool)) { // We are using a drawing tool (brush, eraser, rect). These tools change the layer's rendered appearance, so we // should never be cached. - if (this.konva.group.isCached()) { - this.konva.group.clearCache(); + if (this.konva.objectGroup.isCached()) { + this.konva.objectGroup.clearCache(); } } else { // We are using a non-drawing tool (move, view, bbox), so we should cache the layer. // We should update the cache if we drew to the layer. - if (!this.konva.group.isCached() || didDraw) { - // this.konva.group.cache(); + if (!this.konva.objectGroup.isCached() || didDraw) { + // this.konva.objectGroup.cache(); } } } else if (!isSelected) { @@ -301,8 +334,8 @@ export class CanvasLayer { // The transformer also does not need to be active. this.konva.transformer.nodes([]); // Update the layer's cache if it's not already cached or we drew to it. - if (!this.konva.group.isCached() || didDraw) { - // this.konva.group.cache(); + if (!this.konva.objectGroup.isCached() || didDraw) { + // this.konva.objectGroup.cache(); } } } @@ -310,17 +343,33 @@ export class CanvasLayer { renderBbox() { const isSelected = this.manager.stateApi.getIsSelected(this.id); const selectedTool = this.manager.stateApi.getToolState().selected; + const hasBbox = this.bbox.width !== 0 && this.bbox.height !== 0; + + this.konva.bbox.visible(hasBbox); + this.konva.interactionRect.visible(hasBbox); this.konva.bbox.setAttrs({ - ...this.bbox, + x: this.bbox.x, + y: this.bbox.y, + width: this.bbox.width, + height: this.bbox.height, + scaleX: 1, + scaleY: 1, strokeWidth: 1 / this.manager.stage.scaleX(), - visible: this.bbox !== null && selectedTool === 'move' && isSelected, + }); + this.konva.interactionRect.setAttrs({ + x: this.bbox.x, + y: this.bbox.y, + width: this.bbox.width, + height: this.bbox.height, + scaleX: 1, + scaleY: 1, }); } private _getBbox() { if (this.objects.size === 0) { - this.bbox = null; + this.bbox = CanvasLayer.DEFAULT_BBOX_RECT; this.renderBbox(); return; } @@ -338,7 +387,7 @@ export class CanvasLayer { if (!needsPixelBbox) { if (rect.width === 0 || rect.height === 0) { - this.bbox = null; + this.bbox = CanvasLayer.DEFAULT_BBOX_RECT; } else { this.bbox = rect; } @@ -370,13 +419,13 @@ export class CanvasLayer { if (extents) { const { minX, minY, maxX, maxY } = extents; this.bbox = { - x: minX + rect.x - Math.floor(this.konva.layer.x()), - y: minY + rect.y - Math.floor(this.konva.layer.y()), + x: rect.x + minX, + y: rect.y + minY, width: maxX - minX, height: maxY - minY, }; } else { - this.bbox = null; + this.bbox = CanvasLayer.DEFAULT_BBOX_RECT; } this.renderBbox(); clone.destroy(); From a15944774c5adab8e7bc4c82102b0148b0660a79 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 19 Jul 2024 13:20:29 +1000 Subject: [PATCH 238/678] fix(ui): transform tool seems to be working --- .../controlLayers/konva/CanvasLayer.ts | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index e3073df65b9..0420700a9ed 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -76,31 +76,31 @@ export class CanvasLayer { this.konva.group.add(this.konva.interactionRect); this.konva.group.add(this.konva.bbox); - this.konva.transformer.on('transform', () => { - console.log(this.konva.interactionRect.position()); - this.konva.objectGroup.setAttrs({ - scaleX: this.konva.interactionRect.scaleX(), - scaleY: this.konva.interactionRect.scaleY(), - // rotation: this.konva.interactionRect.rotation(), - x: this.konva.interactionRect.x(), - t: this.konva.interactionRect.y(), - }); - }); + // this.konva.transformer.on('transform', () => { + // console.log(this.konva.interactionRect.position()); + // this.konva.objectGroup.setAttrs({ + // scaleX: this.konva.interactionRect.scaleX(), + // scaleY: this.konva.interactionRect.scaleY(), + // // rotation: this.konva.interactionRect.rotation(), + // x: this.konva.interactionRect.x(), + // t: this.konva.interactionRect.y(), + // }); + // }); this.konva.transformer.on('transformend', () => { console.log(this.bbox); this.bbox = { - x: this.konva.interactionRect.x(), - y: this.konva.interactionRect.y(), - width: this.konva.interactionRect.width() * this.konva.interactionRect.scaleX(), - height: this.konva.interactionRect.height() * this.konva.interactionRect.scaleY(), + x: this.bbox.x * this.konva.group.scaleX(), + y: this.bbox.y * this.konva.group.scaleY(), + width: this.bbox.width * this.konva.group.scaleX(), + height: this.bbox.height * this.konva.group.scaleY(), }; console.log(this.bbox); this.renderBbox(); this.manager.stateApi.onScaleChanged( { id: this.id, - scale: this.konva.objectGroup.scaleX(), - position: { x: this.konva.objectGroup.x(), y: this.konva.objectGroup.y() }, + scale: this.konva.group.scaleX(), + position: { x: this.konva.group.x(), y: this.konva.group.y() }, }, 'layer' ); @@ -297,8 +297,7 @@ export class CanvasLayer { // this.konva.objectGroup.cache(); } // Activate the transformer - this.konva.transformer.nodes([this.konva.interactionRect]); - this.konva.transformer.enabledAnchors(['top-left', 'top-right', 'bottom-left', 'bottom-right']); + this.konva.transformer.nodes([this.konva.group]); this.konva.transformer.forceUpdate(); this.konva.transformer.visible(true); } else if (selectedTool === 'move') { @@ -308,8 +307,7 @@ export class CanvasLayer { // this.konva.objectGroup.cache(); } // Activate the transformer - this.konva.transformer.nodes([this.konva.interactionRect]); - this.konva.transformer.enabledAnchors([]); + this.konva.transformer.nodes([]); this.konva.transformer.forceUpdate(); this.konva.transformer.visible(false); } else if (isSelected) { From 879161ed4cb93a4318248fe8796ff00c1bfaba76 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 24 Jul 2024 19:09:33 +1000 Subject: [PATCH 239/678] wip --- .../controlLayers/konva/CanvasLayer.ts | 314 +++++++++++++++--- .../features/controlLayers/konva/events.ts | 4 +- .../controlLayers/store/layersReducers.ts | 36 +- .../src/features/controlLayers/store/types.ts | 2 +- 4 files changed, 286 insertions(+), 70 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 0420700a9ed..0ae6ca3c62b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -1,3 +1,4 @@ +import { deepClone } from 'common/util/deepClone'; import { CanvasBrushLine } from 'features/controlLayers/konva/CanvasBrushLine'; import { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine'; import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; @@ -10,6 +11,8 @@ import Konva from 'konva'; import { debounce } from 'lodash-es'; import { assert } from 'tsafe'; +const MIN_LAYER_SIZE_PX = 10; + export class CanvasLayer { static NAME_PREFIX = 'layer'; static LAYER_NAME = `${CanvasLayer.NAME_PREFIX}_layer`; @@ -19,6 +22,8 @@ export class CanvasLayer { static OBJECT_GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_object-group`; static BBOX_NAME = `${CanvasLayer.NAME_PREFIX}_bbox`; + private static BBOX_PADDING_PX = 5; + private drawingBuffer: BrushLine | EraserLine | RectShape | null; private state: LayerEntity; @@ -27,10 +32,11 @@ export class CanvasLayer { konva: { layer: Konva.Layer; - group: Konva.Group; bbox: Konva.Rect; - objectGroup: Konva.Group; + objectGroupBbox: Konva.Rect; + positionXLine: Konva.Line; + positionYLine: Konva.Line; transformer: Konva.Transformer; interactionRect: Konva.Rect; }; @@ -44,7 +50,6 @@ export class CanvasLayer { this.manager = manager; this.konva = { layer: new Konva.Layer({ id: this.id, name: CanvasLayer.LAYER_NAME, listening: false }), - group: new Konva.Group({ name: CanvasLayer.GROUP_NAME, listening: true, draggable: true }), bbox: new Konva.Rect({ listening: false, draggable: false, @@ -54,68 +59,252 @@ export class CanvasLayer { strokeHitEnabled: false, }), objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }), + objectGroupBbox: new Konva.Rect({ fill: 'green', opacity: 0.5, listening: false }), + positionXLine: new Konva.Line({ stroke: 'white', strokeWidth: 1 }), + positionYLine: new Konva.Line({ stroke: 'white', strokeWidth: 1 }), transformer: new Konva.Transformer({ name: CanvasLayer.TRANSFORMER_NAME, draggable: false, - enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], + // enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], rotateEnabled: false, flipEnabled: false, listening: false, + padding: CanvasLayer.BBOX_PADDING_PX, + stroke: 'hsl(200deg 76% 59%)', // invokeBlue.400 }), interactionRect: new Konva.Rect({ name: CanvasLayer.INTERACTION_RECT_NAME, listening: false, - draggable: false, + draggable: true, fill: 'rgba(255,0,0,0.5)', }), }; - this.konva.layer.add(this.konva.group); + this.konva.layer.add(this.konva.objectGroup); this.konva.layer.add(this.konva.transformer); - this.konva.group.add(this.konva.objectGroup); - this.konva.group.add(this.konva.interactionRect); - this.konva.group.add(this.konva.bbox); + this.konva.layer.add(this.konva.interactionRect); + this.konva.layer.add(this.konva.bbox); + this.konva.layer.add(this.konva.objectGroupBbox); + this.konva.layer.add(this.konva.positionXLine); + this.konva.layer.add(this.konva.positionYLine); + + this.konva.transformer.on('transformstart', () => { + console.log('>>> transformstart'); + console.log('interactionRect', { + x: this.konva.interactionRect.x(), + y: this.konva.interactionRect.y(), + scaleX: this.konva.interactionRect.scaleX(), + scaleY: this.konva.interactionRect.scaleY(), + width: this.konva.interactionRect.width(), + height: this.konva.interactionRect.height(), + }); + console.log('this.bbox', deepClone(this.bbox)); + console.log('this.state.position', this.state.position); + }); + + this.konva.transformer.on('transform', () => { + // Always snap the interaction rect to the nearest pixel when transforming + + const x = Math.round(this.konva.interactionRect.x()); + const y = Math.round(this.konva.interactionRect.y()); + // Snap its position + this.konva.interactionRect.x(x); + this.konva.interactionRect.y(y); + + // Calculate the new scale of the interaction rect such that its width and height snap to the nearest pixel + const targetWidth = Math.max( + Math.round(this.konva.interactionRect.width() * Math.abs(this.konva.interactionRect.scaleX())), + MIN_LAYER_SIZE_PX + ); + const scaleX = targetWidth / this.konva.interactionRect.width(); + const targetHeight = Math.max( + Math.round(this.konva.interactionRect.height() * Math.abs(this.konva.interactionRect.scaleY())), + MIN_LAYER_SIZE_PX + ); + const scaleY = targetHeight / this.konva.interactionRect.height(); + + // Snap the width and height (via scale) of the interaction rect + this.konva.interactionRect.scaleX(scaleX); + this.konva.interactionRect.scaleY(scaleY); + this.konva.interactionRect.rotation(0); + + console.log('>>> transform'); + console.log('activeAnchor', this.konva.transformer.getActiveAnchor()); + console.log('interactionRect', { + x: this.konva.interactionRect.x(), + y: this.konva.interactionRect.y(), + scaleX: this.konva.interactionRect.scaleX(), + scaleY: this.konva.interactionRect.scaleY(), + width: this.konva.interactionRect.width(), + height: this.konva.interactionRect.height(), + rotation: this.konva.interactionRect.rotation(), + }); + + // Handle anchor-specific transformations of the layer's objects + const anchor = this.konva.transformer.getActiveAnchor(); + // 'top-left' + // 'top-center' + // 'top-right' + // 'middle-right' + // 'middle-left' + // 'bottom-left' + // 'bottom-center' + // 'bottom-right' + if (anchor === 'middle-right') { + // Dragging the anchor to the right + this.konva.objectGroup.setAttrs({ + scaleX, + x: x - (x - this.state.position.x) * scaleX, + }); + } else if (anchor === 'middle-left') { + // Dragging the anchor to the right + this.konva.objectGroup.setAttrs({ + scaleX, + x: x - (x - this.state.position.x) * scaleX, + }); + } else if (anchor === 'bottom-center') { + // Resize the interaction rect downwards + this.konva.objectGroup.setAttrs({ + scaleY, + y: y - (y - this.state.position.y) * scaleY, + }); + } else if (anchor === 'bottom-right') { + // Resize the interaction rect to the right and downwards via scale + this.konva.objectGroup.setAttrs({ + scaleX, + scaleY, + x: x - (x - this.state.position.x) * scaleX, + y: y - (y - this.state.position.y) * scaleY, + }); + } else if (anchor === 'top-center') { + // Resize the interaction rect to the upwards via scale & y position + this.konva.objectGroup.setAttrs({ + y, + scaleY, + }); + } + this.konva.objectGroupBbox.setAttrs({ + x: this.konva.objectGroup.x(), + y: this.konva.objectGroup.y(), + rotation: this.konva.objectGroup.rotation(), + scaleX: this.konva.objectGroup.scaleX(), + scaleY: this.konva.objectGroup.scaleY(), + }); + }); // this.konva.transformer.on('transform', () => { - // console.log(this.konva.interactionRect.position()); + // // We need to snap the transform to the nearest pixel - both the position and the scale + + // // Snap the interaction rect to the nearest pixel + // this.konva.interactionRect.x(Math.round(this.konva.interactionRect.x())); + // this.konva.interactionRect.y(Math.round(this.konva.interactionRect.y())); + + // // Calculate the new scale of the interaction rect such that its width and height snap to the nearest pixel + // const roundedScaledWidth = Math.round(this.konva.interactionRect.width() * this.konva.interactionRect.scaleX()); + // const correctedScaleX = roundedScaledWidth / this.konva.interactionRect.width(); + // const roundedScaledHeight = Math.round(this.konva.interactionRect.height() * this.konva.interactionRect.scaleY()); + // const correctedScaleY = roundedScaledHeight / this.konva.interactionRect.height(); + + // // Update the interaction rect's scale to the corrected scale + // this.konva.interactionRect.scaleX(correctedScaleX); + // this.konva.interactionRect.scaleY(correctedScaleY); + + // console.log('>>> transform'); + // console.log('activeAnchor', this.konva.transformer.getActiveAnchor()); + // console.log('interactionRect', { + // x: this.konva.interactionRect.x(), + // y: this.konva.interactionRect.y(), + // scaleX: this.konva.interactionRect.scaleX(), + // scaleY: this.konva.interactionRect.scaleY(), + // width: this.konva.interactionRect.width(), + // height: this.konva.interactionRect.height(), + // rotation: this.konva.interactionRect.rotation(), + // }); + + // // Update the object group to reflect the new scale and position of the interaction rect // this.konva.objectGroup.setAttrs({ + // // The scale is the same as the interaction rect // scaleX: this.konva.interactionRect.scaleX(), // scaleY: this.konva.interactionRect.scaleY(), - // // rotation: this.konva.interactionRect.rotation(), - // x: this.konva.interactionRect.x(), - // t: this.konva.interactionRect.y(), + // rotation: this.konva.interactionRect.rotation(), + // // We need to do some compensation for the new position. The bounds of the object group may be different from the + // // interaction rect/bbox, because the object group may have eraser strokes that are not included in the bbox. + // x: + // this.konva.interactionRect.x() - + // Math.abs(this.konva.interactionRect.x() - this.state.position.x) * this.konva.interactionRect.scaleX(), + // y: + // this.konva.interactionRect.y() - + // Math.abs(this.konva.interactionRect.y() - this.state.position.y) * this.konva.interactionRect.scaleY(), + // // x: this.konva.interactionRect.x() + (this.konva.interactionRect.x() - this.state.position.x) * this.konva.interactionRect.scaleX(), + // // y: this.konva.interactionRect.y() + (this.konva.interactionRect.y() - this.state.position.y) * this.konva.interactionRect.scaleY(), + // }); + // this.konva.objectGroupBbox.setAttrs({ + // x: this.konva.objectGroup.x(), + // y: this.konva.objectGroup.y(), + // scaleX: this.konva.objectGroup.scaleX(), + // scaleY: this.konva.objectGroup.scaleY(), // }); // }); this.konva.transformer.on('transformend', () => { - console.log(this.bbox); this.bbox = { - x: this.bbox.x * this.konva.group.scaleX(), - y: this.bbox.y * this.konva.group.scaleY(), - width: this.bbox.width * this.konva.group.scaleX(), - height: this.bbox.height * this.konva.group.scaleY(), + x: this.konva.interactionRect.x(), + y: this.konva.interactionRect.y(), + width: Math.round(this.konva.interactionRect.width() * this.konva.interactionRect.scaleX()), + height: Math.round(this.konva.interactionRect.height() * this.konva.interactionRect.scaleY()), }; - console.log(this.bbox); - this.renderBbox(); - this.manager.stateApi.onScaleChanged( + + // this.manager.stateApi.onPosChanged( + // { + // id: this.id, + // position: { x: this.konva.objectGroup.x(), y: this.konva.objectGroup.y() }, + // }, + // 'layer' + // ); + }); + + this.konva.interactionRect.on('dragmove', () => { + // Snap the interaction rect to the nearest pixel + this.konva.interactionRect.x(Math.round(this.konva.interactionRect.x())); + this.konva.interactionRect.y(Math.round(this.konva.interactionRect.y())); + + // The bbox should be updated to reflect the new position of the interaction rect, taking into account its padding + // and border + this.konva.bbox.setAttrs({ + x: this.konva.interactionRect.x() - CanvasLayer.BBOX_PADDING_PX / this.manager.stage.scaleX(), + y: this.konva.interactionRect.y() - CanvasLayer.BBOX_PADDING_PX / this.manager.stage.scaleX(), + }); + + // The object group is translated by the difference between the interaction rect's new and old positions (which is + // stored as this.bbox) + this.konva.objectGroup.setAttrs({ + x: this.state.position.x + this.konva.interactionRect.x() - this.bbox.x, + y: this.state.position.y + this.konva.interactionRect.y() - this.bbox.y, + }); + + const rect = this.konva.objectGroup.getClientRect({ skipTransform: true }); + this.konva.objectGroupBbox.setAttrs({ ...rect, x: this.konva.objectGroup.x(), y: this.konva.objectGroup.y() }); + }); + this.konva.interactionRect.on('dragend', () => { + // Update the bbox + this.bbox.x = this.konva.interactionRect.x(); + this.bbox.y = this.konva.interactionRect.y(); + + // Update internal state + this.manager.stateApi.onPosChanged( { id: this.id, - scale: this.konva.group.scaleX(), - position: { x: this.konva.group.x(), y: this.konva.group.y() }, + position: { x: this.konva.objectGroup.x(), y: this.konva.objectGroup.y() }, }, 'layer' ); }); - this.konva.group.on('dragend', () => { - this.manager.stateApi.onPosChanged( - { id: this.id, position: { x: this.konva.group.x(), y: this.konva.group.y() } }, - 'layer' - ); - }); this.objects = new Map(); this.drawingBuffer = null; this.state = state; this.bbox = CanvasLayer.DEFAULT_BBOX_RECT; + + console.log(this); } private static get DEFAULT_BBOX_RECT() { @@ -158,12 +347,24 @@ export class CanvasLayer { this.state = state; // Update the layer's position and listening state - this.konva.group.setAttrs({ + this.konva.objectGroup.setAttrs({ x: state.position.x, y: state.position.y, scaleX: 1, scaleY: 1, }); + this.konva.positionXLine.points([ + state.position.x, + -this.manager.stage.y(), + state.position.x, + this.manager.stage.y() + this.manager.stage.height() / this.manager.stage.scaleY(), + ]); + this.konva.positionYLine.points([ + -this.manager.stage.x(), + state.position.y, + this.manager.stage.x() + this.manager.stage.width() / this.manager.stage.scaleX(), + state.position.y, + ]); let didDraw = false; @@ -264,7 +465,7 @@ export class CanvasLayer { if (didDraw) { if (this.objects.size > 0) { - this.getBbox(); + // this.getBbox(); } else { this.bbox = CanvasLayer.DEFAULT_BBOX_RECT; this.renderBbox(); @@ -296,8 +497,8 @@ export class CanvasLayer { if (!this.konva.objectGroup.isCached() || didDraw) { // this.konva.objectGroup.cache(); } - // Activate the transformer - this.konva.transformer.nodes([this.konva.group]); + // Activate the transformer - it *must* be transforming the interactionRect, not the group! + this.konva.transformer.nodes([this.konva.interactionRect]); this.konva.transformer.forceUpdate(); this.konva.transformer.visible(true); } else if (selectedTool === 'move') { @@ -341,16 +542,24 @@ export class CanvasLayer { renderBbox() { const isSelected = this.manager.stateApi.getIsSelected(this.id); const selectedTool = this.manager.stateApi.getToolState().selected; + const scale = this.manager.stage.scaleX(); const hasBbox = this.bbox.width !== 0 && this.bbox.height !== 0; - this.konva.bbox.visible(hasBbox); + this.konva.bbox.visible(hasBbox && isSelected && selectedTool === 'move'); this.konva.interactionRect.visible(hasBbox); - + const rect = this.konva.objectGroup.getClientRect({ skipTransform: true }); + this.konva.objectGroupBbox.setAttrs({ + ...rect, + x: this.konva.objectGroup.x(), + y: this.konva.objectGroup.y(), + scaleX: 1, + scaleY: 1, + }); this.konva.bbox.setAttrs({ - x: this.bbox.x, - y: this.bbox.y, - width: this.bbox.width, - height: this.bbox.height, + x: this.bbox.x - CanvasLayer.BBOX_PADDING_PX / scale, + y: this.bbox.y - CanvasLayer.BBOX_PADDING_PX / scale, + width: this.bbox.width + (CanvasLayer.BBOX_PADDING_PX / scale) * 2, + height: this.bbox.height + (CanvasLayer.BBOX_PADDING_PX / scale) * 2, scaleX: 1, scaleY: 1, strokeWidth: 1 / this.manager.stage.scaleX(), @@ -374,7 +583,9 @@ export class CanvasLayer { let needsPixelBbox = false; const rect = this.konva.objectGroup.getClientRect({ skipTransform: true }); - // console.log('rect', rect); + + console.log('getBbox rect', rect); + // If there are no eraser strokes, we can use the client rect directly for (const obj of this.objects.values()) { if (obj instanceof CanvasEraserLine) { @@ -387,7 +598,12 @@ export class CanvasLayer { if (rect.width === 0 || rect.height === 0) { this.bbox = CanvasLayer.DEFAULT_BBOX_RECT; } else { - this.bbox = rect; + this.bbox = { + x: this.konva.objectGroup.x() + rect.x, + y: this.konva.objectGroup.y() + rect.y, + width: rect.width, + height: rect.height, + }; } this.renderBbox(); return; @@ -395,41 +611,31 @@ export class CanvasLayer { // We have eraser strokes - we must calculate the bbox using pixel data - // const a = window.performance.now(); const clone = this.konva.objectGroup.clone(); - // const b = window.performance.now(); - // console.log('cloned layer', b - a); - // const c = window.performance.now(); const canvas = clone.toCanvas(); - // const d = window.performance.now(); - // console.log('got canvas', d - c); const ctx = canvas.getContext('2d'); if (!ctx) { return; } const imageData = ctx.getImageData(0, 0, rect.width, rect.height); - // const e = window.performance.now(); - // console.log('got image data', e - d); this.manager.requestBbox( { buffer: imageData.data.buffer, width: imageData.width, height: imageData.height }, (extents) => { - // console.log('extents', extents); if (extents) { const { minX, minY, maxX, maxY } = extents; this.bbox = { - x: rect.x + minX, - y: rect.y + minY, + x: this.konva.objectGroup.x() + rect.x + minX, + y: this.konva.objectGroup.y() + rect.y + minY, width: maxX - minX, height: maxY - minY, }; } else { this.bbox = CanvasLayer.DEFAULT_BBOX_RECT; } + console.log('new bbox', deepClone(this.bbox)); this.renderBbox(); clone.destroy(); - // console.log('bbox', this.bbox); } ); - // console.log('transferred message', window.performance.now() - e); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index e49f6d0b1c4..d3a94b13ced 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -1,5 +1,5 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { getScaledCursorPosition } from 'features/controlLayers/konva/util'; +import { getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util'; import type { CanvasV2State, Coordinate, @@ -24,7 +24,7 @@ import { getBrushLineId, getEraserLineId, getRectShapeId } from './naming'; * @param setLastCursorPos The callback to store the cursor pos */ const updateLastCursorPos = (stage: Konva.Stage, setLastCursorPos: CanvasManager['stateApi']['setLastCursorPos']) => { - const pos = getScaledCursorPosition(stage); + const pos = getScaledFlooredCursorPosition(stage); if (!pos) { return null; } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index 6e29c191835..f0cf91e1d0e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -216,24 +216,25 @@ export const layersReducers = { } for (const obj of layer.objects) { if (obj.type === 'brush_line') { - obj.points = obj.points.map((point) => point * scale); - obj.strokeWidth *= scale; + obj.points = obj.points.map((point) => Math.round(point * scale)); + obj.strokeWidth = Math.round(obj.strokeWidth * scale); } else if (obj.type === 'eraser_line') { - obj.points = obj.points.map((point) => point * scale); - obj.strokeWidth *= scale; + obj.points = obj.points.map((point) => Math.round(point * scale)); + obj.strokeWidth = Math.round(obj.strokeWidth * scale); } else if (obj.type === 'rect_shape') { - obj.x *= scale; - obj.y *= scale; - obj.height *= scale; - obj.width *= scale; + obj.x = Math.round(obj.x * scale); + obj.y = Math.round(obj.y * scale); + obj.height = Math.round(obj.height * scale); + obj.width = Math.round(obj.width * scale); } else if (obj.type === 'image') { - obj.x *= scale; - obj.y *= scale; - obj.height *= scale; - obj.width *= scale; + obj.x = Math.round(obj.x * scale); + obj.y = Math.round(obj.y * scale); + obj.height = Math.round(obj.height * scale); + obj.width = Math.round(obj.width * scale); } } - layer.position = position; + layer.position.x = Math.round(position.x); + layer.position.y = Math.round(position.y); layer.bboxNeedsUpdate = true; state.layers.imageCache = null; }, @@ -265,3 +266,12 @@ export const layersReducers = { state.layers.imageCache = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; }, } satisfies SliceCaseReducers; + +const scalePoints = (points: number[], scaleX: number, scaleY: number) => { + const newPoints: number[] = []; + for (let i = 0; i < points.length; i += 2) { + newPoints.push(points[i]! * scaleX); + newPoints.push(points[i + 1]! * scaleY); + } + return newPoints; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index eaa2e32c647..06b7bfa0644 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -924,7 +924,7 @@ export type CanvasV2State = { export type StageAttrs = { position: Coordinate; dimensions: Dimensions; scale: number }; export type PositionChangedArg = { id: string; position: Coordinate }; -export type ScaleChangedArg = { id: string; scale: number; position: Coordinate }; +export type ScaleChangedArg = { id: string; scale: Coordinate; position: Coordinate }; export type BboxChangedArg = { id: string; bbox: Rect | null }; export type EraserLineAddedArg = { id: string; From af1c8cc7e0fcef0b61cd6dcbc07a56e754907d8d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 24 Jul 2024 19:34:49 +1000 Subject: [PATCH 240/678] fix(ui): imageDropped listener --- .../listenerMiddleware/listeners/imageDropped.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index cb93b9a2551..fb4ffbca7cb 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -4,7 +4,6 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware' import { parseify } from 'common/util/serialize'; import { caImageChanged, - iiImageChanged, ipaImageChanged, layerImageAdded, rgIPAdapterImageChanged, @@ -111,18 +110,6 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => return; } - /** - * Image dropped on Raster layer - */ - if ( - overData.actionType === 'SET_INITIAL_IMAGE' && - activeData.payloadType === 'IMAGE_DTO' && - activeData.payload.imageDTO - ) { - dispatch(iiImageChanged({ imageDTO: activeData.payload.imageDTO })); - return; - } - /** * Image dropped on node image field */ From c6bfeba61a2bc138a1b6572b19435b306d5c6861 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 24 Jul 2024 19:46:51 +1000 Subject: [PATCH 241/678] fix(ui): conflicts after rebasing --- .../nodes/util/graph/buildAdHocPostProcessingGraph.ts | 3 +-- .../util/graph/buildMultidiffusionUpscaleGraph.ts | 11 +++++------ .../nodes/util/graph/generation/Graph.test.ts | 2 +- .../src/features/nodes/util/graph/generation/Graph.ts | 2 +- .../AdvancedSettingsAccordion.tsx | 2 +- .../UpscaleSettingsAccordion/UpscaleWarning.tsx | 2 +- .../ParametersPanels/ParametersPanelUpscale.tsx | 1 + 7 files changed, 11 insertions(+), 12 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildAdHocPostProcessingGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildAdHocPostProcessingGraph.ts index 7e943500cbb..30ca457cdac 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildAdHocPostProcessingGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildAdHocPostProcessingGraph.ts @@ -7,7 +7,6 @@ import type { ImageDTO } from 'services/api/types'; import { isSpandrelImageToImageModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; -import { getModelMetadataField } from './canvas/metadata'; import { SPANDREL } from './constants'; type Arg = { @@ -33,7 +32,7 @@ export const buildAdHocPostProcessingGraph = async ({ image, state }: Arg): Prom const modelConfig = await fetchModelConfigWithTypeGuard(postProcessingModel.key, isSpandrelImageToImageModelConfig); g.upsertMetadata({ - upscale_model: getModelMetadataField(modelConfig), + upscale_model: Graph.getModelMetadataField(modelConfig), }); return g.getGraph(); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildMultidiffusionUpscaleGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildMultidiffusionUpscaleGraph.ts index 7c17b406ded..31a22fa6fb8 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildMultidiffusionUpscaleGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildMultidiffusionUpscaleGraph.ts @@ -1,6 +1,6 @@ import type { RootState } from 'app/store/store'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; -import type { GraphType } from 'features/nodes/util/graph/generation/Graph'; +import { addSDXLLoRAs } from 'features/nodes/util/graph/generation/addSDXLLoRAs'; import { Graph } from 'features/nodes/util/graph/generation/Graph'; import { isNonRefinerMainModelConfig, isSpandrelImageToImageModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; @@ -21,11 +21,10 @@ import { VAE_LOADER, } from './constants'; import { addLoRAs } from './generation/addLoRAs'; -import { addSDXLLoRas } from './generation/addSDXLLoRAs'; import { getBoardField, getPresetModifiedPrompts } from './graphBuilderUtils'; -export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise => { - const { model, cfgScale: cfg_scale, scheduler, steps, vaePrecision, seed, vae } = state.generation; +export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise => { + const { model, cfgScale: cfg_scale, scheduler, steps, vaePrecision, seed, vae } = state.canvasV2.params; const { upscaleModel, upscaleInitialImage, structure, creativity, tileControlnetModel, scale } = state.upscale; assert(model, 'No model found in state'); @@ -123,7 +122,7 @@ export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise g.addEdge(modelNode, 'clip2', posCondNode, 'clip2'); g.addEdge(modelNode, 'clip2', negCondNode, 'clip2'); g.addEdge(modelNode, 'unet', tiledMultidiffusionNode, 'unet'); - addSDXLLoRas(state, g, tiledMultidiffusionNode, modelNode, null, posCondNode, negCondNode); + addSDXLLoRAs(state, g, tiledMultidiffusionNode, modelNode, null, posCondNode, negCondNode); g.upsertMetadata({ positive_prompt: positivePrompt, @@ -245,5 +244,5 @@ export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise g.addEdge(collectNode, 'collection', tiledMultidiffusionNode, 'control'); - return g.getGraph(); + return g; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.test.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.test.ts index f2e09adfa1c..3c26edc787d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.test.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.test.ts @@ -600,7 +600,7 @@ describe('Graph', () => { }); g.upsertMetadata({ test: 'test' }); g.addEdgeToMetadata(n1, 'width', 'width'); - const metadata = g._getMetadataNode(); + const metadata = g.getMetadataNode(); expect(g.getEdgesFrom(n1).length).toBe(1); expect(g.getEdgesTo(metadata as unknown as AnyInvocation).length).toBe(1); }); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.ts index d223d1f500f..3510d20f15a 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/Graph.ts @@ -409,7 +409,7 @@ export class Graph { metadataField: string ): Edge { // @ts-expect-error `Graph` excludes `core_metadata` nodes due to its excessively wide typing - return this.addEdge(fromNode, fromField, this._getMetadataNode(), metadataField); + return this.addEdge(fromNode, fromField, this.getMetadataNode(), metadataField); } /** * Set the node that should receive metadata. All other edges from the metadata node are deleted. diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx index 2ca7c926b09..daad281fc86 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx @@ -54,7 +54,7 @@ export const AdvancedSettingsAccordion = memo(() => { if (params.seamlessXAxis || params.seamlessYAxis) { badges.push('seamless'); } - if (activeTabName === 'upscaling' && !generation.shouldRandomizeSeed) { + if (activeTabName === 'upscaling' && !params.shouldRandomizeSeed) { badges.push('Manual Seed'); } return badges; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleWarning.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleWarning.tsx index d12b4e62ef5..0992ac865a3 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleWarning.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleWarning.tsx @@ -10,7 +10,7 @@ import { useControlNetModels } from 'services/api/hooks/modelsByType'; export const UpscaleWarning = () => { const { t } = useTranslation(); - const model = useAppSelector((s) => s.generation.model); + const model = useAppSelector((s) => s.canvasV2.params.model); const upscaleModel = useAppSelector((s) => s.upscale.upscaleModel); const tileControlnetModel = useAppSelector((s) => s.upscale.tileControlnetModel); const upscaleInitialImage = useAppSelector((s) => s.upscale.upscaleInitialImage); diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelUpscale.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelUpscale.tsx index af9e0dd2ef2..1e9d7ec3288 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelUpscale.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelUpscale.tsx @@ -20,6 +20,7 @@ const overlayScrollbarsStyles: CSSProperties = { const ParametersPanelUpscale = () => { const isMenuOpen = useStore($isMenuOpen); + return ( From 6b5d7406d68999f7cc93121bb5d8a55b2d9f86d0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 26 Jul 2024 13:34:15 +1000 Subject: [PATCH 242/678] fix(ui): dnd to canvas broke --- .../listenerMiddleware/listeners/imageDropped.ts | 6 +++--- .../frontend/web/src/features/dnd/types/index.ts | 12 ------------ .../web/src/features/dnd/util/isValidDrop.ts | 2 -- 3 files changed, 3 insertions(+), 17 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index fb4ffbca7cb..8368e42ec33 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -101,12 +101,12 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => * Image dropped on Raster layer */ if ( - overData.actionType === 'ADD_RASTER_LAYER_IMAGE' && + overData.actionType === 'ADD_LAYER_IMAGE' && activeData.payloadType === 'IMAGE_DTO' && activeData.payload.imageDTO ) { - const { layerId } = overData.context; - dispatch(layerImageAdded({ id: layerId, imageDTO: activeData.payload.imageDTO })); + const { id } = overData.context; + dispatch(layerImageAdded({ id, imageDTO: activeData.payload.imageDTO })); return; } diff --git a/invokeai/frontend/web/src/features/dnd/types/index.ts b/invokeai/frontend/web/src/features/dnd/types/index.ts index b041546ec8a..19dcbf19c0b 100644 --- a/invokeai/frontend/web/src/features/dnd/types/index.ts +++ b/invokeai/frontend/web/src/features/dnd/types/index.ts @@ -51,17 +51,6 @@ export type LayerImageDropData = BaseDropData & { }; }; -export type RasterLayerImageDropData = BaseDropData & { - actionType: 'ADD_RASTER_LAYER_IMAGE'; - context: { - layerId: string; - }; -}; - -export type CanvasInitialImageDropData = BaseDropData & { - actionType: 'SET_CANVAS_INITIAL_IMAGE'; -}; - type UpscaleInitialImageDropData = BaseDropData & { actionType: 'SET_UPSCALE_INITIAL_IMAGE'; }; @@ -104,7 +93,6 @@ export type TypesafeDroppableData = | IPAImageDropData | RGIPAdapterImageDropData | SelectForCompareDropData - | RasterLayerImageDropData | UpscaleInitialImageDropData | LayerImageDropData; diff --git a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts index 80ea701727b..128e5c5d501 100644 --- a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts +++ b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts @@ -29,8 +29,6 @@ export const isValidDrop = (overData?: TypesafeDroppableData | null, activeData? return payloadType === 'IMAGE_DTO'; case 'SELECT_FOR_COMPARE': return payloadType === 'IMAGE_DTO'; - case 'SET_INITIAL_IMAGE': - return payloadType === 'IMAGE_DTO'; case 'ADD_TO_BOARD': { // If the board is the same, don't allow the drop From 54e5401a96151075e6d2ea1a5d3b46c4e8be2562 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 26 Jul 2024 18:28:02 +1000 Subject: [PATCH 243/678] feat(ui): wip transform mode --- .../components/BboxToolButton.tsx | 7 +- .../components/BrushToolButton.tsx | 7 +- .../components/EraserToolButton.tsx | 5 +- .../components/MoveToolButton.tsx | 5 +- .../components/RectToolButton.tsx | 5 +- .../controlLayers/components/ToolChooser.tsx | 21 +- .../components/TransformToolButton.tsx | 33 +- .../components/ViewToolButton.tsx | 7 +- .../controlLayers/konva/CanvasLayer.ts | 373 +++++++----------- .../controlLayers/konva/CanvasTool.ts | 2 +- .../controlLayers/store/canvasV2Slice.ts | 1 + .../controlLayers/store/toolReducers.ts | 3 + .../src/features/controlLayers/store/types.ts | 3 +- 13 files changed, 202 insertions(+), 270 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/BboxToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/BboxToolButton.tsx index 70440f10ac2..6eecd06860e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/BboxToolButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/BboxToolButton.tsx @@ -9,21 +9,22 @@ import { PiBoundingBoxBold } from 'react-icons/pi'; export const BboxToolButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const isDisabled = useAppSelector((s) => s.canvasV2.session.isStaging); + const isDisabled = useAppSelector((s) => s.canvasV2.session.isStaging || s.canvasV2.tool.isTransforming); const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'bbox'); const onClick = useCallback(() => { dispatch(toolChanged('bbox')); }, [dispatch]); - useHotkeys('q', onClick, [onClick]); + useHotkeys('q', onClick, { enabled: !isDisabled }, [onClick, isDisabled]); return ( } - variant={isSelected ? 'solid' : 'outline'} + colorScheme={isSelected ? 'invokeBlue' : 'base'} + variant="outline" onClick={onClick} isDisabled={isDisabled} /> diff --git a/invokeai/frontend/web/src/features/controlLayers/components/BrushToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/BrushToolButton.tsx index 0dcaa7fa7c4..551568e7cf3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/BrushToolButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/BrushToolButton.tsx @@ -15,13 +15,13 @@ export const BrushToolButton = memo(() => { const entityType = s.canvasV2.selectedEntityIdentifier?.type; const isDrawingToolAllowed = entityType ? isDrawableEntityType(entityType) : false; const isStaging = s.canvasV2.session.isStaging; - return !isDrawingToolAllowed || isStaging; + return !isDrawingToolAllowed || isStaging || s.canvasV2.tool.isTransforming; }); const onClick = useCallback(() => { dispatch(toolChanged('brush')); }, [dispatch]); - + useHotkeys('b', onClick, { enabled: !isDisabled }, [isDisabled, onClick]); return ( @@ -29,7 +29,8 @@ export const BrushToolButton = memo(() => { aria-label={`${t('unifiedCanvas.brush')} (B)`} tooltip={`${t('unifiedCanvas.brush')} (B)`} icon={} - variant={isSelected ? 'solid' : 'outline'} + colorScheme={isSelected ? 'invokeBlue' : 'base'} + variant="outline" onClick={onClick} isDisabled={isDisabled} /> diff --git a/invokeai/frontend/web/src/features/controlLayers/components/EraserToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/EraserToolButton.tsx index 698b37c81f6..f73d38a7695 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/EraserToolButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/EraserToolButton.tsx @@ -15,7 +15,7 @@ export const EraserToolButton = memo(() => { const entityType = s.canvasV2.selectedEntityIdentifier?.type; const isDrawingToolAllowed = entityType ? isDrawableEntityType(entityType) : false; const isStaging = s.canvasV2.session.isStaging; - return !isDrawingToolAllowed || isStaging; + return !isDrawingToolAllowed || isStaging || s.canvasV2.tool.isTransforming; }); const onClick = useCallback(() => { @@ -29,7 +29,8 @@ export const EraserToolButton = memo(() => { aria-label={`${t('unifiedCanvas.eraser')} (E)`} tooltip={`${t('unifiedCanvas.eraser')} (E)`} icon={} - variant={isSelected ? 'solid' : 'outline'} + colorScheme={isSelected ? 'invokeBlue' : 'base'} + variant="outline" onClick={onClick} isDisabled={isDisabled} /> diff --git a/invokeai/frontend/web/src/features/controlLayers/components/MoveToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/MoveToolButton.tsx index 48dcfeb247a..5d97542369c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/MoveToolButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/MoveToolButton.tsx @@ -11,7 +11,7 @@ export const MoveToolButton = memo(() => { const dispatch = useAppDispatch(); const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'move'); const isDisabled = useAppSelector( - (s) => s.canvasV2.selectedEntityIdentifier === null || s.canvasV2.session.isStaging + (s) => s.canvasV2.selectedEntityIdentifier === null || s.canvasV2.session.isStaging || s.canvasV2.tool.isTransforming ); const onClick = useCallback(() => { @@ -25,7 +25,8 @@ export const MoveToolButton = memo(() => { aria-label={`${t('unifiedCanvas.move')} (V)`} tooltip={`${t('unifiedCanvas.move')} (V)`} icon={} - variant={isSelected ? 'solid' : 'outline'} + colorScheme={isSelected ? 'invokeBlue' : 'base'} + variant="outline" onClick={onClick} isDisabled={isDisabled} /> diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RectToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RectToolButton.tsx index 4a8ccadd095..3c8acd4ae8e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RectToolButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RectToolButton.tsx @@ -15,7 +15,7 @@ export const RectToolButton = memo(() => { const entityType = s.canvasV2.selectedEntityIdentifier?.type; const isDrawingToolAllowed = entityType ? isDrawableEntityType(entityType) : false; const isStaging = s.canvasV2.session.isStaging; - return !isDrawingToolAllowed || isStaging; + return !isDrawingToolAllowed || isStaging || s.canvasV2.tool.isTransforming; }); const onClick = useCallback(() => { @@ -29,7 +29,8 @@ export const RectToolButton = memo(() => { aria-label={`${t('controlLayers.rectangle')} (U)`} tooltip={`${t('controlLayers.rectangle')} (U)`} icon={} - variant={isSelected ? 'solid' : 'outline'} + colorScheme={isSelected ? 'invokeBlue' : 'base'} + variant="outline" onClick={onClick} isDisabled={isDisabled} /> diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx index b9b6c8ca845..6d440391271 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx @@ -14,23 +14,26 @@ export const ToolChooser: React.FC = () => { useCanvasResetLayerHotkey(); useCanvasDeleteLayerHotkey(); const isCanvasSessionActive = useAppSelector((s) => s.canvasV2.session.isActive); + const isTransforming = useAppSelector((s) => s.canvasV2.tool.isTransforming); if (isCanvasSessionActive) { return ( - - - - - + <> + + + + + + + + - - - + ); } return ( - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx index e8ac2e2577e..607e5acc3db 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx @@ -1,6 +1,6 @@ -import { IconButton } from '@invoke-ai/ui-library'; +import { Button, IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { toolIsTransformingChanged } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; @@ -9,24 +9,41 @@ import { PiResizeBold } from 'react-icons/pi'; export const TransformToolButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'transform'); + const isTransforming = useAppSelector((s) => s.canvasV2.tool.isTransforming); const isDisabled = useAppSelector( (s) => s.canvasV2.selectedEntityIdentifier === null || s.canvasV2.session.isStaging ); - const onClick = useCallback(() => { - dispatch(toolChanged('transform')); + const onTransform = useCallback(() => { + dispatch(toolIsTransformingChanged(true)); }, [dispatch]); - useHotkeys(['ctrl+t', 'meta+t'], onClick, { enabled: !isDisabled }, [isDisabled, onClick]); + const onApplyTransformation = useCallback(() => { + false && dispatch(toolIsTransformingChanged(true)); + }, [dispatch]); + + const onCancelTransformation = useCallback(() => { + dispatch(toolIsTransformingChanged(false)); + }, [dispatch]); + + useHotkeys(['ctrl+t', 'meta+t'], onTransform, { enabled: !isDisabled }, [isDisabled, onTransform]); + + if (isTransforming) { + return ( + <> + + + + ); + } return ( } - variant={isSelected ? 'solid' : 'outline'} - onClick={onClick} + variant="solid" + onClick={onTransform} isDisabled={isDisabled} /> ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ViewToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ViewToolButton.tsx index b9f6b1691dc..184e38d7ad9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ViewToolButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ViewToolButton.tsx @@ -10,19 +10,20 @@ export const ViewToolButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'view'); - const isDisabled = useAppSelector((s) => s.canvasV2.session.isStaging); + const isDisabled = useAppSelector((s) => s.canvasV2.session.isStaging || s.canvasV2.tool.isTransforming); const onClick = useCallback(() => { dispatch(toolChanged('view')); }, [dispatch]); - useHotkeys('h', onClick, [onClick]); + useHotkeys('h', onClick, { enabled: !isDisabled }, [onClick]); return ( } - variant={isSelected ? 'solid' : 'outline'} + colorScheme={isSelected ? 'invokeBlue' : 'base'} + variant="outline" onClick={onClick} isDisabled={isDisabled} /> diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 0ae6ca3c62b..66fa5ca47d9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -5,14 +5,12 @@ import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; import { mapId } from 'features/controlLayers/konva/util'; -import type { BrushLine, EraserLine, LayerEntity, Rect, RectShape } from 'features/controlLayers/store/types'; +import type { BrushLine, EraserLine, LayerEntity, RectShape } from 'features/controlLayers/store/types'; import { isDrawingTool } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { debounce } from 'lodash-es'; import { assert } from 'tsafe'; -const MIN_LAYER_SIZE_PX = 10; - export class CanvasLayer { static NAME_PREFIX = 'layer'; static LAYER_NAME = `${CanvasLayer.NAME_PREFIX}_layer`; @@ -34,14 +32,15 @@ export class CanvasLayer { layer: Konva.Layer; bbox: Konva.Rect; objectGroup: Konva.Group; - objectGroupBbox: Konva.Rect; - positionXLine: Konva.Line; - positionYLine: Konva.Line; transformer: Konva.Transformer; interactionRect: Konva.Rect; }; objects: Map; - bbox: Rect; + + offsetX: number; + offsetY: number; + width: number; + height: number; getBbox = debounce(this._getBbox, 300); @@ -59,18 +58,16 @@ export class CanvasLayer { strokeHitEnabled: false, }), objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }), - objectGroupBbox: new Konva.Rect({ fill: 'green', opacity: 0.5, listening: false }), - positionXLine: new Konva.Line({ stroke: 'white', strokeWidth: 1 }), - positionYLine: new Konva.Line({ stroke: 'white', strokeWidth: 1 }), transformer: new Konva.Transformer({ name: CanvasLayer.TRANSFORMER_NAME, - draggable: false, + draggable: true, // enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], - rotateEnabled: false, - flipEnabled: false, + rotateEnabled: true, + flipEnabled: true, listening: false, padding: CanvasLayer.BBOX_PADDING_PX, stroke: 'hsl(200deg 76% 59%)', // invokeBlue.400 + keepRatio: false, }), interactionRect: new Konva.Rect({ name: CanvasLayer.INTERACTION_RECT_NAME, @@ -84,9 +81,6 @@ export class CanvasLayer { this.konva.layer.add(this.konva.transformer); this.konva.layer.add(this.konva.interactionRect); this.konva.layer.add(this.konva.bbox); - this.konva.layer.add(this.konva.objectGroupBbox); - this.konva.layer.add(this.konva.positionXLine); - this.konva.layer.add(this.konva.positionYLine); this.konva.transformer.on('transformstart', () => { console.log('>>> transformstart'); @@ -98,35 +92,35 @@ export class CanvasLayer { width: this.konva.interactionRect.width(), height: this.konva.interactionRect.height(), }); - console.log('this.bbox', deepClone(this.bbox)); + this.logBbox('transformstart bbox'); console.log('this.state.position', this.state.position); }); this.konva.transformer.on('transform', () => { // Always snap the interaction rect to the nearest pixel when transforming - const x = Math.round(this.konva.interactionRect.x()); - const y = Math.round(this.konva.interactionRect.y()); - // Snap its position - this.konva.interactionRect.x(x); - this.konva.interactionRect.y(y); + // const x = Math.round(this.konva.interactionRect.x()); + // const y = Math.round(this.konva.interactionRect.y()); + // // Snap its position + // this.konva.interactionRect.x(x); + // this.konva.interactionRect.y(y); - // Calculate the new scale of the interaction rect such that its width and height snap to the nearest pixel - const targetWidth = Math.max( - Math.round(this.konva.interactionRect.width() * Math.abs(this.konva.interactionRect.scaleX())), - MIN_LAYER_SIZE_PX - ); - const scaleX = targetWidth / this.konva.interactionRect.width(); - const targetHeight = Math.max( - Math.round(this.konva.interactionRect.height() * Math.abs(this.konva.interactionRect.scaleY())), - MIN_LAYER_SIZE_PX - ); - const scaleY = targetHeight / this.konva.interactionRect.height(); + // // Calculate the new scale of the interaction rect such that its width and height snap to the nearest pixel + // const targetWidth = Math.max( + // Math.round(this.konva.interactionRect.width() * Math.abs(this.konva.interactionRect.scaleX())), + // MIN_LAYER_SIZE_PX + // ); + // const scaleX = targetWidth / this.konva.interactionRect.width(); + // const targetHeight = Math.max( + // Math.round(this.konva.interactionRect.height() * Math.abs(this.konva.interactionRect.scaleY())), + // MIN_LAYER_SIZE_PX + // ); + // const scaleY = targetHeight / this.konva.interactionRect.height(); - // Snap the width and height (via scale) of the interaction rect - this.konva.interactionRect.scaleX(scaleX); - this.konva.interactionRect.scaleY(scaleY); - this.konva.interactionRect.rotation(0); + // // Snap the width and height (via scale) of the interaction rect + // this.konva.interactionRect.scaleX(scaleX); + // this.konva.interactionRect.scaleY(scaleY); + // this.konva.interactionRect.rotation(0); console.log('>>> transform'); console.log('activeAnchor', this.konva.transformer.getActiveAnchor()); @@ -140,119 +134,20 @@ export class CanvasLayer { rotation: this.konva.interactionRect.rotation(), }); - // Handle anchor-specific transformations of the layer's objects - const anchor = this.konva.transformer.getActiveAnchor(); - // 'top-left' - // 'top-center' - // 'top-right' - // 'middle-right' - // 'middle-left' - // 'bottom-left' - // 'bottom-center' - // 'bottom-right' - if (anchor === 'middle-right') { - // Dragging the anchor to the right - this.konva.objectGroup.setAttrs({ - scaleX, - x: x - (x - this.state.position.x) * scaleX, - }); - } else if (anchor === 'middle-left') { - // Dragging the anchor to the right - this.konva.objectGroup.setAttrs({ - scaleX, - x: x - (x - this.state.position.x) * scaleX, - }); - } else if (anchor === 'bottom-center') { - // Resize the interaction rect downwards - this.konva.objectGroup.setAttrs({ - scaleY, - y: y - (y - this.state.position.y) * scaleY, - }); - } else if (anchor === 'bottom-right') { - // Resize the interaction rect to the right and downwards via scale - this.konva.objectGroup.setAttrs({ - scaleX, - scaleY, - x: x - (x - this.state.position.x) * scaleX, - y: y - (y - this.state.position.y) * scaleY, - }); - } else if (anchor === 'top-center') { - // Resize the interaction rect to the upwards via scale & y position - this.konva.objectGroup.setAttrs({ - y, - scaleY, - }); - } - this.konva.objectGroupBbox.setAttrs({ - x: this.konva.objectGroup.x(), - y: this.konva.objectGroup.y(), - rotation: this.konva.objectGroup.rotation(), - scaleX: this.konva.objectGroup.scaleX(), - scaleY: this.konva.objectGroup.scaleY(), + this.konva.objectGroup.setAttrs({ + x: this.konva.interactionRect.x(), + y: this.konva.interactionRect.y(), + scaleX: this.konva.interactionRect.scaleX(), + scaleY: this.konva.interactionRect.scaleY(), + rotation: this.konva.interactionRect.rotation(), }); }); - // this.konva.transformer.on('transform', () => { - // // We need to snap the transform to the nearest pixel - both the position and the scale - - // // Snap the interaction rect to the nearest pixel - // this.konva.interactionRect.x(Math.round(this.konva.interactionRect.x())); - // this.konva.interactionRect.y(Math.round(this.konva.interactionRect.y())); - - // // Calculate the new scale of the interaction rect such that its width and height snap to the nearest pixel - // const roundedScaledWidth = Math.round(this.konva.interactionRect.width() * this.konva.interactionRect.scaleX()); - // const correctedScaleX = roundedScaledWidth / this.konva.interactionRect.width(); - // const roundedScaledHeight = Math.round(this.konva.interactionRect.height() * this.konva.interactionRect.scaleY()); - // const correctedScaleY = roundedScaledHeight / this.konva.interactionRect.height(); - - // // Update the interaction rect's scale to the corrected scale - // this.konva.interactionRect.scaleX(correctedScaleX); - // this.konva.interactionRect.scaleY(correctedScaleY); - - // console.log('>>> transform'); - // console.log('activeAnchor', this.konva.transformer.getActiveAnchor()); - // console.log('interactionRect', { - // x: this.konva.interactionRect.x(), - // y: this.konva.interactionRect.y(), - // scaleX: this.konva.interactionRect.scaleX(), - // scaleY: this.konva.interactionRect.scaleY(), - // width: this.konva.interactionRect.width(), - // height: this.konva.interactionRect.height(), - // rotation: this.konva.interactionRect.rotation(), - // }); - - // // Update the object group to reflect the new scale and position of the interaction rect - // this.konva.objectGroup.setAttrs({ - // // The scale is the same as the interaction rect - // scaleX: this.konva.interactionRect.scaleX(), - // scaleY: this.konva.interactionRect.scaleY(), - // rotation: this.konva.interactionRect.rotation(), - // // We need to do some compensation for the new position. The bounds of the object group may be different from the - // // interaction rect/bbox, because the object group may have eraser strokes that are not included in the bbox. - // x: - // this.konva.interactionRect.x() - - // Math.abs(this.konva.interactionRect.x() - this.state.position.x) * this.konva.interactionRect.scaleX(), - // y: - // this.konva.interactionRect.y() - - // Math.abs(this.konva.interactionRect.y() - this.state.position.y) * this.konva.interactionRect.scaleY(), - // // x: this.konva.interactionRect.x() + (this.konva.interactionRect.x() - this.state.position.x) * this.konva.interactionRect.scaleX(), - // // y: this.konva.interactionRect.y() + (this.konva.interactionRect.y() - this.state.position.y) * this.konva.interactionRect.scaleY(), - // }); - // this.konva.objectGroupBbox.setAttrs({ - // x: this.konva.objectGroup.x(), - // y: this.konva.objectGroup.y(), - // scaleX: this.konva.objectGroup.scaleX(), - // scaleY: this.konva.objectGroup.scaleY(), - // }); - // }); this.konva.transformer.on('transformend', () => { - this.bbox = { - x: this.konva.interactionRect.x(), - y: this.konva.interactionRect.y(), - width: Math.round(this.konva.interactionRect.width() * this.konva.interactionRect.scaleX()), - height: Math.round(this.konva.interactionRect.height() * this.konva.interactionRect.scaleY()), - }; - + this.offsetX = this.konva.interactionRect.x() - this.state.position.x; + this.offsetY = this.konva.interactionRect.y() - this.state.position.y; + this.width = Math.round(this.konva.interactionRect.width() * this.konva.interactionRect.scaleX()); + this.height = Math.round(this.konva.interactionRect.height() * this.konva.interactionRect.scaleY()); // this.manager.stateApi.onPosChanged( // { // id: this.id, @@ -260,6 +155,7 @@ export class CanvasLayer { // }, // 'layer' // ); + this.logBbox('transformend bbox'); }); this.konva.interactionRect.on('dragmove', () => { @@ -277,19 +173,15 @@ export class CanvasLayer { // The object group is translated by the difference between the interaction rect's new and old positions (which is // stored as this.bbox) this.konva.objectGroup.setAttrs({ - x: this.state.position.x + this.konva.interactionRect.x() - this.bbox.x, - y: this.state.position.y + this.konva.interactionRect.y() - this.bbox.y, + x: this.konva.interactionRect.x(), + y: this.konva.interactionRect.y(), }); - - const rect = this.konva.objectGroup.getClientRect({ skipTransform: true }); - this.konva.objectGroupBbox.setAttrs({ ...rect, x: this.konva.objectGroup.x(), y: this.konva.objectGroup.y() }); }); this.konva.interactionRect.on('dragend', () => { - // Update the bbox - this.bbox.x = this.konva.interactionRect.x(); - this.bbox.y = this.konva.interactionRect.y(); + this.logBbox('dragend bbox'); // Update internal state + // this.state.position = { x: this.konva.objectGroup.x(), y: this.konva.objectGroup.y() }; this.manager.stateApi.onPosChanged( { id: this.id, @@ -302,7 +194,10 @@ export class CanvasLayer { this.objects = new Map(); this.drawingBuffer = null; this.state = state; - this.bbox = CanvasLayer.DEFAULT_BBOX_RECT; + this.offsetX = 0; + this.offsetY = 0; + this.width = 0; + this.height = 0; console.log(this); } @@ -319,6 +214,32 @@ export class CanvasLayer { return this.drawingBuffer; } + updatePosition() { + const scale = this.manager.stage.scaleX(); + const onePixel = 1 / scale; + const bboxPadding = CanvasLayer.BBOX_PADDING_PX / scale; + + this.konva.objectGroup.setAttrs({ + x: this.state.position.x, + y: this.state.position.y, + offsetX: this.offsetX, + offsetY: this.offsetY, + }); + this.konva.bbox.setAttrs({ + x: this.state.position.x - bboxPadding, + y: this.state.position.y - bboxPadding, + width: this.width + bboxPadding * 2, + height: this.height + bboxPadding * 2, + strokeWidth: onePixel, + }); + this.konva.interactionRect.setAttrs({ + x: this.state.position.x, + y: this.state.position.y, + width: this.width, + height: this.height, + }); + } + async setDrawingBuffer(obj: BrushLine | EraserLine | RectShape | null) { if (obj) { this.drawingBuffer = obj; @@ -344,27 +265,7 @@ export class CanvasLayer { } async render(state: LayerEntity) { - this.state = state; - - // Update the layer's position and listening state - this.konva.objectGroup.setAttrs({ - x: state.position.x, - y: state.position.y, - scaleX: 1, - scaleY: 1, - }); - this.konva.positionXLine.points([ - state.position.x, - -this.manager.stage.y(), - state.position.x, - this.manager.stage.y() + this.manager.stage.height() / this.manager.stage.scaleY(), - ]); - this.konva.positionYLine.points([ - -this.manager.stage.x(), - state.position.y, - this.manager.stage.x() + this.manager.stage.width() / this.manager.stage.scaleX(), - state.position.y, - ]); + this.state = deepClone(state); let didDraw = false; @@ -465,9 +366,12 @@ export class CanvasLayer { if (didDraw) { if (this.objects.size > 0) { - // this.getBbox(); + this.getBbox(); } else { - this.bbox = CanvasLayer.DEFAULT_BBOX_RECT; + this.offsetX = 0; + this.offsetY = 0; + this.width = 0; + this.height = 0; this.renderBbox(); } } @@ -475,15 +379,14 @@ export class CanvasLayer { this.konva.layer.visible(true); this.konva.objectGroup.opacity(this.state.opacity); const isSelected = this.manager.stateApi.getIsSelected(this.id); - const selectedTool = this.manager.stateApi.getToolState().selected; + const toolState = this.manager.stateApi.getToolState(); - const isTransforming = selectedTool === 'transform' && isSelected; - const isMoving = selectedTool === 'move' && isSelected; + const isMoving = toolState.selected === 'move' && isSelected; - this.konva.layer.listening(isTransforming || isMoving); - this.konva.transformer.listening(isTransforming); + this.konva.layer.listening(toolState.isTransforming || isMoving); + this.konva.transformer.listening(toolState.isTransforming); this.konva.bbox.visible(isMoving); - this.konva.interactionRect.listening(isMoving); + this.konva.interactionRect.listening(toolState.isTransforming || isMoving); if (this.objects.size === 0) { // If the layer is totally empty, reset the cache and bail out. @@ -491,7 +394,7 @@ export class CanvasLayer { if (this.konva.objectGroup.isCached()) { this.konva.objectGroup.clearCache(); } - } else if (isSelected && selectedTool === 'transform') { + } else if (isSelected && toolState.isTransforming) { // When the layer is selected and being moved, we should always cache it. // We should update the cache if we drew to the layer. if (!this.konva.objectGroup.isCached() || didDraw) { @@ -501,7 +404,7 @@ export class CanvasLayer { this.konva.transformer.nodes([this.konva.interactionRect]); this.konva.transformer.forceUpdate(); this.konva.transformer.visible(true); - } else if (selectedTool === 'move') { + } else if (toolState.selected === 'move') { // When the layer is selected and being moved, we should always cache it. // We should update the cache if we drew to the layer. if (!this.konva.objectGroup.isCached() || didDraw) { @@ -515,7 +418,7 @@ export class CanvasLayer { // If the layer is selected but not using the move tool, we don't want the layer to be listening. // The transformer also does not need to be active. this.konva.transformer.nodes([]); - if (isDrawingTool(selectedTool)) { + if (isDrawingTool(toolState.selected)) { // We are using a drawing tool (brush, eraser, rect). These tools change the layer's rendered appearance, so we // should never be cached. if (this.konva.objectGroup.isCached()) { @@ -540,43 +443,23 @@ export class CanvasLayer { } renderBbox() { + const toolState = this.manager.stateApi.getToolState(); + if (toolState.isTransforming) { + return; + } const isSelected = this.manager.stateApi.getIsSelected(this.id); - const selectedTool = this.manager.stateApi.getToolState().selected; - const scale = this.manager.stage.scaleX(); - const hasBbox = this.bbox.width !== 0 && this.bbox.height !== 0; - - this.konva.bbox.visible(hasBbox && isSelected && selectedTool === 'move'); + const hasBbox = this.width !== 0 && this.height !== 0; + this.konva.bbox.visible(hasBbox && isSelected && toolState.selected === 'move'); this.konva.interactionRect.visible(hasBbox); - const rect = this.konva.objectGroup.getClientRect({ skipTransform: true }); - this.konva.objectGroupBbox.setAttrs({ - ...rect, - x: this.konva.objectGroup.x(), - y: this.konva.objectGroup.y(), - scaleX: 1, - scaleY: 1, - }); - this.konva.bbox.setAttrs({ - x: this.bbox.x - CanvasLayer.BBOX_PADDING_PX / scale, - y: this.bbox.y - CanvasLayer.BBOX_PADDING_PX / scale, - width: this.bbox.width + (CanvasLayer.BBOX_PADDING_PX / scale) * 2, - height: this.bbox.height + (CanvasLayer.BBOX_PADDING_PX / scale) * 2, - scaleX: 1, - scaleY: 1, - strokeWidth: 1 / this.manager.stage.scaleX(), - }); - this.konva.interactionRect.setAttrs({ - x: this.bbox.x, - y: this.bbox.y, - width: this.bbox.width, - height: this.bbox.height, - scaleX: 1, - scaleY: 1, - }); + this.updatePosition(); } private _getBbox() { if (this.objects.size === 0) { - this.bbox = CanvasLayer.DEFAULT_BBOX_RECT; + this.offsetX = 0; + this.offsetY = 0; + this.width = 0; + this.height = 0; this.renderBbox(); return; } @@ -595,16 +478,21 @@ export class CanvasLayer { } if (!needsPixelBbox) { - if (rect.width === 0 || rect.height === 0) { - this.bbox = CanvasLayer.DEFAULT_BBOX_RECT; - } else { - this.bbox = { - x: this.konva.objectGroup.x() + rect.x, - y: this.konva.objectGroup.y() + rect.y, - width: rect.width, - height: rect.height, - }; - } + this.offsetX = rect.x; + this.offsetY = rect.y; + this.width = rect.width; + this.height = rect.height; + // if (rect.width === 0 || rect.height === 0) { + // this.bbox = CanvasLayer.DEFAULT_BBOX_RECT; + // } else { + // this.bbox = { + // x: rect.x, + // y: rect.y, + // width: rect.width, + // height: rect.height, + // }; + // } + this.logBbox('new bbox from client rect'); this.renderBbox(); return; } @@ -621,21 +509,34 @@ export class CanvasLayer { this.manager.requestBbox( { buffer: imageData.data.buffer, width: imageData.width, height: imageData.height }, (extents) => { + console.log('extents', extents); if (extents) { const { minX, minY, maxX, maxY } = extents; - this.bbox = { - x: this.konva.objectGroup.x() + rect.x + minX, - y: this.konva.objectGroup.y() + rect.y + minY, - width: maxX - minX, - height: maxY - minY, - }; + this.offsetX = minX + rect.x; + this.offsetY = minY + rect.y; + this.width = maxX - minX; + this.height = maxY - minY; } else { - this.bbox = CanvasLayer.DEFAULT_BBOX_RECT; + this.offsetX = 0; + this.offsetY = 0; + this.width = 0; + this.height = 0; } - console.log('new bbox', deepClone(this.bbox)); + this.logBbox('new bbox from worker'); this.renderBbox(); clone.destroy(); } ); } + + logBbox(msg: string = 'bbox') { + console.log(msg, { + x: this.state.position.x, + y: this.state.position.y, + offsetX: this.offsetX, + offsetY: this.offsetY, + width: this.width, + height: this.height, + }); + } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts index 41291ee80bd..ea7452343cc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts @@ -160,7 +160,7 @@ export class CanvasTool { } else if (!isDrawableEntity) { // Non-drawable layers don't have tools stage.container().style.cursor = 'not-allowed'; - } else if (tool === 'move') { + } else if (tool === 'move' || toolState.isTransforming) { // Move tool gets a pointer stage.container().style.cursor = 'default'; } else if (tool === 'rect') { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 30e673cdc38..15b816f8e78 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -194,6 +194,7 @@ export const { allEntitiesDeleted, clipToBboxChanged, canvasReset, + toolIsTransformingChanged, // bbox bboxChanged, bboxScaledSizeChanged, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts index c1f14d7df40..3724f4942ba 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts @@ -20,4 +20,7 @@ export const toolReducers = { toolBufferChanged: (state, action: PayloadAction) => { state.tool.selectedBuffer = action.payload; }, + toolIsTransformingChanged: (state, action: PayloadAction) => { + state.tool.isTransforming = action.payload; + }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 06b7bfa0644..a6aa5936f8d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -464,7 +464,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }, }; -const zTool = z.enum(['brush', 'eraser', 'move', 'rect', 'view', 'bbox', 'transform']); +const zTool = z.enum(['brush', 'eraser', 'move', 'rect', 'view', 'bbox']); export type Tool = z.infer; export function isDrawingTool(tool: Tool): tool is 'brush' | 'eraser' | 'rect' { return tool === 'brush' || tool === 'eraser' || tool === 'rect'; @@ -850,6 +850,7 @@ export type CanvasV2State = { brush: { width: number }; eraser: { width: number }; fill: RgbaColor; + isTransforming: boolean; }; settings: { imageSmoothing: boolean; From 99d432785cd0729b7afeae43df10538406465a49 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 29 Jul 2024 23:51:03 +1000 Subject: [PATCH 244/678] feat(ui): wip transform mode --- .../listeners/enqueueRequestedLinear.ts | 6 +- .../components/ControlLayersToolbar.tsx | 12 +- .../components/StageComponent.tsx | 4 +- .../components/TransformToolButton.tsx | 46 +- .../controlLayers/konva/CanvasBrushLine.ts | 4 +- .../controlLayers/konva/CanvasEraserLine.ts | 4 +- .../controlLayers/konva/CanvasLayer.ts | 475 +++++++++++------- .../controlLayers/konva/CanvasManager.ts | 173 ++++--- .../features/controlLayers/konva/events.ts | 28 +- .../controlLayers/store/canvasV2Slice.ts | 6 +- .../controlLayers/store/layersReducers.ts | 23 +- .../controlLayers/store/toolReducers.ts | 3 - .../src/features/controlLayers/store/types.ts | 3 - .../web/src/services/api/endpoints/images.ts | 7 +- 14 files changed, 495 insertions(+), 299 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index 6d53d3a84d3..f802319a068 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -1,6 +1,6 @@ import { enqueueRequested } from 'app/store/actions'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { getCanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { $canvasManager } from 'features/controlLayers/konva/CanvasManager'; import { sessionStagingAreaReset, sessionStartedStaging } from 'features/controlLayers/store/canvasV2Slice'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph'; @@ -17,6 +17,9 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) const model = state.canvasV2.params.model; const { prepend } = action.payload; + const manager = $canvasManager.get(); + assert(manager, 'No model found in state'); + let didStartStaging = false; if (!state.canvasV2.session.isStaging && state.canvasV2.session.isActive) { dispatch(sessionStartedStaging()); @@ -26,7 +29,6 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) try { let g; - const manager = getCanvasManager(); assert(model, 'No model found in state'); const base = model.base; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index 7386461c27a..7a3da1aacb8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -1,6 +1,7 @@ /* eslint-disable i18next/no-literal-string */ import { Button } from '@chakra-ui/react'; import { Flex } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; import { BrushWidth } from 'features/controlLayers/components/BrushWidth'; import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover'; @@ -10,19 +11,22 @@ import { NewSessionButton } from 'features/controlLayers/components/NewSessionBu import { ResetCanvasButton } from 'features/controlLayers/components/ResetCanvasButton'; import { ToolChooser } from 'features/controlLayers/components/ToolChooser'; import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup'; -import { getCanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { $canvasManager } from 'features/controlLayers/konva/CanvasManager'; import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton'; import { ViewerToggleMenu } from 'features/gallery/components/ImageViewer/ViewerToggleMenu'; import { memo, useCallback } from 'react'; export const ControlLayersToolbar = memo(() => { const tool = useAppSelector((s) => s.canvasV2.tool.selected); + const canvasManager = useStore($canvasManager); const bbox = useCallback(() => { - const manager = getCanvasManager(); - for (const l of manager.layers.values()) { + if (!canvasManager) { + return; + } + for (const l of canvasManager.layers.values()) { l.getBbox(); } - }, []); + }, [canvasManager]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index 31072b0fa19..d6d63e1d0c7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -2,7 +2,7 @@ import { Flex } from '@invoke-ai/ui-library'; import { logger } from 'app/logging/logger'; import { useAppStore } from 'app/store/storeHooks'; import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay'; -import { CanvasManager, setCanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { $canvasManager, CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import Konva from 'konva'; import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react'; import { useDevicePixelRatio } from 'use-device-pixel-ratio'; @@ -28,7 +28,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, } const manager = new CanvasManager(stage, container, store); - setCanvasManager(manager); + $canvasManager.set(manager); console.log(manager); const cleanup = manager.initialize(); return cleanup; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx index 607e5acc3db..cf70f59ee91 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx @@ -1,38 +1,58 @@ import { Button, IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { toolIsTransformingChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { memo, useCallback } from 'react'; +import { useStore } from '@nanostores/react'; +import { useAppSelector } from 'app/store/storeHooks'; +import { $canvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { memo, useCallback, useEffect, useState } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiResizeBold } from 'react-icons/pi'; export const TransformToolButton = memo(() => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const isTransforming = useAppSelector((s) => s.canvasV2.tool.isTransforming); + const canvasManager = useStore($canvasManager); + const [isTransforming, setIsTransforming] = useState(false); const isDisabled = useAppSelector( (s) => s.canvasV2.selectedEntityIdentifier === null || s.canvasV2.session.isStaging ); + useEffect(() => { + if (!canvasManager) { + return; + } + canvasManager.onTransform = setIsTransforming; + return () => { + canvasManager.onTransform = null; + }; + }, [canvasManager]); + const onTransform = useCallback(() => { - dispatch(toolIsTransformingChanged(true)); - }, [dispatch]); + if (!canvasManager) { + return; + } + canvasManager.startTransform(); + }, [canvasManager]); const onApplyTransformation = useCallback(() => { - false && dispatch(toolIsTransformingChanged(true)); - }, [dispatch]); + if (!canvasManager) { + return; + } + canvasManager.applyTransform(); + }, [canvasManager]); const onCancelTransformation = useCallback(() => { - dispatch(toolIsTransformingChanged(false)); - }, [dispatch]); + if (!canvasManager) { + return; + } + canvasManager.cancelTransform(); + }, [canvasManager]); useHotkeys(['ctrl+t', 'meta+t'], onTransform, { enabled: !isDisabled }, [isDisabled, onTransform]); if (isTransforming) { return ( <> - - + + ); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts index 84c824022fa..3de306581f1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts @@ -44,8 +44,8 @@ export class CanvasBrushLine { this.state = state; } - update(state: BrushLine, force?: boolean): boolean { - if (this.state !== state || force) { + async update(state: BrushLine, force?: boolean): Promise { + if (force || this.state !== state) { const { points, color, clip, strokeWidth } = state; this.konva.line.setAttrs({ // A line with only one point will not be rendered, so we duplicate the points to make it visible diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts index f1d93fe9a95..32e7ccbd248 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts @@ -44,8 +44,8 @@ export class CanvasEraserLine { this.state = state; } - update(state: EraserLine, force?: boolean): boolean { - if (this.state !== state || force) { + async update(state: EraserLine, force?: boolean): Promise { + if (force || this.state !== state) { const { points, clip, strokeWidth } = state; this.konva.line.setAttrs({ // A line with only one point will not be rendered, so we duplicate the points to make it visible diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 66fa5ca47d9..cea1624f27c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -1,14 +1,23 @@ -import { deepClone } from 'common/util/deepClone'; +import { getStore } from 'app/store/nanostores/store'; import { CanvasBrushLine } from 'features/controlLayers/konva/CanvasBrushLine'; import { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine'; import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; -import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; -import { mapId } from 'features/controlLayers/konva/util'; -import type { BrushLine, EraserLine, LayerEntity, RectShape } from 'features/controlLayers/store/types'; -import { isDrawingTool } from 'features/controlLayers/store/types'; +import { konvaNodeToBlob, mapId, previewBlob } from 'features/controlLayers/konva/util'; +import { layerRasterized } from 'features/controlLayers/store/canvasV2Slice'; +import type { + BrushLine, + CanvasV2State, + Coordinate, + EraserLine, + LayerEntity, + RectShape, +} from 'features/controlLayers/store/types'; import Konva from 'konva'; -import { debounce } from 'lodash-es'; +import { debounce, get } from 'lodash-es'; +import type { Logger } from 'roarr'; +import { uploadImage } from 'services/api/endpoints/images'; import { assert } from 'tsafe'; export class CanvasLayer { @@ -20,8 +29,6 @@ export class CanvasLayer { static OBJECT_GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_object-group`; static BBOX_NAME = `${CanvasLayer.NAME_PREFIX}_bbox`; - private static BBOX_PADDING_PX = 5; - private drawingBuffer: BrushLine | EraserLine | RectShape | null; private state: LayerEntity; @@ -41,8 +48,10 @@ export class CanvasLayer { offsetY: number; width: number; height: number; - - getBbox = debounce(this._getBbox, 300); + log: Logger; + bboxNeedsUpdate: boolean; + isTransforming: boolean; + isFirstRender: boolean; constructor(state: LayerEntity, manager: CanvasManager) { this.id = state.id; @@ -60,12 +69,12 @@ export class CanvasLayer { objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }), transformer: new Konva.Transformer({ name: CanvasLayer.TRANSFORMER_NAME, - draggable: true, + draggable: false, // enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], rotateEnabled: true, flipEnabled: true, listening: false, - padding: CanvasLayer.BBOX_PADDING_PX, + padding: this.manager.getScaledBboxPadding(), stroke: 'hsl(200deg 76% 59%)', // invokeBlue.400 keepRatio: false, }), @@ -135,19 +144,30 @@ export class CanvasLayer { }); this.konva.objectGroup.setAttrs({ - x: this.konva.interactionRect.x(), - y: this.konva.interactionRect.y(), + x: this.konva.interactionRect.x() - this.offsetX * this.konva.interactionRect.scaleX(), + y: this.konva.interactionRect.y() - this.offsetY * this.konva.interactionRect.scaleY(), scaleX: this.konva.interactionRect.scaleX(), scaleY: this.konva.interactionRect.scaleY(), rotation: this.konva.interactionRect.rotation(), }); + console.log('objectGroup', { + x: this.konva.objectGroup.x(), + y: this.konva.objectGroup.y(), + scaleX: this.konva.objectGroup.scaleX(), + scaleY: this.konva.objectGroup.scaleY(), + offsetX: this.offsetX, + offsetY: this.offsetY, + width: this.konva.objectGroup.width(), + height: this.konva.objectGroup.height(), + rotation: this.konva.objectGroup.rotation(), + }); }); this.konva.transformer.on('transformend', () => { - this.offsetX = this.konva.interactionRect.x() - this.state.position.x; - this.offsetY = this.konva.interactionRect.y() - this.state.position.y; - this.width = Math.round(this.konva.interactionRect.width() * this.konva.interactionRect.scaleX()); - this.height = Math.round(this.konva.interactionRect.height() * this.konva.interactionRect.scaleY()); + // this.offsetX = this.konva.interactionRect.x() - this.state.position.x; + // this.offsetY = this.konva.interactionRect.y() - this.state.position.y; + // this.width = Math.round(this.konva.interactionRect.width() * this.konva.interactionRect.scaleX()); + // this.height = Math.round(this.konva.interactionRect.height() * this.konva.interactionRect.scaleY()); // this.manager.stateApi.onPosChanged( // { // id: this.id, @@ -166,26 +186,33 @@ export class CanvasLayer { // The bbox should be updated to reflect the new position of the interaction rect, taking into account its padding // and border this.konva.bbox.setAttrs({ - x: this.konva.interactionRect.x() - CanvasLayer.BBOX_PADDING_PX / this.manager.stage.scaleX(), - y: this.konva.interactionRect.y() - CanvasLayer.BBOX_PADDING_PX / this.manager.stage.scaleX(), + x: this.konva.interactionRect.x() - this.manager.getScaledBboxPadding(), + y: this.konva.interactionRect.y() - this.manager.getScaledBboxPadding(), }); // The object group is translated by the difference between the interaction rect's new and old positions (which is // stored as this.bbox) this.konva.objectGroup.setAttrs({ - x: this.konva.interactionRect.x(), - y: this.konva.interactionRect.y(), + x: this.konva.interactionRect.x() - this.offsetX * this.konva.interactionRect.scaleX(), + y: this.konva.interactionRect.y() - this.offsetY * this.konva.interactionRect.scaleY(), }); }); this.konva.interactionRect.on('dragend', () => { this.logBbox('dragend bbox'); - // Update internal state - // this.state.position = { x: this.konva.objectGroup.x(), y: this.konva.objectGroup.y() }; + if (this.isTransforming) { + // When the user cancels the transformation, we need to reset the layer, so we should not update the layer's + // positition while we are transforming - bail out early. + return; + } + this.manager.stateApi.onPosChanged( { id: this.id, - position: { x: this.konva.objectGroup.x(), y: this.konva.objectGroup.y() }, + position: { + x: this.konva.interactionRect.x() - this.offsetX * this.konva.interactionRect.scaleX(), + y: this.konva.interactionRect.y() - this.offsetY * this.konva.interactionRect.scaleY(), + }, }, 'layer' ); @@ -198,15 +225,16 @@ export class CanvasLayer { this.offsetY = 0; this.width = 0; this.height = 0; + this.bboxNeedsUpdate = true; + this.isTransforming = false; + this.isFirstRender = true; + this.log = this.manager.getLogger(`layer_${this.id}`); console.log(this); } - private static get DEFAULT_BBOX_RECT() { - return { x: 0, y: 0, width: 0, height: 0 }; - } - destroy(): void { + this.log.debug(`Layer ${this.id} - destroying`); this.konva.layer.destroy(); } @@ -214,99 +242,222 @@ export class CanvasLayer { return this.drawingBuffer; } - updatePosition() { - const scale = this.manager.stage.scaleX(); - const onePixel = 1 / scale; - const bboxPadding = CanvasLayer.BBOX_PADDING_PX / scale; - - this.konva.objectGroup.setAttrs({ - x: this.state.position.x, - y: this.state.position.y, - offsetX: this.offsetX, - offsetY: this.offsetY, - }); - this.konva.bbox.setAttrs({ - x: this.state.position.x - bboxPadding, - y: this.state.position.y - bboxPadding, - width: this.width + bboxPadding * 2, - height: this.height + bboxPadding * 2, - strokeWidth: onePixel, - }); - this.konva.interactionRect.setAttrs({ - x: this.state.position.x, - y: this.state.position.y, - width: this.width, - height: this.height, - }); - } - async setDrawingBuffer(obj: BrushLine | EraserLine | RectShape | null) { if (obj) { this.drawingBuffer = obj; - await this.renderObject(this.drawingBuffer, true); - this.updateGroup(true); + await this._renderObject(this.drawingBuffer, true); } else { this.drawingBuffer = null; } } - finalizeDrawingBuffer() { + async finalizeDrawingBuffer() { if (!this.drawingBuffer) { return; } - if (this.drawingBuffer.type === 'brush_line') { - this.manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: this.drawingBuffer }, 'layer'); - } else if (this.drawingBuffer.type === 'eraser_line') { - this.manager.stateApi.onEraserLineAdded({ id: this.id, eraserLine: this.drawingBuffer }, 'layer'); - } else if (this.drawingBuffer.type === 'rect_shape') { - this.manager.stateApi.onRectShapeAdded({ id: this.id, rectShape: this.drawingBuffer }, 'layer'); - } + const drawingBuffer = this.drawingBuffer; this.setDrawingBuffer(null); + + if (drawingBuffer.type === 'brush_line') { + this.manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: drawingBuffer }, 'layer'); + } else if (drawingBuffer.type === 'eraser_line') { + this.manager.stateApi.onEraserLineAdded({ id: this.id, eraserLine: drawingBuffer }, 'layer'); + } else if (drawingBuffer.type === 'rect_shape') { + this.manager.stateApi.onRectShapeAdded({ id: this.id, rectShape: drawingBuffer }, 'layer'); + } } - async render(state: LayerEntity) { - this.state = deepClone(state); + async update(arg?: { state: LayerEntity; toolState: CanvasV2State['tool']; isSelected: boolean }) { + const state = get(arg, 'state', this.state); + const toolState = get(arg, 'toolState', this.manager.stateApi.getToolState()); + const isSelected = get(arg, 'isSelected', this.manager.stateApi.getIsSelected(this.id)); - let didDraw = false; + if (!this.isFirstRender && state === this.state) { + this.log.trace('State unchanged, skipping update'); + return; + } - const objectIds = state.objects.map(mapId); + this.log.debug('Updating'); + const { position, objects, opacity, isEnabled } = state; + + if (this.isFirstRender || position !== this.state.position) { + await this.updatePosition({ position }); + } + if (this.isFirstRender || objects !== this.state.objects) { + await this.updateObjects({ objects }); + } + if (this.isFirstRender || opacity !== this.state.opacity) { + await this.updateOpacity({ opacity }); + } + if (this.isFirstRender || isEnabled !== this.state.isEnabled) { + await this.updateVisibility({ isEnabled }); + } + await this.updateInteraction({ toolState, isSelected }); + this.state = state; + } + + async updateVisibility(arg?: { isEnabled: boolean }) { + this.log.trace('Updating visibility'); + const isEnabled = get(arg, 'isEnabled', this.state.isEnabled); + const hasObjects = this.objects.size > 0 || this.drawingBuffer !== null; + this.konva.layer.visible(isEnabled || hasObjects); + } + + async updatePosition(arg?: { position: Coordinate }) { + this.log.trace('Updating position'); + const position = get(arg, 'position', this.state.position); + const bboxPadding = this.manager.getScaledBboxPadding(); + + this.konva.objectGroup.setAttrs({ + x: position.x, + y: position.y, + }); + this.konva.bbox.setAttrs({ + x: position.x + this.offsetX * this.konva.interactionRect.scaleX() - bboxPadding, + y: position.y + this.offsetY * this.konva.interactionRect.scaleY() - bboxPadding, + }); + this.konva.interactionRect.setAttrs({ + x: position.x + this.offsetX * this.konva.interactionRect.scaleX(), + y: position.y + this.offsetY * this.konva.interactionRect.scaleY(), + }); + } + + async updateObjects(arg?: { objects: LayerEntity['objects'] }) { + this.log.trace('Updating objects'); + + const objects = get(arg, 'objects', this.state.objects); + + const objectIds = objects.map(mapId); // Destroy any objects that are no longer in state for (const object of this.objects.values()) { if (!objectIds.includes(object.id) && object.id !== this.drawingBuffer?.id) { this.objects.delete(object.id); object.destroy(); - didDraw = true; + this.bboxNeedsUpdate = true; } } - for (const obj of state.objects) { - if (await this.renderObject(obj)) { - didDraw = true; + for (const obj of objects) { + if (await this._renderObject(obj)) { + this.bboxNeedsUpdate = true; } } if (this.drawingBuffer) { - if (await this.renderObject(this.drawingBuffer)) { - didDraw = true; + if (await this._renderObject(this.drawingBuffer)) { + this.bboxNeedsUpdate = true; } } + } + + async updateOpacity(arg?: { opacity: number }) { + this.log.trace('Updating opacity'); + + const opacity = get(arg, 'opacity', this.state.opacity); + + this.konva.objectGroup.opacity(opacity); + } + + async updateInteraction(arg?: { toolState: CanvasV2State['tool']; isSelected: boolean }) { + this.log.trace('Updating interaction'); + + const toolState = get(arg, 'toolState', this.manager.stateApi.getToolState()); + const isSelected = get(arg, 'isSelected', this.manager.stateApi.getIsSelected(this.id)); + + if (this.objects.size === 0) { + // The layer is totally empty, we can just disable the layer + this.konva.layer.listening(false); + return; + } + + if (isSelected && !this.isTransforming && toolState.selected === 'move') { + // We are moving this layer, it must be listening + this.konva.layer.listening(true); + + // The transformer is not needed + this.konva.transformer.listening(false); + this.konva.transformer.nodes([]); + + // The bbox rect should be visible and interaction rect listening for dragging + this.konva.bbox.visible(true); + this.konva.interactionRect.listening(true); + } else if (isSelected && this.isTransforming) { + // When transforming, we want the stage to still be movable if the view tool is selected. If the transformer or + // interaction rect are listening, it will interrupt the stage's drag events. So we should disable listening + // when the view tool is selected + const listening = toolState.selected !== 'view'; + this.konva.layer.listening(listening); + this.konva.interactionRect.listening(listening); + this.konva.transformer.listening(listening); + + // The transformer transforms the interaction rect, not the object group + this.konva.transformer.nodes([this.konva.interactionRect]); + + // Hide the bbox rect, the transformer will has its own bbox + this.konva.bbox.visible(false); + } else { + // The layer is not selected, or we are using a tool that doesn't need the layer to be listening - disable interaction stuff + this.konva.layer.listening(false); + + // The transformer, bbox and interaction rect should be inactive + this.konva.transformer.listening(false); + this.konva.transformer.nodes([]); + this.konva.bbox.visible(false); + this.konva.interactionRect.listening(false); + } + } + + async updateBbox() { + this.log.trace('Updating bbox'); + + const onePixel = this.manager.getScaledPixel(); + const bboxPadding = this.manager.getScaledBboxPadding(); - this.renderBbox(); - this.updateGroup(didDraw); + this.konva.bbox.setAttrs({ + x: this.state.position.x + this.offsetX * this.konva.interactionRect.scaleX() - bboxPadding, + y: this.state.position.y + this.offsetY * this.konva.interactionRect.scaleY() - bboxPadding, + width: this.width + bboxPadding * 2, + height: this.height + bboxPadding * 2, + strokeWidth: onePixel, + }); + this.konva.interactionRect.setAttrs({ + x: this.state.position.x + this.offsetX * this.konva.interactionRect.scaleX(), + y: this.state.position.y + this.offsetY * this.konva.interactionRect.scaleY(), + width: this.width, + height: this.height, + }); + } + + async syncStageScale() { + this.log.trace('Syncing scale to stage'); + + const onePixel = this.manager.getScaledPixel(); + const bboxPadding = this.manager.getScaledBboxPadding(); + + this.konva.bbox.setAttrs({ + x: this.konva.interactionRect.x() - bboxPadding, + y: this.konva.interactionRect.y() - bboxPadding, + width: this.konva.interactionRect.width() * this.konva.interactionRect.scaleX() + bboxPadding * 2, + height: this.konva.interactionRect.height() * this.konva.interactionRect.scaleY() + bboxPadding * 2, + strokeWidth: onePixel, + }); + this.konva.transformer.forceUpdate(); } - private async renderObject(obj: LayerEntity['objects'][number], force = false): Promise { + async _renderObject(obj: LayerEntity['objects'][number], force = false): Promise { if (obj.type === 'brush_line') { let brushLine = this.objects.get(obj.id); assert(brushLine instanceof CanvasBrushLine || brushLine === undefined); if (!brushLine) { + console.log('creating new brush line'); brushLine = new CanvasBrushLine(obj); this.objects.set(brushLine.id, brushLine); this.konva.objectGroup.add(brushLine.konva.group); return true; } else { - if (brushLine.update(obj, force)) { + console.log('updating brush line'); + if (await brushLine.update(obj, force)) { return true; } } @@ -320,7 +471,7 @@ export class CanvasLayer { this.konva.objectGroup.add(eraserLine.konva.group); return true; } else { - if (eraserLine.update(obj, force)) { + if (await eraserLine.update(obj, force)) { return true; } } @@ -358,109 +509,70 @@ export class CanvasLayer { return false; } - updateGroup(didDraw: boolean) { - if (!this.state.isEnabled) { - this.konva.layer.visible(false); - return; - } + async startTransform() { + this.isTransforming = true; - if (didDraw) { - if (this.objects.size > 0) { - this.getBbox(); - } else { - this.offsetX = 0; - this.offsetY = 0; - this.width = 0; - this.height = 0; - this.renderBbox(); - } - } + // When transforming, we want the stage to still be movable if the view tool is selected. If the transformer or + // interaction rect are listening, it will interrupt the stage's drag events. So we should disable listening + // when the view tool is selected + const listening = this.manager.stateApi.getToolState().selected !== 'view'; - this.konva.layer.visible(true); - this.konva.objectGroup.opacity(this.state.opacity); - const isSelected = this.manager.stateApi.getIsSelected(this.id); - const toolState = this.manager.stateApi.getToolState(); + this.konva.layer.listening(listening); + this.konva.interactionRect.listening(listening); + this.konva.transformer.listening(listening); - const isMoving = toolState.selected === 'move' && isSelected; + // The transformer transforms the interaction rect, not the object group + this.konva.transformer.nodes([this.konva.interactionRect]); - this.konva.layer.listening(toolState.isTransforming || isMoving); - this.konva.transformer.listening(toolState.isTransforming); - this.konva.bbox.visible(isMoving); - this.konva.interactionRect.listening(toolState.isTransforming || isMoving); + // Hide the bbox rect, the transformer will has its own bbox + this.konva.bbox.visible(false); + } - if (this.objects.size === 0) { - // If the layer is totally empty, reset the cache and bail out. - this.konva.transformer.nodes([]); - if (this.konva.objectGroup.isCached()) { - this.konva.objectGroup.clearCache(); - } - } else if (isSelected && toolState.isTransforming) { - // When the layer is selected and being moved, we should always cache it. - // We should update the cache if we drew to the layer. - if (!this.konva.objectGroup.isCached() || didDraw) { - // this.konva.objectGroup.cache(); - } - // Activate the transformer - it *must* be transforming the interactionRect, not the group! - this.konva.transformer.nodes([this.konva.interactionRect]); - this.konva.transformer.forceUpdate(); - this.konva.transformer.visible(true); - } else if (toolState.selected === 'move') { - // When the layer is selected and being moved, we should always cache it. - // We should update the cache if we drew to the layer. - if (!this.konva.objectGroup.isCached() || didDraw) { - // this.konva.objectGroup.cache(); - } - // Activate the transformer - this.konva.transformer.nodes([]); - this.konva.transformer.forceUpdate(); - this.konva.transformer.visible(false); - } else if (isSelected) { - // If the layer is selected but not using the move tool, we don't want the layer to be listening. - // The transformer also does not need to be active. - this.konva.transformer.nodes([]); - if (isDrawingTool(toolState.selected)) { - // We are using a drawing tool (brush, eraser, rect). These tools change the layer's rendered appearance, so we - // should never be cached. - if (this.konva.objectGroup.isCached()) { - this.konva.objectGroup.clearCache(); - } - } else { - // We are using a non-drawing tool (move, view, bbox), so we should cache the layer. - // We should update the cache if we drew to the layer. - if (!this.konva.objectGroup.isCached() || didDraw) { - // this.konva.objectGroup.cache(); - } - } - } else if (!isSelected) { - // Unselected layers should not be listening - // The transformer also does not need to be active. - this.konva.transformer.nodes([]); - // Update the layer's cache if it's not already cached or we drew to it. - if (!this.konva.objectGroup.isCached() || didDraw) { - // this.konva.objectGroup.cache(); - } - } + async resetScale() { + this.konva.objectGroup.scaleX(1); + this.konva.objectGroup.scaleY(1); + this.konva.bbox.scaleX(1); + this.konva.bbox.scaleY(1); + this.konva.interactionRect.scaleX(1); + this.konva.interactionRect.scaleY(1); } - renderBbox() { - const toolState = this.manager.stateApi.getToolState(); - if (toolState.isTransforming) { - return; - } - const isSelected = this.manager.stateApi.getIsSelected(this.id); - const hasBbox = this.width !== 0 && this.height !== 0; - this.konva.bbox.visible(hasBbox && isSelected && toolState.selected === 'move'); - this.konva.interactionRect.visible(hasBbox); - this.updatePosition(); + async applyTransform() { + this.isTransforming = false; + const objectGroupClone = this.konva.objectGroup.clone(); + const rect = { + x: this.konva.interactionRect.x(), + y: this.konva.interactionRect.y(), + width: this.konva.interactionRect.width() * this.konva.interactionRect.scaleX(), + height: this.konva.interactionRect.height() * this.konva.interactionRect.scaleY(), + }; + const blob = await konvaNodeToBlob(objectGroupClone, rect); + previewBlob(blob, 'transformed layer'); + const imageDTO = await uploadImage(blob, `${this.id}_transform.png`, 'other', true, true); + const { dispatch } = getStore(); + dispatch(layerRasterized({ id: this.id, imageDTO, position: this.konva.interactionRect.position() })); + this.isTransforming = false; + this.resetScale(); + } + + async cancelTransform() { + this.isTransforming = false; + this.resetScale(); + await this.updatePosition({ position: this.state.position }); + await this.updateBbox(); + await this.updateInteraction({ + toolState: this.manager.stateApi.getToolState(), + isSelected: this.manager.stateApi.getIsSelected(this.id), + }); } - private _getBbox() { + getBbox = debounce(() => { if (this.objects.size === 0) { this.offsetX = 0; this.offsetY = 0; this.width = 0; this.height = 0; - this.renderBbox(); + this.updateBbox(); return; } @@ -482,18 +594,8 @@ export class CanvasLayer { this.offsetY = rect.y; this.width = rect.width; this.height = rect.height; - // if (rect.width === 0 || rect.height === 0) { - // this.bbox = CanvasLayer.DEFAULT_BBOX_RECT; - // } else { - // this.bbox = { - // x: rect.x, - // y: rect.y, - // width: rect.width, - // height: rect.height, - // }; - // } this.logBbox('new bbox from client rect'); - this.renderBbox(); + this.updateBbox(); return; } @@ -523,11 +625,11 @@ export class CanvasLayer { this.height = 0; } this.logBbox('new bbox from worker'); - this.renderBbox(); + this.updateBbox(); clone.destroy(); } ); - } + }, CanvasManager.BBOX_DEBOUNCE_MS); logBbox(msg: string = 'bbox') { console.log(msg, { @@ -539,4 +641,13 @@ export class CanvasLayer { height: this.height, }); } + + getLayerRect() { + return { + x: this.state.position.x + this.offsetX, + y: this.state.position.y + this.offsetY, + width: this.width, + height: this.height, + }; + } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index a294f715069..fa1caf11b4c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -16,6 +16,7 @@ import { $lastProgressEvent, $shouldShowStagedImage } from 'features/controlLaye import type { CanvasV2State, GenerationMode } from 'features/controlLayers/store/types'; import type Konva from 'konva'; import { atom } from 'nanostores'; +import type { Logger } from 'roarr'; import { getImageDTO as defaultGetImageDTO, uploadImage as defaultUploadImage } from 'services/api/endpoints/images'; import type { ImageCategory, ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; @@ -32,9 +33,6 @@ import { CanvasStateApi } from './CanvasStateApi'; import { CanvasTool } from './CanvasTool'; import { setStageEventHandlers } from './events'; -const log = logger('canvas'); -const workerLog = logger('worker'); - // type Extents = { // minX: number; // minY: number; @@ -63,17 +61,12 @@ type Util = { ) => Promise; }; -const $canvasManager = atom(null); -export function getCanvasManager() { - const nodeManager = $canvasManager.get(); - assert(nodeManager !== null, 'Node manager not initialized'); - return nodeManager; -} -export function setCanvasManager(nodeManager: CanvasManager) { - $canvasManager.set(nodeManager); -} +export const $canvasManager = atom(null); export class CanvasManager { + private static BBOX_PADDING_PX = 5; + static BBOX_DEBOUNCE_MS = 300; + stage: Konva.Stage; container: HTMLDivElement; controlAdapters: Map; @@ -86,6 +79,11 @@ export class CanvasManager { preview: CanvasPreview; background: CanvasBackground; + log: Logger; + workerLog: Logger; + + onTransform: ((isTransforming: boolean) => void) | null; + private store: Store; private isFirstRender: boolean; private prevState: CanvasV2State; @@ -106,6 +104,9 @@ export class CanvasManager { this.prevState = this.stateApi.getState(); this.isFirstRender = true; + this.log = logger('canvas'); + this.workerLog = logger('worker'); + this.util = { getImageDTO, uploadImage, @@ -138,9 +139,9 @@ export class CanvasManager { const { type, data } = event.data; if (type === 'log') { if (data.ctx) { - workerLog[data.level](data.ctx, data.message); + this.workerLog[data.level](data.ctx, data.message); } else { - workerLog[data.level](data.message); + this.workerLog[data.level](data.message); } } else if (type === 'extents') { const task = this.tasks.get(data.id); @@ -151,11 +152,17 @@ export class CanvasManager { } }; this.worker.onerror = (event) => { - log.error({ message: event.message }, 'Worker error'); + this.log.error({ message: event.message }, 'Worker error'); }; this.worker.onmessageerror = () => { - log.error('Worker message error'); + this.log.error('Worker message error'); }; + this.onTransform = null; + } + + getLogger(namespace: string) { + const managerNamespace = this.log.getContext().namespace; + return this.log.child({ namespace: `${managerNamespace}.${namespace}` }); } requestBbox(data: Omit, onComplete: (extents: Extents | null) => void) { @@ -172,27 +179,6 @@ export class CanvasManager { await this.initialImage.render(this.stateApi.getInitialImageState()); } - async renderLayers() { - const { entities } = this.stateApi.getLayersState(); - - for (const canvasLayer of this.layers.values()) { - if (!entities.find((l) => l.id === canvasLayer.id)) { - canvasLayer.destroy(); - this.layers.delete(canvasLayer.id); - } - } - - for (const entity of entities) { - let adapter = this.layers.get(entity.id); - if (!adapter) { - adapter = new CanvasLayer(entity, this); - this.layers.set(adapter.id, adapter); - this.stage.add(adapter.konva.layer); - } - await adapter.render(entity); - } - } - async renderRegions() { const { entities } = this.stateApi.getRegionsState(); @@ -245,9 +231,9 @@ export class CanvasManager { } } - renderBboxes() { + syncStageScale() { for (const layer of this.layers.values()) { - layer.renderBbox(); + layer.syncStageScale(); } } @@ -283,22 +269,84 @@ export class CanvasManager { this.background.render(); } + getTransformingLayer() { + return Array.from(this.layers.values()).find((layer) => layer.isTransforming); + } + + getIsTransforming() { + return Boolean(this.getTransformingLayer()); + } + + startTransform() { + if (this.getIsTransforming()) { + return; + } + const layer = this.getSelectedEntityAdapter(); + assert(layer instanceof CanvasLayer, 'No selected layer'); + layer.startTransform(); + this.onTransform?.(true); + } + + applyTransform() { + const layer = this.getTransformingLayer(); + if (layer) { + layer.applyTransform(); + } + this.onTransform?.(false); + } + + cancelTransform() { + const layer = this.getTransformingLayer(); + if (layer) { + layer.cancelTransform(); + } + this.onTransform?.(false); + } + render = async () => { const state = this.stateApi.getState(); if (this.prevState === state && !this.isFirstRender) { - log.trace('No changes detected, skipping render'); + this.log.trace('No changes detected, skipping render'); return; } + if (this.isFirstRender || state.layers.entities !== this.prevState.layers.entities) { + this.log.debug('Rendering layers'); + + for (const canvasLayer of this.layers.values()) { + if (!state.layers.entities.find((l) => l.id === canvasLayer.id)) { + this.log.debug(`Destroying deleted layer ${canvasLayer.id}`); + canvasLayer.destroy(); + this.layers.delete(canvasLayer.id); + } + } + + for (const entityState of state.layers.entities) { + let adapter = this.layers.get(entityState.id); + if (!adapter) { + this.log.debug(`Creating layer layer ${entityState.id}`); + adapter = new CanvasLayer(entityState, this); + this.layers.set(adapter.id, adapter); + this.stage.add(adapter.konva.layer); + } + await adapter.update({ + state: entityState, + toolState: state.tool, + isSelected: state.selectedEntityIdentifier?.id === entityState.id, + }); + } + } + if ( this.isFirstRender || - state.layers.entities !== this.prevState.layers.entities || state.tool.selected !== this.prevState.tool.selected || state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id ) { - log.debug('Rendering layers'); - await this.renderLayers(); + this.log.debug('Updating interaction'); + for (const layer of this.layers.values()) { + layer.updateInteraction({ toolState: state.tool, isSelected: state.selectedEntityIdentifier?.id === layer.id }); + } } if ( @@ -308,7 +356,7 @@ export class CanvasManager { state.tool.selected !== this.prevState.tool.selected || state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id ) { - log.debug('Rendering initial image'); + this.log.debug('Rendering initial image'); await this.renderInitialImage(); } @@ -319,7 +367,7 @@ export class CanvasManager { state.tool.selected !== this.prevState.tool.selected || state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id ) { - log.debug('Rendering regions'); + this.log.debug('Rendering regions'); await this.renderRegions(); } @@ -330,7 +378,7 @@ export class CanvasManager { state.tool.selected !== this.prevState.tool.selected || state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id ) { - log.debug('Rendering inpaint mask'); + this.log.debug('Rendering inpaint mask'); await this.renderInpaintMask(); } @@ -340,7 +388,7 @@ export class CanvasManager { state.tool.selected !== this.prevState.tool.selected || state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id ) { - log.debug('Rendering control adapters'); + this.log.debug('Rendering control adapters'); await this.renderControlAdapters(); } @@ -350,7 +398,7 @@ export class CanvasManager { state.tool.selected !== this.prevState.tool.selected || state.session.isActive !== this.prevState.session.isActive ) { - log.debug('Rendering generation bbox'); + this.log.debug('Rendering generation bbox'); await this.preview.bbox.render(); } @@ -360,12 +408,12 @@ export class CanvasManager { state.controlAdapters !== this.prevState.controlAdapters || state.regions !== this.prevState.regions ) { - // log.debug('Updating entity bboxes'); + // this.log.debug('Updating entity bboxes'); // debouncedUpdateBboxes(stage, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions, onBboxChanged); } if (this.isFirstRender || state.session !== this.prevState.session) { - log.debug('Rendering staging area'); + this.log.debug('Rendering staging area'); await this.preview.stagingArea.render(); } @@ -377,7 +425,7 @@ export class CanvasManager { state.inpaintMask !== this.prevState.inpaintMask || state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id ) { - log.debug('Arranging entities'); + this.log.debug('Arranging entities'); await this.arrangeEntities(); } @@ -389,7 +437,7 @@ export class CanvasManager { }; initialize = () => { - log.debug('Initializing renderer'); + this.log.debug('Initializing renderer'); this.stage.container(this.container); const cleanupListeners = setStageEventHandlers(this); @@ -405,24 +453,24 @@ export class CanvasManager { // When we this flag, we need to render the staging area $shouldShowStagedImage.subscribe(async (shouldShowStagedImage, prevShouldShowStagedImage) => { if (shouldShowStagedImage !== prevShouldShowStagedImage) { - log.debug('Rendering staging area'); + this.log.debug('Rendering staging area'); await this.preview.stagingArea.render(); } }); $lastProgressEvent.subscribe(async (lastProgressEvent, prevLastProgressEvent) => { if (lastProgressEvent !== prevLastProgressEvent) { - log.debug('Rendering progress image'); + this.log.debug('Rendering progress image'); await this.preview.progressPreview.render(lastProgressEvent); } }); - log.debug('First render of konva stage'); + this.log.debug('First render of konva stage'); this.preview.tool.render(); this.render(); return () => { - log.debug('Cleaning up konva renderer'); + this.log.debug('Cleaning up konva renderer'); unsubscribeRenderer(); cleanupListeners(); $shouldShowStagedImage.off(); @@ -430,6 +478,19 @@ export class CanvasManager { }; }; + getStageScale(): number { + // The stage is never scaled differently in x and y + return this.stage.scaleX(); + } + + getScaledPixel(): number { + return 1 / this.getStageScale(); + } + + getScaledBboxPadding(): number { + return CanvasManager.BBOX_PADDING_PX / this.getStageScale(); + } + getSelectedEntityAdapter = (): CanvasLayer | CanvasRegion | CanvasControlAdapter | CanvasInpaintMask | null => { const state = this.stateApi.getState(); const identifier = state.selectedEntityIdentifier; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index d3a94b13ced..6b54330f6a6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -185,7 +185,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (e.evt.shiftKey && lastLinePoint) { // Create a straight line from the last line point if (selectedEntityAdapter.getDrawingBuffer()) { - selectedEntityAdapter.finalizeDrawingBuffer(); + await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ id: getBrushLineId(selectedEntityAdapter.id, uuidv4()), @@ -203,7 +203,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { }); } else { if (selectedEntityAdapter.getDrawingBuffer()) { - selectedEntityAdapter.finalizeDrawingBuffer(); + await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ id: getBrushLineId(selectedEntityAdapter.id, uuidv4()), @@ -222,7 +222,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (e.evt.shiftKey && lastLinePoint) { // Create a straight line from the last line point if (selectedEntityAdapter.getDrawingBuffer()) { - selectedEntityAdapter.finalizeDrawingBuffer(); + await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ id: getBrushLineId(selectedEntityAdapter.id, uuidv4()), @@ -239,7 +239,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { }); } else { if (selectedEntityAdapter.getDrawingBuffer()) { - selectedEntityAdapter.finalizeDrawingBuffer(); + await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ id: getEraserLineId(selectedEntityAdapter.id, uuidv4()), @@ -254,7 +254,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (toolState.selected === 'rect') { if (selectedEntityAdapter.getDrawingBuffer()) { - selectedEntityAdapter.finalizeDrawingBuffer(); + await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ id: getRectShapeId(selectedEntityAdapter.id, uuidv4()), @@ -290,7 +290,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (toolState.selected === 'brush') { const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); if (drawingBuffer?.type === 'brush_line') { - selectedEntityAdapter.finalizeDrawingBuffer(); + await selectedEntityAdapter.finalizeDrawingBuffer(); } else { await selectedEntityAdapter.setDrawingBuffer(null); } @@ -299,7 +299,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (toolState.selected === 'eraser') { const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); if (drawingBuffer?.type === 'eraser_line') { - selectedEntityAdapter.finalizeDrawingBuffer(); + await selectedEntityAdapter.finalizeDrawingBuffer(); } else { await selectedEntityAdapter.setDrawingBuffer(null); } @@ -308,7 +308,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (toolState.selected === 'rect') { const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); if (drawingBuffer?.type === 'rect_shape') { - selectedEntityAdapter.finalizeDrawingBuffer(); + await selectedEntityAdapter.finalizeDrawingBuffer(); } else { await selectedEntityAdapter.setDrawingBuffer(null); } @@ -354,7 +354,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { } } else { if (selectedEntityAdapter.getDrawingBuffer()) { - selectedEntityAdapter.finalizeDrawingBuffer(); + await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ id: getBrushLineId(selectedEntityAdapter.id, uuidv4()), @@ -386,7 +386,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { } } else { if (selectedEntityAdapter.getDrawingBuffer()) { - selectedEntityAdapter.finalizeDrawingBuffer(); + await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ id: getEraserLineId(selectedEntityAdapter.id, uuidv4()), @@ -437,16 +437,16 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (toolState.selected === 'brush' && drawingBuffer?.type === 'brush_line') { drawingBuffer.points.push(pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y); await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); - selectedEntityAdapter.finalizeDrawingBuffer(); + await selectedEntityAdapter.finalizeDrawingBuffer(); } else if (toolState.selected === 'eraser' && drawingBuffer?.type === 'eraser_line') { drawingBuffer.points.push(pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y); await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); - selectedEntityAdapter.finalizeDrawingBuffer(); + await selectedEntityAdapter.finalizeDrawingBuffer(); } else if (toolState.selected === 'rect' && drawingBuffer?.type === 'rect_shape') { drawingBuffer.width = pos.x - selectedEntity.position.x - drawingBuffer.x; drawingBuffer.height = pos.y - selectedEntity.position.y - drawingBuffer.y; await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); - selectedEntityAdapter.finalizeDrawingBuffer(); + await selectedEntityAdapter.finalizeDrawingBuffer(); } } @@ -496,7 +496,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { scale: newScale, }); manager.background.render(); - manager.renderBboxes(); + manager.syncStageScale(); } } manager.preview.tool.render(); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 15b816f8e78..b302a00ba71 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -66,6 +66,7 @@ const initialState: CanvasV2State = { eraser: { width: 50, }, + isTransforming: false, }, bbox: { rect: { x: 0, y: 0, width: 512, height: 512 }, @@ -194,7 +195,6 @@ export const { allEntitiesDeleted, clipToBboxChanged, canvasReset, - toolIsTransformingChanged, // bbox bboxChanged, bboxScaledSizeChanged, @@ -226,6 +226,7 @@ export const { layerBrushLineAdded, layerEraserLineAdded, layerRectShapeAdded, + layerRasterized, // IP Adapters ipaAdded, ipaRecalled, @@ -396,3 +397,6 @@ export const sessionRequested = createAction(`${canvasV2Slice.name}/sessionReque export const sessionStagingAreaImageAccepted = createAction<{ index: number }>( `${canvasV2Slice.name}/sessionStagingAreaImageAccepted` ); +export const transformationApplied = createAction( + `${canvasV2Slice.name}/transformationApplied` +); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index f0cf91e1d0e..5d538bd821b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -34,8 +34,6 @@ export const layersReducers = { id, type: 'layer', isEnabled: true, - bbox: null, - bboxNeedsUpdate: false, objects: [], opacity: 1, position: { x: 0, y: 0 }, @@ -57,8 +55,6 @@ export const layersReducers = { id, type: 'layer', isEnabled: true, - bbox: null, - bboxNeedsUpdate: true, objects: [imageObject], opacity: 1, position: { x: position.x + offsetX, y: position.y + offsetY }, @@ -100,8 +96,6 @@ export const layersReducers = { if (!layer) { return; } - layer.bbox = bbox; - layer.bboxNeedsUpdate = false; if (bbox === null) { // TODO(psyche): Clear objects when bbox is cleared - right now this doesn't work bc bbox calculation for layers // doesn't work - always returns null @@ -116,8 +110,6 @@ export const layersReducers = { } layer.isEnabled = true; layer.objects = []; - layer.bbox = null; - layer.bboxNeedsUpdate = false; state.layers.imageCache = null; layer.position = { x: 0, y: 0 }; }, @@ -183,7 +175,6 @@ export const layersReducers = { } layer.objects.push(brushLine); - layer.bboxNeedsUpdate = true; state.layers.imageCache = null; }, layerEraserLineAdded: (state, action: PayloadAction<{ id: string; eraserLine: EraserLine }>) => { @@ -194,7 +185,6 @@ export const layersReducers = { } layer.objects.push(eraserLine); - layer.bboxNeedsUpdate = true; state.layers.imageCache = null; }, layerRectShapeAdded: (state, action: PayloadAction<{ id: string; rectShape: RectShape }>) => { @@ -205,7 +195,6 @@ export const layersReducers = { } layer.objects.push(rectShape); - layer.bboxNeedsUpdate = true; state.layers.imageCache = null; }, layerScaled: (state, action: PayloadAction) => { @@ -235,7 +224,6 @@ export const layersReducers = { } layer.position.x = Math.round(position.x); layer.position.y = Math.round(position.y); - layer.bboxNeedsUpdate = true; state.layers.imageCache = null; }, layerImageAdded: { @@ -254,7 +242,6 @@ export const layersReducers = { imageObject.y = pos.y; } layer.objects.push(imageObject); - layer.bboxNeedsUpdate = true; state.layers.imageCache = null; }, prepare: (payload: ImageObjectAddedArg & { pos?: { x: number; y: number } }) => ({ @@ -265,6 +252,16 @@ export const layersReducers = { const { imageDTO } = action.payload; state.layers.imageCache = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; }, + layerRasterized: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO; position: Coordinate }>) => { + const { id, imageDTO, position } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + layer.objects = [imageDTOToImageObject(id, uuidv4(), imageDTO)]; + layer.position = position; + state.layers.imageCache = null; + }, } satisfies SliceCaseReducers; const scalePoints = (points: number[], scaleX: number, scaleY: number) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts index 3724f4942ba..c1f14d7df40 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts @@ -20,7 +20,4 @@ export const toolReducers = { toolBufferChanged: (state, action: PayloadAction) => { state.tool.selectedBuffer = action.payload; }, - toolIsTransformingChanged: (state, action: PayloadAction) => { - state.tool.isTransforming = action.payload; - }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index a6aa5936f8d..c23708b9955 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -579,8 +579,6 @@ export const zLayerEntity = z.object({ type: z.literal('layer'), isEnabled: z.boolean(), position: zCoordinate, - bbox: zRect.nullable(), - bboxNeedsUpdate: z.boolean(), opacity: zOpacity, objects: z.array(zRenderableObject), }); @@ -850,7 +848,6 @@ export type CanvasV2State = { brush: { width: number }; eraser: { width: number }; fill: RgbaColor; - isTransforming: boolean; }; settings: { imageSmoothing: boolean; diff --git a/invokeai/frontend/web/src/services/api/endpoints/images.ts b/invokeai/frontend/web/src/services/api/endpoints/images.ts index 43b6347b5fa..9770cfd3de3 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/images.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/images.ts @@ -590,11 +590,14 @@ export const uploadImage = async ( blob: Blob, fileName: string, image_category: ImageCategory, - is_intermediate: boolean + is_intermediate: boolean, + crop_visible: boolean = false ): Promise => { const { dispatch } = getStore(); const file = new File([blob], fileName, { type: 'image/png' }); - const req = dispatch(imagesApi.endpoints.uploadImage.initiate({ file, image_category, is_intermediate })); + const req = dispatch( + imagesApi.endpoints.uploadImage.initiate({ file, image_category, is_intermediate, crop_visible }) + ); req.reset(); return await req.unwrap(); }; From 9f1f8d62f0b76eb783e7f71fbac49587aadc4b0f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 30 Jul 2024 13:51:59 +1000 Subject: [PATCH 245/678] feat(ui): wip transform mode 2 --- .../components/ControlLayersToolbar.tsx | 2 +- .../controlLayers/konva/CanvasBrushLine.ts | 18 ++++- .../controlLayers/konva/CanvasEraserLine.ts | 15 +++- .../controlLayers/konva/CanvasImage.ts | 20 ++++- .../controlLayers/konva/CanvasLayer.ts | 74 ++++++++++++++----- .../controlLayers/konva/CanvasRect.ts | 18 ++++- .../controlLayers/konva/CanvasStateApi.ts | 8 +- .../features/controlLayers/konva/events.ts | 14 ++-- .../features/controlLayers/konva/naming.ts | 9 ++- 9 files changed, 136 insertions(+), 42 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index 7a3da1aacb8..7ae7d61fcbc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -24,7 +24,7 @@ export const ControlLayersToolbar = memo(() => { return; } for (const l of canvasManager.layers.values()) { - l.getBbox(); + l.calculateBbox(); } }, [canvasManager]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts index 3de306581f1..a35b8bf12b3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts @@ -1,4 +1,5 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; +import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { BrushLine } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -7,18 +8,25 @@ export class CanvasBrushLine { static GROUP_NAME = `${CanvasBrushLine.NAME_PREFIX}_group`; static LINE_NAME = `${CanvasBrushLine.NAME_PREFIX}_line`; - private state: BrushLine; + state: BrushLine; + type = 'brush_line'; id: string; konva: { group: Konva.Group; line: Konva.Line; }; - constructor(state: BrushLine) { - this.state = state; - const { id, strokeWidth, clip, color, points } = this.state; + parent: CanvasLayer; + + constructor(state: BrushLine, parent: CanvasLayer) { + const { id, strokeWidth, clip, color, points } = state; + this.id = id; + + this.parent = parent; + this.parent.log.trace(`Creating brush line ${this.id}`); + this.konva = { group: new Konva.Group({ name: CanvasBrushLine.GROUP_NAME, @@ -46,6 +54,7 @@ export class CanvasBrushLine { async update(state: BrushLine, force?: boolean): Promise { if (force || this.state !== state) { + this.parent.log.trace(`Updating brush line ${this.id}`); const { points, color, clip, strokeWidth } = state; this.konva.line.setAttrs({ // A line with only one point will not be rendered, so we duplicate the points to make it visible @@ -62,6 +71,7 @@ export class CanvasBrushLine { } destroy() { + this.parent.log.trace(`Destroying brush line ${this.id}`); this.konva.group.destroy(); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts index 32e7ccbd248..910426506b2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts @@ -1,4 +1,5 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; +import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { EraserLine } from 'features/controlLayers/store/types'; import { RGBA_RED } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -8,17 +9,25 @@ export class CanvasEraserLine { static GROUP_NAME = `${CanvasEraserLine.NAME_PREFIX}_group`; static LINE_NAME = `${CanvasEraserLine.NAME_PREFIX}_line`; - private state: EraserLine; + state: EraserLine; + type = 'eraser_line'; id: string; konva: { group: Konva.Group; line: Konva.Line; }; - constructor(state: EraserLine) { + parent: CanvasLayer; + + constructor(state: EraserLine, parent: CanvasLayer) { const { id, strokeWidth, clip, points } = state; + this.id = id; + + this.parent = parent; + this.parent.log.trace(`Creating eraser line ${this.id}`); + this.konva = { group: new Konva.Group({ name: CanvasEraserLine.GROUP_NAME, @@ -46,6 +55,7 @@ export class CanvasEraserLine { async update(state: EraserLine, force?: boolean): Promise { if (force || this.state !== state) { + this.parent.log.trace(`Updating eraser line ${this.id}`); const { points, clip, strokeWidth } = state; this.konva.line.setAttrs({ // A line with only one point will not be rendered, so we duplicate the points to make it visible @@ -61,6 +71,7 @@ export class CanvasEraserLine { } destroy() { + this.parent.log.trace(`Destroying eraser line ${this.id}`); this.konva.group.destroy(); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts index 1dc29e63721..a0f801f2b0e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -1,3 +1,4 @@ +import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import { FILTER_MAP } from 'features/controlLayers/konva/filters'; import { loadImage } from 'features/controlLayers/konva/util'; import type { ImageObject } from 'features/controlLayers/store/types'; @@ -14,7 +15,9 @@ export class CanvasImage { static PLACEHOLDER_RECT_NAME = `${CanvasImage.NAME_PREFIX}_placeholder-rect`; static PLACEHOLDER_TEXT_NAME = `${CanvasImage.NAME_PREFIX}_placeholder-text`; - private state: ImageObject; + state: ImageObject; + + type = 'image'; id: string; konva: { @@ -26,8 +29,15 @@ export class CanvasImage { isLoading: boolean; isError: boolean; - constructor(state: ImageObject) { + parent: CanvasLayer; + + constructor(state: ImageObject, parent: CanvasLayer) { const { id, width, height, x, y } = state; + this.id = id; + + this.parent = parent; + this.parent.log.trace(`Creating image ${this.id}`); + this.konva = { group: new Konva.Group({ name: CanvasImage.GROUP_NAME, listening: false, x, y }), placeholder: { @@ -58,7 +68,6 @@ export class CanvasImage { this.konva.placeholder.group.add(this.konva.placeholder.text); this.konva.group.add(this.konva.placeholder.group); - this.id = id; this.imageName = null; this.image = null; this.isLoading = false; @@ -68,6 +77,8 @@ export class CanvasImage { async updateImageSource(imageName: string) { try { + this.parent.log.trace(`Updating image source ${this.id}`); + this.isLoading = true; this.konva.group.visible(true); @@ -119,6 +130,8 @@ export class CanvasImage { async update(state: ImageObject, force?: boolean): Promise { if (this.state !== state || force) { + this.parent.log.trace(`Updating image ${this.id}`); + const { width, height, x, y, image, filters } = state; if (this.state.image.name !== image.name || force) { await this.updateImageSource(image.name); @@ -141,6 +154,7 @@ export class CanvasImage { } destroy() { + this.parent.log.trace(`Destroying image ${this.id}`); this.konva.group.destroy(); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index cea1624f27c..36f5c1d960c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -4,6 +4,7 @@ import { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine' import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; import { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; +import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; import { konvaNodeToBlob, mapId, previewBlob } from 'features/controlLayers/konva/util'; import { layerRasterized } from 'features/controlLayers/store/canvasV2Slice'; import type { @@ -19,6 +20,7 @@ import { debounce, get } from 'lodash-es'; import type { Logger } from 'roarr'; import { uploadImage } from 'services/api/endpoints/images'; import { assert } from 'tsafe'; +import { v4 as uuidv4 } from 'uuid'; export class CanvasLayer { static NAME_PREFIX = 'layer'; @@ -82,7 +84,7 @@ export class CanvasLayer { name: CanvasLayer.INTERACTION_RECT_NAME, listening: false, draggable: true, - fill: 'rgba(255,0,0,0.5)', + // fill: 'rgba(255,0,0,0.5)', }), }; @@ -150,6 +152,7 @@ export class CanvasLayer { scaleY: this.konva.interactionRect.scaleY(), rotation: this.konva.interactionRect.rotation(), }); + console.log('objectGroup', { x: this.konva.objectGroup.x(), y: this.konva.objectGroup.y(), @@ -241,7 +244,6 @@ export class CanvasLayer { getDrawingBuffer() { return this.drawingBuffer; } - async setDrawingBuffer(obj: BrushLine | EraserLine | RectShape | null) { if (obj) { this.drawingBuffer = obj; @@ -258,11 +260,17 @@ export class CanvasLayer { const drawingBuffer = this.drawingBuffer; this.setDrawingBuffer(null); + // We need to give the objects a fresh ID else they will be considered the same object when they are re-rendered as + // a non-buffer object, and we won't trigger things like bbox calculation + if (drawingBuffer.type === 'brush_line') { + drawingBuffer.id = getBrushLineId(this.id, uuidv4()); this.manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: drawingBuffer }, 'layer'); } else if (drawingBuffer.type === 'eraser_line') { + drawingBuffer.id = getEraserLineId(this.id, uuidv4()); this.manager.stateApi.onEraserLineAdded({ id: this.id, eraserLine: drawingBuffer }, 'layer'); } else if (drawingBuffer.type === 'rect_shape') { + drawingBuffer.id = getRectShapeId(this.id, uuidv4()); this.manager.stateApi.onRectShapeAdded({ id: this.id, rectShape: drawingBuffer }, 'layer'); } } @@ -328,26 +336,33 @@ export class CanvasLayer { const objects = get(arg, 'objects', this.state.objects); const objectIds = objects.map(mapId); + + let didUpdate = false; + // Destroy any objects that are no longer in state for (const object of this.objects.values()) { if (!objectIds.includes(object.id) && object.id !== this.drawingBuffer?.id) { this.objects.delete(object.id); object.destroy(); - this.bboxNeedsUpdate = true; + didUpdate = true; } } for (const obj of objects) { if (await this._renderObject(obj)) { - this.bboxNeedsUpdate = true; + didUpdate = true; } } if (this.drawingBuffer) { if (await this._renderObject(this.drawingBuffer)) { - this.bboxNeedsUpdate = true; + didUpdate = true; } } + + if (didUpdate) { + this.calculateBbox(); + } } async updateOpacity(arg?: { opacity: number }) { @@ -410,6 +425,14 @@ export class CanvasLayer { async updateBbox() { this.log.trace('Updating bbox'); + // If the bbox has no width or height, that means the layer is fully transparent. This can happen if it is only + // eraser lines, fully clipped brush lines or if it has been fully erased. In this case, we should reset the layer + // so we aren't drawing shapes that do not render anything. + if (this.width === 0 || this.height === 0) { + this.manager.stateApi.onEntityReset({ id: this.id }, 'layer'); + return; + } + const onePixel = this.manager.getScaledPixel(); const bboxPadding = this.manager.getScaledBboxPadding(); @@ -450,23 +473,19 @@ export class CanvasLayer { assert(brushLine instanceof CanvasBrushLine || brushLine === undefined); if (!brushLine) { - console.log('creating new brush line'); - brushLine = new CanvasBrushLine(obj); + brushLine = new CanvasBrushLine(obj, this); this.objects.set(brushLine.id, brushLine); this.konva.objectGroup.add(brushLine.konva.group); return true; } else { - console.log('updating brush line'); - if (await brushLine.update(obj, force)) { - return true; - } + return await brushLine.update(obj, force); } } else if (obj.type === 'eraser_line') { let eraserLine = this.objects.get(obj.id); assert(eraserLine instanceof CanvasEraserLine || eraserLine === undefined); if (!eraserLine) { - eraserLine = new CanvasEraserLine(obj); + eraserLine = new CanvasEraserLine(obj, this); this.objects.set(eraserLine.id, eraserLine); this.konva.objectGroup.add(eraserLine.konva.group); return true; @@ -480,12 +499,12 @@ export class CanvasLayer { assert(rect instanceof CanvasRect || rect === undefined); if (!rect) { - rect = new CanvasRect(obj); + rect = new CanvasRect(obj, this); this.objects.set(rect.id, rect); this.konva.objectGroup.add(rect.konva.group); return true; } else { - if (rect.update(obj, force)) { + if (await rect.update(obj, force)) { return true; } } @@ -494,7 +513,7 @@ export class CanvasLayer { assert(image instanceof CanvasImage || image === undefined); if (!image) { - image = new CanvasImage(obj); + image = new CanvasImage(obj, this); this.objects.set(image.id, image); this.konva.objectGroup.add(image.konva.group); await image.updateImageSource(obj.image.name); @@ -510,6 +529,7 @@ export class CanvasLayer { } async startTransform() { + this.log.debug('Starting transform'); this.isTransforming = true; // When transforming, we want the stage to still be movable if the view tool is selected. If the transformer or @@ -538,6 +558,8 @@ export class CanvasLayer { } async applyTransform() { + this.log.debug('Applying transform'); + this.isTransforming = false; const objectGroupClone = this.konva.objectGroup.clone(); const rect = { @@ -556,6 +578,8 @@ export class CanvasLayer { } async cancelTransform() { + this.log.debug('Canceling transform'); + this.isTransforming = false; this.resetScale(); await this.updatePosition({ position: this.state.position }); @@ -566,7 +590,9 @@ export class CanvasLayer { }); } - getBbox = debounce(() => { + calculateBbox = debounce(() => { + this.log.debug('Calculating bbox'); + if (this.objects.size === 0) { this.offsetX = 0; this.offsetY = 0; @@ -581,9 +607,21 @@ export class CanvasLayer { console.log('getBbox rect', rect); - // If there are no eraser strokes, we can use the client rect directly + /** + * In some cases, we can use konva's getClientRect as the bbox, but there are some cases where we need to calculate + * the bbox using pixel data: + * + * - Eraser lines are normal lines, except they composite as transparency. Konva's getClientRect includes them when + * calculating the bbox. + * - Clipped portions of lines will be included in the client rect. + * + * TODO(psyche): Using pixel data is slow. Is it possible to be clever and somehow subtract the eraser lines and + * clipped areas from the client rect? + */ for (const obj of this.objects.values()) { - if (obj instanceof CanvasEraserLine) { + const isEraserLine = obj instanceof CanvasEraserLine; + const hasClip = obj instanceof CanvasBrushLine && obj.state.clip; + if (isEraserLine || hasClip) { needsPixelBbox = true; break; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts index 2e5bbb0f799..042fb1e6444 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts @@ -1,4 +1,5 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; +import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { RectShape } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -7,7 +8,9 @@ export class CanvasRect { static GROUP_NAME = `${CanvasRect.NAME_PREFIX}_group`; static RECT_NAME = `${CanvasRect.NAME_PREFIX}_rect`; - private state: RectShape; + state: RectShape; + + type = 'rect'; id: string; konva: { @@ -15,9 +18,16 @@ export class CanvasRect { rect: Konva.Rect; }; - constructor(state: RectShape) { + parent: CanvasLayer; + + constructor(state: RectShape, parent: CanvasLayer) { const { id, x, y, width, height, color } = state; + this.id = id; + + this.parent = parent; + this.parent.log.trace(`Creating rect ${this.id}`); + this.konva = { group: new Konva.Group({ name: CanvasRect.GROUP_NAME, listening: false }), rect: new Konva.Rect({ @@ -35,8 +45,9 @@ export class CanvasRect { this.state = state; } - update(state: RectShape, force?: boolean): boolean { + async update(state: RectShape, force?: boolean): Promise { if (this.state !== state || force) { + this.parent.log.trace(`Updating rect ${this.id}`); const { x, y, width, height, color } = state; this.konva.rect.setAttrs({ x, @@ -53,6 +64,7 @@ export class CanvasRect { } destroy() { + this.parent.log.trace(`Destroying rect ${this.id}`); this.konva.group.destroy(); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index f16fd45ec5b..e3d734a8a6e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -31,6 +31,7 @@ import { layerEraserLineAdded, layerImageCacheChanged, layerRectShapeAdded, + layerReset, layerScaled, layerTranslated, rgBboxChanged, @@ -70,7 +71,12 @@ export class CanvasStateApi { getState = () => { return this.store.getState().canvasV2; }; - + onEntityReset = (arg: { id: string }, entityType: CanvasEntity['type']) => { + log.debug('onEntityReset'); + if (entityType === 'layer') { + this.store.dispatch(layerReset(arg)); + } + }; onPosChanged = (arg: PositionChangedArg, entityType: CanvasEntity['type']) => { log.debug('onPosChanged'); if (entityType === 'layer') { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 6b54330f6a6..0b79e7167a5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -188,7 +188,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getBrushLineId(selectedEntityAdapter.id, uuidv4()), + id: getBrushLineId(selectedEntityAdapter.id, uuidv4(), true), type: 'brush_line', points: [ // The last point of the last line is already normalized to the entity's coordinates @@ -206,7 +206,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getBrushLineId(selectedEntityAdapter.id, uuidv4()), + id: getBrushLineId(selectedEntityAdapter.id, uuidv4(), true), type: 'brush_line', points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y], strokeWidth: toolState.brush.width, @@ -225,7 +225,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getBrushLineId(selectedEntityAdapter.id, uuidv4()), + id: getEraserLineId(selectedEntityAdapter.id, uuidv4(), true), type: 'eraser_line', points: [ // The last point of the last line is already normalized to the entity's coordinates @@ -242,7 +242,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getEraserLineId(selectedEntityAdapter.id, uuidv4()), + id: getEraserLineId(selectedEntityAdapter.id, uuidv4(), true), type: 'eraser_line', points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y], strokeWidth: toolState.eraser.width, @@ -257,7 +257,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getRectShapeId(selectedEntityAdapter.id, uuidv4()), + id: getRectShapeId(selectedEntityAdapter.id, uuidv4(), true), type: 'rect_shape', x: pos.x - selectedEntity.position.x, y: pos.y - selectedEntity.position.y, @@ -357,7 +357,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getBrushLineId(selectedEntityAdapter.id, uuidv4()), + id: getBrushLineId(selectedEntityAdapter.id, uuidv4(), true), type: 'brush_line', points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y], strokeWidth: toolState.brush.width, @@ -389,7 +389,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getEraserLineId(selectedEntityAdapter.id, uuidv4()), + id: getEraserLineId(selectedEntityAdapter.id, uuidv4(), true), type: 'eraser_line', points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y], strokeWidth: toolState.eraser.width, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts index c9888d27dfd..a5d3cdde2e9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts @@ -5,9 +5,12 @@ // Getters for non-singleton layer and object IDs export const getRGId = (entityId: string) => `region_${entityId}`; export const getLayerId = (entityId: string) => `layer_${entityId}`; -export const getBrushLineId = (entityId: string, lineId: string) => `${entityId}.brush_line_${lineId}`; -export const getEraserLineId = (entityId: string, lineId: string) => `${entityId}.eraser_line_${lineId}`; -export const getRectShapeId = (entityId: string, rectId: string) => `${entityId}.rect_${rectId}`; +export const getBrushLineId = (entityId: string, lineId: string, isBuffer?: boolean) => + `${entityId}.${isBuffer ? 'buffer_' : ''}brush_line_${lineId}`; +export const getEraserLineId = (entityId: string, lineId: string, isBuffer?: boolean) => + `${entityId}.${isBuffer ? 'buffer_' : ''}eraser_line_${lineId}`; +export const getRectShapeId = (entityId: string, rectId: string, isBuffer?: boolean) => + `${entityId}.${isBuffer ? 'buffer_' : ''}rect_${rectId}`; export const getImageObjectId = (entityId: string, imageId: string) => `${entityId}.image_${imageId}`; export const getObjectGroupId = (entityId: string, groupId: string) => `${entityId}.objectGroup_${groupId}`; export const getLayerBboxId = (entityId: string) => `${entityId}.bbox`; From 20125dc04b28ff22c13e41f98432db25023c3700 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 30 Jul 2024 14:11:47 +1000 Subject: [PATCH 246/678] fix(ui): transformer padding --- .../web/src/features/controlLayers/konva/CanvasLayer.ts | 2 +- .../web/src/features/controlLayers/konva/CanvasManager.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 36f5c1d960c..e52cc166b1f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -76,7 +76,7 @@ export class CanvasLayer { rotateEnabled: true, flipEnabled: true, listening: false, - padding: this.manager.getScaledBboxPadding(), + padding: this.manager.getTransformerPadding(), stroke: 'hsl(200deg 76% 59%)', // invokeBlue.400 keepRatio: false, }), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index fa1caf11b4c..607a7e08a76 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -491,6 +491,10 @@ export class CanvasManager { return CanvasManager.BBOX_PADDING_PX / this.getStageScale(); } + getTransformerPadding(): number { + return CanvasManager.BBOX_PADDING_PX; + } + getSelectedEntityAdapter = (): CanvasLayer | CanvasRegion | CanvasControlAdapter | CanvasInpaintMask | null => { const state = this.stateApi.getState(); const identifier = state.selectedEntityIdentifier; From 073f63251af5413f1d6e4b44e65d2b9d3674c2f2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 30 Jul 2024 16:35:27 +1000 Subject: [PATCH 247/678] feat(ui): add debug button --- .../controlLayers/components/ControlLayersToolbar.tsx | 7 +++++++ .../web/src/features/controlLayers/konva/CanvasManager.ts | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index 7ae7d61fcbc..3818167eeca 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -27,6 +27,12 @@ export const ControlLayersToolbar = memo(() => { l.calculateBbox(); } }, [canvasManager]); + const debug = useCallback(() => { + if (!canvasManager) { + return; + } + canvasManager.logDebugInfo(); + }, [canvasManager]); return ( @@ -40,6 +46,7 @@ export const ControlLayersToolbar = memo(() => { {tool === 'eraser' && } + diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 607a7e08a76..cb5954c6194 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -547,4 +547,11 @@ export class CanvasManager { return getInitialImage({ ...arg, manager: this }); } } + + logDebugInfo() { + console.log(this); + for (const layer of this.layers.values()) { + console.log(layer); + } + } } From 79fee16629b4adc2535555a50d4633f50c253d7a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 30 Jul 2024 17:09:33 +1000 Subject: [PATCH 248/678] feat(ui): hallelujah (???) --- .../controlLayers/konva/CanvasLayer.ts | 142 ++++++++---------- 1 file changed, 65 insertions(+), 77 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index e52cc166b1f..8fa63c6a0b0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -1,4 +1,5 @@ import { getStore } from 'app/store/nanostores/store'; +import { deepClone } from 'common/util/deepClone'; import { CanvasBrushLine } from 'features/controlLayers/konva/CanvasBrushLine'; import { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine'; import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; @@ -13,6 +14,7 @@ import type { Coordinate, EraserLine, LayerEntity, + Rect, RectShape, } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -46,15 +48,14 @@ export class CanvasLayer { }; objects: Map; - offsetX: number; - offsetY: number; - width: number; - height: number; log: Logger; bboxNeedsUpdate: boolean; isTransforming: boolean; isFirstRender: boolean; + rect: Rect; + bbox: Rect; + constructor(state: LayerEntity, manager: CanvasManager) { this.id = state.id; this.manager = manager; @@ -146,8 +147,8 @@ export class CanvasLayer { }); this.konva.objectGroup.setAttrs({ - x: this.konva.interactionRect.x() - this.offsetX * this.konva.interactionRect.scaleX(), - y: this.konva.interactionRect.y() - this.offsetY * this.konva.interactionRect.scaleY(), + x: this.konva.interactionRect.x(), + y: this.konva.interactionRect.y(), scaleX: this.konva.interactionRect.scaleX(), scaleY: this.konva.interactionRect.scaleY(), rotation: this.konva.interactionRect.rotation(), @@ -158,8 +159,8 @@ export class CanvasLayer { y: this.konva.objectGroup.y(), scaleX: this.konva.objectGroup.scaleX(), scaleY: this.konva.objectGroup.scaleY(), - offsetX: this.offsetX, - offsetY: this.offsetY, + offsetX: this.konva.objectGroup.offsetX(), + offsetY: this.konva.objectGroup.offsetY(), width: this.konva.objectGroup.width(), height: this.konva.objectGroup.height(), rotation: this.konva.objectGroup.rotation(), @@ -196,8 +197,8 @@ export class CanvasLayer { // The object group is translated by the difference between the interaction rect's new and old positions (which is // stored as this.bbox) this.konva.objectGroup.setAttrs({ - x: this.konva.interactionRect.x() - this.offsetX * this.konva.interactionRect.scaleX(), - y: this.konva.interactionRect.y() - this.offsetY * this.konva.interactionRect.scaleY(), + x: this.konva.interactionRect.x(), + y: this.konva.interactionRect.y(), }); }); this.konva.interactionRect.on('dragend', () => { @@ -213,8 +214,8 @@ export class CanvasLayer { { id: this.id, position: { - x: this.konva.interactionRect.x() - this.offsetX * this.konva.interactionRect.scaleX(), - y: this.konva.interactionRect.y() - this.offsetY * this.konva.interactionRect.scaleY(), + x: this.konva.interactionRect.x() - this.bbox.x, + y: this.konva.interactionRect.y() - this.bbox.y, }, }, 'layer' @@ -224,16 +225,12 @@ export class CanvasLayer { this.objects = new Map(); this.drawingBuffer = null; this.state = state; - this.offsetX = 0; - this.offsetY = 0; - this.width = 0; - this.height = 0; + this.rect = this.getDefaultRect(); + this.bbox = this.getDefaultRect(); this.bboxNeedsUpdate = true; this.isTransforming = false; this.isFirstRender = true; this.log = this.manager.getLogger(`layer_${this.id}`); - - console.log(this); } destroy(): void { @@ -317,16 +314,18 @@ export class CanvasLayer { const bboxPadding = this.manager.getScaledBboxPadding(); this.konva.objectGroup.setAttrs({ - x: position.x, - y: position.y, + x: position.x + this.bbox.x, + y: position.y + this.bbox.y, + offsetX: this.bbox.x, + offsetY: this.bbox.y, }); this.konva.bbox.setAttrs({ - x: position.x + this.offsetX * this.konva.interactionRect.scaleX() - bboxPadding, - y: position.y + this.offsetY * this.konva.interactionRect.scaleY() - bboxPadding, + x: position.x + this.bbox.x - bboxPadding, + y: position.y + this.bbox.y - bboxPadding, }); this.konva.interactionRect.setAttrs({ - x: position.x + this.offsetX * this.konva.interactionRect.scaleX(), - y: position.y + this.offsetY * this.konva.interactionRect.scaleY(), + x: position.x + this.bbox.x * this.konva.interactionRect.scaleX(), + y: position.y + this.bbox.y * this.konva.interactionRect.scaleY(), }); } @@ -428,7 +427,7 @@ export class CanvasLayer { // If the bbox has no width or height, that means the layer is fully transparent. This can happen if it is only // eraser lines, fully clipped brush lines or if it has been fully erased. In this case, we should reset the layer // so we aren't drawing shapes that do not render anything. - if (this.width === 0 || this.height === 0) { + if (this.bbox.width === 0 || this.bbox.height === 0) { this.manager.stateApi.onEntityReset({ id: this.id }, 'layer'); return; } @@ -437,17 +436,23 @@ export class CanvasLayer { const bboxPadding = this.manager.getScaledBboxPadding(); this.konva.bbox.setAttrs({ - x: this.state.position.x + this.offsetX * this.konva.interactionRect.scaleX() - bboxPadding, - y: this.state.position.y + this.offsetY * this.konva.interactionRect.scaleY() - bboxPadding, - width: this.width + bboxPadding * 2, - height: this.height + bboxPadding * 2, + x: this.state.position.x + this.bbox.x - bboxPadding, + y: this.state.position.y + this.bbox.y - bboxPadding, + width: this.bbox.width + bboxPadding * 2, + height: this.bbox.height + bboxPadding * 2, strokeWidth: onePixel, }); this.konva.interactionRect.setAttrs({ - x: this.state.position.x + this.offsetX * this.konva.interactionRect.scaleX(), - y: this.state.position.y + this.offsetY * this.konva.interactionRect.scaleY(), - width: this.width, - height: this.height, + x: this.state.position.x + this.bbox.x, + y: this.state.position.y + this.bbox.y, + width: this.bbox.width, + height: this.bbox.height, + }); + this.konva.objectGroup.setAttrs({ + x: this.state.position.x + this.bbox.x, + y: this.state.position.y + this.bbox.y, + offsetX: this.bbox.x, + offsetY: this.bbox.y, }); } @@ -550,11 +555,14 @@ export class CanvasLayer { async resetScale() { this.konva.objectGroup.scaleX(1); - this.konva.objectGroup.scaleY(1); + this.konva.objectGroup.scaleX(1); + this.konva.objectGroup.rotation(0); this.konva.bbox.scaleX(1); - this.konva.bbox.scaleY(1); + this.konva.bbox.scaleX(1); + this.konva.bbox.rotation(0); + this.konva.interactionRect.scaleX(1); this.konva.interactionRect.scaleX(1); - this.konva.interactionRect.scaleY(1); + this.konva.interactionRect.rotation(0); } async applyTransform() { @@ -562,13 +570,7 @@ export class CanvasLayer { this.isTransforming = false; const objectGroupClone = this.konva.objectGroup.clone(); - const rect = { - x: this.konva.interactionRect.x(), - y: this.konva.interactionRect.y(), - width: this.konva.interactionRect.width() * this.konva.interactionRect.scaleX(), - height: this.konva.interactionRect.height() * this.konva.interactionRect.scaleY(), - }; - const blob = await konvaNodeToBlob(objectGroupClone, rect); + const blob = await konvaNodeToBlob(objectGroupClone, objectGroupClone.getClientRect()); previewBlob(blob, 'transformed layer'); const imageDTO = await uploadImage(blob, `${this.id}_transform.png`, 'other', true, true); const { dispatch } = getStore(); @@ -590,14 +592,16 @@ export class CanvasLayer { }); } + getDefaultRect(): Rect { + return { x: 0, y: 0, width: 0, height: 0 }; + } + calculateBbox = debounce(() => { this.log.debug('Calculating bbox'); if (this.objects.size === 0) { - this.offsetX = 0; - this.offsetY = 0; - this.width = 0; - this.height = 0; + this.rect = this.getDefaultRect(); + this.bbox = this.getDefaultRect(); this.updateBbox(); return; } @@ -605,8 +609,6 @@ export class CanvasLayer { let needsPixelBbox = false; const rect = this.konva.objectGroup.getClientRect({ skipTransform: true }); - console.log('getBbox rect', rect); - /** * In some cases, we can use konva's getClientRect as the bbox, but there are some cases where we need to calculate * the bbox using pixel data: @@ -628,11 +630,9 @@ export class CanvasLayer { } if (!needsPixelBbox) { - this.offsetX = rect.x; - this.offsetY = rect.y; - this.width = rect.width; - this.height = rect.height; - this.logBbox('new bbox from client rect'); + this.rect = deepClone(rect); + this.bbox = deepClone(rect); + this.log.trace({ bbox: this.bbox, rect: this.rect }, 'Got bbox from client rect'); this.updateBbox(); return; } @@ -649,20 +649,19 @@ export class CanvasLayer { this.manager.requestBbox( { buffer: imageData.data.buffer, width: imageData.width, height: imageData.height }, (extents) => { - console.log('extents', extents); + this.rect = deepClone(rect); if (extents) { const { minX, minY, maxX, maxY } = extents; - this.offsetX = minX + rect.x; - this.offsetY = minY + rect.y; - this.width = maxX - minX; - this.height = maxY - minY; + this.bbox = { + x: rect.x + minX, + y: rect.y + minY, + width: maxX - minX, + height: maxY - minY, + }; } else { - this.offsetX = 0; - this.offsetY = 0; - this.width = 0; - this.height = 0; + this.bbox = deepClone(rect); } - this.logBbox('new bbox from worker'); + this.log.trace({ bbox: this.bbox, rect: this.rect, extents }, `Got bbox from worker`); this.updateBbox(); clone.destroy(); } @@ -673,19 +672,8 @@ export class CanvasLayer { console.log(msg, { x: this.state.position.x, y: this.state.position.y, - offsetX: this.offsetX, - offsetY: this.offsetY, - width: this.width, - height: this.height, + rect: deepClone(this.rect), + bbox: deepClone(this.bbox), }); } - - getLayerRect() { - return { - x: this.state.position.x + this.offsetX, - y: this.state.position.y + this.offsetY, - width: this.width, - height: this.height, - }; - } } From f6f6462590655ce475b1b0c6dbe14f64059c191e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 30 Jul 2024 17:46:05 +1000 Subject: [PATCH 249/678] fix(ui): transforming when axes flipped --- .../controlLayers/konva/CanvasLayer.ts | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 8fa63c6a0b0..19adc309cb7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -554,15 +554,14 @@ export class CanvasLayer { } async resetScale() { - this.konva.objectGroup.scaleX(1); - this.konva.objectGroup.scaleX(1); - this.konva.objectGroup.rotation(0); - this.konva.bbox.scaleX(1); - this.konva.bbox.scaleX(1); - this.konva.bbox.rotation(0); - this.konva.interactionRect.scaleX(1); - this.konva.interactionRect.scaleX(1); - this.konva.interactionRect.rotation(0); + const attrs = { + scaleX: 1, + scaleY: 1, + rotation: 0, + }; + this.konva.objectGroup.setAttrs(attrs); + this.konva.bbox.setAttrs(attrs); + this.konva.interactionRect.setAttrs(attrs); } async applyTransform() { @@ -570,11 +569,14 @@ export class CanvasLayer { this.isTransforming = false; const objectGroupClone = this.konva.objectGroup.clone(); - const blob = await konvaNodeToBlob(objectGroupClone, objectGroupClone.getClientRect()); + const interactionRectClone = this.konva.interactionRect.clone(); + const rect = interactionRectClone.getClientRect(); + const blob = await konvaNodeToBlob(objectGroupClone, rect); + console.log('transform rect', rect); previewBlob(blob, 'transformed layer'); const imageDTO = await uploadImage(blob, `${this.id}_transform.png`, 'other', true, true); const { dispatch } = getStore(); - dispatch(layerRasterized({ id: this.id, imageDTO, position: this.konva.interactionRect.position() })); + dispatch(layerRasterized({ id: this.id, imageDTO, position: { x: rect.x, y: rect.y } })); this.isTransforming = false; this.resetScale(); } From 2e13e75fc6703c114be5382466e2d8f907a57b94 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 30 Jul 2024 19:30:47 +1000 Subject: [PATCH 250/678] fix(ui): use pixel bbox when image is in layer --- .../web/src/features/controlLayers/konva/CanvasLayer.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 19adc309cb7..3a2bdfeae04 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -618,14 +618,16 @@ export class CanvasLayer { * - Eraser lines are normal lines, except they composite as transparency. Konva's getClientRect includes them when * calculating the bbox. * - Clipped portions of lines will be included in the client rect. + * - Images have transparency, so they will be included in the client rect. * * TODO(psyche): Using pixel data is slow. Is it possible to be clever and somehow subtract the eraser lines and * clipped areas from the client rect? */ for (const obj of this.objects.values()) { const isEraserLine = obj instanceof CanvasEraserLine; + const isImage = obj instanceof CanvasImage; const hasClip = obj instanceof CanvasBrushLine && obj.state.clip; - if (isEraserLine || hasClip) { + if (isEraserLine || hasClip || isImage) { needsPixelBbox = true; break; } From 4d52824895a4f10184ff559e8f635a3303c726cc Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 30 Jul 2024 19:31:13 +1000 Subject: [PATCH 251/678] feat(ui): fix transform when rotated --- .../controlLayers/konva/CanvasLayer.ts | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 3a2bdfeae04..dd5b6903958 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -24,6 +24,24 @@ import { uploadImage } from 'services/api/endpoints/images'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; +const getCenter = (rect: Rect): Coordinate => { + return { + x: rect.x + rect.width / 2, + y: rect.y + rect.height / 2, + }; +}; + +window.getCenter = getCenter; + +function rotatePoint(point: Coordinate, origin: Coordinate, deg: number): Coordinate { + const angle = deg * (Math.PI / 180); // Convert to radians + const rotatedX = Math.cos(angle) * (point.x - origin.x) - Math.sin(angle) * (point.y - origin.y) + origin.x; + const rotatedY = Math.sin(angle) * (point.x - origin.x) + Math.cos(angle) * (point.y - origin.y) + origin.y; + + return { x: rotatedX, y: rotatedY }; +} + +window.rotatePoint = rotatePoint; export class CanvasLayer { static NAME_PREFIX = 'layer'; static LAYER_NAME = `${CanvasLayer.NAME_PREFIX}_layer`; @@ -572,9 +590,8 @@ export class CanvasLayer { const interactionRectClone = this.konva.interactionRect.clone(); const rect = interactionRectClone.getClientRect(); const blob = await konvaNodeToBlob(objectGroupClone, rect); - console.log('transform rect', rect); previewBlob(blob, 'transformed layer'); - const imageDTO = await uploadImage(blob, `${this.id}_transform.png`, 'other', true, true); + const imageDTO = await uploadImage(blob, `${this.id}_transform.png`, 'other', true); const { dispatch } = getStore(); dispatch(layerRasterized({ id: this.id, imageDTO, position: { x: rect.x, y: rect.y } })); this.isTransforming = false; From 690fbdc73d6140aa6339dd4c8959256ab053f621 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 30 Jul 2024 20:03:15 +1000 Subject: [PATCH 252/678] feat(ui): transform cleanup --- .../components/ControlLayersToolbar.tsx | 24 +- .../controlLayers/konva/CanvasBrushLine.ts | 6 +- .../controlLayers/konva/CanvasEraserLine.ts | 6 +- .../controlLayers/konva/CanvasImage.ts | 10 +- .../controlLayers/konva/CanvasLayer.ts | 241 ++++++++---------- .../controlLayers/konva/CanvasManager.ts | 134 +++++----- .../controlLayers/konva/CanvasRect.ts | 6 +- 7 files changed, 212 insertions(+), 215 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index 3818167eeca..05a7ae638f4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -1,6 +1,6 @@ /* eslint-disable i18next/no-literal-string */ import { Button } from '@chakra-ui/react'; -import { Flex } from '@invoke-ai/ui-library'; +import { Flex, Switch } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; import { BrushWidth } from 'features/controlLayers/components/BrushWidth'; @@ -14,6 +14,7 @@ import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoB import { $canvasManager } from 'features/controlLayers/konva/CanvasManager'; import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton'; import { ViewerToggleMenu } from 'features/gallery/components/ImageViewer/ViewerToggleMenu'; +import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; export const ControlLayersToolbar = memo(() => { @@ -27,12 +28,19 @@ export const ControlLayersToolbar = memo(() => { l.calculateBbox(); } }, [canvasManager]); - const debug = useCallback(() => { - if (!canvasManager) { - return; - } - canvasManager.logDebugInfo(); - }, [canvasManager]); + const onChangeDebugging = useCallback( + (e: ChangeEvent) => { + if (!canvasManager) { + return; + } + if (e.target.checked) { + canvasManager.enableDebugging(); + } else { + canvasManager.disableDebugging(); + } + }, + [canvasManager] + ); return ( @@ -46,7 +54,7 @@ export const ControlLayersToolbar = memo(() => { {tool === 'eraser' && } - + debug diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts index a35b8bf12b3..c052a55775b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts @@ -25,7 +25,7 @@ export class CanvasBrushLine { this.id = id; this.parent = parent; - this.parent.log.trace(`Creating brush line ${this.id}`); + this.parent._log.trace(`Creating brush line ${this.id}`); this.konva = { group: new Konva.Group({ @@ -54,7 +54,7 @@ export class CanvasBrushLine { async update(state: BrushLine, force?: boolean): Promise { if (force || this.state !== state) { - this.parent.log.trace(`Updating brush line ${this.id}`); + this.parent._log.trace(`Updating brush line ${this.id}`); const { points, color, clip, strokeWidth } = state; this.konva.line.setAttrs({ // A line with only one point will not be rendered, so we duplicate the points to make it visible @@ -71,7 +71,7 @@ export class CanvasBrushLine { } destroy() { - this.parent.log.trace(`Destroying brush line ${this.id}`); + this.parent._log.trace(`Destroying brush line ${this.id}`); this.konva.group.destroy(); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts index 910426506b2..a1d8d193117 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts @@ -26,7 +26,7 @@ export class CanvasEraserLine { this.id = id; this.parent = parent; - this.parent.log.trace(`Creating eraser line ${this.id}`); + this.parent._log.trace(`Creating eraser line ${this.id}`); this.konva = { group: new Konva.Group({ @@ -55,7 +55,7 @@ export class CanvasEraserLine { async update(state: EraserLine, force?: boolean): Promise { if (force || this.state !== state) { - this.parent.log.trace(`Updating eraser line ${this.id}`); + this.parent._log.trace(`Updating eraser line ${this.id}`); const { points, clip, strokeWidth } = state; this.konva.line.setAttrs({ // A line with only one point will not be rendered, so we duplicate the points to make it visible @@ -71,7 +71,7 @@ export class CanvasEraserLine { } destroy() { - this.parent.log.trace(`Destroying eraser line ${this.id}`); + this.parent._log.trace(`Destroying eraser line ${this.id}`); this.konva.group.destroy(); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts index a0f801f2b0e..f88ecfd93bf 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -36,7 +36,7 @@ export class CanvasImage { this.id = id; this.parent = parent; - this.parent.log.trace(`Creating image ${this.id}`); + this.parent._log.trace(`Creating image ${this.id}`); this.konva = { group: new Konva.Group({ name: CanvasImage.GROUP_NAME, listening: false, x, y }), @@ -77,13 +77,13 @@ export class CanvasImage { async updateImageSource(imageName: string) { try { - this.parent.log.trace(`Updating image source ${this.id}`); + this.parent._log.trace(`Updating image source ${this.id}`); this.isLoading = true; this.konva.group.visible(true); if (!this.image) { - this.konva.placeholder.group.visible(true); + this.konva.placeholder.group.visible(false); this.konva.placeholder.text.text(t('common.loadingImage', 'Loading Image')); } @@ -130,7 +130,7 @@ export class CanvasImage { async update(state: ImageObject, force?: boolean): Promise { if (this.state !== state || force) { - this.parent.log.trace(`Updating image ${this.id}`); + this.parent._log.trace(`Updating image ${this.id}`); const { width, height, x, y, image, filters } = state; if (this.state.image.name !== image.name || force) { @@ -154,7 +154,7 @@ export class CanvasImage { } destroy() { - this.parent.log.trace(`Destroying image ${this.id}`); + this.parent._log.trace(`Destroying image ${this.id}`); this.konva.group.destroy(); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index dd5b6903958..670fa7a1b8b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -24,24 +24,6 @@ import { uploadImage } from 'services/api/endpoints/images'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; -const getCenter = (rect: Rect): Coordinate => { - return { - x: rect.x + rect.width / 2, - y: rect.y + rect.height / 2, - }; -}; - -window.getCenter = getCenter; - -function rotatePoint(point: Coordinate, origin: Coordinate, deg: number): Coordinate { - const angle = deg * (Math.PI / 180); // Convert to radians - const rotatedX = Math.cos(angle) * (point.x - origin.x) - Math.sin(angle) * (point.y - origin.y) + origin.x; - const rotatedY = Math.sin(angle) * (point.x - origin.x) + Math.cos(angle) * (point.y - origin.y) + origin.y; - - return { x: rotatedX, y: rotatedY }; -} - -window.rotatePoint = rotatePoint; export class CanvasLayer { static NAME_PREFIX = 'layer'; static LAYER_NAME = `${CanvasLayer.NAME_PREFIX}_layer`; @@ -51,8 +33,8 @@ export class CanvasLayer { static OBJECT_GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_object-group`; static BBOX_NAME = `${CanvasLayer.NAME_PREFIX}_bbox`; - private drawingBuffer: BrushLine | EraserLine | RectShape | null; - private state: LayerEntity; + _drawingBuffer: BrushLine | EraserLine | RectShape | null; + _state: LayerEntity; id: string; manager: CanvasManager; @@ -66,10 +48,11 @@ export class CanvasLayer { }; objects: Map; - log: Logger; - bboxNeedsUpdate: boolean; + _log: Logger; + _bboxNeedsUpdate: boolean; + _isFirstRender: boolean; + isTransforming: boolean; - isFirstRender: boolean; rect: Rect; bbox: Rect; @@ -113,17 +96,7 @@ export class CanvasLayer { this.konva.layer.add(this.konva.bbox); this.konva.transformer.on('transformstart', () => { - console.log('>>> transformstart'); - console.log('interactionRect', { - x: this.konva.interactionRect.x(), - y: this.konva.interactionRect.y(), - scaleX: this.konva.interactionRect.scaleX(), - scaleY: this.konva.interactionRect.scaleY(), - width: this.konva.interactionRect.width(), - height: this.konva.interactionRect.height(), - }); - this.logBbox('transformstart bbox'); - console.log('this.state.position', this.state.position); + this.logDebugInfo("'transformstart' fired"); }); this.konva.transformer.on('transform', () => { @@ -152,17 +125,7 @@ export class CanvasLayer { // this.konva.interactionRect.scaleY(scaleY); // this.konva.interactionRect.rotation(0); - console.log('>>> transform'); - console.log('activeAnchor', this.konva.transformer.getActiveAnchor()); - console.log('interactionRect', { - x: this.konva.interactionRect.x(), - y: this.konva.interactionRect.y(), - scaleX: this.konva.interactionRect.scaleX(), - scaleY: this.konva.interactionRect.scaleY(), - width: this.konva.interactionRect.width(), - height: this.konva.interactionRect.height(), - rotation: this.konva.interactionRect.rotation(), - }); + this.logDebugInfo("'transform' fired"); this.konva.objectGroup.setAttrs({ x: this.konva.interactionRect.x(), @@ -171,33 +134,10 @@ export class CanvasLayer { scaleY: this.konva.interactionRect.scaleY(), rotation: this.konva.interactionRect.rotation(), }); - - console.log('objectGroup', { - x: this.konva.objectGroup.x(), - y: this.konva.objectGroup.y(), - scaleX: this.konva.objectGroup.scaleX(), - scaleY: this.konva.objectGroup.scaleY(), - offsetX: this.konva.objectGroup.offsetX(), - offsetY: this.konva.objectGroup.offsetY(), - width: this.konva.objectGroup.width(), - height: this.konva.objectGroup.height(), - rotation: this.konva.objectGroup.rotation(), - }); }); this.konva.transformer.on('transformend', () => { - // this.offsetX = this.konva.interactionRect.x() - this.state.position.x; - // this.offsetY = this.konva.interactionRect.y() - this.state.position.y; - // this.width = Math.round(this.konva.interactionRect.width() * this.konva.interactionRect.scaleX()); - // this.height = Math.round(this.konva.interactionRect.height() * this.konva.interactionRect.scaleY()); - // this.manager.stateApi.onPosChanged( - // { - // id: this.id, - // position: { x: this.konva.objectGroup.x(), y: this.konva.objectGroup.y() }, - // }, - // 'layer' - // ); - this.logBbox('transformend bbox'); + this.logDebugInfo("'transformend' fired"); }); this.konva.interactionRect.on('dragmove', () => { @@ -220,7 +160,7 @@ export class CanvasLayer { }); }); this.konva.interactionRect.on('dragend', () => { - this.logBbox('dragend bbox'); + this.logDebugInfo("'dragend' fired"); if (this.isTransforming) { // When the user cancels the transformation, we need to reset the layer, so we should not update the layer's @@ -241,38 +181,38 @@ export class CanvasLayer { }); this.objects = new Map(); - this.drawingBuffer = null; - this.state = state; + this._drawingBuffer = null; + this._state = state; this.rect = this.getDefaultRect(); this.bbox = this.getDefaultRect(); - this.bboxNeedsUpdate = true; + this._bboxNeedsUpdate = true; this.isTransforming = false; - this.isFirstRender = true; - this.log = this.manager.getLogger(`layer_${this.id}`); + this._isFirstRender = true; + this._log = this.manager.getLogger(`layer_${this.id}`); } destroy(): void { - this.log.debug(`Layer ${this.id} - destroying`); + this._log.debug(`Layer ${this.id} - destroying`); this.konva.layer.destroy(); } getDrawingBuffer() { - return this.drawingBuffer; + return this._drawingBuffer; } async setDrawingBuffer(obj: BrushLine | EraserLine | RectShape | null) { if (obj) { - this.drawingBuffer = obj; - await this._renderObject(this.drawingBuffer, true); + this._drawingBuffer = obj; + await this._renderObject(this._drawingBuffer, true); } else { - this.drawingBuffer = null; + this._drawingBuffer = null; } } async finalizeDrawingBuffer() { - if (!this.drawingBuffer) { + if (!this._drawingBuffer) { return; } - const drawingBuffer = this.drawingBuffer; + const drawingBuffer = this._drawingBuffer; this.setDrawingBuffer(null); // We need to give the objects a fresh ID else they will be considered the same object when they are re-rendered as @@ -291,44 +231,50 @@ export class CanvasLayer { } async update(arg?: { state: LayerEntity; toolState: CanvasV2State['tool']; isSelected: boolean }) { - const state = get(arg, 'state', this.state); + const state = get(arg, 'state', this._state); const toolState = get(arg, 'toolState', this.manager.stateApi.getToolState()); const isSelected = get(arg, 'isSelected', this.manager.stateApi.getIsSelected(this.id)); - if (!this.isFirstRender && state === this.state) { - this.log.trace('State unchanged, skipping update'); + if (!this._isFirstRender && state === this._state) { + this._log.trace('State unchanged, skipping update'); return; } - this.log.debug('Updating'); + this._log.debug('Updating'); const { position, objects, opacity, isEnabled } = state; - if (this.isFirstRender || position !== this.state.position) { + if (this._isFirstRender || position !== this._state.position) { await this.updatePosition({ position }); } - if (this.isFirstRender || objects !== this.state.objects) { + if (this._isFirstRender || objects !== this._state.objects) { await this.updateObjects({ objects }); } - if (this.isFirstRender || opacity !== this.state.opacity) { + if (this._isFirstRender || opacity !== this._state.opacity) { await this.updateOpacity({ opacity }); } - if (this.isFirstRender || isEnabled !== this.state.isEnabled) { + if (this._isFirstRender || isEnabled !== this._state.isEnabled) { await this.updateVisibility({ isEnabled }); } await this.updateInteraction({ toolState, isSelected }); - this.state = state; + + if (this._isFirstRender) { + await this.updateBbox(); + } + + this._state = state; + this._isFirstRender = false; } async updateVisibility(arg?: { isEnabled: boolean }) { - this.log.trace('Updating visibility'); - const isEnabled = get(arg, 'isEnabled', this.state.isEnabled); - const hasObjects = this.objects.size > 0 || this.drawingBuffer !== null; + this._log.trace('Updating visibility'); + const isEnabled = get(arg, 'isEnabled', this._state.isEnabled); + const hasObjects = this.objects.size > 0 || this._drawingBuffer !== null; this.konva.layer.visible(isEnabled || hasObjects); } async updatePosition(arg?: { position: Coordinate }) { - this.log.trace('Updating position'); - const position = get(arg, 'position', this.state.position); + this._log.trace('Updating position'); + const position = get(arg, 'position', this._state.position); const bboxPadding = this.manager.getScaledBboxPadding(); this.konva.objectGroup.setAttrs({ @@ -348,9 +294,9 @@ export class CanvasLayer { } async updateObjects(arg?: { objects: LayerEntity['objects'] }) { - this.log.trace('Updating objects'); + this._log.trace('Updating objects'); - const objects = get(arg, 'objects', this.state.objects); + const objects = get(arg, 'objects', this._state.objects); const objectIds = objects.map(mapId); @@ -358,7 +304,7 @@ export class CanvasLayer { // Destroy any objects that are no longer in state for (const object of this.objects.values()) { - if (!objectIds.includes(object.id) && object.id !== this.drawingBuffer?.id) { + if (!objectIds.includes(object.id) && object.id !== this._drawingBuffer?.id) { this.objects.delete(object.id); object.destroy(); didUpdate = true; @@ -371,8 +317,8 @@ export class CanvasLayer { } } - if (this.drawingBuffer) { - if (await this._renderObject(this.drawingBuffer)) { + if (this._drawingBuffer) { + if (await this._renderObject(this._drawingBuffer)) { didUpdate = true; } } @@ -383,15 +329,15 @@ export class CanvasLayer { } async updateOpacity(arg?: { opacity: number }) { - this.log.trace('Updating opacity'); + this._log.trace('Updating opacity'); - const opacity = get(arg, 'opacity', this.state.opacity); + const opacity = get(arg, 'opacity', this._state.opacity); this.konva.objectGroup.opacity(opacity); } async updateInteraction(arg?: { toolState: CanvasV2State['tool']; isSelected: boolean }) { - this.log.trace('Updating interaction'); + this._log.trace('Updating interaction'); const toolState = get(arg, 'toolState', this.manager.stateApi.getToolState()); const isSelected = get(arg, 'isSelected', this.manager.stateApi.getIsSelected(this.id)); @@ -440,42 +386,49 @@ export class CanvasLayer { } async updateBbox() { - this.log.trace('Updating bbox'); + this._log.trace('Updating bbox'); // If the bbox has no width or height, that means the layer is fully transparent. This can happen if it is only - // eraser lines, fully clipped brush lines or if it has been fully erased. In this case, we should reset the layer - // so we aren't drawing shapes that do not render anything. + // eraser lines, fully clipped brush lines or if it has been fully erased. if (this.bbox.width === 0 || this.bbox.height === 0) { - this.manager.stateApi.onEntityReset({ id: this.id }, 'layer'); + if (this.objects.size > 0) { + // The layer is fully transparent but has objects - reset it + this.manager.stateApi.onEntityReset({ id: this.id }, 'layer'); + } + this.konva.bbox.visible(false); + this.konva.interactionRect.visible(false); return; } + this.konva.bbox.visible(true); + this.konva.interactionRect.visible(true); + const onePixel = this.manager.getScaledPixel(); const bboxPadding = this.manager.getScaledBboxPadding(); this.konva.bbox.setAttrs({ - x: this.state.position.x + this.bbox.x - bboxPadding, - y: this.state.position.y + this.bbox.y - bboxPadding, + x: this._state.position.x + this.bbox.x - bboxPadding, + y: this._state.position.y + this.bbox.y - bboxPadding, width: this.bbox.width + bboxPadding * 2, height: this.bbox.height + bboxPadding * 2, strokeWidth: onePixel, }); this.konva.interactionRect.setAttrs({ - x: this.state.position.x + this.bbox.x, - y: this.state.position.y + this.bbox.y, + x: this._state.position.x + this.bbox.x, + y: this._state.position.y + this.bbox.y, width: this.bbox.width, height: this.bbox.height, }); this.konva.objectGroup.setAttrs({ - x: this.state.position.x + this.bbox.x, - y: this.state.position.y + this.bbox.y, + x: this._state.position.x + this.bbox.x, + y: this._state.position.y + this.bbox.y, offsetX: this.bbox.x, offsetY: this.bbox.y, }); } async syncStageScale() { - this.log.trace('Syncing scale to stage'); + this._log.trace('Syncing scale to stage'); const onePixel = this.manager.getScaledPixel(); const bboxPadding = this.manager.getScaledBboxPadding(); @@ -552,7 +505,7 @@ export class CanvasLayer { } async startTransform() { - this.log.debug('Starting transform'); + this._log.debug('Starting transform'); this.isTransforming = true; // When transforming, we want the stage to still be movable if the view tool is selected. If the transformer or @@ -583,14 +536,15 @@ export class CanvasLayer { } async applyTransform() { - this.log.debug('Applying transform'); + this._log.debug('Applying transform'); - this.isTransforming = false; const objectGroupClone = this.konva.objectGroup.clone(); const interactionRectClone = this.konva.interactionRect.clone(); const rect = interactionRectClone.getClientRect(); const blob = await konvaNodeToBlob(objectGroupClone, rect); - previewBlob(blob, 'transformed layer'); + if (this.manager._isDebugging) { + previewBlob(blob, 'transformed layer'); + } const imageDTO = await uploadImage(blob, `${this.id}_transform.png`, 'other', true); const { dispatch } = getStore(); dispatch(layerRasterized({ id: this.id, imageDTO, position: { x: rect.x, y: rect.y } })); @@ -599,11 +553,11 @@ export class CanvasLayer { } async cancelTransform() { - this.log.debug('Canceling transform'); + this._log.debug('Canceling transform'); this.isTransforming = false; this.resetScale(); - await this.updatePosition({ position: this.state.position }); + await this.updatePosition({ position: this._state.position }); await this.updateBbox(); await this.updateInteraction({ toolState: this.manager.stateApi.getToolState(), @@ -616,7 +570,7 @@ export class CanvasLayer { } calculateBbox = debounce(() => { - this.log.debug('Calculating bbox'); + this._log.debug('Calculating bbox'); if (this.objects.size === 0) { this.rect = this.getDefaultRect(); @@ -625,7 +579,6 @@ export class CanvasLayer { return; } - let needsPixelBbox = false; const rect = this.konva.objectGroup.getClientRect({ skipTransform: true }); /** @@ -640,6 +593,7 @@ export class CanvasLayer { * TODO(psyche): Using pixel data is slow. Is it possible to be clever and somehow subtract the eraser lines and * clipped areas from the client rect? */ + let needsPixelBbox = false; for (const obj of this.objects.values()) { const isEraserLine = obj instanceof CanvasEraserLine; const isImage = obj instanceof CanvasImage; @@ -653,7 +607,7 @@ export class CanvasLayer { if (!needsPixelBbox) { this.rect = deepClone(rect); this.bbox = deepClone(rect); - this.log.trace({ bbox: this.bbox, rect: this.rect }, 'Got bbox from client rect'); + this._log.trace({ bbox: this.bbox, rect: this.rect }, 'Got bbox from client rect'); this.updateBbox(); return; } @@ -682,19 +636,42 @@ export class CanvasLayer { } else { this.bbox = deepClone(rect); } - this.log.trace({ bbox: this.bbox, rect: this.rect, extents }, `Got bbox from worker`); + this._log.trace({ bbox: this.bbox, rect: this.rect, extents }, `Got bbox from worker`); this.updateBbox(); clone.destroy(); } ); }, CanvasManager.BBOX_DEBOUNCE_MS); - logBbox(msg: string = 'bbox') { - console.log(msg, { - x: this.state.position.x, - y: this.state.position.y, - rect: deepClone(this.rect), - bbox: deepClone(this.bbox), - }); + logDebugInfo(msg = 'Debug info') { + const debugInfo = { + id: this.id, + state: this._state, + rect: this.rect, + bbox: this.bbox, + objects: Array.from(this.objects.values()).map((obj) => obj.id), + isTransforming: this.isTransforming, + interactionRectAttrs: { + x: this.konva.interactionRect.x(), + y: this.konva.interactionRect.y(), + scaleX: this.konva.interactionRect.scaleX(), + scaleY: this.konva.interactionRect.scaleY(), + width: this.konva.interactionRect.width(), + height: this.konva.interactionRect.height(), + rotation: this.konva.interactionRect.rotation(), + }, + objectGroupAttrs: { + x: this.konva.objectGroup.x(), + y: this.konva.objectGroup.y(), + scaleX: this.konva.objectGroup.scaleX(), + scaleY: this.konva.objectGroup.scaleY(), + width: this.konva.objectGroup.width(), + height: this.konva.objectGroup.height(), + rotation: this.konva.objectGroup.rotation(), + offsetX: this.konva.objectGroup.offsetX(), + offsetY: this.konva.objectGroup.offsetY(), + }, + }; + this._log.debug(debugInfo, msg); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index cb5954c6194..d1e0fbee6be 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -64,7 +64,7 @@ type Util = { export const $canvasManager = atom(null); export class CanvasManager { - private static BBOX_PADDING_PX = 5; + static BBOX_PADDING_PX = 5; static BBOX_DEBOUNCE_MS = 300; stage: Konva.Stage; @@ -82,13 +82,15 @@ export class CanvasManager { log: Logger; workerLog: Logger; + _isDebugging: boolean; + onTransform: ((isTransforming: boolean) => void) | null; - private store: Store; - private isFirstRender: boolean; - private prevState: CanvasV2State; - private worker: Worker; - private tasks: Map void }>; + _store: Store; + _isFirstRender: boolean; + _prevState: CanvasV2State; + _worker: Worker; + _tasks: Map void }>; constructor( stage: Konva.Stage, @@ -99,10 +101,10 @@ export class CanvasManager { ) { this.stage = stage; this.container = container; - this.store = store; - this.stateApi = new CanvasStateApi(this.store); - this.prevState = this.stateApi.getState(); - this.isFirstRender = true; + this._store = store; + this.stateApi = new CanvasStateApi(this._store); + this._prevState = this.stateApi.getState(); + this._isFirstRender = true; this.log = logger('canvas'); this.workerLog = logger('worker'); @@ -133,9 +135,9 @@ export class CanvasManager { this.initialImage = new CanvasInitialImage(this.stateApi.getInitialImageState(), this); this.stage.add(this.initialImage.konva.layer); - this.worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module', name: 'worker' }); - this.tasks = new Map(); - this.worker.onmessage = (event: MessageEvent) => { + this._worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module', name: 'worker' }); + this._tasks = new Map(); + this._worker.onmessage = (event: MessageEvent) => { const { type, data } = event.data; if (type === 'log') { if (data.ctx) { @@ -144,20 +146,30 @@ export class CanvasManager { this.workerLog[data.level](data.message); } } else if (type === 'extents') { - const task = this.tasks.get(data.id); + const task = this._tasks.get(data.id); if (!task) { return; } task.onComplete(data.extents); } }; - this.worker.onerror = (event) => { + this._worker.onerror = (event) => { this.log.error({ message: event.message }, 'Worker error'); }; - this.worker.onmessageerror = () => { + this._worker.onmessageerror = () => { this.log.error('Worker message error'); }; this.onTransform = null; + this._isDebugging = false; + } + + enableDebugging() { + this._isDebugging = true; + this.logDebugInfo(); + } + + disableDebugging() { + this._isDebugging = false; } getLogger(namespace: string) { @@ -171,8 +183,8 @@ export class CanvasManager { type: 'get_bbox', data: { ...data, id }, }; - this.tasks.set(id, { task, onComplete }); - this.worker.postMessage(task, [data.buffer]); + this._tasks.set(id, { task, onComplete }); + this._worker.postMessage(task, [data.buffer]); } async renderInitialImage() { @@ -306,12 +318,12 @@ export class CanvasManager { render = async () => { const state = this.stateApi.getState(); - if (this.prevState === state && !this.isFirstRender) { + if (this._prevState === state && !this._isFirstRender) { this.log.trace('No changes detected, skipping render'); return; } - if (this.isFirstRender || state.layers.entities !== this.prevState.layers.entities) { + if (this._isFirstRender || state.layers.entities !== this._prevState.layers.entities) { this.log.debug('Rendering layers'); for (const canvasLayer of this.layers.values()) { @@ -339,9 +351,9 @@ export class CanvasManager { } if ( - this.isFirstRender || - state.tool.selected !== this.prevState.tool.selected || - state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id + this._isFirstRender || + state.tool.selected !== this._prevState.tool.selected || + state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id ) { this.log.debug('Updating interaction'); for (const layer of this.layers.values()) { @@ -350,89 +362,89 @@ export class CanvasManager { } if ( - this.isFirstRender || - state.initialImage !== this.prevState.initialImage || - state.bbox.rect !== this.prevState.bbox.rect || - state.tool.selected !== this.prevState.tool.selected || - state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id + this._isFirstRender || + state.initialImage !== this._prevState.initialImage || + state.bbox.rect !== this._prevState.bbox.rect || + state.tool.selected !== this._prevState.tool.selected || + state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id ) { this.log.debug('Rendering initial image'); await this.renderInitialImage(); } if ( - this.isFirstRender || - state.regions.entities !== this.prevState.regions.entities || - state.settings.maskOpacity !== this.prevState.settings.maskOpacity || - state.tool.selected !== this.prevState.tool.selected || - state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id + this._isFirstRender || + state.regions.entities !== this._prevState.regions.entities || + state.settings.maskOpacity !== this._prevState.settings.maskOpacity || + state.tool.selected !== this._prevState.tool.selected || + state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id ) { this.log.debug('Rendering regions'); await this.renderRegions(); } if ( - this.isFirstRender || - state.inpaintMask !== this.prevState.inpaintMask || - state.settings.maskOpacity !== this.prevState.settings.maskOpacity || - state.tool.selected !== this.prevState.tool.selected || - state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id + this._isFirstRender || + state.inpaintMask !== this._prevState.inpaintMask || + state.settings.maskOpacity !== this._prevState.settings.maskOpacity || + state.tool.selected !== this._prevState.tool.selected || + state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id ) { this.log.debug('Rendering inpaint mask'); await this.renderInpaintMask(); } if ( - this.isFirstRender || - state.controlAdapters.entities !== this.prevState.controlAdapters.entities || - state.tool.selected !== this.prevState.tool.selected || - state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id + this._isFirstRender || + state.controlAdapters.entities !== this._prevState.controlAdapters.entities || + state.tool.selected !== this._prevState.tool.selected || + state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id ) { this.log.debug('Rendering control adapters'); await this.renderControlAdapters(); } if ( - this.isFirstRender || - state.bbox !== this.prevState.bbox || - state.tool.selected !== this.prevState.tool.selected || - state.session.isActive !== this.prevState.session.isActive + this._isFirstRender || + state.bbox !== this._prevState.bbox || + state.tool.selected !== this._prevState.tool.selected || + state.session.isActive !== this._prevState.session.isActive ) { this.log.debug('Rendering generation bbox'); await this.preview.bbox.render(); } if ( - this.isFirstRender || - state.layers !== this.prevState.layers || - state.controlAdapters !== this.prevState.controlAdapters || - state.regions !== this.prevState.regions + this._isFirstRender || + state.layers !== this._prevState.layers || + state.controlAdapters !== this._prevState.controlAdapters || + state.regions !== this._prevState.regions ) { // this.log.debug('Updating entity bboxes'); // debouncedUpdateBboxes(stage, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions, onBboxChanged); } - if (this.isFirstRender || state.session !== this.prevState.session) { + if (this._isFirstRender || state.session !== this._prevState.session) { this.log.debug('Rendering staging area'); await this.preview.stagingArea.render(); } if ( - this.isFirstRender || - state.layers.entities !== this.prevState.layers.entities || - state.controlAdapters.entities !== this.prevState.controlAdapters.entities || - state.regions.entities !== this.prevState.regions.entities || - state.inpaintMask !== this.prevState.inpaintMask || - state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id + this._isFirstRender || + state.layers.entities !== this._prevState.layers.entities || + state.controlAdapters.entities !== this._prevState.controlAdapters.entities || + state.regions.entities !== this._prevState.regions.entities || + state.inpaintMask !== this._prevState.inpaintMask || + state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id ) { this.log.debug('Arranging entities'); await this.arrangeEntities(); } - this.prevState = state; + this._prevState = state; - if (this.isFirstRender) { - this.isFirstRender = false; + if (this._isFirstRender) { + this._isFirstRender = false; } }; @@ -448,7 +460,7 @@ export class CanvasManager { resizeObserver.observe(this.container); this.fitStageToContainer(); - const unsubscribeRenderer = this.store.subscribe(this.render); + const unsubscribeRenderer = this._store.subscribe(this.render); // When we this flag, we need to render the staging area $shouldShowStagedImage.subscribe(async (shouldShowStagedImage, prevShouldShowStagedImage) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts index 042fb1e6444..47386717b13 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts @@ -26,7 +26,7 @@ export class CanvasRect { this.id = id; this.parent = parent; - this.parent.log.trace(`Creating rect ${this.id}`); + this.parent._log.trace(`Creating rect ${this.id}`); this.konva = { group: new Konva.Group({ name: CanvasRect.GROUP_NAME, listening: false }), @@ -47,7 +47,7 @@ export class CanvasRect { async update(state: RectShape, force?: boolean): Promise { if (this.state !== state || force) { - this.parent.log.trace(`Updating rect ${this.id}`); + this.parent._log.trace(`Updating rect ${this.id}`); const { x, y, width, height, color } = state; this.konva.rect.setAttrs({ x, @@ -64,7 +64,7 @@ export class CanvasRect { } destroy() { - this.parent.log.trace(`Destroying rect ${this.id}`); + this.parent._log.trace(`Destroying rect ${this.id}`); this.konva.group.destroy(); } } From 49de11c3aeeaca84b936f38652361cceaf790639 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 30 Jul 2024 21:10:45 +1000 Subject: [PATCH 253/678] feat(ui): trying to fix flicker after transform --- .../controlLayers/konva/CanvasLayer.ts | 47 ++++++++++++++----- .../controlLayers/konva/CanvasManager.ts | 2 +- .../controlLayers/store/canvasV2Slice.ts | 1 + .../controlLayers/store/layersReducers.ts | 16 +++++-- 4 files changed, 51 insertions(+), 15 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 670fa7a1b8b..ef56026251d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -7,15 +7,16 @@ import { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; import { konvaNodeToBlob, mapId, previewBlob } from 'features/controlLayers/konva/util'; -import { layerRasterized } from 'features/controlLayers/store/canvasV2Slice'; -import type { - BrushLine, - CanvasV2State, - Coordinate, - EraserLine, - LayerEntity, - Rect, - RectShape, +import { layerAllObjectsDeletedExceptOne, layerRasterized } from 'features/controlLayers/store/canvasV2Slice'; +import { + type BrushLine, + type CanvasV2State, + type Coordinate, + type EraserLine, + imageDTOToImageObject, + type LayerEntity, + type Rect, + type RectShape, } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { debounce, get } from 'lodash-es'; @@ -53,6 +54,8 @@ export class CanvasLayer { _isFirstRender: boolean; isTransforming: boolean; + isPendingBboxCalculation: boolean; + rasterizedObjectId: string | null; rect: Rect; bbox: Rect; @@ -188,6 +191,8 @@ export class CanvasLayer { this._bboxNeedsUpdate = true; this.isTransforming = false; this._isFirstRender = true; + this.rasterizedObjectId = null; + this.isPendingBboxCalculation = false; this._log = this.manager.getLogger(`layer_${this.id}`); } @@ -326,6 +331,12 @@ export class CanvasLayer { if (didUpdate) { this.calculateBbox(); } + + if (this.isTransforming && this.rasterizedObjectId) { + this.manager._store.dispatch(layerAllObjectsDeletedExceptOne({ id: this.id, objectId: this.rasterizedObjectId })); + this.isTransforming = false; + this.rasterizedObjectId = null; + } } async updateOpacity(arg?: { opacity: number }) { @@ -388,6 +399,10 @@ export class CanvasLayer { async updateBbox() { this._log.trace('Updating bbox'); + if (this.isPendingBboxCalculation) { + return; + } + // If the bbox has no width or height, that means the layer is fully transparent. This can happen if it is only // eraser lines, fully clipped brush lines or if it has been fully erased. if (this.bbox.width === 0 || this.bbox.height === 0) { @@ -547,11 +562,16 @@ export class CanvasLayer { } const imageDTO = await uploadImage(blob, `${this.id}_transform.png`, 'other', true); const { dispatch } = getStore(); - dispatch(layerRasterized({ id: this.id, imageDTO, position: { x: rect.x, y: rect.y } })); - this.isTransforming = false; + const imageObject = imageDTOToImageObject(this.id, uuidv4(), imageDTO); + dispatch(layerRasterized({ id: this.id, imageObject, position: { x: rect.x, y: rect.y } })); + this.rasterizedObjectId = imageObject.id; this.resetScale(); } + async finalizeTransform() { + // + } + async cancelTransform() { this._log.debug('Canceling transform'); @@ -572,9 +592,12 @@ export class CanvasLayer { calculateBbox = debounce(() => { this._log.debug('Calculating bbox'); + this.isPendingBboxCalculation = true; + if (this.objects.size === 0) { this.rect = this.getDefaultRect(); this.bbox = this.getDefaultRect(); + this.isPendingBboxCalculation = false; this.updateBbox(); return; } @@ -607,6 +630,7 @@ export class CanvasLayer { if (!needsPixelBbox) { this.rect = deepClone(rect); this.bbox = deepClone(rect); + this.isPendingBboxCalculation = false; this._log.trace({ bbox: this.bbox, rect: this.rect }, 'Got bbox from client rect'); this.updateBbox(); return; @@ -636,6 +660,7 @@ export class CanvasLayer { } else { this.bbox = deepClone(rect); } + this.isPendingBboxCalculation = false; this._log.trace({ bbox: this.bbox, rect: this.rect, extents }, `Got bbox from worker`); this.updateBbox(); clone.destroy(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index d1e0fbee6be..f028c6a2437 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -329,7 +329,7 @@ export class CanvasManager { for (const canvasLayer of this.layers.values()) { if (!state.layers.entities.find((l) => l.id === canvasLayer.id)) { this.log.debug(`Destroying deleted layer ${canvasLayer.id}`); - canvasLayer.destroy(); + await canvasLayer.destroy(); this.layers.delete(canvasLayer.id); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index b302a00ba71..90d0f7f933a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -220,6 +220,7 @@ export const { layerTranslated, layerBboxChanged, layerImageAdded, + layerAllObjectsDeletedExceptOne, layerAllDeleted, layerImageCacheChanged, layerScaled, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index 5d538bd821b..ae2eaf3e1ac 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -10,6 +10,7 @@ import type { CanvasV2State, Coordinate, EraserLine, + ImageObject, ImageObjectAddedArg, LayerEntity, PositionChangedArg, @@ -252,16 +253,25 @@ export const layersReducers = { const { imageDTO } = action.payload; state.layers.imageCache = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; }, - layerRasterized: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO; position: Coordinate }>) => { - const { id, imageDTO, position } = action.payload; + layerRasterized: (state, action: PayloadAction<{ id: string; imageObject: ImageObject; position: Coordinate }>) => { + const { id, imageObject, position } = action.payload; const layer = selectLayer(state, id); if (!layer) { return; } - layer.objects = [imageDTOToImageObject(id, uuidv4(), imageDTO)]; + layer.objects.push(imageObject); layer.position = position; state.layers.imageCache = null; }, + layerAllObjectsDeletedExceptOne: (state, action: PayloadAction<{ id: string; objectId: string }>) => { + const { id, objectId } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + layer.objects = layer.objects.filter((obj) => obj.id === objectId); + state.layers.imageCache = null; + }, } satisfies SliceCaseReducers; const scalePoints = (points: number[], scaleX: number, scaleY: number) => { From 9baa594a56d54c638ddf999f4ddcfc7acf69216a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 31 Jul 2024 14:16:02 +1000 Subject: [PATCH 254/678] build(ui): add eslint rules for async stuff --- invokeai/frontend/web/.eslintrc.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/invokeai/frontend/web/.eslintrc.js b/invokeai/frontend/web/.eslintrc.js index 519e725fb4c..6c409cb164a 100644 --- a/invokeai/frontend/web/.eslintrc.js +++ b/invokeai/frontend/web/.eslintrc.js @@ -12,6 +12,10 @@ module.exports = { 'i18next/no-literal-string': 'error', // https://eslint.org/docs/latest/rules/no-console 'no-console': 'error', + // https://eslint.org/docs/latest/rules/no-promise-executor-return + 'no-promise-executor-return': 'error', + // https://eslint.org/docs/latest/rules/require-await + 'require-await': 'error', }, overrides: [ /** From a3d2ba14447cbc5cefbb06c393776a6bc6837af9 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 31 Jul 2024 14:27:23 +1000 Subject: [PATCH 255/678] fix(ui): prevent flash when applying transform --- .../controlLayers/konva/CanvasLayer.ts | 70 ++++++++----------- .../controlLayers/konva/CanvasManager.ts | 7 +- .../controlLayers/store/canvasV2Slice.ts | 1 - .../controlLayers/store/layersReducers.ts | 11 +-- 4 files changed, 34 insertions(+), 55 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index ef56026251d..40d90fcb17b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -7,7 +7,7 @@ import { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; import { konvaNodeToBlob, mapId, previewBlob } from 'features/controlLayers/konva/util'; -import { layerAllObjectsDeletedExceptOne, layerRasterized } from 'features/controlLayers/store/canvasV2Slice'; +import { layerRasterized } from 'features/controlLayers/store/canvasV2Slice'; import { type BrushLine, type CanvasV2State, @@ -55,7 +55,6 @@ export class CanvasLayer { isTransforming: boolean; isPendingBboxCalculation: boolean; - rasterizedObjectId: string | null; rect: Rect; bbox: Rect; @@ -191,7 +190,6 @@ export class CanvasLayer { this._bboxNeedsUpdate = true; this.isTransforming = false; this._isFirstRender = true; - this.rasterizedObjectId = null; this.isPendingBboxCalculation = false; this._log = this.manager.getLogger(`layer_${this.id}`); } @@ -218,7 +216,7 @@ export class CanvasLayer { return; } const drawingBuffer = this._drawingBuffer; - this.setDrawingBuffer(null); + await this.setDrawingBuffer(null); // We need to give the objects a fresh ID else they will be considered the same object when they are re-rendered as // a non-buffer object, and we won't trigger things like bbox calculation @@ -248,12 +246,12 @@ export class CanvasLayer { this._log.debug('Updating'); const { position, objects, opacity, isEnabled } = state; - if (this._isFirstRender || position !== this._state.position) { - await this.updatePosition({ position }); - } if (this._isFirstRender || objects !== this._state.objects) { await this.updateObjects({ objects }); } + if (this._isFirstRender || position !== this._state.position) { + await this.updatePosition({ position }); + } if (this._isFirstRender || opacity !== this._state.opacity) { await this.updateOpacity({ opacity }); } @@ -270,14 +268,14 @@ export class CanvasLayer { this._isFirstRender = false; } - async updateVisibility(arg?: { isEnabled: boolean }) { + updateVisibility(arg?: { isEnabled: boolean }) { this._log.trace('Updating visibility'); const isEnabled = get(arg, 'isEnabled', this._state.isEnabled); const hasObjects = this.objects.size > 0 || this._drawingBuffer !== null; this.konva.layer.visible(isEnabled || hasObjects); } - async updatePosition(arg?: { position: Coordinate }) { + updatePosition(arg?: { position: Coordinate }) { this._log.trace('Updating position'); const position = get(arg, 'position', this._state.position); const bboxPadding = this.manager.getScaledBboxPadding(); @@ -331,23 +329,15 @@ export class CanvasLayer { if (didUpdate) { this.calculateBbox(); } - - if (this.isTransforming && this.rasterizedObjectId) { - this.manager._store.dispatch(layerAllObjectsDeletedExceptOne({ id: this.id, objectId: this.rasterizedObjectId })); - this.isTransforming = false; - this.rasterizedObjectId = null; - } } - async updateOpacity(arg?: { opacity: number }) { + updateOpacity(arg?: { opacity: number }) { this._log.trace('Updating opacity'); - const opacity = get(arg, 'opacity', this._state.opacity); - this.konva.objectGroup.opacity(opacity); } - async updateInteraction(arg?: { toolState: CanvasV2State['tool']; isSelected: boolean }) { + updateInteraction(arg?: { toolState: CanvasV2State['tool']; isSelected: boolean }) { this._log.trace('Updating interaction'); const toolState = get(arg, 'toolState', this.manager.stateApi.getToolState()); @@ -396,7 +386,7 @@ export class CanvasLayer { } } - async updateBbox() { + updateBbox() { this._log.trace('Updating bbox'); if (this.isPendingBboxCalculation) { @@ -442,7 +432,7 @@ export class CanvasLayer { }); } - async syncStageScale() { + syncStageScale() { this._log.trace('Syncing scale to stage'); const onePixel = this.manager.getScaledPixel(); @@ -519,7 +509,7 @@ export class CanvasLayer { return false; } - async startTransform() { + startTransform() { this._log.debug('Starting transform'); this.isTransforming = true; @@ -539,7 +529,7 @@ export class CanvasLayer { this.konva.bbox.visible(false); } - async resetScale() { + resetScale() { const attrs = { scaleX: 1, scaleY: 1, @@ -550,39 +540,37 @@ export class CanvasLayer { this.konva.interactionRect.setAttrs(attrs); } - async applyTransform() { - this._log.debug('Applying transform'); + async rasterizeLayer() { + this._log.debug('Rasterizing layer'); const objectGroupClone = this.konva.objectGroup.clone(); const interactionRectClone = this.konva.interactionRect.clone(); const rect = interactionRectClone.getClientRect(); const blob = await konvaNodeToBlob(objectGroupClone, rect); if (this.manager._isDebugging) { - previewBlob(blob, 'transformed layer'); + previewBlob(blob, 'Rasterized layer'); } - const imageDTO = await uploadImage(blob, `${this.id}_transform.png`, 'other', true); + const imageDTO = await uploadImage(blob, `${this.id}_rasterized.png`, 'other', true); const { dispatch } = getStore(); const imageObject = imageDTOToImageObject(this.id, uuidv4(), imageDTO); - dispatch(layerRasterized({ id: this.id, imageObject, position: { x: rect.x, y: rect.y } })); - this.rasterizedObjectId = imageObject.id; + await this._renderObject(imageObject, true); + for (const obj of this.objects.values()) { + if (obj.id !== imageObject.id) { + obj.konva.group.visible(false); + } + } this.resetScale(); + dispatch(layerRasterized({ id: this.id, imageObject, position: { x: rect.x, y: rect.y } })); } - async finalizeTransform() { - // - } - - async cancelTransform() { - this._log.debug('Canceling transform'); + stopTransform() { + this._log.debug('Stopping transform'); this.isTransforming = false; this.resetScale(); - await this.updatePosition({ position: this._state.position }); - await this.updateBbox(); - await this.updateInteraction({ - toolState: this.manager.stateApi.getToolState(), - isSelected: this.manager.stateApi.getIsSelected(this.id), - }); + this.updatePosition(); + this.updateBbox(); + this.updateInteraction(); } getDefaultRect(): Rect { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index f028c6a2437..cf5435561a3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -299,10 +299,11 @@ export class CanvasManager { this.onTransform?.(true); } - applyTransform() { + async applyTransform() { const layer = this.getTransformingLayer(); if (layer) { - layer.applyTransform(); + await layer.rasterizeLayer(); + layer.stopTransform(); } this.onTransform?.(false); } @@ -310,7 +311,7 @@ export class CanvasManager { cancelTransform() { const layer = this.getTransformingLayer(); if (layer) { - layer.cancelTransform(); + layer.stopTransform(); } this.onTransform?.(false); } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 90d0f7f933a..b302a00ba71 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -220,7 +220,6 @@ export const { layerTranslated, layerBboxChanged, layerImageAdded, - layerAllObjectsDeletedExceptOne, layerAllDeleted, layerImageCacheChanged, layerScaled, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index ae2eaf3e1ac..944704d971a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -259,19 +259,10 @@ export const layersReducers = { if (!layer) { return; } - layer.objects.push(imageObject); + layer.objects = [imageObject]; layer.position = position; state.layers.imageCache = null; }, - layerAllObjectsDeletedExceptOne: (state, action: PayloadAction<{ id: string; objectId: string }>) => { - const { id, objectId } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - layer.objects = layer.objects.filter((obj) => obj.id === objectId); - state.layers.imageCache = null; - }, } satisfies SliceCaseReducers; const scalePoints = (points: number[], scaleX: number, scaleY: number) => { From a3e1ff637de320a8760092d6905f8e9e7ced30ad Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 31 Jul 2024 14:35:04 +1000 Subject: [PATCH 256/678] fix(ui): move CanvasImage's konva image to correct object --- .../controlLayers/konva/CanvasImage.ts | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts index f88ecfd93bf..91650fef60c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -23,9 +23,9 @@ export class CanvasImage { konva: { group: Konva.Group; placeholder: { group: Konva.Group; rect: Konva.Rect; text: Konva.Text }; + image: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately }; imageName: string | null; - image: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately isLoading: boolean; isError: boolean; @@ -63,13 +63,13 @@ export class CanvasImage { listening: false, }), }, + image: null, }; this.konva.placeholder.group.add(this.konva.placeholder.rect); this.konva.placeholder.group.add(this.konva.placeholder.text); this.konva.group.add(this.konva.placeholder.group); this.imageName = null; - this.image = null; this.isLoading = false; this.isError = false; this.state = state; @@ -82,7 +82,7 @@ export class CanvasImage { this.isLoading = true; this.konva.group.visible(true); - if (!this.image) { + if (!this.konva.image) { this.konva.placeholder.group.visible(false); this.konva.placeholder.text.text(t('common.loadingImage', 'Loading Image')); } @@ -91,27 +91,27 @@ export class CanvasImage { assert(imageDTO !== null, 'imageDTO is null'); const imageEl = await loadImage(imageDTO.image_url); - if (this.image) { - this.image.setAttrs({ + if (this.konva.image) { + this.konva.image.setAttrs({ image: imageEl, }); } else { - this.image = new Konva.Image({ + this.konva.image = new Konva.Image({ name: CanvasImage.IMAGE_NAME, listening: false, image: imageEl, width: this.state.width, height: this.state.height, }); - this.konva.group.add(this.image); + this.konva.group.add(this.konva.image); } if (this.state.filters.length > 0) { - this.image.cache(); - this.image.filters(this.state.filters.map((f) => FILTER_MAP[f])); + this.konva.image.cache(); + this.konva.image.filters(this.state.filters.map((f) => FILTER_MAP[f])); } else { - this.image.clearCache(); - this.image.filters([]); + this.konva.image.clearCache(); + this.konva.image.filters([]); } this.imageName = imageName; @@ -119,7 +119,7 @@ export class CanvasImage { this.isError = false; this.konva.placeholder.group.visible(false); } catch { - this.image?.visible(false); + this.konva.image?.visible(false); this.imageName = null; this.isLoading = false; this.isError = true; @@ -136,13 +136,13 @@ export class CanvasImage { if (this.state.image.name !== image.name || force) { await this.updateImageSource(image.name); } - this.image?.setAttrs({ x, y, width, height }); + this.konva.image?.setAttrs({ x, y, width, height }); if (filters.length > 0) { - this.image?.cache(); - this.image?.filters(filters.map((f) => FILTER_MAP[f])); + this.konva.image?.cache(); + this.konva.image?.filters(filters.map((f) => FILTER_MAP[f])); } else { - this.image?.clearCache(); - this.image?.filters([]); + this.konva.image?.clearCache(); + this.konva.image?.filters([]); } this.konva.placeholder.rect.setAttrs({ width, height }); this.konva.placeholder.text.setAttrs({ width, height, fontSize: width / 16 }); From 71e4b60a3006cefa78c40026690834ecad29ef5e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 31 Jul 2024 15:02:57 +1000 Subject: [PATCH 257/678] build(ui): add nanoid as explicit dep --- invokeai/frontend/web/package.json | 1 + invokeai/frontend/web/pnpm-lock.yaml | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 5ddb0d03faf..79a4ce114b7 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -74,6 +74,7 @@ "jsondiffpatch": "^0.6.0", "konva": "^9.3.14", "lodash-es": "^4.17.21", + "nanoid": "^5.0.7", "nanostores": "^0.11.2", "new-github-issue-url": "^1.0.0", "overlayscrollbars": "^2.10.0", diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index f9c906d0dc8..a4778ac733d 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -71,6 +71,9 @@ dependencies: lodash-es: specifier: ^4.17.21 version: 4.17.21 + nanoid: + specifier: ^5.0.7 + version: 5.0.7 nanostores: specifier: ^0.11.2 version: 0.11.2 @@ -8976,6 +8979,12 @@ packages: hasBin: true dev: true + /nanoid@5.0.7: + resolution: {integrity: sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==} + engines: {node: ^18 || >=20} + hasBin: true + dev: false + /nanostores@0.11.2: resolution: {integrity: sha512-6bucNxMJA5rNV554WQl+MWGng0QVMzlRgpKTHHfIbVLrhQ+yRXBychV9ECGVuuUfCMQPjfIG9bj8oJFZ9hYP/Q==} engines: {node: ^18.0.0 || >=20.0.0} From 17e7364bdb229cace2dc6c9d194bf4c0c06f5bf4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 31 Jul 2024 15:06:33 +1000 Subject: [PATCH 258/678] feat(ui): use nanoid(10) instead of uuidv4 for canvas Shorter ids makes it much more readable --- .../features/controlLayers/konva/CanvasLayer.ts | 11 +++++------ .../controlLayers/konva/CanvasManager.ts | 3 ++- .../src/features/controlLayers/konva/events.ts | 17 ++++++++--------- .../src/features/controlLayers/konva/util.ts | 3 +++ .../controlLayers/store/layersReducers.ts | 8 ++++---- 5 files changed, 22 insertions(+), 20 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 40d90fcb17b..d6f91974386 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -6,7 +6,7 @@ import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; import { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; -import { konvaNodeToBlob, mapId, previewBlob } from 'features/controlLayers/konva/util'; +import { konvaNodeToBlob, mapId, nanoid, previewBlob } from 'features/controlLayers/konva/util'; import { layerRasterized } from 'features/controlLayers/store/canvasV2Slice'; import { type BrushLine, @@ -23,7 +23,6 @@ import { debounce, get } from 'lodash-es'; import type { Logger } from 'roarr'; import { uploadImage } from 'services/api/endpoints/images'; import { assert } from 'tsafe'; -import { v4 as uuidv4 } from 'uuid'; export class CanvasLayer { static NAME_PREFIX = 'layer'; @@ -222,13 +221,13 @@ export class CanvasLayer { // a non-buffer object, and we won't trigger things like bbox calculation if (drawingBuffer.type === 'brush_line') { - drawingBuffer.id = getBrushLineId(this.id, uuidv4()); + drawingBuffer.id = getBrushLineId(this.id, nanoid()); this.manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: drawingBuffer }, 'layer'); } else if (drawingBuffer.type === 'eraser_line') { - drawingBuffer.id = getEraserLineId(this.id, uuidv4()); + drawingBuffer.id = getEraserLineId(this.id, nanoid()); this.manager.stateApi.onEraserLineAdded({ id: this.id, eraserLine: drawingBuffer }, 'layer'); } else if (drawingBuffer.type === 'rect_shape') { - drawingBuffer.id = getRectShapeId(this.id, uuidv4()); + drawingBuffer.id = getRectShapeId(this.id, nanoid()); this.manager.stateApi.onRectShapeAdded({ id: this.id, rectShape: drawingBuffer }, 'layer'); } } @@ -552,7 +551,7 @@ export class CanvasLayer { } const imageDTO = await uploadImage(blob, `${this.id}_rasterized.png`, 'other', true); const { dispatch } = getStore(); - const imageObject = imageDTOToImageObject(this.id, uuidv4(), imageDTO); + const imageObject = imageDTOToImageObject(this.id, nanoid(), imageDTO); await this._renderObject(imageObject, true); for (const obj of this.objects.values()) { if (obj.id !== imageObject.id) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index cf5435561a3..1fd787ce3bb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -10,6 +10,7 @@ import { getInitialImage, getInpaintMaskImage, getRegionMaskImage, + nanoid, } from 'features/controlLayers/konva/util'; import type { Extents, ExtentsResult, GetBboxTask, WorkerLogMessage } from 'features/controlLayers/konva/worker'; import { $lastProgressEvent, $shouldShowStagedImage } from 'features/controlLayers/store/canvasV2Slice'; @@ -178,7 +179,7 @@ export class CanvasManager { } requestBbox(data: Omit, onComplete: (extents: Extents | null) => void) { - const id = crypto.randomUUID(); + const id = nanoid(); const task: GetBboxTask = { type: 'get_bbox', data: { ...data, id }, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 0b79e7167a5..90d29178288 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -1,5 +1,5 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util'; +import { getScaledFlooredCursorPosition, nanoid } from 'features/controlLayers/konva/util'; import type { CanvasV2State, Coordinate, @@ -12,7 +12,6 @@ import { isDrawableEntity, isDrawableEntityAdapter } from 'features/controlLayer import type Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; import { clamp } from 'lodash-es'; -import { v4 as uuidv4 } from 'uuid'; import { BRUSH_SPACING_TARGET_SCALE, CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from './constants'; import { getBrushLineId, getEraserLineId, getRectShapeId } from './naming'; @@ -188,7 +187,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getBrushLineId(selectedEntityAdapter.id, uuidv4(), true), + id: getBrushLineId(selectedEntityAdapter.id, nanoid(), true), type: 'brush_line', points: [ // The last point of the last line is already normalized to the entity's coordinates @@ -206,7 +205,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getBrushLineId(selectedEntityAdapter.id, uuidv4(), true), + id: getBrushLineId(selectedEntityAdapter.id, nanoid(), true), type: 'brush_line', points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y], strokeWidth: toolState.brush.width, @@ -225,7 +224,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getEraserLineId(selectedEntityAdapter.id, uuidv4(), true), + id: getEraserLineId(selectedEntityAdapter.id, nanoid(), true), type: 'eraser_line', points: [ // The last point of the last line is already normalized to the entity's coordinates @@ -242,7 +241,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getEraserLineId(selectedEntityAdapter.id, uuidv4(), true), + id: getEraserLineId(selectedEntityAdapter.id, nanoid(), true), type: 'eraser_line', points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y], strokeWidth: toolState.eraser.width, @@ -257,7 +256,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getRectShapeId(selectedEntityAdapter.id, uuidv4(), true), + id: getRectShapeId(selectedEntityAdapter.id, nanoid(), true), type: 'rect_shape', x: pos.x - selectedEntity.position.x, y: pos.y - selectedEntity.position.y, @@ -357,7 +356,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getBrushLineId(selectedEntityAdapter.id, uuidv4(), true), + id: getBrushLineId(selectedEntityAdapter.id, nanoid(), true), type: 'brush_line', points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y], strokeWidth: toolState.brush.width, @@ -389,7 +388,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getEraserLineId(selectedEntityAdapter.id, uuidv4(), true), + id: getEraserLineId(selectedEntityAdapter.id, nanoid(), true), type: 'eraser_line', points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y], strokeWidth: toolState.eraser.width, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index fc764afe04a..9c8740d2be0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -6,6 +6,7 @@ import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers'; import Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; import type { Vector2d } from 'konva/lib/types'; +import { customAlphabet, urlAlphabet } from 'nanoid'; import type { ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; @@ -573,3 +574,5 @@ export function loadImage(src: string, imageEl?: HTMLImageElement): Promise ({ payload: { id: uuidv4() } }), + prepare: () => ({ payload: { id: nanoid() } }), }, layerAddedFromStagingArea: { reducer: ( @@ -64,7 +64,7 @@ export const layersReducers = { state.layers.imageCache = null; }, prepare: (payload: { stagingAreaImage: StagingAreaImage; position: Coordinate }) => ({ - payload: { ...payload, id: uuidv4(), objectId: uuidv4() }, + payload: { ...payload, id: nanoid(), objectId: nanoid() }, }), }, layerRecalled: (state, action: PayloadAction<{ data: LayerEntity }>) => { @@ -246,7 +246,7 @@ export const layersReducers = { state.layers.imageCache = null; }, prepare: (payload: ImageObjectAddedArg & { pos?: { x: number; y: number } }) => ({ - payload: { ...payload, objectId: uuidv4() }, + payload: { ...payload, objectId: nanoid() }, }), }, layerImageCacheChanged: (state, action: PayloadAction<{ imageDTO: ImageDTO | null }>) => { From 86ff52ec36b54ea3e9061a44ae56625eb6b65d1c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 31 Jul 2024 15:06:46 +1000 Subject: [PATCH 259/678] feat(ui): add `repr` methods to layer and object classes --- .../controlLayers/konva/CanvasBrushLine.ts | 20 +++++++++++++++- .../controlLayers/konva/CanvasEraserLine.ts | 20 +++++++++++++++- .../controlLayers/konva/CanvasImage.ts | 21 ++++++++++++++++ .../controlLayers/konva/CanvasLayer.ts | 24 +++++++++++++------ .../controlLayers/konva/CanvasRect.ts | 20 +++++++++++++++- 5 files changed, 95 insertions(+), 10 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts index c052a55775b..794ce82b17b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts @@ -1,4 +1,5 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; +import { deepClone } from 'common/util/deepClone'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { BrushLine } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -52,7 +53,7 @@ export class CanvasBrushLine { this.state = state; } - async update(state: BrushLine, force?: boolean): Promise { + update(state: BrushLine, force?: boolean): boolean { if (force || this.state !== state) { this.parent._log.trace(`Updating brush line ${this.id}`); const { points, color, clip, strokeWidth } = state; @@ -74,4 +75,21 @@ export class CanvasBrushLine { this.parent._log.trace(`Destroying brush line ${this.id}`); this.konva.group.destroy(); } + + show() { + this.konva.group.visible(true); + } + + hide() { + this.konva.group.visible(false); + } + + repr() { + return { + id: this.id, + type: this.type, + parent: this.parent.id, + 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 a1d8d193117..7ba26b02f9d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts @@ -1,4 +1,5 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; +import { deepClone } from 'common/util/deepClone'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { EraserLine } from 'features/controlLayers/store/types'; import { RGBA_RED } from 'features/controlLayers/store/types'; @@ -53,7 +54,7 @@ export class CanvasEraserLine { this.state = state; } - async update(state: EraserLine, force?: boolean): Promise { + update(state: EraserLine, force?: boolean): boolean { if (force || this.state !== state) { this.parent._log.trace(`Updating eraser line ${this.id}`); const { points, clip, strokeWidth } = state; @@ -74,4 +75,21 @@ export class CanvasEraserLine { this.parent._log.trace(`Destroying eraser line ${this.id}`); this.konva.group.destroy(); } + + show() { + this.konva.group.visible(true); + } + + hide() { + this.konva.group.visible(false); + } + + repr() { + return { + id: this.id, + type: this.type, + parent: this.parent.id, + 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 91650fef60c..9fb722a3bc0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -1,3 +1,4 @@ +import { deepClone } from 'common/util/deepClone'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import { FILTER_MAP } from 'features/controlLayers/konva/filters'; import { loadImage } from 'features/controlLayers/konva/util'; @@ -157,4 +158,24 @@ export class CanvasImage { this.parent._log.trace(`Destroying image ${this.id}`); this.konva.group.destroy(); } + + show() { + this.konva.group.visible(true); + } + + hide() { + this.konva.group.visible(false); + } + + repr() { + return { + id: this.id, + type: this.type, + parent: this.parent.id, + imageName: this.imageName, + isLoading: this.isLoading, + isError: this.isError, + 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 d6f91974386..7cf905053cb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -655,14 +655,24 @@ export class CanvasLayer { ); }, CanvasManager.BBOX_DEBOUNCE_MS); - logDebugInfo(msg = 'Debug info') { - const debugInfo = { + repr() { + return { id: this.id, - state: this._state, - rect: this.rect, - bbox: this.bbox, - objects: Array.from(this.objects.values()).map((obj) => obj.id), + type: 'layer', + state: deepClone(this._state), + rect: deepClone(this.rect), + bbox: deepClone(this.bbox), + bboxNeedsUpdate: this._bboxNeedsUpdate, + isFirstRender: this._isFirstRender, isTransforming: this.isTransforming, + isPendingBboxCalculation: this.isPendingBboxCalculation, + objects: Array.from(this.objects.values()).map((obj) => obj.repr()), + }; + } + + logDebugInfo(msg = 'Debug info') { + const info = { + repr: this.repr(), interactionRectAttrs: { x: this.konva.interactionRect.x(), y: this.konva.interactionRect.y(), @@ -684,6 +694,6 @@ export class CanvasLayer { offsetY: this.konva.objectGroup.offsetY(), }, }; - this._log.debug(debugInfo, msg); + this._log.trace(info, msg); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts index 47386717b13..13d1ef1d657 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts @@ -1,4 +1,5 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; +import { deepClone } from 'common/util/deepClone'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { RectShape } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -45,7 +46,7 @@ export class CanvasRect { this.state = state; } - async update(state: RectShape, force?: boolean): Promise { + update(state: RectShape, force?: boolean): boolean { if (this.state !== state || force) { this.parent._log.trace(`Updating rect ${this.id}`); const { x, y, width, height, color } = state; @@ -67,4 +68,21 @@ export class CanvasRect { this.parent._log.trace(`Destroying rect ${this.id}`); this.konva.group.destroy(); } + + show() { + this.konva.group.visible(true); + } + + hide() { + this.konva.group.visible(false); + } + + repr() { + return { + id: this.id, + type: this.type, + parent: this.parent.id, + state: deepClone(this.state), + }; + } } From f59c2cf7f5a76bbb6ea42a76a4a60010d255ef32 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:04:14 +1000 Subject: [PATCH 260/678] feat(ui): revised logging and naming setup, fix staging area --- .../addCommitStagingAreaImageListener.ts | 15 +++- .../controlLayers/konva/CanvasBrushLine.ts | 35 ++++----- .../controlLayers/konva/CanvasEntity.ts | 27 +++++++ .../controlLayers/konva/CanvasEraserLine.ts | 35 ++++----- .../controlLayers/konva/CanvasImage.ts | 46 ++++++------ .../controlLayers/konva/CanvasLayer.ts | 69 +++++++++-------- .../controlLayers/konva/CanvasManager.ts | 38 ++++++++-- .../controlLayers/konva/CanvasObject.ts | 48 ++++++++++++ .../controlLayers/konva/CanvasRect.ts | 36 ++++----- .../controlLayers/konva/CanvasStagingArea.ts | 52 ++++++++----- .../controlLayers/konva/CanvasStateApi.ts | 3 + .../features/controlLayers/konva/events.ts | 17 ++--- .../features/controlLayers/konva/naming.ts | 10 +-- .../src/features/controlLayers/konva/util.ts | 21 +++++- .../controlLayers/store/canvasV2Slice.ts | 1 - .../store/controlAdaptersReducers.ts | 6 +- .../store/initialImageReducers.ts | 2 +- .../controlLayers/store/ipAdaptersReducers.ts | 10 +-- .../controlLayers/store/layersReducers.ts | 74 +++++++------------ .../src/features/controlLayers/store/types.ts | 11 +-- 20 files changed, 309 insertions(+), 247 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasObject.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts index dcd3d7f70c0..6917c83a217 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts @@ -2,10 +2,12 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { $lastProgressEvent, - layerAddedFromStagingArea, + layerAdded, sessionStagingAreaImageAccepted, sessionStagingAreaReset, } from 'features/controlLayers/store/canvasV2Slice'; +import type { LayerEntity } from 'features/controlLayers/store/types'; +import { imageDTOToImageObject } from 'features/controlLayers/store/types'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { queueApi } from 'services/api/endpoints/queue'; @@ -50,7 +52,7 @@ export const addStagingListeners = (startAppListening: AppStartListening) => { startAppListening({ actionCreator: sessionStagingAreaImageAccepted, - effect: async (action, api) => { + effect: (action, api) => { const { index } = action.payload; const state = api.getState(); const stagingAreaImage = state.canvasV2.session.stagedImages[index]; @@ -58,7 +60,14 @@ export const addStagingListeners = (startAppListening: AppStartListening) => { assert(stagingAreaImage, 'No staged image found to accept'); const { x, y } = state.canvasV2.bbox.rect; - api.dispatch(layerAddedFromStagingArea({ stagingAreaImage, position: { x, y } })); + const { imageDTO, offsetX, offsetY } = stagingAreaImage; + const imageObject = imageDTOToImageObject(imageDTO); + const overrides: Partial = { + position: { x: x + offsetX, y: y + offsetY }, + objects: [imageObject], + }; + + api.dispatch(layerAdded({ overrides })); api.dispatch(sessionStagingAreaReset()); }, }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts index 794ce82b17b..5d4d4159172 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts @@ -1,32 +1,27 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; +import { CanvasObject } from 'features/controlLayers/konva/CanvasObject'; import type { BrushLine } from 'features/controlLayers/store/types'; import Konva from 'konva'; -export class CanvasBrushLine { +export class CanvasBrushLine extends CanvasObject { static NAME_PREFIX = 'brush-line'; static GROUP_NAME = `${CanvasBrushLine.NAME_PREFIX}_group`; static LINE_NAME = `${CanvasBrushLine.NAME_PREFIX}_line`; + static TYPE = 'brush_line'; state: BrushLine; - - type = 'brush_line'; - id: string; konva: { group: Konva.Group; line: Konva.Line; }; - parent: CanvasLayer; - constructor(state: BrushLine, parent: CanvasLayer) { - const { id, strokeWidth, clip, color, points } = state; - - this.id = id; + super(state.id, parent); + this._log.trace({ state }, 'Creating brush line'); - this.parent = parent; - this.parent._log.trace(`Creating brush line ${this.id}`); + const { strokeWidth, clip, color, points } = state; this.konva = { group: new Konva.Group({ @@ -36,7 +31,6 @@ export class CanvasBrushLine { }), line: new Konva.Line({ name: CanvasBrushLine.LINE_NAME, - id, listening: false, shadowForStrokeEnabled: false, strokeWidth, @@ -55,7 +49,7 @@ export class CanvasBrushLine { update(state: BrushLine, force?: boolean): boolean { if (force || this.state !== state) { - this.parent._log.trace(`Updating brush line ${this.id}`); + this._log.trace({ state }, 'Updating brush line'); const { points, color, clip, strokeWidth } = state; this.konva.line.setAttrs({ // A line with only one point will not be rendered, so we duplicate the points to make it visible @@ -72,23 +66,20 @@ export class CanvasBrushLine { } destroy() { - this.parent._log.trace(`Destroying brush line ${this.id}`); + this._log.trace('Destroying brush line'); this.konva.group.destroy(); } - show() { - this.konva.group.visible(true); - } - - hide() { - this.konva.group.visible(false); + setVisibility(isVisible: boolean): void { + this._log.trace({ isVisible }, 'Setting brush line visibility'); + this.konva.group.visible(isVisible); } repr() { return { id: this.id, - type: this.type, - parent: this.parent.id, + type: CanvasBrushLine.TYPE, + parent: this._parent.id, state: deepClone(this.state), }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity.ts new file mode 100644 index 00000000000..390d17d5cc5 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity.ts @@ -0,0 +1,27 @@ +import type { JSONObject } from 'common/types'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import type { Logger } from 'roarr'; + +export abstract class CanvasEntity { + id: string; + _manager: CanvasManager; + _log: Logger; + + constructor(id: string, manager: CanvasManager) { + this.id = id; + this._manager = manager; + this._log = this._manager.buildLogger(this._getLoggingContext); + } + /** + * Get a serializable representation of the entity. + */ + abstract repr(): JSONObject; + + _getLoggingContext = (extra?: Record) => { + return { + ...this._manager._getLoggingContext(), + layerId: this.id, + ...extra, + }; + }; +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts index 7ba26b02f9d..dfe1ee5708c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts @@ -1,33 +1,28 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; +import { CanvasObject } from 'features/controlLayers/konva/CanvasObject'; import type { EraserLine } from 'features/controlLayers/store/types'; import { RGBA_RED } from 'features/controlLayers/store/types'; import Konva from 'konva'; -export class CanvasEraserLine { +export class CanvasEraserLine extends CanvasObject { static NAME_PREFIX = 'eraser-line'; static GROUP_NAME = `${CanvasEraserLine.NAME_PREFIX}_group`; static LINE_NAME = `${CanvasEraserLine.NAME_PREFIX}_line`; + static TYPE = 'eraser_line'; state: EraserLine; - - type = 'eraser_line'; - id: string; konva: { group: Konva.Group; line: Konva.Line; }; - parent: CanvasLayer; - constructor(state: EraserLine, parent: CanvasLayer) { - const { id, strokeWidth, clip, points } = state; - - this.id = id; + super(state.id, parent); + this._log.trace({ state }, 'Creating eraser line'); - this.parent = parent; - this.parent._log.trace(`Creating eraser line ${this.id}`); + const { strokeWidth, clip, points } = state; this.konva = { group: new Konva.Group({ @@ -37,7 +32,6 @@ export class CanvasEraserLine { }), line: new Konva.Line({ name: CanvasEraserLine.LINE_NAME, - id, listening: false, shadowForStrokeEnabled: false, strokeWidth, @@ -56,7 +50,7 @@ export class CanvasEraserLine { update(state: EraserLine, force?: boolean): boolean { if (force || this.state !== state) { - this.parent._log.trace(`Updating eraser line ${this.id}`); + this._log.trace({ state }, 'Updating eraser line'); const { points, clip, strokeWidth } = state; this.konva.line.setAttrs({ // A line with only one point will not be rendered, so we duplicate the points to make it visible @@ -72,23 +66,20 @@ export class CanvasEraserLine { } destroy() { - this.parent._log.trace(`Destroying eraser line ${this.id}`); + this._log.trace('Destroying eraser line'); this.konva.group.destroy(); } - show() { - this.konva.group.visible(true); - } - - hide() { - this.konva.group.visible(false); + setVisibility(isVisible: boolean): void { + this._log.trace({ isVisible }, 'Setting brush line visibility'); + this.konva.group.visible(isVisible); } repr() { return { id: this.id, - type: this.type, - parent: this.parent.id, + type: CanvasEraserLine.TYPE, + parent: this._parent.id, 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 9fb722a3bc0..96c14ba27d0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -1,26 +1,24 @@ import { deepClone } from 'common/util/deepClone'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; +import { CanvasObject } from 'features/controlLayers/konva/CanvasObject'; +import type { CanvasStagingArea } from 'features/controlLayers/konva/CanvasStagingArea'; import { FILTER_MAP } from 'features/controlLayers/konva/filters'; import { loadImage } from 'features/controlLayers/konva/util'; import type { ImageObject } from 'features/controlLayers/store/types'; import { t } from 'i18next'; import Konva from 'konva'; import { getImageDTO } from 'services/api/endpoints/images'; -import { assert } from 'tsafe'; -export class CanvasImage { +export class CanvasImage extends CanvasObject { static NAME_PREFIX = 'canvas-image'; static GROUP_NAME = `${CanvasImage.NAME_PREFIX}_group`; static IMAGE_NAME = `${CanvasImage.NAME_PREFIX}_image`; static PLACEHOLDER_GROUP_NAME = `${CanvasImage.NAME_PREFIX}_placeholder-group`; static PLACEHOLDER_RECT_NAME = `${CanvasImage.NAME_PREFIX}_placeholder-rect`; static PLACEHOLDER_TEXT_NAME = `${CanvasImage.NAME_PREFIX}_placeholder-text`; + static TYPE = 'image'; state: ImageObject; - - type = 'image'; - - id: string; konva: { group: Konva.Group; placeholder: { group: Konva.Group; rect: Konva.Rect; text: Konva.Text }; @@ -30,14 +28,11 @@ export class CanvasImage { isLoading: boolean; isError: boolean; - parent: CanvasLayer; - - constructor(state: ImageObject, parent: CanvasLayer) { - const { id, width, height, x, y } = state; - this.id = id; + constructor(state: ImageObject, parent: CanvasLayer | CanvasStagingArea) { + super(state.id, parent); + this._log.trace({ state }, 'Creating image'); - this.parent = parent; - this.parent._log.trace(`Creating image ${this.id}`); + const { width, height, x, y } = state; this.konva = { group: new Konva.Group({ name: CanvasImage.GROUP_NAME, listening: false, x, y }), @@ -78,7 +73,7 @@ export class CanvasImage { async updateImageSource(imageName: string) { try { - this.parent._log.trace(`Updating image source ${this.id}`); + this._log.trace({ imageName }, 'Updating image source'); this.isLoading = true; this.konva.group.visible(true); @@ -89,7 +84,10 @@ export class CanvasImage { } const imageDTO = await getImageDTO(imageName); - assert(imageDTO !== null, 'imageDTO is null'); + if (imageDTO === null) { + this._log.error({ imageName }, 'Image not found'); + return; + } const imageEl = await loadImage(imageDTO.image_url); if (this.konva.image) { @@ -120,6 +118,7 @@ export class CanvasImage { this.isError = false; this.konva.placeholder.group.visible(false); } catch { + this._log({ imageName }, 'Failed to load image'); this.konva.image?.visible(false); this.imageName = null; this.isLoading = false; @@ -131,7 +130,7 @@ export class CanvasImage { async update(state: ImageObject, force?: boolean): Promise { if (this.state !== state || force) { - this.parent._log.trace(`Updating image ${this.id}`); + this._log.trace({ state }, 'Updating image'); const { width, height, x, y, image, filters } = state; if (this.state.image.name !== image.name || force) { @@ -155,23 +154,20 @@ export class CanvasImage { } destroy() { - this.parent._log.trace(`Destroying image ${this.id}`); + this._log.trace('Destroying image'); this.konva.group.destroy(); } - show() { - this.konva.group.visible(true); - } - - hide() { - this.konva.group.visible(false); + setVisibility(isVisible: boolean): void { + this._log.trace({ isVisible }, 'Setting image visibility'); + this.konva.group.visible(isVisible); } repr() { return { id: this.id, - type: this.type, - parent: this.parent.id, + type: CanvasImage.TYPE, + parent: this._parent.id, imageName: this.imageName, isLoading: this.isLoading, isError: this.isError, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 7cf905053cb..665d7daacdb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -1,12 +1,12 @@ import { getStore } from 'app/store/nanostores/store'; import { deepClone } from 'common/util/deepClone'; import { CanvasBrushLine } from 'features/controlLayers/konva/CanvasBrushLine'; +import { CanvasEntity } from 'features/controlLayers/konva/CanvasEntity'; import { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine'; import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; import { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; -import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; -import { konvaNodeToBlob, mapId, nanoid, previewBlob } from 'features/controlLayers/konva/util'; +import { getPrefixedId, konvaNodeToBlob, mapId, previewBlob } from 'features/controlLayers/konva/util'; import { layerRasterized } from 'features/controlLayers/store/canvasV2Slice'; import { type BrushLine, @@ -20,11 +20,10 @@ import { } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { debounce, get } from 'lodash-es'; -import type { Logger } from 'roarr'; import { uploadImage } from 'services/api/endpoints/images'; import { assert } from 'tsafe'; -export class CanvasLayer { +export class CanvasLayer extends CanvasEntity { static NAME_PREFIX = 'layer'; static LAYER_NAME = `${CanvasLayer.NAME_PREFIX}_layer`; static TRANSFORMER_NAME = `${CanvasLayer.NAME_PREFIX}_transformer`; @@ -36,8 +35,7 @@ export class CanvasLayer { _drawingBuffer: BrushLine | EraserLine | RectShape | null; _state: LayerEntity; - id: string; - manager: CanvasManager; + type = 'layer'; konva: { layer: Konva.Layer; @@ -48,7 +46,6 @@ export class CanvasLayer { }; objects: Map; - _log: Logger; _bboxNeedsUpdate: boolean; _isFirstRender: boolean; @@ -59,8 +56,9 @@ export class CanvasLayer { bbox: Rect; constructor(state: LayerEntity, manager: CanvasManager) { - this.id = state.id; - this.manager = manager; + super(state.id, manager); + this._log.debug({ state }, 'Creating layer'); + this.konva = { layer: new Konva.Layer({ id: this.id, name: CanvasLayer.LAYER_NAME, listening: false }), bbox: new Konva.Rect({ @@ -79,7 +77,7 @@ export class CanvasLayer { rotateEnabled: true, flipEnabled: true, listening: false, - padding: this.manager.getTransformerPadding(), + padding: this._manager.getTransformerPadding(), stroke: 'hsl(200deg 76% 59%)', // invokeBlue.400 keepRatio: false, }), @@ -149,8 +147,8 @@ export class CanvasLayer { // The bbox should be updated to reflect the new position of the interaction rect, taking into account its padding // and border this.konva.bbox.setAttrs({ - x: this.konva.interactionRect.x() - this.manager.getScaledBboxPadding(), - y: this.konva.interactionRect.y() - this.manager.getScaledBboxPadding(), + x: this.konva.interactionRect.x() - this._manager.getScaledBboxPadding(), + y: this.konva.interactionRect.y() - this._manager.getScaledBboxPadding(), }); // The object group is translated by the difference between the interaction rect's new and old positions (which is @@ -169,7 +167,7 @@ export class CanvasLayer { return; } - this.manager.stateApi.onPosChanged( + this._manager.stateApi.onPosChanged( { id: this.id, position: { @@ -190,11 +188,10 @@ export class CanvasLayer { this.isTransforming = false; this._isFirstRender = true; this.isPendingBboxCalculation = false; - this._log = this.manager.getLogger(`layer_${this.id}`); } destroy(): void { - this._log.debug(`Layer ${this.id} - destroying`); + this._log.debug('Destroying layer'); this.konva.layer.destroy(); } @@ -221,21 +218,21 @@ export class CanvasLayer { // a non-buffer object, and we won't trigger things like bbox calculation if (drawingBuffer.type === 'brush_line') { - drawingBuffer.id = getBrushLineId(this.id, nanoid()); - this.manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: drawingBuffer }, 'layer'); + drawingBuffer.id = getPrefixedId('brush_line'); + this._manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: drawingBuffer }, 'layer'); } else if (drawingBuffer.type === 'eraser_line') { - drawingBuffer.id = getEraserLineId(this.id, nanoid()); - this.manager.stateApi.onEraserLineAdded({ id: this.id, eraserLine: drawingBuffer }, 'layer'); + drawingBuffer.id = getPrefixedId('brush_line'); + this._manager.stateApi.onEraserLineAdded({ id: this.id, eraserLine: drawingBuffer }, 'layer'); } else if (drawingBuffer.type === 'rect_shape') { - drawingBuffer.id = getRectShapeId(this.id, nanoid()); - this.manager.stateApi.onRectShapeAdded({ id: this.id, rectShape: drawingBuffer }, 'layer'); + drawingBuffer.id = getPrefixedId('brush_line'); + this._manager.stateApi.onRectShapeAdded({ id: this.id, rectShape: drawingBuffer }, 'layer'); } } async update(arg?: { state: LayerEntity; toolState: CanvasV2State['tool']; isSelected: boolean }) { const state = get(arg, 'state', this._state); - const toolState = get(arg, 'toolState', this.manager.stateApi.getToolState()); - const isSelected = get(arg, 'isSelected', this.manager.stateApi.getIsSelected(this.id)); + const toolState = get(arg, 'toolState', this._manager.stateApi.getToolState()); + const isSelected = get(arg, 'isSelected', this._manager.stateApi.getIsSelected(this.id)); if (!this._isFirstRender && state === this._state) { this._log.trace('State unchanged, skipping update'); @@ -277,7 +274,7 @@ export class CanvasLayer { updatePosition(arg?: { position: Coordinate }) { this._log.trace('Updating position'); const position = get(arg, 'position', this._state.position); - const bboxPadding = this.manager.getScaledBboxPadding(); + const bboxPadding = this._manager.getScaledBboxPadding(); this.konva.objectGroup.setAttrs({ x: position.x + this.bbox.x, @@ -339,8 +336,8 @@ export class CanvasLayer { updateInteraction(arg?: { toolState: CanvasV2State['tool']; isSelected: boolean }) { this._log.trace('Updating interaction'); - const toolState = get(arg, 'toolState', this.manager.stateApi.getToolState()); - const isSelected = get(arg, 'isSelected', this.manager.stateApi.getIsSelected(this.id)); + const toolState = get(arg, 'toolState', this._manager.stateApi.getToolState()); + const isSelected = get(arg, 'isSelected', this._manager.stateApi.getIsSelected(this.id)); if (this.objects.size === 0) { // The layer is totally empty, we can just disable the layer @@ -397,7 +394,7 @@ export class CanvasLayer { if (this.bbox.width === 0 || this.bbox.height === 0) { if (this.objects.size > 0) { // The layer is fully transparent but has objects - reset it - this.manager.stateApi.onEntityReset({ id: this.id }, 'layer'); + this._manager.stateApi.onEntityReset({ id: this.id }, 'layer'); } this.konva.bbox.visible(false); this.konva.interactionRect.visible(false); @@ -407,8 +404,8 @@ export class CanvasLayer { this.konva.bbox.visible(true); this.konva.interactionRect.visible(true); - const onePixel = this.manager.getScaledPixel(); - const bboxPadding = this.manager.getScaledBboxPadding(); + const onePixel = this._manager.getScaledPixel(); + const bboxPadding = this._manager.getScaledBboxPadding(); this.konva.bbox.setAttrs({ x: this._state.position.x + this.bbox.x - bboxPadding, @@ -434,8 +431,8 @@ export class CanvasLayer { syncStageScale() { this._log.trace('Syncing scale to stage'); - const onePixel = this.manager.getScaledPixel(); - const bboxPadding = this.manager.getScaledBboxPadding(); + const onePixel = this._manager.getScaledPixel(); + const bboxPadding = this._manager.getScaledBboxPadding(); this.konva.bbox.setAttrs({ x: this.konva.interactionRect.x() - bboxPadding, @@ -515,7 +512,7 @@ export class CanvasLayer { // When transforming, we want the stage to still be movable if the view tool is selected. If the transformer or // interaction rect are listening, it will interrupt the stage's drag events. So we should disable listening // when the view tool is selected - const listening = this.manager.stateApi.getToolState().selected !== 'view'; + const listening = this._manager.stateApi.getToolState().selected !== 'view'; this.konva.layer.listening(listening); this.konva.interactionRect.listening(listening); @@ -546,12 +543,12 @@ export class CanvasLayer { const interactionRectClone = this.konva.interactionRect.clone(); const rect = interactionRectClone.getClientRect(); const blob = await konvaNodeToBlob(objectGroupClone, rect); - if (this.manager._isDebugging) { + if (this._manager._isDebugging) { previewBlob(blob, 'Rasterized layer'); } const imageDTO = await uploadImage(blob, `${this.id}_rasterized.png`, 'other', true); const { dispatch } = getStore(); - const imageObject = imageDTOToImageObject(this.id, nanoid(), imageDTO); + const imageObject = imageDTOToImageObject(imageDTO); await this._renderObject(imageObject, true); for (const obj of this.objects.values()) { if (obj.id !== imageObject.id) { @@ -632,7 +629,7 @@ export class CanvasLayer { return; } const imageData = ctx.getImageData(0, 0, rect.width, rect.height); - this.manager.requestBbox( + this._manager.requestBbox( { buffer: imageData.data.buffer, width: imageData.width, height: imageData.height }, (extents) => { this.rect = deepClone(rect); @@ -658,7 +655,7 @@ export class CanvasLayer { repr() { return { id: this.id, - type: 'layer', + type: this.type, state: deepClone(this._state), rect: deepClone(this.rect), bbox: deepClone(this.bbox), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 1fd787ce3bb..5ddfc1b6811 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -1,6 +1,7 @@ import type { Store } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; +import type { JSONObject } from 'common/types'; import { CanvasInitialImage } from 'features/controlLayers/konva/CanvasInitialImage'; import { CanvasProgressPreview } from 'features/controlLayers/konva/CanvasProgressPreview'; import { @@ -107,7 +108,15 @@ export class CanvasManager { this._prevState = this.stateApi.getState(); this._isFirstRender = true; - this.log = logger('canvas'); + this.log = logger('canvas').child((message) => { + return { + ...message, + context: { + ...message.context, + ...this._getLoggingContext(), + }, + }; + }); this.workerLog = logger('worker'); this.util = { @@ -173,11 +182,6 @@ export class CanvasManager { this._isDebugging = false; } - getLogger(namespace: string) { - const managerNamespace = this.log.getContext().namespace; - return this.log.child({ namespace: `${managerNamespace}.${namespace}` }); - } - requestBbox(data: Omit, onComplete: (extents: Extents | null) => void) { const id = nanoid(); const task: GetBboxTask = { @@ -330,7 +334,6 @@ export class CanvasManager { for (const canvasLayer of this.layers.values()) { if (!state.layers.entities.find((l) => l.id === canvasLayer.id)) { - this.log.debug(`Destroying deleted layer ${canvasLayer.id}`); await canvasLayer.destroy(); this.layers.delete(canvasLayer.id); } @@ -339,7 +342,6 @@ export class CanvasManager { for (const entityState of state.layers.entities) { let adapter = this.layers.get(entityState.id); if (!adapter) { - this.log.debug(`Creating layer layer ${entityState.id}`); adapter = new CanvasLayer(entityState, this); this.layers.set(adapter.id, adapter); this.stage.add(adapter.konva.layer); @@ -562,9 +564,29 @@ export class CanvasManager { } } + _getLoggingContext() { + return { + // timestamp: new Date().toISOString(), + }; + } + + buildLogger(getContext: () => JSONObject): Logger { + return this.log.child((message) => { + return { + ...message, + context: { + ...message.context, + ...getContext(), + }, + }; + }); + } + logDebugInfo() { + // eslint-disable-next-line no-console console.log(this); for (const layer of this.layers.values()) { + // eslint-disable-next-line no-console console.log(layer); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObject.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObject.ts new file mode 100644 index 00000000000..3a07b77e835 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObject.ts @@ -0,0 +1,48 @@ +import type { JSONObject } from 'common/types'; +import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import type { CanvasStagingArea } from 'features/controlLayers/konva/CanvasStagingArea'; +import type { Logger } from 'roarr'; + +export abstract class CanvasObject { + id: string; + + _parent: CanvasLayer | CanvasStagingArea; + _manager: CanvasManager; + _log: Logger; + + constructor(id: string, parent: CanvasLayer | CanvasStagingArea) { + this.id = id; + this._parent = parent; + this._manager = parent._manager; + this._log = this._manager.buildLogger(this._getLoggingContext); + } + + /** + * Destroy the object's konva nodes. + */ + abstract destroy(): void; + + /** + * Set the visibility of the object's konva nodes. + */ + abstract setVisibility(isVisible: boolean): void; + + /** + * Get a serializable representation of the object. + */ + abstract repr(): JSONObject; + + /** + * Get the logging context for this object. + * @param extra Extra data to merge into the context + * @returns The logging context for this object + */ + _getLoggingContext = (extra?: Record) => { + return { + ...this._parent._getLoggingContext(), + objectId: this.id, + ...extra, + }; + }; +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts index 13d1ef1d657..96b4ac1c060 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts @@ -1,39 +1,32 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; +import { CanvasObject } from 'features/controlLayers/konva/CanvasObject'; import type { RectShape } from 'features/controlLayers/store/types'; import Konva from 'konva'; -export class CanvasRect { +export class CanvasRect extends CanvasObject { static NAME_PREFIX = 'canvas-rect'; static GROUP_NAME = `${CanvasRect.NAME_PREFIX}_group`; static RECT_NAME = `${CanvasRect.NAME_PREFIX}_rect`; + static TYPE = 'rect'; state: RectShape; - - type = 'rect'; - - id: string; konva: { group: Konva.Group; rect: Konva.Rect; }; - parent: CanvasLayer; - constructor(state: RectShape, parent: CanvasLayer) { - const { id, x, y, width, height, color } = state; + super(state.id, parent); + this._log.trace({ state }, 'Creating rect'); - this.id = id; - - this.parent = parent; - this.parent._log.trace(`Creating rect ${this.id}`); + const { x, y, width, height, color } = state; this.konva = { group: new Konva.Group({ name: CanvasRect.GROUP_NAME, listening: false }), rect: new Konva.Rect({ name: CanvasRect.RECT_NAME, - id, x, y, width, @@ -48,7 +41,7 @@ export class CanvasRect { update(state: RectShape, force?: boolean): boolean { if (this.state !== state || force) { - this.parent._log.trace(`Updating rect ${this.id}`); + this._log.trace({ state }, 'Updating rect'); const { x, y, width, height, color } = state; this.konva.rect.setAttrs({ x, @@ -65,23 +58,20 @@ export class CanvasRect { } destroy() { - this.parent._log.trace(`Destroying rect ${this.id}`); + this._log.trace('Destroying rect'); this.konva.group.destroy(); } - show() { - this.konva.group.visible(true); - } - - hide() { - this.konva.group.visible(false); + setVisibility(isVisible: boolean): void { + this._log.trace({ isVisible }, 'Setting rect visibility'); + this.konva.group.visible(isVisible); } repr() { return { id: this.id, - type: this.type, - parent: this.parent.id, + type: CanvasRect.TYPE, + parent: this._parent.id, state: deepClone(this.state), }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts index cd4ae82e326..c7f83c51730 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts @@ -1,29 +1,30 @@ +import { CanvasEntity } from 'features/controlLayers/konva/CanvasEntity'; import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { StagingAreaImage } from 'features/controlLayers/store/types'; import Konva from 'konva'; -export class CanvasStagingArea { +export class CanvasStagingArea extends CanvasEntity { static NAME_PREFIX = 'staging-area'; static GROUP_NAME = `${CanvasStagingArea.NAME_PREFIX}_group`; + type = 'staging_area'; konva: { group: Konva.Group }; image: CanvasImage | null; selectedImage: StagingAreaImage | null; - manager: CanvasManager; constructor(manager: CanvasManager) { - this.manager = manager; + super('staging-area', manager); this.konva = { group: new Konva.Group({ name: CanvasStagingArea.GROUP_NAME, listening: false }) }; this.image = null; this.selectedImage = null; } async render() { - const session = this.manager.stateApi.getSession(); - const bboxRect = this.manager.stateApi.getBbox().rect; - const shouldShowStagedImage = this.manager.stateApi.getShouldShowStagedImage(); + const session = this._manager.stateApi.getSession(); + const bboxRect = this._manager.stateApi.getBbox().rect; + const shouldShowStagedImage = this._manager.stateApi.getShouldShowStagedImage(); this.selectedImage = session.stagedImages[session.selectedStagedImageIndex] ?? null; @@ -32,34 +33,45 @@ export class CanvasStagingArea { if (!this.image) { const { image_name, width, height } = imageDTO; - this.image = new CanvasImage({ - id: 'staging-area-image', - type: 'image', - x: 0, - y: 0, - width, - height, - filters: [], - image: { - name: image_name, + this.image = new CanvasImage( + { + id: 'staging-area-image', + type: 'image', + x: 0, + y: 0, width, height, + filters: [], + image: { + name: image_name, + width, + height, + }, }, - }); + this + ); this.konva.group.add(this.image.konva.group); } if (!this.image.isLoading && !this.image.isError && this.image.imageName !== imageDTO.image_name) { - this.image.image?.width(imageDTO.width); - this.image.image?.height(imageDTO.height); + this.image.konva.image?.width(imageDTO.width); + this.image.konva.image?.height(imageDTO.height); this.image.konva.group.x(bboxRect.x + offsetX); this.image.konva.group.y(bboxRect.y + offsetY); await this.image.updateImageSource(imageDTO.image_name); - this.manager.stateApi.resetLastProgressEvent(); + this._manager.stateApi.resetLastProgressEvent(); } this.image.konva.group.visible(shouldShowStagedImage); } else { this.image?.konva.group.visible(false); } } + + repr() { + return { + id: this.id, + type: this.type, + selectedImage: this.selectedImage, + }; + } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index e3d734a8a6e..967b42ebca2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -248,6 +248,9 @@ export class CanvasStateApi { getIsSelected = (id: string) => { return this.getSelectedEntity()?.id === id; }; + getLogLevel = () => { + return this.store.getState().system.consoleLogLevel; + }; // Read-only state, derived from nanostores resetLastProgressEvent = () => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 90d29178288..f590ea49396 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -1,5 +1,5 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { getScaledFlooredCursorPosition, nanoid } from 'features/controlLayers/konva/util'; +import { getObjectId, getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util'; import type { CanvasV2State, Coordinate, @@ -14,7 +14,6 @@ import type { KonvaEventObject } from 'konva/lib/Node'; import { clamp } from 'lodash-es'; import { BRUSH_SPACING_TARGET_SCALE, CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from './constants'; -import { getBrushLineId, getEraserLineId, getRectShapeId } from './naming'; /** * Updates the last cursor position atom with the current cursor position, returning the new position or `null` if the @@ -187,7 +186,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getBrushLineId(selectedEntityAdapter.id, nanoid(), true), + id: getObjectId('brush_line', true), type: 'brush_line', points: [ // The last point of the last line is already normalized to the entity's coordinates @@ -205,7 +204,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getBrushLineId(selectedEntityAdapter.id, nanoid(), true), + id: getObjectId('brush_line', true), type: 'brush_line', points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y], strokeWidth: toolState.brush.width, @@ -224,7 +223,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getEraserLineId(selectedEntityAdapter.id, nanoid(), true), + id: getObjectId('eraser_line', true), type: 'eraser_line', points: [ // The last point of the last line is already normalized to the entity's coordinates @@ -241,7 +240,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getEraserLineId(selectedEntityAdapter.id, nanoid(), true), + id: getObjectId('eraser_line', true), type: 'eraser_line', points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y], strokeWidth: toolState.eraser.width, @@ -256,7 +255,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getRectShapeId(selectedEntityAdapter.id, nanoid(), true), + id: getObjectId('rect_shape', true), type: 'rect_shape', x: pos.x - selectedEntity.position.x, y: pos.y - selectedEntity.position.y, @@ -356,7 +355,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getBrushLineId(selectedEntityAdapter.id, nanoid(), true), + id: getObjectId('brush_line', true), type: 'brush_line', points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y], strokeWidth: toolState.brush.width, @@ -388,7 +387,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getEraserLineId(selectedEntityAdapter.id, nanoid(), true), + id: getObjectId('eraser_line', true), type: 'eraser_line', points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y], strokeWidth: toolState.eraser.width, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts index a5d3cdde2e9..2fbe23ccff7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts @@ -6,13 +6,13 @@ export const getRGId = (entityId: string) => `region_${entityId}`; export const getLayerId = (entityId: string) => `layer_${entityId}`; export const getBrushLineId = (entityId: string, lineId: string, isBuffer?: boolean) => - `${entityId}.${isBuffer ? 'buffer_' : ''}brush_line_${lineId}`; + `${isBuffer ? 'buffer_' : ''}brush_line_${lineId}`; export const getEraserLineId = (entityId: string, lineId: string, isBuffer?: boolean) => - `${entityId}.${isBuffer ? 'buffer_' : ''}eraser_line_${lineId}`; + `${isBuffer ? 'buffer_' : ''}eraser_line_${lineId}`; export const getRectShapeId = (entityId: string, rectId: string, isBuffer?: boolean) => - `${entityId}.${isBuffer ? 'buffer_' : ''}rect_${rectId}`; -export const getImageObjectId = (entityId: string, imageId: string) => `${entityId}.image_${imageId}`; -export const getObjectGroupId = (entityId: string, groupId: string) => `${entityId}.objectGroup_${groupId}`; + `${isBuffer ? 'buffer_' : ''}rect_${rectId}`; +export const getImageObjectId = (entityId: string, imageId: string) => `image_${imageId}`; +export const getObjectGroupId = (entityId: string, groupId: string) => `objectGroup_${groupId}`; export const getLayerBboxId = (entityId: string) => `${entityId}.bbox`; export const getCAId = (entityId: string) => `control_adapter_${entityId}`; export const getIPAId = (entityId: string) => `ip_adapter_${entityId}`; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index 9c8740d2be0..8273c0455e7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -1,12 +1,12 @@ import { getImageDataTransparency } from 'common/util/arrayBuffer'; import { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { GenerationMode, Rect, RgbaColor } from 'features/controlLayers/store/types'; +import type { GenerationMode, Rect, RenderableObject, RgbaColor } from 'features/controlLayers/store/types'; import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers'; import Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; import type { Vector2d } from 'konva/lib/types'; -import { customAlphabet, urlAlphabet } from 'nanoid'; +import { customAlphabet } from 'nanoid'; import type { ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; @@ -575,4 +575,19 @@ export function loadImage(src: string, imageEl?: HTMLImageElement): Promise ({ payload: { ...payload, objectId: uuidv4() } }), }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/initialImageReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/initialImageReducers.ts index b30af45ab5b..f50edeefaa2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/initialImageReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/initialImageReducers.ts @@ -25,7 +25,7 @@ export const initialImageReducers = { if (!state.initialImage) { return; } - const newImageObject = imageDTOToImageObject('initial_image', 'initial_image_object', imageDTO); + const newImageObject = imageDTOToImageObject(imageDTO); if (isEqual(newImageObject, state.initialImage.imageObject)) { return; } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts index ce29909b7d0..60c4c78d08f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts @@ -4,13 +4,7 @@ import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; -import type { - CanvasV2State, - CLIPVisionModelV2, - IPAdapterConfig, - IPAdapterEntity, - IPMethodV2, -} from './types'; +import type { CanvasV2State, CLIPVisionModelV2, IPAdapterConfig, IPAdapterEntity, IPMethodV2 } from './types'; import { imageDTOToImageObject } from './types'; export const selectIPA = (state: CanvasV2State, id: string) => state.ipAdapters.entities.find((ipa) => ipa.id === id); @@ -61,7 +55,7 @@ export const ipAdaptersReducers = { if (!ipa) { return; } - ipa.imageObject = imageDTO ? imageDTOToImageObject(id, objectId, imageDTO) : null; + ipa.imageObject = imageDTO ? imageDTOToImageObject(imageDTO) : null; }, prepare: (payload: { id: string; imageDTO: ImageDTO | null }) => ({ payload: { ...payload, objectId: uuidv4() } }), }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index 56e7fda305d..a05849153ac 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -1,7 +1,8 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; -import { nanoid } from 'features/controlLayers/konva/util'; +import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { IRect } from 'konva/lib/types'; +import { merge } from 'lodash-es'; import type { ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; @@ -16,7 +17,6 @@ import type { PositionChangedArg, RectShape, ScaleChangedArg, - StagingAreaImage, } from './types'; import { imageDTOToImageObject, imageDTOToImageWithDims } from './types'; @@ -29,42 +29,23 @@ export const selectLayerOrThrow = (state: CanvasV2State, id: string) => { export const layersReducers = { layerAdded: { - reducer: (state, action: PayloadAction<{ id: string }>) => { + reducer: (state, action: PayloadAction<{ id: string; overrides?: Partial }>) => { const { id } = action.payload; - state.layers.entities.push({ + const layer: LayerEntity = { id, type: 'layer', isEnabled: true, objects: [], opacity: 1, position: { x: 0, y: 0 }, - }); + }; + merge(layer, action.payload.overrides); + state.layers.entities.push(layer); state.selectedEntityIdentifier = { type: 'layer', id }; state.layers.imageCache = null; }, - prepare: () => ({ payload: { id: nanoid() } }), - }, - layerAddedFromStagingArea: { - reducer: ( - state, - action: PayloadAction<{ id: string; objectId: string; stagingAreaImage: StagingAreaImage; position: Coordinate }> - ) => { - const { id, objectId, stagingAreaImage, position } = action.payload; - const { imageDTO, offsetX, offsetY } = stagingAreaImage; - const imageObject = imageDTOToImageObject(id, objectId, imageDTO); - state.layers.entities.push({ - id, - type: 'layer', - isEnabled: true, - objects: [imageObject], - opacity: 1, - position: { x: position.x + offsetX, y: position.y + offsetY }, - }); - state.selectedEntityIdentifier = { type: 'layer', id }; - state.layers.imageCache = null; - }, - prepare: (payload: { stagingAreaImage: StagingAreaImage; position: Coordinate }) => ({ - payload: { ...payload, id: nanoid(), objectId: nanoid() }, + prepare: (payload: { overrides?: Partial }) => ({ + payload: { ...payload, id: getPrefixedId('layer') }, }), }, layerRecalled: (state, action: PayloadAction<{ data: LayerEntity }>) => { @@ -227,27 +208,22 @@ export const layersReducers = { layer.position.y = Math.round(position.y); state.layers.imageCache = null; }, - layerImageAdded: { - reducer: ( - state, - action: PayloadAction - ) => { - const { id, objectId, imageDTO, pos } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - const imageObject = imageDTOToImageObject(id, objectId, imageDTO); - if (pos) { - imageObject.x = pos.x; - imageObject.y = pos.y; - } - layer.objects.push(imageObject); - state.layers.imageCache = null; - }, - prepare: (payload: ImageObjectAddedArg & { pos?: { x: number; y: number } }) => ({ - payload: { ...payload, objectId: nanoid() }, - }), + layerImageAdded: ( + state, + action: PayloadAction + ) => { + const { id, imageDTO, pos } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + const imageObject = imageDTOToImageObject(imageDTO); + if (pos) { + imageObject.x = pos.x; + imageObject.y = pos.y; + } + layer.objects.push(imageObject); + state.layers.imageCache = null; }, layerImageCacheChanged: (state, action: PayloadAction<{ imageDTO: ImageDTO | null }>) => { const { imageDTO } = action.payload; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index c23708b9955..901bda00e11 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -2,7 +2,7 @@ import type { CanvasControlAdapter } from 'features/controlLayers/konva/CanvasCo import { CanvasInpaintMask } from 'features/controlLayers/konva/CanvasInpaintMask'; import { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import { CanvasRegion } from 'features/controlLayers/konva/CanvasRegion'; -import { getImageObjectId } from 'features/controlLayers/konva/naming'; +import { getObjectId } from 'features/controlLayers/konva/util'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { AspectRatioState } from 'features/parameters/components/DocumentSize/types'; import type { @@ -777,15 +777,10 @@ export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO) height, }); -export const imageDTOToImageObject = ( - entityId: string, - objectId: string, - imageDTO: ImageDTO, - overrides?: Partial -): ImageObject => { +export const imageDTOToImageObject = (imageDTO: ImageDTO, overrides?: Partial): ImageObject => { const { width, height, image_name } = imageDTO; return { - id: getImageObjectId(entityId, objectId), + id: getObjectId('image'), type: 'image', x: 0, y: 0, From 28affcc60a51c93bea6b28057e4a7f63e18a8972 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:22:00 +1000 Subject: [PATCH 261/678] fix(ui): do not reset layer on first render --- .../src/features/controlLayers/konva/CanvasLayer.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 665d7daacdb..512c5b11f73 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -325,6 +325,8 @@ export class CanvasLayer extends CanvasEntity { if (didUpdate) { this.calculateBbox(); } + + this._isFirstRender = false; } updateOpacity(arg?: { opacity: number }) { @@ -392,7 +394,8 @@ export class CanvasLayer extends CanvasEntity { // If the bbox has no width or height, that means the layer is fully transparent. This can happen if it is only // eraser lines, fully clipped brush lines or if it has been fully erased. if (this.bbox.width === 0 || this.bbox.height === 0) { - if (this.objects.size > 0) { + // We shouldn't reset on the first render - the bbox will be calculated on the next render + if (!this._isFirstRender && this.objects.size > 0) { // The layer is fully transparent but has objects - reset it this._manager.stateApi.onEntityReset({ id: this.id }, 'layer'); } @@ -579,6 +582,7 @@ export class CanvasLayer extends CanvasEntity { this.isPendingBboxCalculation = true; if (this.objects.size === 0) { + this._log.trace('No objects, resetting bbox'); this.rect = this.getDefaultRect(); this.bbox = this.getDefaultRect(); this.isPendingBboxCalculation = false; @@ -632,9 +636,9 @@ export class CanvasLayer extends CanvasEntity { this._manager.requestBbox( { buffer: imageData.data.buffer, width: imageData.width, height: imageData.height }, (extents) => { - this.rect = deepClone(rect); if (extents) { const { minX, minY, maxX, maxY } = extents; + this.rect = deepClone(rect); this.bbox = { x: rect.x + minX, y: rect.y + minY, @@ -642,7 +646,8 @@ export class CanvasLayer extends CanvasEntity { height: maxY - minY, }; } else { - this.bbox = deepClone(rect); + this.bbox = this.getDefaultRect(); + this.rect = this.getDefaultRect(); } this.isPendingBboxCalculation = false; this._log.trace({ bbox: this.bbox, rect: this.rect, extents }, `Got bbox from worker`); From 1f3163942a8cca984b3672bd0f53592c836bf18d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:22:31 +1000 Subject: [PATCH 262/678] fix(nodes): fix canvas mask erode it wasn't eroding enough and caused incorrect transparency in result images --- invokeai/app/invocations/image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index a80cddbe9ba..340dc32f96e 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -1038,7 +1038,7 @@ class CanvasV2MaskAndCropInvocation(BaseInvocation, WithMetadata, WithBoard): def _prepare_mask(self, mask: Image.Image) -> Image.Image: mask_array = numpy.array(mask) kernel = numpy.ones((self.mask_blur, self.mask_blur), numpy.uint8) - dilated_mask_array = cv2.erode(mask_array, kernel) + dilated_mask_array = cv2.erode(mask_array, kernel, iterations=3) dilated_mask = Image.fromarray(dilated_mask_array) if self.mask_blur > 0: mask = dilated_mask.filter(ImageFilter.GaussianBlur(self.mask_blur)) From 3f2c1139eac1a26364e6346ca9412d93b5bb95dd Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:53:46 +1000 Subject: [PATCH 263/678] fix(ui): layer visibility toggle --- .../web/src/features/controlLayers/konva/CanvasLayer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 512c5b11f73..360f6c59866 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -268,7 +268,7 @@ export class CanvasLayer extends CanvasEntity { this._log.trace('Updating visibility'); const isEnabled = get(arg, 'isEnabled', this._state.isEnabled); const hasObjects = this.objects.size > 0 || this._drawingBuffer !== null; - this.konva.layer.visible(isEnabled || hasObjects); + this.konva.layer.visible(isEnabled && hasObjects); } updatePosition(arg?: { position: Coordinate }) { From 286deb277b751afce1747808e60348fe1e5d4f16 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 1 Aug 2024 13:10:44 +1000 Subject: [PATCH 264/678] fix(ui): pixel-perfect transforms --- .../controlLayers/konva/CanvasLayer.ts | 143 +++++++++++++----- .../controlLayers/konva/CanvasManager.ts | 6 +- 2 files changed, 107 insertions(+), 42 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 360f6c59866..da6480981ee 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -94,38 +94,52 @@ export class CanvasLayer extends CanvasEntity { this.konva.layer.add(this.konva.interactionRect); this.konva.layer.add(this.konva.bbox); + this.konva.transformer.anchorDragBoundFunc((oldPos: Coordinate, newPos: Coordinate) => { + if (this.konva.transformer.getActiveAnchor() === 'rotater') { + return newPos; + } + const stageScale = this._manager.getStageScale(); + const stagePos = this._manager.getStagePosition(); + const targetX = Math.round(newPos.x / stageScale); + const targetY = Math.round(newPos.y / stageScale); + // Because the stage position may be a float, we need to calculate the offset of the stage position to the nearest + // pixel, then add that back to the target position. This ensures the anchors snap to the nearest pixel. + const scaledOffsetX = stagePos.x % stageScale; + const scaledOffsetY = stagePos.y % stageScale; + const scaledTargetX = targetX * stageScale + scaledOffsetX; + const scaledTargetY = targetY * stageScale + scaledOffsetY; + this._log.trace( + { + oldPos, + newPos, + stageScale, + stagePos, + targetX, + targetY, + scaledOffsetX, + scaledOffsetY, + scaledTargetX, + scaledTargetY, + }, + 'Anchor drag bound' + ); + return { x: scaledTargetX, y: scaledTargetY }; + }); + this.konva.transformer.on('transformstart', () => { - this.logDebugInfo("'transformstart' fired"); + this._log.trace( + { + x: this.konva.interactionRect.x(), + y: this.konva.interactionRect.y(), + scaleX: this.konva.interactionRect.scaleX(), + scaleY: this.konva.interactionRect.scaleY(), + rotation: this.konva.interactionRect.rotation(), + }, + 'Transform started' + ); }); this.konva.transformer.on('transform', () => { - // Always snap the interaction rect to the nearest pixel when transforming - - // const x = Math.round(this.konva.interactionRect.x()); - // const y = Math.round(this.konva.interactionRect.y()); - // // Snap its position - // this.konva.interactionRect.x(x); - // this.konva.interactionRect.y(y); - - // // Calculate the new scale of the interaction rect such that its width and height snap to the nearest pixel - // const targetWidth = Math.max( - // Math.round(this.konva.interactionRect.width() * Math.abs(this.konva.interactionRect.scaleX())), - // MIN_LAYER_SIZE_PX - // ); - // const scaleX = targetWidth / this.konva.interactionRect.width(); - // const targetHeight = Math.max( - // Math.round(this.konva.interactionRect.height() * Math.abs(this.konva.interactionRect.scaleY())), - // MIN_LAYER_SIZE_PX - // ); - // const scaleY = targetHeight / this.konva.interactionRect.height(); - - // // Snap the width and height (via scale) of the interaction rect - // this.konva.interactionRect.scaleX(scaleX); - // this.konva.interactionRect.scaleY(scaleY); - // this.konva.interactionRect.rotation(0); - - this.logDebugInfo("'transform' fired"); - this.konva.objectGroup.setAttrs({ x: this.konva.interactionRect.x(), y: this.konva.interactionRect.y(), @@ -136,7 +150,59 @@ export class CanvasLayer extends CanvasEntity { }); this.konva.transformer.on('transformend', () => { - this.logDebugInfo("'transformend' fired"); + // Always snap the interaction rect to the nearest pixel when transforming + const x = this.konva.interactionRect.x(); + const y = this.konva.interactionRect.y(); + const width = this.konva.interactionRect.width(); + const height = this.konva.interactionRect.height(); + const scaleX = this.konva.interactionRect.scaleX(); + const scaleY = this.konva.interactionRect.scaleY(); + const rotation = this.konva.interactionRect.rotation(); + + // Round to the nearest pixel + const snappedX = Math.round(x); + const snappedY = Math.round(y); + + // Calculate a rounded width and height - must be at least 1! + const targetWidth = Math.max(Math.round(width * scaleX), 1); + const targetHeight = Math.max(Math.round(height * scaleY), 1); + + // Calculate the scale we need to use to get the target width and height + const snappedScaleX = targetWidth / width; + const snappedScaleY = targetHeight / height; + + // Update interaction rect and object group + this.konva.interactionRect.setAttrs({ + x: snappedX, + y: snappedY, + scaleX: snappedScaleX, + scaleY: snappedScaleY, + }); + this.konva.objectGroup.setAttrs({ + x: snappedX, + y: snappedY, + scaleX: snappedScaleX, + scaleY: snappedScaleY, + }); + + this._log.trace( + { + x, + y, + width, + height, + scaleX, + scaleY, + rotation, + snappedX, + snappedY, + targetWidth, + targetHeight, + snappedScaleX, + snappedScaleY, + }, + 'Transform ended' + ); }); this.konva.interactionRect.on('dragmove', () => { @@ -159,24 +225,19 @@ export class CanvasLayer extends CanvasEntity { }); }); this.konva.interactionRect.on('dragend', () => { - this.logDebugInfo("'dragend' fired"); - if (this.isTransforming) { // When the user cancels the transformation, we need to reset the layer, so we should not update the layer's // positition while we are transforming - bail out early. return; } - this._manager.stateApi.onPosChanged( - { - id: this.id, - position: { - x: this.konva.interactionRect.x() - this.bbox.x, - y: this.konva.interactionRect.y() - this.bbox.y, - }, - }, - 'layer' - ); + const position = { + x: this.konva.interactionRect.x() - this.bbox.x, + y: this.konva.interactionRect.y() - this.bbox.y, + }; + + this._log.trace({ position }, 'Position changed'); + this._manager.stateApi.onPosChanged({ id: this.id, position }, 'layer'); }); this.objects = new Map(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 5ddfc1b6811..ce54d1ba533 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -15,7 +15,7 @@ import { } from 'features/controlLayers/konva/util'; import type { Extents, ExtentsResult, GetBboxTask, WorkerLogMessage } from 'features/controlLayers/konva/worker'; import { $lastProgressEvent, $shouldShowStagedImage } from 'features/controlLayers/store/canvasV2Slice'; -import type { CanvasV2State, GenerationMode } from 'features/controlLayers/store/types'; +import type { CanvasV2State, Coordinate, GenerationMode } from 'features/controlLayers/store/types'; import type Konva from 'konva'; import { atom } from 'nanostores'; import type { Logger } from 'roarr'; @@ -499,6 +499,10 @@ export class CanvasManager { return this.stage.scaleX(); } + getStagePosition(): Coordinate { + return this.stage.position(); + } + getScaledPixel(): number { return 1 / this.getStageScale(); } From 0d087f84a5a78eb6ebfdb2ee05f909c2cc98aa84 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 1 Aug 2024 13:11:09 +1000 Subject: [PATCH 265/678] tidy(ui): remove layer scaling reducers --- .../controlLayers/konva/CanvasStateApi.ts | 5 +-- .../controlLayers/store/canvasV2Slice.ts | 2 - .../controlLayers/store/layersReducers.ts | 39 ------------------- 3 files changed, 1 insertion(+), 45 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index 967b42ebca2..ff949645af5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -32,7 +32,6 @@ import { layerImageCacheChanged, layerRectShapeAdded, layerReset, - layerScaled, layerTranslated, rgBboxChanged, rgBrushLineAdded, @@ -91,9 +90,7 @@ export class CanvasStateApi { }; onScaleChanged = (arg: ScaleChangedArg, entityType: CanvasEntity['type']) => { log.debug('onScaleChanged'); - if (entityType === 'layer') { - this.store.dispatch(layerScaled(arg)); - } else if (entityType === 'inpaint_mask') { + if (entityType === 'inpaint_mask') { this.store.dispatch(imScaled(arg)); } else if (entityType === 'regional_guidance') { this.store.dispatch(rgScaled(arg)); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 8cf74b8bed8..9435358d927 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -66,7 +66,6 @@ const initialState: CanvasV2State = { eraser: { width: 50, }, - isTransforming: false, }, bbox: { rect: { x: 0, y: 0, width: 512, height: 512 }, @@ -221,7 +220,6 @@ export const { layerImageAdded, layerAllDeleted, layerImageCacheChanged, - layerScaled, layerBrushLineAdded, layerEraserLineAdded, layerRectShapeAdded, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index a05849153ac..e58c2294a80 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -16,7 +16,6 @@ import type { LayerEntity, PositionChangedArg, RectShape, - ScaleChangedArg, } from './types'; import { imageDTOToImageObject, imageDTOToImageWithDims } from './types'; @@ -179,35 +178,6 @@ export const layersReducers = { layer.objects.push(rectShape); state.layers.imageCache = null; }, - layerScaled: (state, action: PayloadAction) => { - const { id, scale, position } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - for (const obj of layer.objects) { - if (obj.type === 'brush_line') { - obj.points = obj.points.map((point) => Math.round(point * scale)); - obj.strokeWidth = Math.round(obj.strokeWidth * scale); - } else if (obj.type === 'eraser_line') { - obj.points = obj.points.map((point) => Math.round(point * scale)); - obj.strokeWidth = Math.round(obj.strokeWidth * scale); - } else if (obj.type === 'rect_shape') { - obj.x = Math.round(obj.x * scale); - obj.y = Math.round(obj.y * scale); - obj.height = Math.round(obj.height * scale); - obj.width = Math.round(obj.width * scale); - } else if (obj.type === 'image') { - obj.x = Math.round(obj.x * scale); - obj.y = Math.round(obj.y * scale); - obj.height = Math.round(obj.height * scale); - obj.width = Math.round(obj.width * scale); - } - } - layer.position.x = Math.round(position.x); - layer.position.y = Math.round(position.y); - state.layers.imageCache = null; - }, layerImageAdded: ( state, action: PayloadAction @@ -240,12 +210,3 @@ export const layersReducers = { state.layers.imageCache = null; }, } satisfies SliceCaseReducers; - -const scalePoints = (points: number[], scaleX: number, scaleY: number) => { - const newPoints: number[] = []; - for (let i = 0; i < points.length; i += 2) { - newPoints.push(points[i]! * scaleX); - newPoints.push(points[i + 1]! * scaleY); - } - return newPoints; -}; From 4432244bc3f08873ada7f89bc32f7dc3b5c6474e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 1 Aug 2024 15:08:18 +1000 Subject: [PATCH 266/678] feat(ui): expose subscribe method for nanostores --- .../controlLayers/konva/CanvasStateApi.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index ff949645af5..e463a0dbd84 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -257,30 +257,47 @@ export class CanvasStateApi { // Read-write state, ephemeral interaction state getIsDrawing = $isDrawing.get; setIsDrawing = $isDrawing.set; + onIsDrawingChanged = $isDrawing.subscribe; getIsMouseDown = $isMouseDown.get; setIsMouseDown = $isMouseDown.set; + onIsMouseDownChanged = $isMouseDown.subscribe; getLastAddedPoint = $lastAddedPoint.get; setLastAddedPoint = $lastAddedPoint.set; + onLastAddedPointChanged = $lastAddedPoint.subscribe; getLastMouseDownPos = $lastMouseDownPos.get; setLastMouseDownPos = $lastMouseDownPos.set; + onLastMouseDownPosChanged = $lastMouseDownPos.subscribe; getLastCursorPos = $lastCursorPos.get; setLastCursorPos = $lastCursorPos.set; + onLastCursorPosChanged = $lastCursorPos.subscribe; getSpaceKey = $spaceKey.get; setSpaceKey = $spaceKey.set; + onSpaceKeyChanged = $spaceKey.subscribe; getLastProgressEvent = $lastProgressEvent.get; setLastProgressEvent = $lastProgressEvent.set; + onLastProgressEventChanged = $lastProgressEvent.subscribe; getAltKey = $alt.get; + onAltChanged = $alt.subscribe; + getCtrlKey = $ctrl.get; + onCtrlChanged = $ctrl.subscribe; + getMetaKey = $meta.get; + onMetaChanged = $meta.subscribe; + getShiftKey = $shift.get; + onShiftChanged = $shift.subscribe; getShouldShowStagedImage = $shouldShowStagedImage.get; + onGetShouldShowStagedImageChanged = $shouldShowStagedImage.subscribe; + setStageAttrs = $stageAttrs.set; + onStageAttrsChanged = $stageAttrs.subscribe; } From 1e17461601a81f2e5a7330e7a6dc08690ed45da5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 1 Aug 2024 15:08:42 +1000 Subject: [PATCH 267/678] feat(ui): rotation snap to nearest 45deg when holding shift --- .../features/controlLayers/konva/CanvasLayer.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index da6480981ee..fb48c184349 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -126,6 +126,15 @@ export class CanvasLayer extends CanvasEntity { return { x: scaledTargetX, y: scaledTargetY }; }); + this.konva.transformer.boundBoxFunc((oldBoundBox, newBoundBox) => { + if (this._manager.stateApi.getShiftKey()) { + if (Math.abs(newBoundBox.rotation % (Math.PI / 4)) > 0) { + return oldBoundBox; + } + } + return newBoundBox; + }); + this.konva.transformer.on('transformstart', () => { this._log.trace( { @@ -249,6 +258,11 @@ export class CanvasLayer extends CanvasEntity { this.isTransforming = false; this._isFirstRender = true; this.isPendingBboxCalculation = false; + + this._manager.stateApi.onShiftChanged((isPressed) => { + // Use shift enable/disable rotation snaps + this.konva.transformer.rotationSnaps(isPressed ? [0, 45, 90, 135, 180, 225, 270, 315] : []); + }); } destroy(): void { From c8ac4d9e1bf4d7949f5f208291593fd7335bc661 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 1 Aug 2024 19:18:34 +1000 Subject: [PATCH 268/678] feat(ui): split & document transformer logic, iterate on class structures --- .../controlLayers/konva/CanvasBrushLine.ts | 10 +- .../konva/CanvasControlAdapter.ts | 62 +-- .../controlLayers/konva/CanvasEntity.ts | 12 +- .../controlLayers/konva/CanvasEraserLine.ts | 10 +- .../controlLayers/konva/CanvasImage.ts | 19 +- .../konva/CanvasInteractionRect.ts | 71 +++ .../controlLayers/konva/CanvasLayer.ts | 422 ++++++------------ .../controlLayers/konva/CanvasObject.ts | 29 +- .../controlLayers/konva/CanvasRect.ts | 10 +- .../controlLayers/konva/CanvasStagingArea.ts | 8 +- .../controlLayers/konva/CanvasTransformer.ts | 228 ++++++++++ 11 files changed, 507 insertions(+), 374 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasInteractionRect.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts index 5d4d4159172..acafcf3b27a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts @@ -19,7 +19,7 @@ export class CanvasBrushLine extends CanvasObject { constructor(state: BrushLine, parent: CanvasLayer) { super(state.id, parent); - this._log.trace({ state }, 'Creating brush line'); + this.log.trace({ state }, 'Creating brush line'); const { strokeWidth, clip, color, points } = state; @@ -49,7 +49,7 @@ export class CanvasBrushLine extends CanvasObject { update(state: BrushLine, force?: boolean): boolean { if (force || this.state !== state) { - this._log.trace({ state }, 'Updating brush line'); + this.log.trace({ state }, 'Updating brush line'); const { points, color, clip, strokeWidth } = state; this.konva.line.setAttrs({ // A line with only one point will not be rendered, so we duplicate the points to make it visible @@ -66,12 +66,12 @@ export class CanvasBrushLine extends CanvasObject { } destroy() { - this._log.trace('Destroying brush line'); + this.log.trace('Destroying brush line'); this.konva.group.destroy(); } setVisibility(isVisible: boolean): void { - this._log.trace({ isVisible }, 'Setting brush line visibility'); + this.log.trace({ isVisible }, 'Setting brush line visibility'); this.konva.group.visible(isVisible); } @@ -79,7 +79,7 @@ export class CanvasBrushLine extends CanvasObject { return { id: this.id, type: CanvasBrushLine.TYPE, - parent: this._parent.id, + parent: this.parent.id, state: deepClone(this.state), }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts index a16d0a158cc..0065d876d3c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts @@ -1,33 +1,31 @@ +import { CanvasEntity } from 'features/controlLayers/konva/CanvasEntity'; import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; import { type ControlAdapterEntity, isDrawingTool } from 'features/controlLayers/store/types'; import Konva from 'konva'; -export class CanvasControlAdapter { +export class CanvasControlAdapter extends CanvasEntity { static NAME_PREFIX = 'control-adapter'; static LAYER_NAME = `${CanvasControlAdapter.NAME_PREFIX}_layer`; static TRANSFORMER_NAME = `${CanvasControlAdapter.NAME_PREFIX}_transformer`; static GROUP_NAME = `${CanvasControlAdapter.NAME_PREFIX}_group`; static OBJECT_GROUP_NAME = `${CanvasControlAdapter.NAME_PREFIX}_object-group`; - private state: ControlAdapterEntity; - - id: string; - manager: CanvasManager; + type = 'control_adapter'; + _state: ControlAdapterEntity; konva: { layer: Konva.Layer; group: Konva.Group; objectGroup: Konva.Group; - transformer: Konva.Transformer; }; image: CanvasImage | null; + transformer: CanvasTransformer; constructor(state: ControlAdapterEntity, manager: CanvasManager) { - const { id } = state; - this.id = id; - this.manager = manager; + super(state.id, manager); this.konva = { layer: new Konva.Layer({ name: CanvasControlAdapter.LAYER_NAME, @@ -39,42 +37,18 @@ export class CanvasControlAdapter { listening: false, }), objectGroup: new Konva.Group({ name: CanvasControlAdapter.GROUP_NAME, listening: false }), - transformer: new Konva.Transformer({ - name: CanvasControlAdapter.TRANSFORMER_NAME, - shouldOverdrawWholeArea: true, - draggable: true, - dragDistance: 0, - enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], - rotateEnabled: false, - flipEnabled: false, - }), }; - this.konva.transformer.on('transformend', () => { - this.manager.stateApi.onScaleChanged( - { - id: this.id, - scale: this.konva.group.scaleX(), - position: { x: this.konva.group.x(), y: this.konva.group.y() }, - }, - 'control_adapter' - ); - }); - this.konva.transformer.on('dragend', () => { - this.manager.stateApi.onPosChanged( - { id: this.id, position: { x: this.konva.group.x(), y: this.konva.group.y() } }, - 'control_adapter' - ); - }); + this.transformer = new CanvasTransformer(this); this.konva.group.add(this.konva.objectGroup); this.konva.layer.add(this.konva.group); this.konva.layer.add(this.konva.transformer); this.image = null; - this.state = state; + this._state = state; } async render(state: ControlAdapterEntity) { - this.state = state; + this._state = state; // Update the layer's position and listening state this.konva.group.setAttrs({ @@ -94,7 +68,7 @@ export class CanvasControlAdapter { didDraw = true; } } else if (!this.image) { - this.image = new CanvasImage(imageObject); + this.image = new CanvasImage(imageObject, this); this.updateGroup(true); this.konva.objectGroup.add(this.image.konva.group); await this.image.updateImageSource(imageObject.image.name); @@ -108,13 +82,13 @@ export class CanvasControlAdapter { } updateGroup(didDraw: boolean) { - this.konva.layer.visible(this.state.isEnabled); + this.konva.layer.visible(this._state.isEnabled); - this.konva.group.opacity(this.state.opacity); + this.konva.group.opacity(this._state.opacity); const isSelected = this.manager.stateApi.getIsSelected(this.id); const selectedTool = this.manager.stateApi.getToolState().selected; - if (!this.image?.image) { + if (!this.image?.konva.image) { // If the layer is totally empty, reset the cache and bail out. this.konva.layer.listening(false); this.konva.transformer.nodes([]); @@ -175,4 +149,12 @@ export class CanvasControlAdapter { destroy(): void { this.konva.layer.destroy(); } + + repr() { + return { + id: this.id, + type: this.type, + state: this._state, + }; + } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity.ts index 390d17d5cc5..b9775cd2c1c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity.ts @@ -4,22 +4,22 @@ import type { Logger } from 'roarr'; export abstract class CanvasEntity { id: string; - _manager: CanvasManager; - _log: Logger; + manager: CanvasManager; + log: Logger; constructor(id: string, manager: CanvasManager) { this.id = id; - this._manager = manager; - this._log = this._manager.buildLogger(this._getLoggingContext); + this.manager = manager; + this.log = this.manager.buildLogger(this.getLoggingContext); } /** * Get a serializable representation of the entity. */ abstract repr(): JSONObject; - _getLoggingContext = (extra?: Record) => { + getLoggingContext = (extra?: Record) => { return { - ...this._manager._getLoggingContext(), + ...this.manager._getLoggingContext(), layerId: this.id, ...extra, }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts index dfe1ee5708c..64e7595f0eb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts @@ -20,7 +20,7 @@ export class CanvasEraserLine extends CanvasObject { constructor(state: EraserLine, parent: CanvasLayer) { super(state.id, parent); - this._log.trace({ state }, 'Creating eraser line'); + this.log.trace({ state }, 'Creating eraser line'); const { strokeWidth, clip, points } = state; @@ -50,7 +50,7 @@ export class CanvasEraserLine extends CanvasObject { update(state: EraserLine, force?: boolean): boolean { if (force || this.state !== state) { - this._log.trace({ state }, 'Updating eraser line'); + this.log.trace({ state }, 'Updating eraser line'); const { points, clip, strokeWidth } = state; this.konva.line.setAttrs({ // A line with only one point will not be rendered, so we duplicate the points to make it visible @@ -66,12 +66,12 @@ export class CanvasEraserLine extends CanvasObject { } destroy() { - this._log.trace('Destroying eraser line'); + this.log.trace('Destroying eraser line'); this.konva.group.destroy(); } setVisibility(isVisible: boolean): void { - this._log.trace({ isVisible }, 'Setting brush line visibility'); + this.log.trace({ isVisible }, 'Setting brush line visibility'); this.konva.group.visible(isVisible); } @@ -79,7 +79,7 @@ export class CanvasEraserLine extends CanvasObject { return { id: this.id, type: CanvasEraserLine.TYPE, - parent: this._parent.id, + parent: this.parent.id, 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 96c14ba27d0..021ced65816 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -1,4 +1,5 @@ import { deepClone } from 'common/util/deepClone'; +import type { CanvasControlAdapter } from 'features/controlLayers/konva/CanvasControlAdapter'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import { CanvasObject } from 'features/controlLayers/konva/CanvasObject'; import type { CanvasStagingArea } from 'features/controlLayers/konva/CanvasStagingArea'; @@ -28,9 +29,9 @@ export class CanvasImage extends CanvasObject { isLoading: boolean; isError: boolean; - constructor(state: ImageObject, parent: CanvasLayer | CanvasStagingArea) { + constructor(state: ImageObject, parent: CanvasLayer | CanvasStagingArea | CanvasControlAdapter) { super(state.id, parent); - this._log.trace({ state }, 'Creating image'); + this.log.trace({ state }, 'Creating image'); const { width, height, x, y } = state; @@ -73,7 +74,7 @@ export class CanvasImage extends CanvasObject { async updateImageSource(imageName: string) { try { - this._log.trace({ imageName }, 'Updating image source'); + this.log.trace({ imageName }, 'Updating image source'); this.isLoading = true; this.konva.group.visible(true); @@ -85,7 +86,7 @@ export class CanvasImage extends CanvasObject { const imageDTO = await getImageDTO(imageName); if (imageDTO === null) { - this._log.error({ imageName }, 'Image not found'); + this.log.error({ imageName }, 'Image not found'); return; } const imageEl = await loadImage(imageDTO.image_url); @@ -118,7 +119,7 @@ export class CanvasImage extends CanvasObject { this.isError = false; this.konva.placeholder.group.visible(false); } catch { - this._log({ imageName }, 'Failed to load image'); + this.log({ imageName }, 'Failed to load image'); this.konva.image?.visible(false); this.imageName = null; this.isLoading = false; @@ -130,7 +131,7 @@ export class CanvasImage extends CanvasObject { async update(state: ImageObject, force?: boolean): Promise { if (this.state !== state || force) { - this._log.trace({ state }, 'Updating image'); + this.log.trace({ state }, 'Updating image'); const { width, height, x, y, image, filters } = state; if (this.state.image.name !== image.name || force) { @@ -154,12 +155,12 @@ export class CanvasImage extends CanvasObject { } destroy() { - this._log.trace('Destroying image'); + this.log.trace('Destroying image'); this.konva.group.destroy(); } setVisibility(isVisible: boolean): void { - this._log.trace({ isVisible }, 'Setting image visibility'); + this.log.trace({ isVisible }, 'Setting image visibility'); this.konva.group.visible(isVisible); } @@ -167,7 +168,7 @@ export class CanvasImage extends CanvasObject { return { id: this.id, type: CanvasImage.TYPE, - parent: this._parent.id, + parent: this.parent.id, imageName: this.imageName, isLoading: this.isLoading, isError: this.isError, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInteractionRect.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInteractionRect.ts new file mode 100644 index 00000000000..742c8073b18 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInteractionRect.ts @@ -0,0 +1,71 @@ +import { nanoid } from '@reduxjs/toolkit'; +import { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; +import { CanvasObject } from 'features/controlLayers/konva/CanvasObject'; +import Konva from 'konva'; + +export class CanvasInteractionRect extends CanvasObject { + static TYPE = 'interaction_rect'; + + konva: { + rect: Konva.Rect; + }; + + constructor(parent: CanvasLayer) { + super(`${CanvasInteractionRect.TYPE}:${nanoid()}`, parent); + + this.konva = { + rect: new Konva.Rect({ + name: CanvasInteractionRect.TYPE, + listening: false, + draggable: true, + // fill: 'rgba(255,0,0,0.5)', + }), + }; + + this.konva.rect.on('dragmove', () => { + // Snap the interaction rect to the nearest pixel + this.konva.rect.x(Math.round(this.konva.rect.x())); + this.konva.rect.y(Math.round(this.konva.rect.y())); + + // The bbox should be updated to reflect the new position of the interaction rect, taking into account its padding + // and border + this.parent.konva.bbox.setAttrs({ + x: this.konva.rect.x() - this.manager.getScaledBboxPadding(), + y: this.konva.rect.y() - this.manager.getScaledBboxPadding(), + }); + + // The object group is translated by the difference between the interaction rect's new and old positions (which is + // stored as this.bbox) + this.parent.konva.objectGroup.setAttrs({ + x: this.konva.rect.x(), + y: this.konva.rect.y(), + }); + }); + this.konva.rect.on('dragend', () => { + if (this.parent.isTransforming) { + // When the user cancels the transformation, we need to reset the layer, so we should not update the layer's + // positition while we are transforming - bail out early. + return; + } + + const position = { + x: this.konva.rect.x() - this.parent.bbox.x, + y: this.konva.rect.y() - this.parent.bbox.y, + }; + + this.log.trace({ position }, 'Position changed'); + this.manager.stateApi.onPosChanged({ id: this.id, position }, 'layer'); + }); + } + + repr = () => { + return { + id: this.id, + type: CanvasInteractionRect.TYPE, + x: this.konva.rect.x(), + y: this.konva.rect.y(), + width: this.konva.rect.width(), + height: this.konva.rect.height(), + }; + }; +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index fb48c184349..c651df5d5cb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -6,6 +6,7 @@ import { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine' import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; import { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; +import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; import { getPrefixedId, konvaNodeToBlob, mapId, previewBlob } from 'features/controlLayers/konva/util'; import { layerRasterized } from 'features/controlLayers/store/canvasV2Slice'; import { @@ -24,31 +25,28 @@ import { uploadImage } from 'services/api/endpoints/images'; import { assert } from 'tsafe'; export class CanvasLayer extends CanvasEntity { - static NAME_PREFIX = 'layer'; - static LAYER_NAME = `${CanvasLayer.NAME_PREFIX}_layer`; - static TRANSFORMER_NAME = `${CanvasLayer.NAME_PREFIX}_transformer`; - static INTERACTION_RECT_NAME = `${CanvasLayer.NAME_PREFIX}_interaction-rect`; - static GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_group`; - static OBJECT_GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_object-group`; - static BBOX_NAME = `${CanvasLayer.NAME_PREFIX}_bbox`; + static TYPE = 'layer'; + static LAYER_NAME = `${CanvasLayer.TYPE}_layer`; + static TRANSFORMER_NAME = `${CanvasLayer.TYPE}_transformer`; + static INTERACTION_RECT_NAME = `${CanvasLayer.TYPE}_interaction-rect`; + static GROUP_NAME = `${CanvasLayer.TYPE}_group`; + static OBJECT_GROUP_NAME = `${CanvasLayer.TYPE}_object-group`; + static BBOX_NAME = `${CanvasLayer.TYPE}_bbox`; - _drawingBuffer: BrushLine | EraserLine | RectShape | null; - _state: LayerEntity; - - type = 'layer'; + drawingBuffer: BrushLine | EraserLine | RectShape | null; + state: LayerEntity; konva: { layer: Konva.Layer; bbox: Konva.Rect; objectGroup: Konva.Group; - transformer: Konva.Transformer; interactionRect: Konva.Rect; }; objects: Map; + transformer: CanvasTransformer; - _bboxNeedsUpdate: boolean; - _isFirstRender: boolean; - + bboxNeedsUpdate: boolean; + isFirstRender: boolean; isTransforming: boolean; isPendingBboxCalculation: boolean; @@ -57,7 +55,7 @@ export class CanvasLayer extends CanvasEntity { constructor(state: LayerEntity, manager: CanvasManager) { super(state.id, manager); - this._log.debug({ state }, 'Creating layer'); + this.log.debug({ state }, 'Creating layer'); this.konva = { layer: new Konva.Layer({ id: this.id, name: CanvasLayer.LAYER_NAME, listening: false }), @@ -70,17 +68,6 @@ export class CanvasLayer extends CanvasEntity { strokeHitEnabled: false, }), objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }), - transformer: new Konva.Transformer({ - name: CanvasLayer.TRANSFORMER_NAME, - draggable: false, - // enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], - rotateEnabled: true, - flipEnabled: true, - listening: false, - padding: this._manager.getTransformerPadding(), - stroke: 'hsl(200deg 76% 59%)', // invokeBlue.400 - keepRatio: false, - }), interactionRect: new Konva.Rect({ name: CanvasLayer.INTERACTION_RECT_NAME, listening: false, @@ -89,131 +76,13 @@ export class CanvasLayer extends CanvasEntity { }), }; + this.transformer = new CanvasTransformer(this); + this.konva.layer.add(this.konva.objectGroup); - this.konva.layer.add(this.konva.transformer); + this.konva.layer.add(this.transformer.konva.transformer); this.konva.layer.add(this.konva.interactionRect); this.konva.layer.add(this.konva.bbox); - this.konva.transformer.anchorDragBoundFunc((oldPos: Coordinate, newPos: Coordinate) => { - if (this.konva.transformer.getActiveAnchor() === 'rotater') { - return newPos; - } - const stageScale = this._manager.getStageScale(); - const stagePos = this._manager.getStagePosition(); - const targetX = Math.round(newPos.x / stageScale); - const targetY = Math.round(newPos.y / stageScale); - // Because the stage position may be a float, we need to calculate the offset of the stage position to the nearest - // pixel, then add that back to the target position. This ensures the anchors snap to the nearest pixel. - const scaledOffsetX = stagePos.x % stageScale; - const scaledOffsetY = stagePos.y % stageScale; - const scaledTargetX = targetX * stageScale + scaledOffsetX; - const scaledTargetY = targetY * stageScale + scaledOffsetY; - this._log.trace( - { - oldPos, - newPos, - stageScale, - stagePos, - targetX, - targetY, - scaledOffsetX, - scaledOffsetY, - scaledTargetX, - scaledTargetY, - }, - 'Anchor drag bound' - ); - return { x: scaledTargetX, y: scaledTargetY }; - }); - - this.konva.transformer.boundBoxFunc((oldBoundBox, newBoundBox) => { - if (this._manager.stateApi.getShiftKey()) { - if (Math.abs(newBoundBox.rotation % (Math.PI / 4)) > 0) { - return oldBoundBox; - } - } - return newBoundBox; - }); - - this.konva.transformer.on('transformstart', () => { - this._log.trace( - { - x: this.konva.interactionRect.x(), - y: this.konva.interactionRect.y(), - scaleX: this.konva.interactionRect.scaleX(), - scaleY: this.konva.interactionRect.scaleY(), - rotation: this.konva.interactionRect.rotation(), - }, - 'Transform started' - ); - }); - - this.konva.transformer.on('transform', () => { - this.konva.objectGroup.setAttrs({ - x: this.konva.interactionRect.x(), - y: this.konva.interactionRect.y(), - scaleX: this.konva.interactionRect.scaleX(), - scaleY: this.konva.interactionRect.scaleY(), - rotation: this.konva.interactionRect.rotation(), - }); - }); - - this.konva.transformer.on('transformend', () => { - // Always snap the interaction rect to the nearest pixel when transforming - const x = this.konva.interactionRect.x(); - const y = this.konva.interactionRect.y(); - const width = this.konva.interactionRect.width(); - const height = this.konva.interactionRect.height(); - const scaleX = this.konva.interactionRect.scaleX(); - const scaleY = this.konva.interactionRect.scaleY(); - const rotation = this.konva.interactionRect.rotation(); - - // Round to the nearest pixel - const snappedX = Math.round(x); - const snappedY = Math.round(y); - - // Calculate a rounded width and height - must be at least 1! - const targetWidth = Math.max(Math.round(width * scaleX), 1); - const targetHeight = Math.max(Math.round(height * scaleY), 1); - - // Calculate the scale we need to use to get the target width and height - const snappedScaleX = targetWidth / width; - const snappedScaleY = targetHeight / height; - - // Update interaction rect and object group - this.konva.interactionRect.setAttrs({ - x: snappedX, - y: snappedY, - scaleX: snappedScaleX, - scaleY: snappedScaleY, - }); - this.konva.objectGroup.setAttrs({ - x: snappedX, - y: snappedY, - scaleX: snappedScaleX, - scaleY: snappedScaleY, - }); - - this._log.trace( - { - x, - y, - width, - height, - scaleX, - scaleY, - rotation, - snappedX, - snappedY, - targetWidth, - targetHeight, - snappedScaleX, - snappedScaleY, - }, - 'Transform ended' - ); - }); - this.konva.interactionRect.on('dragmove', () => { // Snap the interaction rect to the nearest pixel this.konva.interactionRect.x(Math.round(this.konva.interactionRect.x())); @@ -222,8 +91,8 @@ export class CanvasLayer extends CanvasEntity { // The bbox should be updated to reflect the new position of the interaction rect, taking into account its padding // and border this.konva.bbox.setAttrs({ - x: this.konva.interactionRect.x() - this._manager.getScaledBboxPadding(), - y: this.konva.interactionRect.y() - this._manager.getScaledBboxPadding(), + x: this.konva.interactionRect.x() - this.manager.getScaledBboxPadding(), + y: this.konva.interactionRect.y() - this.manager.getScaledBboxPadding(), }); // The object group is translated by the difference between the interaction rect's new and old positions (which is @@ -245,48 +114,44 @@ export class CanvasLayer extends CanvasEntity { y: this.konva.interactionRect.y() - this.bbox.y, }; - this._log.trace({ position }, 'Position changed'); - this._manager.stateApi.onPosChanged({ id: this.id, position }, 'layer'); + this.log.trace({ position }, 'Position changed'); + this.manager.stateApi.onPosChanged({ id: this.id, position }, 'layer'); }); this.objects = new Map(); - this._drawingBuffer = null; - this._state = state; + this.drawingBuffer = null; + this.state = state; this.rect = this.getDefaultRect(); this.bbox = this.getDefaultRect(); - this._bboxNeedsUpdate = true; + this.bboxNeedsUpdate = true; this.isTransforming = false; - this._isFirstRender = true; + this.isFirstRender = true; this.isPendingBboxCalculation = false; - - this._manager.stateApi.onShiftChanged((isPressed) => { - // Use shift enable/disable rotation snaps - this.konva.transformer.rotationSnaps(isPressed ? [0, 45, 90, 135, 180, 225, 270, 315] : []); - }); } - destroy(): void { - this._log.debug('Destroying layer'); + destroy = (): void => { + this.log.debug('Destroying layer'); this.konva.layer.destroy(); - } + }; - getDrawingBuffer() { - return this._drawingBuffer; - } - async setDrawingBuffer(obj: BrushLine | EraserLine | RectShape | null) { + getDrawingBuffer = () => { + return this.drawingBuffer; + }; + + setDrawingBuffer = async (obj: BrushLine | EraserLine | RectShape | null) => { if (obj) { - this._drawingBuffer = obj; - await this._renderObject(this._drawingBuffer, true); + this.drawingBuffer = obj; + await this._renderObject(this.drawingBuffer, true); } else { - this._drawingBuffer = null; + this.drawingBuffer = null; } - } + }; - async finalizeDrawingBuffer() { - if (!this._drawingBuffer) { + finalizeDrawingBuffer = async () => { + if (!this.drawingBuffer) { return; } - const drawingBuffer = this._drawingBuffer; + const drawingBuffer = this.drawingBuffer; await this.setDrawingBuffer(null); // We need to give the objects a fresh ID else they will be considered the same object when they are re-rendered as @@ -294,62 +159,62 @@ export class CanvasLayer extends CanvasEntity { if (drawingBuffer.type === 'brush_line') { drawingBuffer.id = getPrefixedId('brush_line'); - this._manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: drawingBuffer }, 'layer'); + this.manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: drawingBuffer }, 'layer'); } else if (drawingBuffer.type === 'eraser_line') { drawingBuffer.id = getPrefixedId('brush_line'); - this._manager.stateApi.onEraserLineAdded({ id: this.id, eraserLine: drawingBuffer }, 'layer'); + this.manager.stateApi.onEraserLineAdded({ id: this.id, eraserLine: drawingBuffer }, 'layer'); } else if (drawingBuffer.type === 'rect_shape') { drawingBuffer.id = getPrefixedId('brush_line'); - this._manager.stateApi.onRectShapeAdded({ id: this.id, rectShape: drawingBuffer }, 'layer'); + this.manager.stateApi.onRectShapeAdded({ id: this.id, rectShape: drawingBuffer }, 'layer'); } - } + }; - async update(arg?: { state: LayerEntity; toolState: CanvasV2State['tool']; isSelected: boolean }) { - const state = get(arg, 'state', this._state); - const toolState = get(arg, 'toolState', this._manager.stateApi.getToolState()); - const isSelected = get(arg, 'isSelected', this._manager.stateApi.getIsSelected(this.id)); + update = async (arg?: { state: LayerEntity; toolState: CanvasV2State['tool']; isSelected: boolean }) => { + const state = get(arg, 'state', this.state); + const toolState = get(arg, 'toolState', this.manager.stateApi.getToolState()); + const isSelected = get(arg, 'isSelected', this.manager.stateApi.getIsSelected(this.id)); - if (!this._isFirstRender && state === this._state) { - this._log.trace('State unchanged, skipping update'); + if (!this.isFirstRender && state === this.state) { + this.log.trace('State unchanged, skipping update'); return; } - this._log.debug('Updating'); + this.log.debug('Updating'); const { position, objects, opacity, isEnabled } = state; - if (this._isFirstRender || objects !== this._state.objects) { + if (this.isFirstRender || objects !== this.state.objects) { await this.updateObjects({ objects }); } - if (this._isFirstRender || position !== this._state.position) { + if (this.isFirstRender || position !== this.state.position) { await this.updatePosition({ position }); } - if (this._isFirstRender || opacity !== this._state.opacity) { + if (this.isFirstRender || opacity !== this.state.opacity) { await this.updateOpacity({ opacity }); } - if (this._isFirstRender || isEnabled !== this._state.isEnabled) { + if (this.isFirstRender || isEnabled !== this.state.isEnabled) { await this.updateVisibility({ isEnabled }); } await this.updateInteraction({ toolState, isSelected }); - if (this._isFirstRender) { + if (this.isFirstRender) { await this.updateBbox(); } - this._state = state; - this._isFirstRender = false; - } + this.state = state; + this.isFirstRender = false; + }; - updateVisibility(arg?: { isEnabled: boolean }) { - this._log.trace('Updating visibility'); - const isEnabled = get(arg, 'isEnabled', this._state.isEnabled); - const hasObjects = this.objects.size > 0 || this._drawingBuffer !== null; + updateVisibility = (arg?: { isEnabled: boolean }) => { + this.log.trace('Updating visibility'); + const isEnabled = get(arg, 'isEnabled', this.state.isEnabled); + const hasObjects = this.objects.size > 0 || this.drawingBuffer !== null; this.konva.layer.visible(isEnabled && hasObjects); - } + }; - updatePosition(arg?: { position: Coordinate }) { - this._log.trace('Updating position'); - const position = get(arg, 'position', this._state.position); - const bboxPadding = this._manager.getScaledBboxPadding(); + updatePosition = (arg?: { position: Coordinate }) => { + this.log.trace('Updating position'); + const position = get(arg, 'position', this.state.position); + const bboxPadding = this.manager.getScaledBboxPadding(); this.konva.objectGroup.setAttrs({ x: position.x + this.bbox.x, @@ -365,12 +230,12 @@ export class CanvasLayer extends CanvasEntity { x: position.x + this.bbox.x * this.konva.interactionRect.scaleX(), y: position.y + this.bbox.y * this.konva.interactionRect.scaleY(), }); - } + }; - async updateObjects(arg?: { objects: LayerEntity['objects'] }) { - this._log.trace('Updating objects'); + updateObjects = async (arg?: { objects: LayerEntity['objects'] }) => { + this.log.trace('Updating objects'); - const objects = get(arg, 'objects', this._state.objects); + const objects = get(arg, 'objects', this.state.objects); const objectIds = objects.map(mapId); @@ -378,7 +243,7 @@ export class CanvasLayer extends CanvasEntity { // Destroy any objects that are no longer in state for (const object of this.objects.values()) { - if (!objectIds.includes(object.id) && object.id !== this._drawingBuffer?.id) { + if (!objectIds.includes(object.id) && object.id !== this.drawingBuffer?.id) { this.objects.delete(object.id); object.destroy(); didUpdate = true; @@ -391,8 +256,8 @@ export class CanvasLayer extends CanvasEntity { } } - if (this._drawingBuffer) { - if (await this._renderObject(this._drawingBuffer)) { + if (this.drawingBuffer) { + if (await this._renderObject(this.drawingBuffer)) { didUpdate = true; } } @@ -401,20 +266,20 @@ export class CanvasLayer extends CanvasEntity { this.calculateBbox(); } - this._isFirstRender = false; - } + this.isFirstRender = false; + }; - updateOpacity(arg?: { opacity: number }) { - this._log.trace('Updating opacity'); - const opacity = get(arg, 'opacity', this._state.opacity); + updateOpacity = (arg?: { opacity: number }) => { + this.log.trace('Updating opacity'); + const opacity = get(arg, 'opacity', this.state.opacity); this.konva.objectGroup.opacity(opacity); - } + }; - updateInteraction(arg?: { toolState: CanvasV2State['tool']; isSelected: boolean }) { - this._log.trace('Updating interaction'); + updateInteraction = (arg?: { toolState: CanvasV2State['tool']; isSelected: boolean }) => { + this.log.trace('Updating interaction'); - const toolState = get(arg, 'toolState', this._manager.stateApi.getToolState()); - const isSelected = get(arg, 'isSelected', this._manager.stateApi.getIsSelected(this.id)); + const toolState = get(arg, 'toolState', this.manager.stateApi.getToolState()); + const isSelected = get(arg, 'isSelected', this.manager.stateApi.getIsSelected(this.id)); if (this.objects.size === 0) { // The layer is totally empty, we can just disable the layer @@ -427,8 +292,7 @@ export class CanvasLayer extends CanvasEntity { this.konva.layer.listening(true); // The transformer is not needed - this.konva.transformer.listening(false); - this.konva.transformer.nodes([]); + this.transformer.deactivate(); // The bbox rect should be visible and interaction rect listening for dragging this.konva.bbox.visible(true); @@ -440,10 +304,11 @@ export class CanvasLayer extends CanvasEntity { const listening = toolState.selected !== 'view'; this.konva.layer.listening(listening); this.konva.interactionRect.listening(listening); - this.konva.transformer.listening(listening); - - // The transformer transforms the interaction rect, not the object group - this.konva.transformer.nodes([this.konva.interactionRect]); + if (listening) { + this.transformer.activate(); + } else { + this.transformer.deactivate(); + } // Hide the bbox rect, the transformer will has its own bbox this.konva.bbox.visible(false); @@ -452,15 +317,14 @@ export class CanvasLayer extends CanvasEntity { this.konva.layer.listening(false); // The transformer, bbox and interaction rect should be inactive - this.konva.transformer.listening(false); - this.konva.transformer.nodes([]); + this.transformer.deactivate(); this.konva.bbox.visible(false); this.konva.interactionRect.listening(false); } - } + }; - updateBbox() { - this._log.trace('Updating bbox'); + updateBbox = () => { + this.log.trace('Updating bbox'); if (this.isPendingBboxCalculation) { return; @@ -470,9 +334,9 @@ export class CanvasLayer extends CanvasEntity { // eraser lines, fully clipped brush lines or if it has been fully erased. if (this.bbox.width === 0 || this.bbox.height === 0) { // We shouldn't reset on the first render - the bbox will be calculated on the next render - if (!this._isFirstRender && this.objects.size > 0) { + if (!this.isFirstRender && this.objects.size > 0) { // The layer is fully transparent but has objects - reset it - this._manager.stateApi.onEntityReset({ id: this.id }, 'layer'); + this.manager.stateApi.onEntityReset({ id: this.id }, 'layer'); } this.konva.bbox.visible(false); this.konva.interactionRect.visible(false); @@ -482,35 +346,35 @@ export class CanvasLayer extends CanvasEntity { this.konva.bbox.visible(true); this.konva.interactionRect.visible(true); - const onePixel = this._manager.getScaledPixel(); - const bboxPadding = this._manager.getScaledBboxPadding(); + const onePixel = this.manager.getScaledPixel(); + const bboxPadding = this.manager.getScaledBboxPadding(); this.konva.bbox.setAttrs({ - x: this._state.position.x + this.bbox.x - bboxPadding, - y: this._state.position.y + this.bbox.y - bboxPadding, + x: this.state.position.x + this.bbox.x - bboxPadding, + y: this.state.position.y + this.bbox.y - bboxPadding, width: this.bbox.width + bboxPadding * 2, height: this.bbox.height + bboxPadding * 2, strokeWidth: onePixel, }); this.konva.interactionRect.setAttrs({ - x: this._state.position.x + this.bbox.x, - y: this._state.position.y + this.bbox.y, + x: this.state.position.x + this.bbox.x, + y: this.state.position.y + this.bbox.y, width: this.bbox.width, height: this.bbox.height, }); this.konva.objectGroup.setAttrs({ - x: this._state.position.x + this.bbox.x, - y: this._state.position.y + this.bbox.y, + x: this.state.position.x + this.bbox.x, + y: this.state.position.y + this.bbox.y, offsetX: this.bbox.x, offsetY: this.bbox.y, }); - } + }; - syncStageScale() { - this._log.trace('Syncing scale to stage'); + syncStageScale = () => { + this.log.trace('Syncing scale to stage'); - const onePixel = this._manager.getScaledPixel(); - const bboxPadding = this._manager.getScaledBboxPadding(); + const onePixel = this.manager.getScaledPixel(); + const bboxPadding = this.manager.getScaledBboxPadding(); this.konva.bbox.setAttrs({ x: this.konva.interactionRect.x() - bboxPadding, @@ -519,10 +383,9 @@ export class CanvasLayer extends CanvasEntity { height: this.konva.interactionRect.height() * this.konva.interactionRect.scaleY() + bboxPadding * 2, strokeWidth: onePixel, }); - this.konva.transformer.forceUpdate(); - } + }; - async _renderObject(obj: LayerEntity['objects'][number], force = false): Promise { + _renderObject = async (obj: LayerEntity['objects'][number], force = false): Promise => { if (obj.type === 'brush_line') { let brushLine = this.objects.get(obj.id); assert(brushLine instanceof CanvasBrushLine || brushLine === undefined); @@ -581,29 +444,26 @@ export class CanvasLayer extends CanvasEntity { } return false; - } + }; - startTransform() { - this._log.debug('Starting transform'); + startTransform = () => { + this.log.debug('Starting transform'); this.isTransforming = true; // When transforming, we want the stage to still be movable if the view tool is selected. If the transformer or // interaction rect are listening, it will interrupt the stage's drag events. So we should disable listening // when the view tool is selected - const listening = this._manager.stateApi.getToolState().selected !== 'view'; + const listening = this.manager.stateApi.getToolState().selected !== 'view'; this.konva.layer.listening(listening); this.konva.interactionRect.listening(listening); - this.konva.transformer.listening(listening); - - // The transformer transforms the interaction rect, not the object group - this.konva.transformer.nodes([this.konva.interactionRect]); + this.transformer.activate(); // Hide the bbox rect, the transformer will has its own bbox this.konva.bbox.visible(false); - } + }; - resetScale() { + resetScale = () => { const attrs = { scaleX: 1, scaleY: 1, @@ -612,16 +472,16 @@ export class CanvasLayer extends CanvasEntity { this.konva.objectGroup.setAttrs(attrs); this.konva.bbox.setAttrs(attrs); this.konva.interactionRect.setAttrs(attrs); - } + }; - async rasterizeLayer() { - this._log.debug('Rasterizing layer'); + rasterizeLayer = async () => { + this.log.debug('Rasterizing layer'); const objectGroupClone = this.konva.objectGroup.clone(); const interactionRectClone = this.konva.interactionRect.clone(); const rect = interactionRectClone.getClientRect(); const blob = await konvaNodeToBlob(objectGroupClone, rect); - if (this._manager._isDebugging) { + if (this.manager._isDebugging) { previewBlob(blob, 'Rasterized layer'); } const imageDTO = await uploadImage(blob, `${this.id}_rasterized.png`, 'other', true); @@ -635,29 +495,29 @@ export class CanvasLayer extends CanvasEntity { } this.resetScale(); dispatch(layerRasterized({ id: this.id, imageObject, position: { x: rect.x, y: rect.y } })); - } + }; - stopTransform() { - this._log.debug('Stopping transform'); + stopTransform = () => { + this.log.debug('Stopping transform'); this.isTransforming = false; this.resetScale(); this.updatePosition(); this.updateBbox(); this.updateInteraction(); - } + }; - getDefaultRect(): Rect { + getDefaultRect = (): Rect => { return { x: 0, y: 0, width: 0, height: 0 }; - } + }; calculateBbox = debounce(() => { - this._log.debug('Calculating bbox'); + this.log.debug('Calculating bbox'); this.isPendingBboxCalculation = true; if (this.objects.size === 0) { - this._log.trace('No objects, resetting bbox'); + this.log.trace('No objects, resetting bbox'); this.rect = this.getDefaultRect(); this.bbox = this.getDefaultRect(); this.isPendingBboxCalculation = false; @@ -694,7 +554,7 @@ export class CanvasLayer extends CanvasEntity { this.rect = deepClone(rect); this.bbox = deepClone(rect); this.isPendingBboxCalculation = false; - this._log.trace({ bbox: this.bbox, rect: this.rect }, 'Got bbox from client rect'); + this.log.trace({ bbox: this.bbox, rect: this.rect }, 'Got bbox from client rect'); this.updateBbox(); return; } @@ -708,7 +568,7 @@ export class CanvasLayer extends CanvasEntity { return; } const imageData = ctx.getImageData(0, 0, rect.width, rect.height); - this._manager.requestBbox( + this.manager.requestBbox( { buffer: imageData.data.buffer, width: imageData.width, height: imageData.height }, (extents) => { if (extents) { @@ -725,27 +585,27 @@ export class CanvasLayer extends CanvasEntity { this.rect = this.getDefaultRect(); } this.isPendingBboxCalculation = false; - this._log.trace({ bbox: this.bbox, rect: this.rect, extents }, `Got bbox from worker`); + this.log.trace({ bbox: this.bbox, rect: this.rect, extents }, `Got bbox from worker`); this.updateBbox(); clone.destroy(); } ); }, CanvasManager.BBOX_DEBOUNCE_MS); - repr() { + repr = () => { return { id: this.id, - type: this.type, - state: deepClone(this._state), + type: CanvasLayer.TYPE, + state: deepClone(this.state), rect: deepClone(this.rect), bbox: deepClone(this.bbox), - bboxNeedsUpdate: this._bboxNeedsUpdate, - isFirstRender: this._isFirstRender, + bboxNeedsUpdate: this.bboxNeedsUpdate, + isFirstRender: this.isFirstRender, isTransforming: this.isTransforming, isPendingBboxCalculation: this.isPendingBboxCalculation, objects: Array.from(this.objects.values()).map((obj) => obj.repr()), }; - } + }; logDebugInfo(msg = 'Debug info') { const info = { @@ -771,6 +631,6 @@ export class CanvasLayer extends CanvasEntity { offsetY: this.konva.objectGroup.offsetY(), }, }; - this._log.trace(info, msg); + this.log.trace(info, msg); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObject.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObject.ts index 3a07b77e835..52b84a4cd3c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObject.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObject.ts @@ -1,4 +1,5 @@ import type { JSONObject } from 'common/types'; +import type { CanvasControlAdapter } from 'features/controlLayers/konva/CanvasControlAdapter'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasStagingArea } from 'features/controlLayers/konva/CanvasStagingArea'; @@ -7,27 +8,17 @@ import type { Logger } from 'roarr'; export abstract class CanvasObject { id: string; - _parent: CanvasLayer | CanvasStagingArea; - _manager: CanvasManager; - _log: Logger; + parent: CanvasLayer | CanvasStagingArea | CanvasControlAdapter; + manager: CanvasManager; + log: Logger; - constructor(id: string, parent: CanvasLayer | CanvasStagingArea) { + constructor(id: string, parent: CanvasLayer | CanvasStagingArea | CanvasControlAdapter) { this.id = id; - this._parent = parent; - this._manager = parent._manager; - this._log = this._manager.buildLogger(this._getLoggingContext); + this.parent = parent; + this.manager = parent.manager; + this.log = this.manager.buildLogger(this.getLoggingContext); } - /** - * Destroy the object's konva nodes. - */ - abstract destroy(): void; - - /** - * Set the visibility of the object's konva nodes. - */ - abstract setVisibility(isVisible: boolean): void; - /** * Get a serializable representation of the object. */ @@ -38,9 +29,9 @@ export abstract class CanvasObject { * @param extra Extra data to merge into the context * @returns The logging context for this object */ - _getLoggingContext = (extra?: Record) => { + getLoggingContext = (extra?: Record) => { return { - ...this._parent._getLoggingContext(), + ...this.parent.getLoggingContext(), objectId: this.id, ...extra, }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts index 96b4ac1c060..fe0b875379b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts @@ -19,7 +19,7 @@ export class CanvasRect extends CanvasObject { constructor(state: RectShape, parent: CanvasLayer) { super(state.id, parent); - this._log.trace({ state }, 'Creating rect'); + this.log.trace({ state }, 'Creating rect'); const { x, y, width, height, color } = state; @@ -41,7 +41,7 @@ export class CanvasRect extends CanvasObject { update(state: RectShape, force?: boolean): boolean { if (this.state !== state || force) { - this._log.trace({ state }, 'Updating rect'); + this.log.trace({ state }, 'Updating rect'); const { x, y, width, height, color } = state; this.konva.rect.setAttrs({ x, @@ -58,12 +58,12 @@ export class CanvasRect extends CanvasObject { } destroy() { - this._log.trace('Destroying rect'); + this.log.trace('Destroying rect'); this.konva.group.destroy(); } setVisibility(isVisible: boolean): void { - this._log.trace({ isVisible }, 'Setting rect visibility'); + this.log.trace({ isVisible }, 'Setting rect visibility'); this.konva.group.visible(isVisible); } @@ -71,7 +71,7 @@ export class CanvasRect extends CanvasObject { return { id: this.id, type: CanvasRect.TYPE, - parent: this._parent.id, + parent: this.parent.id, state: deepClone(this.state), }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts index c7f83c51730..88dfa3e3cdd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts @@ -22,9 +22,9 @@ export class CanvasStagingArea extends CanvasEntity { } async render() { - const session = this._manager.stateApi.getSession(); - const bboxRect = this._manager.stateApi.getBbox().rect; - const shouldShowStagedImage = this._manager.stateApi.getShouldShowStagedImage(); + const session = this.manager.stateApi.getSession(); + const bboxRect = this.manager.stateApi.getBbox().rect; + const shouldShowStagedImage = this.manager.stateApi.getShouldShowStagedImage(); this.selectedImage = session.stagedImages[session.selectedStagedImageIndex] ?? null; @@ -59,7 +59,7 @@ export class CanvasStagingArea extends CanvasEntity { this.image.konva.group.x(bboxRect.x + offsetX); this.image.konva.group.y(bboxRect.y + offsetY); await this.image.updateImageSource(imageDTO.image_name); - this._manager.stateApi.resetLastProgressEvent(); + this.manager.stateApi.resetLastProgressEvent(); } this.image.konva.group.visible(shouldShowStagedImage); } else { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts new file mode 100644 index 00000000000..b42cb093a83 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -0,0 +1,228 @@ +import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; +import { CanvasObject } from 'features/controlLayers/konva/CanvasObject'; +import { nanoid } from 'features/controlLayers/konva/util'; +import type { Coordinate } from 'features/controlLayers/store/types'; +import Konva from 'konva'; + +export class CanvasTransformer extends CanvasObject { + static TYPE = 'transformer'; + + isActive: boolean; + konva: { + transformer: Konva.Transformer; + }; + + constructor(parent: CanvasLayer) { + super(`${CanvasTransformer.TYPE}:${nanoid()}`, parent); + + this.isActive = false; + this.konva = { + transformer: new Konva.Transformer({ + name: CanvasTransformer.TYPE, + // The transformer will use the interaction rect as a proxy for the entity it is transforming. + nodes: [parent.konva.interactionRect], + // Visibility and listening are managed via activate() and deactivate() + visible: false, + listening: false, + // Rotation is allowed + rotateEnabled: true, + // When dragging a transform anchor across either the x or y axis, the nodes will be flipped across the axis + flipEnabled: true, + // Transforming will retain aspect ratio only when shift is held + keepRatio: false, + // The padding is the distance between the transformer bbox and the nodes + padding: this.manager.getTransformerPadding(), + // This is `invokeBlue.400` + stroke: 'hsl(200deg 76% 59%)', + // TODO(psyche): The konva Vector2D type is is apparently not compatible with the JSONObject type that the log + // function expects. The in-house Coordinate type is functionally the same - `{x: number; y: number}` - and + // TypeScript is happy with it. + anchorDragBoundFunc: (oldPos: Coordinate, newPos: Coordinate) => { + // The anchorDragBoundFunc callback puts constraints on the movement of the transformer anchors, which in + // turn constrain the transformation. It is called on every anchor move. We'll use this to snap the anchors + // to the nearest pixel. + + // If we are rotating, no need to do anything - just let the rotation happen. + if (this.konva.transformer.getActiveAnchor() === 'rotater') { + return newPos; + } + + // We need to snap the anchor to the nearest pixel, but the positions provided to this callback are absolute, + // scaled coordinates. They need to be converted to stage coordinates, snapped, then converted back to absolute + // before returning them. + const stageScale = this.manager.getStageScale(); + const stagePos = this.manager.getStagePosition(); + + // Unscale and round the target position to the nearest pixel. + const targetX = Math.round(newPos.x / stageScale); + const targetY = Math.round(newPos.y / stageScale); + + // The stage may be offset a fraction of a pixel. To ensure the anchor snaps to the nearest pixel, we need to + // calculate that offset and add it back to the target position. + + // Calculate the offset. It's the remainder of the stage position divided by the scale * desired grid size. In + // this case, the grid size is 1px. For example, if we wanted to snap to the nearest 8px, the calculation would + // be `stagePos.x % (stageScale * 8)`. + const scaledOffsetX = stagePos.x % stageScale; + const scaledOffsetY = stagePos.y % stageScale; + + // Unscale the target position and add the offset to get the absolute position for this anchor. + const scaledTargetX = targetX * stageScale + scaledOffsetX; + const scaledTargetY = targetY * stageScale + scaledOffsetY; + + this.log.trace( + { + oldPos, + newPos, + stageScale, + stagePos, + targetX, + targetY, + scaledOffsetX, + scaledOffsetY, + scaledTargetX, + scaledTargetY, + }, + 'Anchor drag bound' + ); + + return { x: scaledTargetX, y: scaledTargetY }; + }, + boundBoxFunc: (oldBoundBox, newBoundBox) => { + // This transform constraint operates on the bounding box of the transformer. This box has x, y, width, and + // height in stage coordinates, and rotation in radians. This can be used to snap the transformer rotation to + // the nearest 45 degrees when shift is held. + if (this.manager.stateApi.getShiftKey()) { + if (Math.abs(newBoundBox.rotation % (Math.PI / 4)) > 0) { + return oldBoundBox; + } + } + + return newBoundBox; + }, + }), + }; + + this.konva.transformer.on('transformstart', () => { + // Just logging in this callback. Called on mouse down of a transform anchor. + this.log.trace( + { + x: parent.konva.interactionRect.x(), + y: parent.konva.interactionRect.y(), + scaleX: parent.konva.interactionRect.scaleX(), + scaleY: parent.konva.interactionRect.scaleY(), + rotation: parent.konva.interactionRect.rotation(), + }, + 'Transform started' + ); + }); + + this.konva.transformer.on('transform', () => { + // This is called when a transform anchor is dragged. By this time, the transform constraints in the above + // callbacks have been enforced, and the transformer has updated its nodes' attributes. We need to pass the + // updated attributes to the object group, propagating the transformation on down. + parent.konva.objectGroup.setAttrs({ + x: parent.konva.interactionRect.x(), + y: parent.konva.interactionRect.y(), + scaleX: parent.konva.interactionRect.scaleX(), + scaleY: parent.konva.interactionRect.scaleY(), + rotation: parent.konva.interactionRect.rotation(), + }); + }); + + this.konva.transformer.on('transformend', () => { + // Called on mouse up on an anchor. We'll do some final snapping to ensure the transformer is pixel-perfect. + + // Snap the position to the nearest pixel. + const x = parent.konva.interactionRect.x(); + const y = parent.konva.interactionRect.y(); + const snappedX = Math.round(x); + const snappedY = Math.round(y); + + // The transformer doesn't modify the width and height. It only modifies scale. We'll need to apply the scale to + // the width and height, round them to the nearest pixel, and finally calculate a new scale that will result in + // the snapped width and height. + const width = parent.konva.interactionRect.width(); + const height = parent.konva.interactionRect.height(); + const scaleX = parent.konva.interactionRect.scaleX(); + const scaleY = parent.konva.interactionRect.scaleY(); + + // Determine the target width and height, rounded to the nearest pixel. Must be >= 1. Because the scales can be + // negative, we need to take the absolute value of the width and height. + const targetWidth = Math.max(Math.abs(Math.round(width * scaleX)), 1); + const targetHeight = Math.max(Math.abs(Math.round(height * scaleY)), 1); + + // Calculate the scale we need to use to get the target width and height. Restore the sign of the scales. + const snappedScaleX = (targetWidth / width) * Math.sign(scaleX); + const snappedScaleY = (targetHeight / height) * Math.sign(scaleY); + + // Update interaction rect and object group attributes. + parent.konva.interactionRect.setAttrs({ + x: snappedX, + y: snappedY, + scaleX: snappedScaleX, + scaleY: snappedScaleY, + }); + parent.konva.objectGroup.setAttrs({ + x: snappedX, + y: snappedY, + scaleX: snappedScaleX, + scaleY: snappedScaleY, + }); + + // Rotation is only retrieved for logging purposes. + const rotation = parent.konva.interactionRect.rotation(); + + this.log.trace( + { + x, + y, + width, + height, + scaleX, + scaleY, + rotation, + snappedX, + snappedY, + targetWidth, + targetHeight, + snappedScaleX, + snappedScaleY, + }, + 'Transform ended' + ); + }); + + this.manager.stateApi.onShiftChanged((isPressed) => { + // While the user holds shift, we want to snap rotation to 45 degree increments. Listen for the shift key state + // and update the snap angles accordingly. + this.konva.transformer.rotationSnaps(isPressed ? [0, 45, 90, 135, 180, 225, 270, 315] : []); + }); + } + + /** + * Activate the transformer. This will make it visible and listening for events. + */ + activate = () => { + this.isActive = true; + this.konva.transformer.visible(true); + this.konva.transformer.listening(true); + }; + + /** + * Deactivate the transformer. This will make it invisible and not listening for events. + */ + deactivate = () => { + this.isActive = false; + this.konva.transformer.visible(false); + this.konva.transformer.listening(false); + }; + + repr = () => { + return { + id: this.id, + type: CanvasTransformer.TYPE, + isActive: this.isActive, + }; + }; +} From d5e4a965cc39ef91135ea7d2af75fc93f7a46faf Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 1 Aug 2024 19:43:27 +1000 Subject: [PATCH 269/678] feat(ui): remove inheritance of CanvasObject JS is terrible --- .../controlLayers/konva/CanvasBrushLine.ts | 28 +++++++++---- .../controlLayers/konva/CanvasEntity.ts | 2 +- .../controlLayers/konva/CanvasEraserLine.ts | 27 +++++++++---- .../controlLayers/konva/CanvasImage.ts | 37 +++++++++++------- .../konva/CanvasInteractionRect.ts | 27 ++++++++----- .../controlLayers/konva/CanvasManager.ts | 35 ++++++++++++++++- .../controlLayers/konva/CanvasObject.ts | 39 ------------------- .../controlLayers/konva/CanvasRect.ts | 26 +++++++++---- .../controlLayers/konva/CanvasTransformer.ts | 21 ++++++++-- 9 files changed, 149 insertions(+), 93 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasObject.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts index acafcf3b27a..d81a413f83d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts @@ -1,15 +1,22 @@ +import type { JSONObject } from 'common/types'; import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; -import { CanvasObject } from 'features/controlLayers/konva/CanvasObject'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { BrushLine } from 'features/controlLayers/store/types'; import Konva from 'konva'; +import type { Logger } from 'roarr'; -export class CanvasBrushLine extends CanvasObject { - static NAME_PREFIX = 'brush-line'; - static GROUP_NAME = `${CanvasBrushLine.NAME_PREFIX}_group`; - static LINE_NAME = `${CanvasBrushLine.NAME_PREFIX}_line`; +export class CanvasBrushLine { static TYPE = 'brush_line'; + static GROUP_NAME = `${CanvasBrushLine.TYPE}_group`; + static LINE_NAME = `${CanvasBrushLine.TYPE}_line`; + + id: string; + parent: CanvasLayer; + manager: CanvasManager; + log: Logger; + getLoggingContext: (extra?: JSONObject) => JSONObject; state: BrushLine; konva: { @@ -18,10 +25,15 @@ export class CanvasBrushLine extends CanvasObject { }; constructor(state: BrushLine, parent: CanvasLayer) { - super(state.id, parent); - this.log.trace({ state }, 'Creating brush line'); + const { id, strokeWidth, clip, color, points } = state; + this.id = id; + this.parent = parent; + this.manager = parent.manager; - const { strokeWidth, clip, color, points } = state; + this.getLoggingContext = this.manager.buildObjectGetLoggingContext(this); + this.log = this.manager.buildLogger(this.getLoggingContext); + + this.log.trace({ state }, 'Creating brush line'); this.konva = { group: new Konva.Group({ diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity.ts index b9775cd2c1c..626704d2bef 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity.ts @@ -19,7 +19,7 @@ export abstract class CanvasEntity { getLoggingContext = (extra?: Record) => { return { - ...this.manager._getLoggingContext(), + ...this.manager.getLoggingContext(), layerId: this.id, ...extra, }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts index 64e7595f0eb..1fe08559cb1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts @@ -1,16 +1,23 @@ +import type { JSONObject } from 'common/types'; import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; -import { CanvasObject } from 'features/controlLayers/konva/CanvasObject'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { EraserLine } from 'features/controlLayers/store/types'; import { RGBA_RED } from 'features/controlLayers/store/types'; import Konva from 'konva'; +import type { Logger } from 'roarr'; -export class CanvasEraserLine extends CanvasObject { - static NAME_PREFIX = 'eraser-line'; - static GROUP_NAME = `${CanvasEraserLine.NAME_PREFIX}_group`; - static LINE_NAME = `${CanvasEraserLine.NAME_PREFIX}_line`; +export class CanvasEraserLine { static TYPE = 'eraser_line'; + static GROUP_NAME = `${CanvasEraserLine.TYPE}_group`; + static LINE_NAME = `${CanvasEraserLine.TYPE}_line`; + + id: string; + parent: CanvasLayer; + manager: CanvasManager; + log: Logger; + getLoggingContext: (extra?: JSONObject) => JSONObject; state: EraserLine; konva: { @@ -19,10 +26,14 @@ export class CanvasEraserLine extends CanvasObject { }; constructor(state: EraserLine, parent: CanvasLayer) { - super(state.id, parent); - this.log.trace({ state }, 'Creating eraser line'); + const { id, strokeWidth, clip, points } = state; + this.id = id; + this.parent = parent; + this.manager = parent.manager; + this.getLoggingContext = this.manager.buildObjectGetLoggingContext(this); + this.log = this.manager.buildLogger(this.getLoggingContext); - const { strokeWidth, clip, points } = state; + this.log.trace({ state }, 'Creating eraser line'); this.konva = { group: new Konva.Group({ diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts index 021ced65816..521ccc94341 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -1,23 +1,28 @@ +import type { JSONObject } from 'common/types'; import { deepClone } from 'common/util/deepClone'; -import type { CanvasControlAdapter } from 'features/controlLayers/konva/CanvasControlAdapter'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; -import { CanvasObject } from 'features/controlLayers/konva/CanvasObject'; -import type { CanvasStagingArea } from 'features/controlLayers/konva/CanvasStagingArea'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { FILTER_MAP } from 'features/controlLayers/konva/filters'; import { loadImage } from 'features/controlLayers/konva/util'; import type { ImageObject } from 'features/controlLayers/store/types'; import { t } from 'i18next'; import Konva from 'konva'; +import type { Logger } from 'roarr'; import { getImageDTO } from 'services/api/endpoints/images'; -export class CanvasImage extends CanvasObject { - static NAME_PREFIX = 'canvas-image'; - static GROUP_NAME = `${CanvasImage.NAME_PREFIX}_group`; - static IMAGE_NAME = `${CanvasImage.NAME_PREFIX}_image`; - static PLACEHOLDER_GROUP_NAME = `${CanvasImage.NAME_PREFIX}_placeholder-group`; - static PLACEHOLDER_RECT_NAME = `${CanvasImage.NAME_PREFIX}_placeholder-rect`; - static PLACEHOLDER_TEXT_NAME = `${CanvasImage.NAME_PREFIX}_placeholder-text`; +export class CanvasImage { static TYPE = 'image'; + static GROUP_NAME = `${CanvasImage.TYPE}_group`; + static IMAGE_NAME = `${CanvasImage.TYPE}_image`; + static PLACEHOLDER_GROUP_NAME = `${CanvasImage.TYPE}_placeholder-group`; + static PLACEHOLDER_RECT_NAME = `${CanvasImage.TYPE}_placeholder-rect`; + static PLACEHOLDER_TEXT_NAME = `${CanvasImage.TYPE}_placeholder-text`; + + id: string; + parent: CanvasLayer; + manager: CanvasManager; + log: Logger; + getLoggingContext: (extra?: JSONObject) => JSONObject; state: ImageObject; konva: { @@ -29,11 +34,15 @@ export class CanvasImage extends CanvasObject { isLoading: boolean; isError: boolean; - constructor(state: ImageObject, parent: CanvasLayer | CanvasStagingArea | CanvasControlAdapter) { - super(state.id, parent); - this.log.trace({ state }, 'Creating image'); + constructor(state: ImageObject, parent: CanvasLayer) { + const { id, width, height, x, y } = state; + this.id = id; + this.parent = parent; + this.manager = parent.manager; + this.getLoggingContext = this.manager.buildObjectGetLoggingContext(this); + this.log = this.manager.buildLogger(this.getLoggingContext); - const { width, height, x, y } = state; + this.log.trace({ state }, 'Creating image'); this.konva = { group: new Konva.Group({ name: CanvasImage.GROUP_NAME, listening: false, x, y }), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInteractionRect.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInteractionRect.ts index 742c8073b18..e65c9cf1a66 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInteractionRect.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInteractionRect.ts @@ -1,17 +1,30 @@ -import { nanoid } from '@reduxjs/toolkit'; -import { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; -import { CanvasObject } from 'features/controlLayers/konva/CanvasObject'; +import type { JSONObject } from 'common/types'; +import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { getPrefixedId } from 'features/controlLayers/konva/util'; import Konva from 'konva'; +import type { Logger } from 'roarr'; -export class CanvasInteractionRect extends CanvasObject { +export class CanvasInteractionRect { static TYPE = 'interaction_rect'; + id: string; + parent: CanvasLayer; + manager: CanvasManager; + log: Logger; + getLoggingContext: (extra?: JSONObject) => JSONObject; + konva: { rect: Konva.Rect; }; constructor(parent: CanvasLayer) { - super(`${CanvasInteractionRect.TYPE}:${nanoid()}`, parent); + this.id = getPrefixedId(CanvasInteractionRect.TYPE); + this.parent = parent; + this.manager = parent.manager; + + this.getLoggingContext = this.manager.buildObjectGetLoggingContext(this); + this.log = this.manager.buildLogger(this.getLoggingContext); this.konva = { rect: new Konva.Rect({ @@ -62,10 +75,6 @@ export class CanvasInteractionRect extends CanvasObject { return { id: this.id, type: CanvasInteractionRect.TYPE, - x: this.konva.rect.x(), - y: this.konva.rect.y(), - width: this.konva.rect.width(), - height: this.konva.rect.height(), }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index ce54d1ba533..06e2be5d77c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -2,14 +2,21 @@ import type { Store } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import type { JSONObject } from 'common/types'; +import type { CanvasBrushLine } from 'features/controlLayers/konva/CanvasBrushLine'; +import type { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine'; +import type { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; import { CanvasInitialImage } from 'features/controlLayers/konva/CanvasInitialImage'; +import type { CanvasInteractionRect } from 'features/controlLayers/konva/CanvasInteractionRect'; import { CanvasProgressPreview } from 'features/controlLayers/konva/CanvasProgressPreview'; +import type { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; +import type { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; import { getCompositeLayerImage, getControlAdapterImage, getGenerationMode, getInitialImage, getInpaintMaskImage, + getPrefixedId, getRegionMaskImage, nanoid, } from 'features/controlLayers/konva/util'; @@ -113,7 +120,7 @@ export class CanvasManager { ...message, context: { ...message.context, - ...this._getLoggingContext(), + ...this.getLoggingContext(), }, }; }); @@ -568,7 +575,7 @@ export class CanvasManager { } } - _getLoggingContext() { + getLoggingContext() { return { // timestamp: new Date().toISOString(), }; @@ -586,6 +593,28 @@ export class CanvasManager { }); } + buildObjectGetLoggingContext = ( + instance: CanvasBrushLine | CanvasEraserLine | CanvasRect | CanvasImage | CanvasTransformer | CanvasInteractionRect + ) => { + return (extra?: JSONObject): JSONObject => { + return { + ...instance.parent.getLoggingContext(), + objectId: instance.id, + ...extra, + }; + }; + }; + + buildEntityGetLoggingContext = (instance: CanvasLayer) => { + return (extra?: JSONObject): JSONObject => { + return { + ...instance.manager.getLoggingContext(), + entityId: instance.id, + ...extra, + }; + }; + }; + logDebugInfo() { // eslint-disable-next-line no-console console.log(this); @@ -594,4 +623,6 @@ export class CanvasManager { console.log(layer); } } + + getPrefixedId = getPrefixedId; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObject.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObject.ts deleted file mode 100644 index 52b84a4cd3c..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObject.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { JSONObject } from 'common/types'; -import type { CanvasControlAdapter } from 'features/controlLayers/konva/CanvasControlAdapter'; -import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; -import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { CanvasStagingArea } from 'features/controlLayers/konva/CanvasStagingArea'; -import type { Logger } from 'roarr'; - -export abstract class CanvasObject { - id: string; - - parent: CanvasLayer | CanvasStagingArea | CanvasControlAdapter; - manager: CanvasManager; - log: Logger; - - constructor(id: string, parent: CanvasLayer | CanvasStagingArea | CanvasControlAdapter) { - this.id = id; - this.parent = parent; - this.manager = parent.manager; - this.log = this.manager.buildLogger(this.getLoggingContext); - } - - /** - * Get a serializable representation of the object. - */ - abstract repr(): JSONObject; - - /** - * Get the logging context for this object. - * @param extra Extra data to merge into the context - * @returns The logging context for this object - */ - getLoggingContext = (extra?: Record) => { - return { - ...this.parent.getLoggingContext(), - objectId: this.id, - ...extra, - }; - }; -} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts index fe0b875379b..d1503e74029 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts @@ -1,15 +1,22 @@ +import type { JSONObject } from 'common/types'; import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; -import { CanvasObject } from 'features/controlLayers/konva/CanvasObject'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { RectShape } from 'features/controlLayers/store/types'; import Konva from 'konva'; +import type { Logger } from 'roarr'; -export class CanvasRect extends CanvasObject { - static NAME_PREFIX = 'canvas-rect'; - static GROUP_NAME = `${CanvasRect.NAME_PREFIX}_group`; - static RECT_NAME = `${CanvasRect.NAME_PREFIX}_rect`; +export class CanvasRect { static TYPE = 'rect'; + static GROUP_NAME = `${CanvasRect.TYPE}_group`; + static RECT_NAME = `${CanvasRect.TYPE}_rect`; + + id: string; + parent: CanvasLayer; + manager: CanvasManager; + log: Logger; + getLoggingContext: (extra?: JSONObject) => JSONObject; state: RectShape; konva: { @@ -18,11 +25,14 @@ export class CanvasRect extends CanvasObject { }; constructor(state: RectShape, parent: CanvasLayer) { - super(state.id, parent); + const { id, x, y, width, height, color } = state; + this.id = id; + this.parent = parent; + this.manager = parent.manager; + this.getLoggingContext = this.manager.buildObjectGetLoggingContext(this); + this.log = this.manager.buildLogger(this.getLoggingContext); this.log.trace({ state }, 'Creating rect'); - const { x, y, width, height, color } = state; - this.konva = { group: new Konva.Group({ name: CanvasRect.GROUP_NAME, listening: false }), rect: new Konva.Rect({ diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index b42cb093a83..b8022cd0c0f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -1,19 +1,32 @@ +import type { JSONObject } from 'common/types'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; -import { CanvasObject } from 'features/controlLayers/konva/CanvasObject'; -import { nanoid } from 'features/controlLayers/konva/util'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { Coordinate } from 'features/controlLayers/store/types'; import Konva from 'konva'; +import type { Logger } from 'roarr'; -export class CanvasTransformer extends CanvasObject { +export class CanvasTransformer { static TYPE = 'transformer'; + id: string; + parent: CanvasLayer; + manager: CanvasManager; + log: Logger; + getLoggingContext: (extra?: JSONObject) => JSONObject; + isActive: boolean; konva: { transformer: Konva.Transformer; }; constructor(parent: CanvasLayer) { - super(`${CanvasTransformer.TYPE}:${nanoid()}`, parent); + this.parent = parent; + this.manager = parent.manager; + this.id = getPrefixedId(CanvasTransformer.TYPE); + + this.getLoggingContext = this.manager.buildObjectGetLoggingContext(this); + this.log = this.manager.buildLogger(this.getLoggingContext); this.isActive = false; this.konva = { From 148434d8337536d1d7b8bab0b3271b19857ec8a1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 1 Aug 2024 19:52:54 +1000 Subject: [PATCH 270/678] feat(ui): typing for logging context --- .../controlLayers/konva/CanvasEraserLine.ts | 5 ++- .../controlLayers/konva/CanvasImage.ts | 10 +++--- .../konva/CanvasInteractionRect.ts | 4 +-- .../controlLayers/konva/CanvasLayer.ts | 33 ++++++++++++------- .../controlLayers/konva/CanvasManager.ts | 6 ++-- .../controlLayers/konva/CanvasRect.ts | 5 ++- .../controlLayers/konva/CanvasTransformer.ts | 7 ++-- .../src/features/controlLayers/store/types.ts | 3 ++ 8 files changed, 41 insertions(+), 32 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts index 1fe08559cb1..cf54722bf1c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts @@ -1,9 +1,8 @@ -import type { JSONObject } from 'common/types'; import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { EraserLine } from 'features/controlLayers/store/types'; +import type { EraserLine, GetLoggingContext } from 'features/controlLayers/store/types'; import { RGBA_RED } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; @@ -17,7 +16,7 @@ export class CanvasEraserLine { parent: CanvasLayer; manager: CanvasManager; log: Logger; - getLoggingContext: (extra?: JSONObject) => JSONObject; + getLoggingContext: GetLoggingContext; state: EraserLine; konva: { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts index 521ccc94341..8e69b7b41d4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -1,10 +1,10 @@ -import type { JSONObject } from 'common/types'; import { deepClone } from 'common/util/deepClone'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import type { CanvasStagingArea } from 'features/controlLayers/konva/CanvasStagingArea'; import { FILTER_MAP } from 'features/controlLayers/konva/filters'; import { loadImage } from 'features/controlLayers/konva/util'; -import type { ImageObject } from 'features/controlLayers/store/types'; +import type { GetLoggingContext, ImageObject } from 'features/controlLayers/store/types'; import { t } from 'i18next'; import Konva from 'konva'; import type { Logger } from 'roarr'; @@ -19,10 +19,10 @@ export class CanvasImage { static PLACEHOLDER_TEXT_NAME = `${CanvasImage.TYPE}_placeholder-text`; id: string; - parent: CanvasLayer; + parent: CanvasLayer | CanvasStagingArea; manager: CanvasManager; log: Logger; - getLoggingContext: (extra?: JSONObject) => JSONObject; + getLoggingContext: GetLoggingContext; state: ImageObject; konva: { @@ -34,7 +34,7 @@ export class CanvasImage { isLoading: boolean; isError: boolean; - constructor(state: ImageObject, parent: CanvasLayer) { + constructor(state: ImageObject, parent: CanvasLayer | CanvasStagingArea) { const { id, width, height, x, y } = state; this.id = id; this.parent = parent; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInteractionRect.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInteractionRect.ts index e65c9cf1a66..77c9a57d62e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInteractionRect.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInteractionRect.ts @@ -1,7 +1,7 @@ -import type { JSONObject } from 'common/types'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; +import type { GetLoggingContext } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; @@ -12,7 +12,7 @@ export class CanvasInteractionRect { parent: CanvasLayer; manager: CanvasManager; log: Logger; - getLoggingContext: (extra?: JSONObject) => JSONObject; + getLoggingContext: GetLoggingContext; konva: { rect: Konva.Rect; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index c651df5d5cb..c78eb57a813 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -1,7 +1,6 @@ import { getStore } from 'app/store/nanostores/store'; import { deepClone } from 'common/util/deepClone'; import { CanvasBrushLine } from 'features/controlLayers/konva/CanvasBrushLine'; -import { CanvasEntity } from 'features/controlLayers/konva/CanvasEntity'; import { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine'; import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; import { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; @@ -9,22 +8,24 @@ import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; import { getPrefixedId, konvaNodeToBlob, mapId, previewBlob } from 'features/controlLayers/konva/util'; import { layerRasterized } from 'features/controlLayers/store/canvasV2Slice'; -import { - type BrushLine, - type CanvasV2State, - type Coordinate, - type EraserLine, - imageDTOToImageObject, - type LayerEntity, - type Rect, - type RectShape, +import type { + BrushLine, + CanvasV2State, + Coordinate, + EraserLine, + GetLoggingContext, + LayerEntity, + Rect, + RectShape, } from 'features/controlLayers/store/types'; +import { imageDTOToImageObject } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { debounce, get } from 'lodash-es'; +import type { Logger } from 'roarr'; import { uploadImage } from 'services/api/endpoints/images'; import { assert } from 'tsafe'; -export class CanvasLayer extends CanvasEntity { +export class CanvasLayer { static TYPE = 'layer'; static LAYER_NAME = `${CanvasLayer.TYPE}_layer`; static TRANSFORMER_NAME = `${CanvasLayer.TYPE}_transformer`; @@ -33,6 +34,11 @@ export class CanvasLayer extends CanvasEntity { static OBJECT_GROUP_NAME = `${CanvasLayer.TYPE}_object-group`; static BBOX_NAME = `${CanvasLayer.TYPE}_bbox`; + id: string; + manager: CanvasManager; + log: Logger; + getLoggingContext: GetLoggingContext; + drawingBuffer: BrushLine | EraserLine | RectShape | null; state: LayerEntity; @@ -54,7 +60,10 @@ export class CanvasLayer extends CanvasEntity { bbox: Rect; constructor(state: LayerEntity, manager: CanvasManager) { - super(state.id, manager); + this.id = state.id; + this.manager = manager; + this.getLoggingContext = this.manager.buildEntityGetLoggingContext(this); + this.log = this.manager.buildLogger(this.getLoggingContext); this.log.debug({ state }, 'Creating layer'); this.konva = { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 06e2be5d77c..63ff1204c6b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -22,7 +22,7 @@ import { } from 'features/controlLayers/konva/util'; import type { Extents, ExtentsResult, GetBboxTask, WorkerLogMessage } from 'features/controlLayers/konva/worker'; import { $lastProgressEvent, $shouldShowStagedImage } from 'features/controlLayers/store/canvasV2Slice'; -import type { CanvasV2State, Coordinate, GenerationMode } from 'features/controlLayers/store/types'; +import type { CanvasV2State, Coordinate, GenerationMode, GetLoggingContext } from 'features/controlLayers/store/types'; import type Konva from 'konva'; import { atom } from 'nanostores'; import type { Logger } from 'roarr'; @@ -595,7 +595,7 @@ export class CanvasManager { buildObjectGetLoggingContext = ( instance: CanvasBrushLine | CanvasEraserLine | CanvasRect | CanvasImage | CanvasTransformer | CanvasInteractionRect - ) => { + ): GetLoggingContext => { return (extra?: JSONObject): JSONObject => { return { ...instance.parent.getLoggingContext(), @@ -605,7 +605,7 @@ export class CanvasManager { }; }; - buildEntityGetLoggingContext = (instance: CanvasLayer) => { + buildEntityGetLoggingContext = (instance: CanvasLayer | CanvasStagingArea): GetLoggingContext => { return (extra?: JSONObject): JSONObject => { return { ...instance.manager.getLoggingContext(), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts index d1503e74029..a0453048be6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts @@ -1,9 +1,8 @@ -import type { JSONObject } from 'common/types'; import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { RectShape } from 'features/controlLayers/store/types'; +import type { GetLoggingContext, RectShape } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; @@ -16,7 +15,7 @@ export class CanvasRect { parent: CanvasLayer; manager: CanvasManager; log: Logger; - getLoggingContext: (extra?: JSONObject) => JSONObject; + getLoggingContext: GetLoggingContext; state: RectShape; konva: { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index b8022cd0c0f..c34099193b7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -1,8 +1,7 @@ -import type { JSONObject } from 'common/types'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import type { Coordinate } from 'features/controlLayers/store/types'; +import type { Coordinate , GetLoggingContext } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; @@ -10,10 +9,10 @@ export class CanvasTransformer { static TYPE = 'transformer'; id: string; - parent: CanvasLayer; + parent: CanvasLayer manager: CanvasManager; log: Logger; - getLoggingContext: (extra?: JSONObject) => JSONObject; + getLoggingContext: GetLoggingContext isActive: boolean; konva: { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 901bda00e11..6b4874d27fe 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,3 +1,4 @@ +import type { JSONObject } from 'common/types'; import type { CanvasControlAdapter } from 'features/controlLayers/konva/CanvasControlAdapter'; import { CanvasInpaintMask } from 'features/controlLayers/konva/CanvasInpaintMask'; import { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; @@ -963,3 +964,5 @@ export function isDrawableEntityType( ): entityType is 'layer' | 'regional_guidance' | 'inpaint_mask' { return entityType === 'layer' || entityType === 'regional_guidance' || entityType === 'inpaint_mask'; } + +export type GetLoggingContext = (extra?: JSONObject) => JSONObject; From c734385f1867d109d7e2f0d722a7a7c7c57c48a0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 1 Aug 2024 19:53:04 +1000 Subject: [PATCH 271/678] feat(ui): prepare staging area --- .../controlLayers/konva/CanvasStagingArea.ts | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts index 88dfa3e3cdd..278e3dbfc02 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts @@ -1,27 +1,37 @@ -import { CanvasEntity } from 'features/controlLayers/konva/CanvasEntity'; import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { StagingAreaImage } from 'features/controlLayers/store/types'; +import { getPrefixedId } from 'features/controlLayers/konva/util'; +import type { GetLoggingContext, StagingAreaImage } from 'features/controlLayers/store/types'; import Konva from 'konva'; +import type { Logger } from 'roarr'; -export class CanvasStagingArea extends CanvasEntity { - static NAME_PREFIX = 'staging-area'; - static GROUP_NAME = `${CanvasStagingArea.NAME_PREFIX}_group`; +export class CanvasStagingArea { + static TYPE = 'staging_area'; + static GROUP_NAME = `${CanvasStagingArea.TYPE}_group`; + + id: string; + manager: CanvasManager; + log: Logger; + getLoggingContext: GetLoggingContext; - type = 'staging_area'; konva: { group: Konva.Group }; image: CanvasImage | null; selectedImage: StagingAreaImage | null; constructor(manager: CanvasManager) { - super('staging-area', manager); + this.id = getPrefixedId(CanvasStagingArea.TYPE); + this.manager = manager; + this.getLoggingContext = this.manager.buildEntityGetLoggingContext(this); + this.log = this.manager.buildLogger(this.getLoggingContext); + this.log.debug('Creating staging area'); + this.konva = { group: new Konva.Group({ name: CanvasStagingArea.GROUP_NAME, listening: false }) }; this.image = null; this.selectedImage = null; } - async render() { + render = async () => { const session = this.manager.stateApi.getSession(); const bboxRect = this.manager.stateApi.getBbox().rect; const shouldShowStagedImage = this.manager.stateApi.getShouldShowStagedImage(); @@ -65,13 +75,13 @@ export class CanvasStagingArea extends CanvasEntity { } else { this.image?.konva.group.visible(false); } - } + }; - repr() { + repr = () => { return { id: this.id, - type: this.type, + type: CanvasStagingArea.TYPE, selectedImage: this.selectedImage, }; - } + }; } From 3392e1f0bf17345736e4713f5c6027768041c78b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 1 Aug 2024 20:31:54 +1000 Subject: [PATCH 272/678] feat(ui): merge interaction rect into transformer class --- .../konva/CanvasInteractionRect.ts | 80 ----------- .../controlLayers/konva/CanvasLayer.ts | 108 +++++---------- .../controlLayers/konva/CanvasManager.ts | 3 +- .../controlLayers/konva/CanvasTransformer.ts | 129 ++++++++++++------ 4 files changed, 126 insertions(+), 194 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasInteractionRect.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInteractionRect.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInteractionRect.ts deleted file mode 100644 index 77c9a57d62e..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInteractionRect.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; -import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { getPrefixedId } from 'features/controlLayers/konva/util'; -import type { GetLoggingContext } from 'features/controlLayers/store/types'; -import Konva from 'konva'; -import type { Logger } from 'roarr'; - -export class CanvasInteractionRect { - static TYPE = 'interaction_rect'; - - id: string; - parent: CanvasLayer; - manager: CanvasManager; - log: Logger; - getLoggingContext: GetLoggingContext; - - konva: { - rect: Konva.Rect; - }; - - constructor(parent: CanvasLayer) { - this.id = getPrefixedId(CanvasInteractionRect.TYPE); - this.parent = parent; - this.manager = parent.manager; - - this.getLoggingContext = this.manager.buildObjectGetLoggingContext(this); - this.log = this.manager.buildLogger(this.getLoggingContext); - - this.konva = { - rect: new Konva.Rect({ - name: CanvasInteractionRect.TYPE, - listening: false, - draggable: true, - // fill: 'rgba(255,0,0,0.5)', - }), - }; - - this.konva.rect.on('dragmove', () => { - // Snap the interaction rect to the nearest pixel - this.konva.rect.x(Math.round(this.konva.rect.x())); - this.konva.rect.y(Math.round(this.konva.rect.y())); - - // The bbox should be updated to reflect the new position of the interaction rect, taking into account its padding - // and border - this.parent.konva.bbox.setAttrs({ - x: this.konva.rect.x() - this.manager.getScaledBboxPadding(), - y: this.konva.rect.y() - this.manager.getScaledBboxPadding(), - }); - - // The object group is translated by the difference between the interaction rect's new and old positions (which is - // stored as this.bbox) - this.parent.konva.objectGroup.setAttrs({ - x: this.konva.rect.x(), - y: this.konva.rect.y(), - }); - }); - this.konva.rect.on('dragend', () => { - if (this.parent.isTransforming) { - // When the user cancels the transformation, we need to reset the layer, so we should not update the layer's - // positition while we are transforming - bail out early. - return; - } - - const position = { - x: this.konva.rect.x() - this.parent.bbox.x, - y: this.konva.rect.y() - this.parent.bbox.y, - }; - - this.log.trace({ position }, 'Position changed'); - this.manager.stateApi.onPosChanged({ id: this.id, position }, 'layer'); - }); - } - - repr = () => { - return { - id: this.id, - type: CanvasInteractionRect.TYPE, - }; - }; -} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index c78eb57a813..de19c2199e8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -46,7 +46,6 @@ export class CanvasLayer { layer: Konva.Layer; bbox: Konva.Rect; objectGroup: Konva.Group; - interactionRect: Konva.Rect; }; objects: Map; transformer: CanvasTransformer; @@ -77,56 +76,15 @@ export class CanvasLayer { strokeHitEnabled: false, }), objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }), - interactionRect: new Konva.Rect({ - name: CanvasLayer.INTERACTION_RECT_NAME, - listening: false, - draggable: true, - // fill: 'rgba(255,0,0,0.5)', - }), }; this.transformer = new CanvasTransformer(this); this.konva.layer.add(this.konva.objectGroup); this.konva.layer.add(this.transformer.konva.transformer); - this.konva.layer.add(this.konva.interactionRect); + this.konva.layer.add(this.transformer.konva.proxyRect); this.konva.layer.add(this.konva.bbox); - this.konva.interactionRect.on('dragmove', () => { - // Snap the interaction rect to the nearest pixel - this.konva.interactionRect.x(Math.round(this.konva.interactionRect.x())); - this.konva.interactionRect.y(Math.round(this.konva.interactionRect.y())); - - // The bbox should be updated to reflect the new position of the interaction rect, taking into account its padding - // and border - this.konva.bbox.setAttrs({ - x: this.konva.interactionRect.x() - this.manager.getScaledBboxPadding(), - y: this.konva.interactionRect.y() - this.manager.getScaledBboxPadding(), - }); - - // The object group is translated by the difference between the interaction rect's new and old positions (which is - // stored as this.bbox) - this.konva.objectGroup.setAttrs({ - x: this.konva.interactionRect.x(), - y: this.konva.interactionRect.y(), - }); - }); - this.konva.interactionRect.on('dragend', () => { - if (this.isTransforming) { - // When the user cancels the transformation, we need to reset the layer, so we should not update the layer's - // positition while we are transforming - bail out early. - return; - } - - const position = { - x: this.konva.interactionRect.x() - this.bbox.x, - y: this.konva.interactionRect.y() - this.bbox.y, - }; - - this.log.trace({ position }, 'Position changed'); - this.manager.stateApi.onPosChanged({ id: this.id, position }, 'layer'); - }); - this.objects = new Map(); this.drawingBuffer = null; this.state = state; @@ -235,9 +193,9 @@ export class CanvasLayer { x: position.x + this.bbox.x - bboxPadding, y: position.y + this.bbox.y - bboxPadding, }); - this.konva.interactionRect.setAttrs({ - x: position.x + this.bbox.x * this.konva.interactionRect.scaleX(), - y: position.y + this.bbox.y * this.konva.interactionRect.scaleY(), + this.transformer.konva.proxyRect.setAttrs({ + x: position.x + this.bbox.x * this.transformer.konva.proxyRect.scaleX(), + y: position.y + this.bbox.y * this.transformer.konva.proxyRect.scaleY(), }); }; @@ -301,22 +259,23 @@ export class CanvasLayer { this.konva.layer.listening(true); // The transformer is not needed - this.transformer.deactivate(); + this.transformer.disableTransform(); + this.transformer.enableDrag(); // The bbox rect should be visible and interaction rect listening for dragging this.konva.bbox.visible(true); - this.konva.interactionRect.listening(true); } else if (isSelected && this.isTransforming) { // When transforming, we want the stage to still be movable if the view tool is selected. If the transformer or // interaction rect are listening, it will interrupt the stage's drag events. So we should disable listening // when the view tool is selected - const listening = toolState.selected !== 'view'; - this.konva.layer.listening(listening); - this.konva.interactionRect.listening(listening); - if (listening) { - this.transformer.activate(); + if (toolState.selected !== 'view') { + this.konva.layer.listening(true); + this.transformer.enableTransform(); + this.transformer.enableDrag(); } else { - this.transformer.deactivate(); + this.konva.layer.listening(false); + this.transformer.disableTransform(); + this.transformer.disableDrag(); } // Hide the bbox rect, the transformer will has its own bbox @@ -326,9 +285,9 @@ export class CanvasLayer { this.konva.layer.listening(false); // The transformer, bbox and interaction rect should be inactive - this.transformer.deactivate(); + this.transformer.disableTransform(); + this.transformer.disableDrag(); this.konva.bbox.visible(false); - this.konva.interactionRect.listening(false); } }; @@ -348,12 +307,13 @@ export class CanvasLayer { this.manager.stateApi.onEntityReset({ id: this.id }, 'layer'); } this.konva.bbox.visible(false); - this.konva.interactionRect.visible(false); + this.transformer.disableDrag(); + this.transformer.disableTransform(); return; } this.konva.bbox.visible(true); - this.konva.interactionRect.visible(true); + this.transformer.enableDrag(); const onePixel = this.manager.getScaledPixel(); const bboxPadding = this.manager.getScaledBboxPadding(); @@ -365,7 +325,7 @@ export class CanvasLayer { height: this.bbox.height + bboxPadding * 2, strokeWidth: onePixel, }); - this.konva.interactionRect.setAttrs({ + this.transformer.konva.proxyRect.setAttrs({ x: this.state.position.x + this.bbox.x, y: this.state.position.y + this.bbox.y, width: this.bbox.width, @@ -386,10 +346,10 @@ export class CanvasLayer { const bboxPadding = this.manager.getScaledBboxPadding(); this.konva.bbox.setAttrs({ - x: this.konva.interactionRect.x() - bboxPadding, - y: this.konva.interactionRect.y() - bboxPadding, - width: this.konva.interactionRect.width() * this.konva.interactionRect.scaleX() + bboxPadding * 2, - height: this.konva.interactionRect.height() * this.konva.interactionRect.scaleY() + bboxPadding * 2, + x: this.transformer.konva.proxyRect.x() - bboxPadding, + y: this.transformer.konva.proxyRect.y() - bboxPadding, + width: this.transformer.konva.proxyRect.width() * this.transformer.konva.proxyRect.scaleX() + bboxPadding * 2, + height: this.transformer.konva.proxyRect.height() * this.transformer.konva.proxyRect.scaleY() + bboxPadding * 2, strokeWidth: onePixel, }); }; @@ -465,8 +425,8 @@ export class CanvasLayer { const listening = this.manager.stateApi.getToolState().selected !== 'view'; this.konva.layer.listening(listening); - this.konva.interactionRect.listening(listening); - this.transformer.activate(); + this.transformer.enableDrag(); + this.transformer.enableTransform(); // Hide the bbox rect, the transformer will has its own bbox this.konva.bbox.visible(false); @@ -480,14 +440,14 @@ export class CanvasLayer { }; this.konva.objectGroup.setAttrs(attrs); this.konva.bbox.setAttrs(attrs); - this.konva.interactionRect.setAttrs(attrs); + this.transformer.konva.proxyRect.setAttrs(attrs); }; rasterizeLayer = async () => { this.log.debug('Rasterizing layer'); const objectGroupClone = this.konva.objectGroup.clone(); - const interactionRectClone = this.konva.interactionRect.clone(); + const interactionRectClone = this.transformer.konva.proxyRect.clone(); const rect = interactionRectClone.getClientRect(); const blob = await konvaNodeToBlob(objectGroupClone, rect); if (this.manager._isDebugging) { @@ -620,13 +580,13 @@ export class CanvasLayer { const info = { repr: this.repr(), interactionRectAttrs: { - x: this.konva.interactionRect.x(), - y: this.konva.interactionRect.y(), - scaleX: this.konva.interactionRect.scaleX(), - scaleY: this.konva.interactionRect.scaleY(), - width: this.konva.interactionRect.width(), - height: this.konva.interactionRect.height(), - rotation: this.konva.interactionRect.rotation(), + x: this.transformer.konva.proxyRect.x(), + y: this.transformer.konva.proxyRect.y(), + scaleX: this.transformer.konva.proxyRect.scaleX(), + scaleY: this.transformer.konva.proxyRect.scaleY(), + width: this.transformer.konva.proxyRect.width(), + height: this.transformer.konva.proxyRect.height(), + rotation: this.transformer.konva.proxyRect.rotation(), }, objectGroupAttrs: { x: this.konva.objectGroup.x(), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 63ff1204c6b..04fa9c8be4a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -6,7 +6,6 @@ import type { CanvasBrushLine } from 'features/controlLayers/konva/CanvasBrushLi import type { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine'; import type { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; import { CanvasInitialImage } from 'features/controlLayers/konva/CanvasInitialImage'; -import type { CanvasInteractionRect } from 'features/controlLayers/konva/CanvasInteractionRect'; import { CanvasProgressPreview } from 'features/controlLayers/konva/CanvasProgressPreview'; import type { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; import type { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; @@ -594,7 +593,7 @@ export class CanvasManager { } buildObjectGetLoggingContext = ( - instance: CanvasBrushLine | CanvasEraserLine | CanvasRect | CanvasImage | CanvasTransformer | CanvasInteractionRect + instance: CanvasBrushLine | CanvasEraserLine | CanvasRect | CanvasImage | CanvasTransformer ): GetLoggingContext => { return (extra?: JSONObject): JSONObject => { return { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index c34099193b7..7458ee6a0e6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -1,22 +1,27 @@ import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import type { Coordinate , GetLoggingContext } from 'features/controlLayers/store/types'; +import type { Coordinate, GetLoggingContext } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; export class CanvasTransformer { - static TYPE = 'transformer'; + static TYPE = 'entity_transformer'; + static TRANSFORMER_NAME = `${CanvasTransformer.TYPE}:transformer`; + static PROXY_RECT_NAME = `${CanvasTransformer.TYPE}:proxy_rect`; id: string; - parent: CanvasLayer + parent: CanvasLayer; manager: CanvasManager; log: Logger; - getLoggingContext: GetLoggingContext + getLoggingContext: GetLoggingContext; + + isTransformEnabled: boolean; + isDragEnabled: boolean; - isActive: boolean; konva: { transformer: Konva.Transformer; + proxyRect: Konva.Rect; }; constructor(parent: CanvasLayer) { @@ -27,12 +32,12 @@ export class CanvasTransformer { this.getLoggingContext = this.manager.buildObjectGetLoggingContext(this); this.log = this.manager.buildLogger(this.getLoggingContext); - this.isActive = false; + this.isTransformEnabled = false; + this.isDragEnabled = false; + this.konva = { transformer: new Konva.Transformer({ - name: CanvasTransformer.TYPE, - // The transformer will use the interaction rect as a proxy for the entity it is transforming. - nodes: [parent.konva.interactionRect], + name: CanvasTransformer.TRANSFORMER_NAME, // Visibility and listening are managed via activate() and deactivate() visible: false, listening: false, @@ -113,17 +118,22 @@ export class CanvasTransformer { return newBoundBox; }, }), + proxyRect: new Konva.Rect({ + name: CanvasTransformer.PROXY_RECT_NAME, + listening: false, + draggable: true, + }), }; this.konva.transformer.on('transformstart', () => { // Just logging in this callback. Called on mouse down of a transform anchor. this.log.trace( { - x: parent.konva.interactionRect.x(), - y: parent.konva.interactionRect.y(), - scaleX: parent.konva.interactionRect.scaleX(), - scaleY: parent.konva.interactionRect.scaleY(), - rotation: parent.konva.interactionRect.rotation(), + x: this.konva.proxyRect.x(), + y: this.konva.proxyRect.y(), + scaleX: this.konva.proxyRect.scaleX(), + scaleY: this.konva.proxyRect.scaleY(), + rotation: this.konva.proxyRect.rotation(), }, 'Transform started' ); @@ -134,11 +144,11 @@ export class CanvasTransformer { // callbacks have been enforced, and the transformer has updated its nodes' attributes. We need to pass the // updated attributes to the object group, propagating the transformation on down. parent.konva.objectGroup.setAttrs({ - x: parent.konva.interactionRect.x(), - y: parent.konva.interactionRect.y(), - scaleX: parent.konva.interactionRect.scaleX(), - scaleY: parent.konva.interactionRect.scaleY(), - rotation: parent.konva.interactionRect.rotation(), + x: this.konva.proxyRect.x(), + y: this.konva.proxyRect.y(), + scaleX: this.konva.proxyRect.scaleX(), + scaleY: this.konva.proxyRect.scaleY(), + rotation: this.konva.proxyRect.rotation(), }); }); @@ -146,18 +156,18 @@ export class CanvasTransformer { // Called on mouse up on an anchor. We'll do some final snapping to ensure the transformer is pixel-perfect. // Snap the position to the nearest pixel. - const x = parent.konva.interactionRect.x(); - const y = parent.konva.interactionRect.y(); + const x = this.konva.proxyRect.x(); + const y = this.konva.proxyRect.y(); const snappedX = Math.round(x); const snappedY = Math.round(y); // The transformer doesn't modify the width and height. It only modifies scale. We'll need to apply the scale to // the width and height, round them to the nearest pixel, and finally calculate a new scale that will result in // the snapped width and height. - const width = parent.konva.interactionRect.width(); - const height = parent.konva.interactionRect.height(); - const scaleX = parent.konva.interactionRect.scaleX(); - const scaleY = parent.konva.interactionRect.scaleY(); + const width = this.konva.proxyRect.width(); + const height = this.konva.proxyRect.height(); + const scaleX = this.konva.proxyRect.scaleX(); + const scaleY = this.konva.proxyRect.scaleY(); // Determine the target width and height, rounded to the nearest pixel. Must be >= 1. Because the scales can be // negative, we need to take the absolute value of the width and height. @@ -169,7 +179,7 @@ export class CanvasTransformer { const snappedScaleY = (targetHeight / height) * Math.sign(scaleY); // Update interaction rect and object group attributes. - parent.konva.interactionRect.setAttrs({ + this.konva.proxyRect.setAttrs({ x: snappedX, y: snappedY, scaleX: snappedScaleX, @@ -183,7 +193,7 @@ export class CanvasTransformer { }); // Rotation is only retrieved for logging purposes. - const rotation = parent.konva.interactionRect.rotation(); + const rotation = this.konva.proxyRect.rotation(); this.log.trace( { @@ -205,6 +215,41 @@ export class CanvasTransformer { ); }); + this.konva.proxyRect.on('dragmove', () => { + // Snap the interaction rect to the nearest pixel + this.konva.proxyRect.x(Math.round(this.konva.proxyRect.x())); + this.konva.proxyRect.y(Math.round(this.konva.proxyRect.y())); + + // The bbox should be updated to reflect the new position of the interaction rect, taking into account its padding + // and border + this.parent.konva.bbox.setAttrs({ + x: this.konva.proxyRect.x() - this.manager.getScaledBboxPadding(), + y: this.konva.proxyRect.y() - this.manager.getScaledBboxPadding(), + }); + + // The object group is translated by the difference between the interaction rect's new and old positions (which is + // stored as this.bbox) + this.parent.konva.objectGroup.setAttrs({ + x: this.konva.proxyRect.x(), + y: this.konva.proxyRect.y(), + }); + }); + this.konva.proxyRect.on('dragend', () => { + if (this.parent.isTransforming) { + // When the user cancels the transformation, we need to reset the layer, so we should not update the layer's + // positition while we are transforming - bail out early. + return; + } + + const position = { + x: this.konva.proxyRect.x() - this.parent.bbox.x, + y: this.konva.proxyRect.y() - this.parent.bbox.y, + }; + + this.log.trace({ position }, 'Position changed'); + this.manager.stateApi.onPosChanged({ id: this.id, position }, 'layer'); + }); + this.manager.stateApi.onShiftChanged((isPressed) => { // While the user holds shift, we want to snap rotation to 45 degree increments. Listen for the shift key state // and update the snap angles accordingly. @@ -212,29 +257,37 @@ export class CanvasTransformer { }); } - /** - * Activate the transformer. This will make it visible and listening for events. - */ - activate = () => { - this.isActive = true; + enableTransform = () => { + this.isTransformEnabled = true; this.konva.transformer.visible(true); this.konva.transformer.listening(true); + this.konva.transformer.nodes([this.konva.proxyRect]); }; - /** - * Deactivate the transformer. This will make it invisible and not listening for events. - */ - deactivate = () => { - this.isActive = false; + disableTransform = () => { + this.isTransformEnabled = false; this.konva.transformer.visible(false); this.konva.transformer.listening(false); + this.konva.transformer.nodes([]); + }; + + enableDrag = () => { + this.isDragEnabled = true; + this.konva.proxyRect.visible(true); + this.konva.proxyRect.listening(true); + }; + + disableDrag = () => { + this.isDragEnabled = false; + this.konva.proxyRect.visible(false); + this.konva.proxyRect.listening(false); }; repr = () => { return { id: this.id, type: CanvasTransformer.TYPE, - isActive: this.isActive, + isActive: this.isTransformEnabled, }; }; } From 45efa8f40d77948e047f0e39265fbbb834c605ce Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 1 Aug 2024 20:55:36 +1000 Subject: [PATCH 273/678] fix(ui): update parent's pos not transformers --- .../web/src/features/controlLayers/konva/CanvasTransformer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index 7458ee6a0e6..cb8aced1865 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -247,7 +247,7 @@ export class CanvasTransformer { }; this.log.trace({ position }, 'Position changed'); - this.manager.stateApi.onPosChanged({ id: this.id, position }, 'layer'); + this.manager.stateApi.onPosChanged({ id: this.parent.id, position }, 'layer'); }); this.manager.stateApi.onShiftChanged((isPressed) => { From 5978ba32c0add4eb6b024d7dff0b9e31c43b8f5d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 1 Aug 2024 20:55:49 +1000 Subject: [PATCH 274/678] feat(ui): merge bbox outline into transformer --- .../controlLayers/konva/CanvasLayer.ts | 52 +++++------------- .../controlLayers/konva/CanvasTransformer.ts | 54 ++++++++++++++++--- 2 files changed, 58 insertions(+), 48 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index de19c2199e8..e3589c07091 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -44,7 +44,6 @@ export class CanvasLayer { konva: { layer: Konva.Layer; - bbox: Konva.Rect; objectGroup: Konva.Group; }; objects: Map; @@ -67,14 +66,6 @@ export class CanvasLayer { this.konva = { layer: new Konva.Layer({ id: this.id, name: CanvasLayer.LAYER_NAME, listening: false }), - bbox: new Konva.Rect({ - listening: false, - draggable: false, - name: CanvasLayer.BBOX_NAME, - stroke: 'hsl(200deg 76% 59%)', // invokeBlue.400 - perfectDrawEnabled: false, - strokeHitEnabled: false, - }), objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }), }; @@ -83,7 +74,7 @@ export class CanvasLayer { this.konva.layer.add(this.konva.objectGroup); this.konva.layer.add(this.transformer.konva.transformer); this.konva.layer.add(this.transformer.konva.proxyRect); - this.konva.layer.add(this.konva.bbox); + this.konva.layer.add(this.transformer.konva.bboxOutline); this.objects = new Map(); this.drawingBuffer = null; @@ -189,7 +180,7 @@ export class CanvasLayer { offsetX: this.bbox.x, offsetY: this.bbox.y, }); - this.konva.bbox.setAttrs({ + this.transformer.konva.bboxOutline.setAttrs({ x: position.x + this.bbox.x - bboxPadding, y: position.y + this.bbox.y - bboxPadding, }); @@ -258,36 +249,24 @@ export class CanvasLayer { // We are moving this layer, it must be listening this.konva.layer.listening(true); - // The transformer is not needed - this.transformer.disableTransform(); - this.transformer.enableDrag(); - - // The bbox rect should be visible and interaction rect listening for dragging - this.konva.bbox.visible(true); + this.transformer.setMode('drag'); } else if (isSelected && this.isTransforming) { // When transforming, we want the stage to still be movable if the view tool is selected. If the transformer or // interaction rect are listening, it will interrupt the stage's drag events. So we should disable listening // when the view tool is selected if (toolState.selected !== 'view') { this.konva.layer.listening(true); - this.transformer.enableTransform(); - this.transformer.enableDrag(); + this.transformer.setMode('transform'); } else { this.konva.layer.listening(false); - this.transformer.disableTransform(); - this.transformer.disableDrag(); + this.transformer.setMode('off'); } - - // Hide the bbox rect, the transformer will has its own bbox - this.konva.bbox.visible(false); } else { // The layer is not selected, or we are using a tool that doesn't need the layer to be listening - disable interaction stuff this.konva.layer.listening(false); // The transformer, bbox and interaction rect should be inactive - this.transformer.disableTransform(); - this.transformer.disableDrag(); - this.konva.bbox.visible(false); + this.transformer.setMode('off'); } }; @@ -306,19 +285,16 @@ export class CanvasLayer { // The layer is fully transparent but has objects - reset it this.manager.stateApi.onEntityReset({ id: this.id }, 'layer'); } - this.konva.bbox.visible(false); - this.transformer.disableDrag(); - this.transformer.disableTransform(); + this.transformer.setMode('off'); return; } - this.konva.bbox.visible(true); - this.transformer.enableDrag(); + this.transformer.setMode('drag'); const onePixel = this.manager.getScaledPixel(); const bboxPadding = this.manager.getScaledBboxPadding(); - this.konva.bbox.setAttrs({ + this.transformer.konva.bboxOutline.setAttrs({ x: this.state.position.x + this.bbox.x - bboxPadding, y: this.state.position.y + this.bbox.y - bboxPadding, width: this.bbox.width + bboxPadding * 2, @@ -345,7 +321,7 @@ export class CanvasLayer { const onePixel = this.manager.getScaledPixel(); const bboxPadding = this.manager.getScaledBboxPadding(); - this.konva.bbox.setAttrs({ + this.transformer.konva.bboxOutline.setAttrs({ x: this.transformer.konva.proxyRect.x() - bboxPadding, y: this.transformer.konva.proxyRect.y() - bboxPadding, width: this.transformer.konva.proxyRect.width() * this.transformer.konva.proxyRect.scaleX() + bboxPadding * 2, @@ -425,11 +401,7 @@ export class CanvasLayer { const listening = this.manager.stateApi.getToolState().selected !== 'view'; this.konva.layer.listening(listening); - this.transformer.enableDrag(); - this.transformer.enableTransform(); - - // Hide the bbox rect, the transformer will has its own bbox - this.konva.bbox.visible(false); + this.transformer.setMode('transform'); }; resetScale = () => { @@ -439,7 +411,7 @@ export class CanvasLayer { rotation: 0, }; this.konva.objectGroup.setAttrs(attrs); - this.konva.bbox.setAttrs(attrs); + this.transformer.konva.bboxOutline.setAttrs(attrs); this.transformer.konva.proxyRect.setAttrs(attrs); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index cb8aced1865..8ad090c8336 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -9,6 +9,8 @@ export class CanvasTransformer { static TYPE = 'entity_transformer'; static TRANSFORMER_NAME = `${CanvasTransformer.TYPE}:transformer`; static PROXY_RECT_NAME = `${CanvasTransformer.TYPE}:proxy_rect`; + static BBOX_OUTLINE_NAME = `${CanvasTransformer.TYPE}:bbox_outline`; + static STROKE_COLOR = 'hsl(200deg 76% 59%)'; // `invokeBlue.400 id: string; parent: CanvasLayer; @@ -16,12 +18,14 @@ export class CanvasTransformer { log: Logger; getLoggingContext: GetLoggingContext; - isTransformEnabled: boolean; + mode: 'transform' | 'drag' | 'off'; isDragEnabled: boolean; + isTransformEnabled: boolean; konva: { transformer: Konva.Transformer; proxyRect: Konva.Rect; + bboxOutline: Konva.Rect; }; constructor(parent: CanvasLayer) { @@ -32,10 +36,19 @@ export class CanvasTransformer { this.getLoggingContext = this.manager.buildObjectGetLoggingContext(this); this.log = this.manager.buildLogger(this.getLoggingContext); - this.isTransformEnabled = false; + this.mode = 'off'; this.isDragEnabled = false; + this.isTransformEnabled = false; this.konva = { + bboxOutline: new Konva.Rect({ + listening: false, + draggable: false, + name: CanvasTransformer.BBOX_OUTLINE_NAME, + stroke: CanvasTransformer.STROKE_COLOR, + perfectDrawEnabled: false, + strokeHitEnabled: false, + }), transformer: new Konva.Transformer({ name: CanvasTransformer.TRANSFORMER_NAME, // Visibility and listening are managed via activate() and deactivate() @@ -50,7 +63,7 @@ export class CanvasTransformer { // The padding is the distance between the transformer bbox and the nodes padding: this.manager.getTransformerPadding(), // This is `invokeBlue.400` - stroke: 'hsl(200deg 76% 59%)', + stroke: CanvasTransformer.STROKE_COLOR, // TODO(psyche): The konva Vector2D type is is apparently not compatible with the JSONObject type that the log // function expects. The in-house Coordinate type is functionally the same - `{x: number; y: number}` - and // TypeScript is happy with it. @@ -222,7 +235,7 @@ export class CanvasTransformer { // The bbox should be updated to reflect the new position of the interaction rect, taking into account its padding // and border - this.parent.konva.bbox.setAttrs({ + this.konva.bboxOutline.setAttrs({ x: this.konva.proxyRect.x() - this.manager.getScaledBboxPadding(), y: this.konva.proxyRect.y() - this.manager.getScaledBboxPadding(), }); @@ -257,32 +270,57 @@ export class CanvasTransformer { }); } - enableTransform = () => { + setMode = (mode: 'transform' | 'drag' | 'off') => { + this.mode = mode; + if (mode === 'drag') { + this._enableDrag(); + this._disableTransform(); + this._showBboxOutline(); + } else if (mode === 'transform') { + this._enableDrag(); + this._enableTransform(); + this._hideBboxOutline(); + } else if (mode === 'off') { + this._disableDrag(); + this._disableTransform(); + this._hideBboxOutline(); + } + }; + + _enableTransform = () => { this.isTransformEnabled = true; this.konva.transformer.visible(true); this.konva.transformer.listening(true); this.konva.transformer.nodes([this.konva.proxyRect]); }; - disableTransform = () => { + _disableTransform = () => { this.isTransformEnabled = false; this.konva.transformer.visible(false); this.konva.transformer.listening(false); this.konva.transformer.nodes([]); }; - enableDrag = () => { + _enableDrag = () => { this.isDragEnabled = true; this.konva.proxyRect.visible(true); this.konva.proxyRect.listening(true); }; - disableDrag = () => { + _disableDrag = () => { this.isDragEnabled = false; this.konva.proxyRect.visible(false); this.konva.proxyRect.listening(false); }; + _showBboxOutline = () => { + this.konva.bboxOutline.visible(true); + }; + + _hideBboxOutline = () => { + this.konva.bboxOutline.visible(false); + }; + repr = () => { return { id: this.id, From 60484bd45e212b9780ed0a4b2c5c7b0c8796872e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 1 Aug 2024 22:25:00 +1000 Subject: [PATCH 275/678] feat(ui): fix a few things that didn't unsubscribe correctly, add helper to manage subscriptions --- .../controlLayers/konva/CanvasManager.ts | 37 +++++++++---------- .../controlLayers/konva/CanvasStateApi.ts | 5 ++- .../features/controlLayers/konva/events.ts | 1 - .../src/features/controlLayers/konva/util.ts | 21 +++++++++++ 4 files changed, 42 insertions(+), 22 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 04fa9c8be4a..69e69b161aa 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -254,12 +254,6 @@ export class CanvasManager { } } - syncStageScale() { - for (const layer of this.layers.values()) { - layer.syncStageScale(); - } - } - arrangeEntities() { const { getLayersState, getControlAdaptersState, getRegionsState } = this.stateApi; const layers = getLayersState().entities; @@ -462,7 +456,7 @@ export class CanvasManager { this.log.debug('Initializing renderer'); this.stage.container(this.container); - const cleanupListeners = setStageEventHandlers(this); + const unsubscribeListeners = setStageEventHandlers(this); // We can use a resize observer to ensure the stage always fits the container. We also need to re-render the bg and // document bounds overlay when the stage is resized. @@ -473,19 +467,23 @@ export class CanvasManager { const unsubscribeRenderer = this._store.subscribe(this.render); // When we this flag, we need to render the staging area - $shouldShowStagedImage.subscribe(async (shouldShowStagedImage, prevShouldShowStagedImage) => { - if (shouldShowStagedImage !== prevShouldShowStagedImage) { - this.log.debug('Rendering staging area'); - await this.preview.stagingArea.render(); + const unsubscribeShouldShowStagedImage = $shouldShowStagedImage.subscribe( + async (shouldShowStagedImage, prevShouldShowStagedImage) => { + if (shouldShowStagedImage !== prevShouldShowStagedImage) { + this.log.debug('Rendering staging area'); + await this.preview.stagingArea.render(); + } } - }); + ); - $lastProgressEvent.subscribe(async (lastProgressEvent, prevLastProgressEvent) => { - if (lastProgressEvent !== prevLastProgressEvent) { - this.log.debug('Rendering progress image'); - await this.preview.progressPreview.render(lastProgressEvent); + const unsubscribeLastProgressEvent = $lastProgressEvent.subscribe( + async (lastProgressEvent, prevLastProgressEvent) => { + if (lastProgressEvent !== prevLastProgressEvent) { + this.log.debug('Rendering progress image'); + await this.preview.progressPreview.render(lastProgressEvent); + } } - }); + ); this.log.debug('First render of konva stage'); this.preview.tool.render(); @@ -494,8 +492,9 @@ export class CanvasManager { return () => { this.log.debug('Cleaning up konva renderer'); unsubscribeRenderer(); - cleanupListeners(); - $shouldShowStagedImage.off(); + unsubscribeListeners(); + unsubscribeShouldShowStagedImage(); + unsubscribeLastProgressEvent(); resizeObserver.disconnect(); }; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index e463a0dbd84..f1cb1733c6b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -2,6 +2,7 @@ import { $alt, $ctrl, $meta, $shift } from '@invoke-ai/ui-library'; import type { Store } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; +import { buildSubscribe } from 'features/controlLayers/konva/util'; import { $isDrawing, $isMouseDown, @@ -293,11 +294,11 @@ export class CanvasStateApi { onMetaChanged = $meta.subscribe; getShiftKey = $shift.get; - onShiftChanged = $shift.subscribe; + onShiftChanged = buildSubscribe($shift.subscribe, 'onShiftChanged'); getShouldShowStagedImage = $shouldShowStagedImage.get; onGetShouldShowStagedImageChanged = $shouldShowStagedImage.subscribe; setStageAttrs = $stageAttrs.set; - onStageAttrsChanged = $stageAttrs.subscribe; + onStageAttrsChanged = buildSubscribe($stageAttrs.subscribe, 'onStageAttrsChanged'); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index f590ea49396..ebce9006e1b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -494,7 +494,6 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { scale: newScale, }); manager.background.render(); - manager.syncStageScale(); } } manager.preview.tool.render(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index 8273c0455e7..71f66151c4d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -7,6 +7,7 @@ import Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; import type { Vector2d } from 'konva/lib/types'; import { customAlphabet } from 'nanoid'; +import type { WritableAtom } from 'nanostores'; import type { ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; @@ -591,3 +592,23 @@ export function getObjectId(type: RenderableObject['type'], isBuffer?: boolean): return getPrefixedId(type); } } + +export type Subscription = { + name: string; + unsubscribe: () => void; +}; + +/** + * Builds a subscribe function for a nanostores atom. + * @param subscribe The subscribe function of the atom + * @param name The name of the atom + * @returns A subscribe function that returns an object with the name and unsubscribe function + */ +export const buildSubscribe = (subscribe: WritableAtom['subscribe'], name: string) => { + return (cb: Parameters['subscribe']>[0]): Subscription => { + return { + name, + unsubscribe: subscribe(cb), + }; + }; +}; From 0e0cf9cd3ec5e33c6b558ef2febaf3a7836fa7e3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 1 Aug 2024 22:25:20 +1000 Subject: [PATCH 276/678] feat(ui): continue modularizing transform --- .../controlLayers/konva/CanvasLayer.ts | 64 ++------- .../controlLayers/konva/CanvasTransformer.ts | 128 ++++++++++++++++-- 2 files changed, 130 insertions(+), 62 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index e3589c07091..7748b9c7852 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -69,12 +69,10 @@ export class CanvasLayer { objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }), }; - this.transformer = new CanvasTransformer(this); + this.transformer = new CanvasTransformer(this, this.konva.objectGroup); this.konva.layer.add(this.konva.objectGroup); - this.konva.layer.add(this.transformer.konva.transformer); - this.konva.layer.add(this.transformer.konva.proxyRect); - this.konva.layer.add(this.transformer.konva.bboxOutline); + this.konva.layer.add(...this.transformer.getNodes()); this.objects = new Map(); this.drawingBuffer = null; @@ -89,6 +87,11 @@ export class CanvasLayer { destroy = (): void => { this.log.debug('Destroying layer'); + // We need to call the destroy method on all children so they can do their own cleanup. + this.transformer.destroy(); + for (const obj of this.objects.values()) { + obj.destroy(); + } this.konva.layer.destroy(); }; @@ -172,7 +175,6 @@ export class CanvasLayer { updatePosition = (arg?: { position: Coordinate }) => { this.log.trace('Updating position'); const position = get(arg, 'position', this.state.position); - const bboxPadding = this.manager.getScaledBboxPadding(); this.konva.objectGroup.setAttrs({ x: position.x + this.bbox.x, @@ -180,14 +182,8 @@ export class CanvasLayer { offsetX: this.bbox.x, offsetY: this.bbox.y, }); - this.transformer.konva.bboxOutline.setAttrs({ - x: position.x + this.bbox.x - bboxPadding, - y: position.y + this.bbox.y - bboxPadding, - }); - this.transformer.konva.proxyRect.setAttrs({ - x: position.x + this.bbox.x * this.transformer.konva.proxyRect.scaleX(), - y: position.y + this.bbox.y * this.transformer.konva.proxyRect.scaleY(), - }); + + this.transformer.update(position, this.bbox); }; updateObjects = async (arg?: { objects: LayerEntity['objects'] }) => { @@ -242,18 +238,17 @@ export class CanvasLayer { if (this.objects.size === 0) { // The layer is totally empty, we can just disable the layer this.konva.layer.listening(false); + this.transformer.setMode('off'); return; } if (isSelected && !this.isTransforming && toolState.selected === 'move') { // We are moving this layer, it must be listening this.konva.layer.listening(true); - this.transformer.setMode('drag'); } else if (isSelected && this.isTransforming) { - // When transforming, we want the stage to still be movable if the view tool is selected. If the transformer or - // interaction rect are listening, it will interrupt the stage's drag events. So we should disable listening - // when the view tool is selected + // When transforming, we want the stage to still be movable if the view tool is selected. If the transformer is + // active, it will interrupt the stage drag events. So we should disable listening when the view tool is selected. if (toolState.selected !== 'view') { this.konva.layer.listening(true); this.transformer.setMode('transform'); @@ -264,8 +259,6 @@ export class CanvasLayer { } else { // The layer is not selected, or we are using a tool that doesn't need the layer to be listening - disable interaction stuff this.konva.layer.listening(false); - - // The transformer, bbox and interaction rect should be inactive this.transformer.setMode('off'); } }; @@ -290,23 +283,7 @@ export class CanvasLayer { } this.transformer.setMode('drag'); - - const onePixel = this.manager.getScaledPixel(); - const bboxPadding = this.manager.getScaledBboxPadding(); - - this.transformer.konva.bboxOutline.setAttrs({ - x: this.state.position.x + this.bbox.x - bboxPadding, - y: this.state.position.y + this.bbox.y - bboxPadding, - width: this.bbox.width + bboxPadding * 2, - height: this.bbox.height + bboxPadding * 2, - strokeWidth: onePixel, - }); - this.transformer.konva.proxyRect.setAttrs({ - x: this.state.position.x + this.bbox.x, - y: this.state.position.y + this.bbox.y, - width: this.bbox.width, - height: this.bbox.height, - }); + this.transformer.update(this.state.position, this.bbox); this.konva.objectGroup.setAttrs({ x: this.state.position.x + this.bbox.x, y: this.state.position.y + this.bbox.y, @@ -315,21 +292,6 @@ export class CanvasLayer { }); }; - syncStageScale = () => { - this.log.trace('Syncing scale to stage'); - - const onePixel = this.manager.getScaledPixel(); - const bboxPadding = this.manager.getScaledBboxPadding(); - - this.transformer.konva.bboxOutline.setAttrs({ - x: this.transformer.konva.proxyRect.x() - bboxPadding, - y: this.transformer.konva.proxyRect.y() - bboxPadding, - width: this.transformer.konva.proxyRect.width() * this.transformer.konva.proxyRect.scaleX() + bboxPadding * 2, - height: this.transformer.konva.proxyRect.height() * this.transformer.konva.proxyRect.scaleY() + bboxPadding * 2, - strokeWidth: onePixel, - }); - }; - _renderObject = async (obj: LayerEntity['objects'][number], force = false): Promise => { if (obj.type === 'brush_line') { let brushLine = this.objects.get(obj.id); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index 8ad090c8336..68ceaf00377 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -1,10 +1,19 @@ import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import type { Subscription } from 'features/controlLayers/konva/util'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import type { Coordinate, GetLoggingContext } from 'features/controlLayers/store/types'; +import type { Coordinate, GetLoggingContext, Rect } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; +/** + * The CanvasTransformer class is responsible for managing the transformation of a canvas entity: + * - Moving + * - Resizing + * - Rotating + * + * It renders an outline when dragging and resizing the entity, with transform anchors for resizing and rotation. + */ export class CanvasTransformer { static TYPE = 'entity_transformer'; static TRANSFORMER_NAME = `${CanvasTransformer.TYPE}:transformer`; @@ -17,24 +26,46 @@ export class CanvasTransformer { manager: CanvasManager; log: Logger; getLoggingContext: GetLoggingContext; - + subscriptions: Subscription[]; + + /** + * The current mode of the transformer: + * - 'transform': The entity can be moved, resized, and rotated + * - 'drag': The entity can only be moved + * - 'off': The transformer is disabled + */ mode: 'transform' | 'drag' | 'off'; + + /** + * Whether dragging is enabled. Dragging is enabled in both 'transform' and 'drag' modes. + */ isDragEnabled: boolean; + + /** + * Whether transforming is enabled. Transforming is enabled only in 'transform' mode. + */ isTransformEnabled: boolean; + /** + * The konva group that the transformer will manipulate. + */ + transformTarget: Konva.Group; + konva: { transformer: Konva.Transformer; proxyRect: Konva.Rect; bboxOutline: Konva.Rect; }; - constructor(parent: CanvasLayer) { + constructor(parent: CanvasLayer, transformTarget: Konva.Group) { + this.id = getPrefixedId(CanvasTransformer.TYPE); this.parent = parent; this.manager = parent.manager; - this.id = getPrefixedId(CanvasTransformer.TYPE); + this.transformTarget = transformTarget; this.getLoggingContext = this.manager.buildObjectGetLoggingContext(this); this.log = this.manager.buildLogger(this.getLoggingContext); + this.subscriptions = []; this.mode = 'off'; this.isDragEnabled = false; @@ -156,7 +187,7 @@ export class CanvasTransformer { // This is called when a transform anchor is dragged. By this time, the transform constraints in the above // callbacks have been enforced, and the transformer has updated its nodes' attributes. We need to pass the // updated attributes to the object group, propagating the transformation on down. - parent.konva.objectGroup.setAttrs({ + this.transformTarget.setAttrs({ x: this.konva.proxyRect.x(), y: this.konva.proxyRect.y(), scaleX: this.konva.proxyRect.scaleX(), @@ -198,7 +229,7 @@ export class CanvasTransformer { scaleX: snappedScaleX, scaleY: snappedScaleY, }); - parent.konva.objectGroup.setAttrs({ + this.transformTarget.setAttrs({ x: snappedX, y: snappedY, scaleX: snappedScaleX, @@ -242,7 +273,7 @@ export class CanvasTransformer { // The object group is translated by the difference between the interaction rect's new and old positions (which is // stored as this.bbox) - this.parent.konva.objectGroup.setAttrs({ + this.transformTarget.setAttrs({ x: this.konva.proxyRect.x(), y: this.konva.proxyRect.y(), }); @@ -263,13 +294,73 @@ export class CanvasTransformer { this.manager.stateApi.onPosChanged({ id: this.parent.id, position }, 'layer'); }); - this.manager.stateApi.onShiftChanged((isPressed) => { + this.subscriptions.push( + // When the stage scale changes, we may need to re-scale some of the transformer's components. For example, + // the bbox outline should always be 1 screen pixel wide, so we need to update its stroke width. + this.manager.stateApi.onStageAttrsChanged((newAttrs, oldAttrs) => { + if (newAttrs.scale !== oldAttrs?.scale) { + this.scale(); + } + }) + ); + + this.subscriptions.push( // While the user holds shift, we want to snap rotation to 45 degree increments. Listen for the shift key state // and update the snap angles accordingly. - this.konva.transformer.rotationSnaps(isPressed ? [0, 45, 90, 135, 180, 225, 270, 315] : []); - }); + this.manager.stateApi.onShiftChanged((isPressed) => { + this.konva.transformer.rotationSnaps(isPressed ? [0, 45, 90, 135, 180, 225, 270, 315] : []); + }) + ); } + /** + * Updates the transformer's visual components to match the parent entity's position and bounding box. + * @param position The position of the parent entity + * @param bbox The bounding box of the parent entity + */ + update = (position: Coordinate, bbox: Rect) => { + const onePixel = this.manager.getScaledPixel(); + const bboxPadding = this.manager.getScaledBboxPadding(); + + this.konva.bboxOutline.setAttrs({ + x: position.x + bbox.x - bboxPadding, + y: position.y + bbox.y - bboxPadding, + width: bbox.width + bboxPadding * 2, + height: bbox.height + bboxPadding * 2, + strokeWidth: onePixel, + }); + this.konva.proxyRect.setAttrs({ + x: position.x + bbox.x, + y: position.y + bbox.y, + width: bbox.width, + height: bbox.height, + }); + }; + + /** + * Updates the transformer's scale. This is called when the stage is scaled. + */ + scale = () => { + const onePixel = this.manager.getScaledPixel(); + const bboxPadding = this.manager.getScaledBboxPadding(); + + this.konva.bboxOutline.setAttrs({ + x: this.konva.proxyRect.x() - bboxPadding, + y: this.konva.proxyRect.y() - bboxPadding, + width: this.konva.proxyRect.width() * this.konva.proxyRect.scaleX() + bboxPadding * 2, + height: this.konva.proxyRect.height() * this.konva.proxyRect.scaleY() + bboxPadding * 2, + strokeWidth: onePixel, + }); + this.konva.transformer.forceUpdate(); + }; + + /** + * Sets the transformer to a specific mode. + * @param mode The mode to set the transformer to. The transformer can be in one of three modes: + * - 'transform': The entity can be moved, resized, and rotated + * - 'drag': The entity can only be moved + * - 'off': The transformer is disabled + */ setMode = (mode: 'transform' | 'drag' | 'off') => { this.mode = mode; if (mode === 'drag') { @@ -321,11 +412,26 @@ export class CanvasTransformer { this.konva.bboxOutline.visible(false); }; + getNodes = () => [this.konva.transformer, this.konva.proxyRect, this.konva.bboxOutline]; + repr = () => { return { id: this.id, type: CanvasTransformer.TYPE, - isActive: this.isTransformEnabled, + mode: this.mode, + isTransformEnabled: this.isTransformEnabled, + isDragEnabled: this.isDragEnabled, }; }; + + destroy = () => { + this.log.trace('Destroying transformer'); + for (const { name, unsubscribe } of this.subscriptions) { + this.log.trace({ name }, 'Cleaning up listener'); + unsubscribe(); + } + this.konva.bboxOutline.destroy(); + this.konva.transformer.destroy(); + this.konva.proxyRect.destroy(); + }; } From f521704ade8766294c0a2ff0b73a3cff2efbf7a5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 1 Aug 2024 23:57:05 +1000 Subject: [PATCH 277/678] fix(ui): round position when rasterizing layer --- .../web/src/features/controlLayers/konva/CanvasLayer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 7748b9c7852..15917f57765 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -397,7 +397,7 @@ export class CanvasLayer { } } this.resetScale(); - dispatch(layerRasterized({ id: this.id, imageObject, position: { x: rect.x, y: rect.y } })); + dispatch(layerRasterized({ id: this.id, imageObject, position: { x: Math.round(rect.x), y: Math.round(rect.y) } })); }; stopTransform = () => { From dfdd56dbec76d6453ec466b45828817296eb5864 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 1 Aug 2024 23:57:16 +1000 Subject: [PATCH 278/678] feat(ui): disable image smoothing on layers --- .../web/src/features/controlLayers/konva/CanvasLayer.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 15917f57765..4892df4df8d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -65,7 +65,12 @@ export class CanvasLayer { this.log.debug({ state }, 'Creating layer'); this.konva = { - layer: new Konva.Layer({ id: this.id, name: CanvasLayer.LAYER_NAME, listening: false }), + layer: new Konva.Layer({ + id: this.id, + name: CanvasLayer.LAYER_NAME, + listening: false, + imageSmoothingEnabled: false, + }), objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }), }; From 2aaa6c39867f5db9d5c484d068ef586711844e50 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 1 Aug 2024 23:58:52 +1000 Subject: [PATCH 279/678] fix(ui): align all tools to 1px grid - Offset brush tool by 0.5px when width is odd, ensuring each stroke edge is exactly on a pixel boundary - Round the rect tool also --- .../controlLayers/konva/CanvasTool.ts | 13 ++- .../features/controlLayers/konva/events.ts | 79 +++++++++++-------- .../src/features/controlLayers/konva/util.ts | 35 +++++++- 3 files changed, 90 insertions(+), 37 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts index ea7452343cc..9d92f707146 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts @@ -5,6 +5,7 @@ import { BRUSH_BORDER_OUTER_COLOR, BRUSH_ERASER_BORDER_WIDTH, } from 'features/controlLayers/konva/constants'; +import { alignCoordForTool } from 'features/controlLayers/konva/util'; import Konva from 'konva'; export class CanvasTool { @@ -183,12 +184,14 @@ export class CanvasTool { // No need to render the brush preview if the cursor position or color is missing if (cursorPos && tool === 'brush') { + const alignedCursorPos = alignCoordForTool(cursorPos, toolState.brush.width); const scale = stage.scaleX(); // Update the fill circle const radius = toolState.brush.width / 2; + this.konva.brush.fillCircle.setAttrs({ - x: cursorPos.x, - y: cursorPos.y, + x: alignedCursorPos.x, + y: alignedCursorPos.y, radius, fill: isDrawing ? '' : rgbaColorToString(currentFill), }); @@ -209,12 +212,14 @@ export class CanvasTool { this.konva.eraser.group.visible(false); // this.rect.group.visible(false); } else if (cursorPos && tool === 'eraser') { + const alignedCursorPos = alignCoordForTool(cursorPos, toolState.eraser.width); + const scale = stage.scaleX(); // Update the fill circle const radius = toolState.eraser.width / 2; this.konva.eraser.fillCircle.setAttrs({ - x: cursorPos.x, - y: cursorPos.y, + x: alignedCursorPos.x, + y: alignedCursorPos.y, radius, fill: 'white', }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index ebce9006e1b..89d0ac6a754 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -1,5 +1,10 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { getObjectId, getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util'; +import { + alignCoordForTool, + getObjectId, + getScaledCursorPosition, + offsetCoord, +} from 'features/controlLayers/konva/util'; import type { CanvasV2State, Coordinate, @@ -22,7 +27,7 @@ import { BRUSH_SPACING_TARGET_SCALE, CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANV * @param setLastCursorPos The callback to store the cursor pos */ const updateLastCursorPos = (stage: Konva.Stage, setLastCursorPos: CanvasManager['stateApi']['setLastCursorPos']) => { - const pos = getScaledFlooredCursorPosition(stage); + const pos = getScaledCursorPosition(stage); if (!pos) { return null; } @@ -177,14 +182,17 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { getIsPrimaryMouseDown(e) ) { setLastMouseDownPos(pos); + const normalizedPoint = offsetCoord(pos, selectedEntity.position); if (toolState.selected === 'brush') { const lastLinePoint = getLastPointOfLastLineOfEntity(selectedEntity, toolState.selected); + const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); if (e.evt.shiftKey && lastLinePoint) { // Create a straight line from the last line point if (selectedEntityAdapter.getDrawingBuffer()) { await selectedEntityAdapter.finalizeDrawingBuffer(); } + await selectedEntityAdapter.setDrawingBuffer({ id: getObjectId('brush_line', true), type: 'brush_line', @@ -192,8 +200,8 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { // The last point of the last line is already normalized to the entity's coordinates lastLinePoint.x, lastLinePoint.y, - pos.x - selectedEntity.position.x, - pos.y - selectedEntity.position.y, + alignedPoint.x, + alignedPoint.y, ], strokeWidth: toolState.brush.width, color: getCurrentFill(), @@ -206,17 +214,18 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.setDrawingBuffer({ id: getObjectId('brush_line', true), type: 'brush_line', - points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y], + points: [alignedPoint.x, alignedPoint.y], strokeWidth: toolState.brush.width, color: getCurrentFill(), clip: getClip(selectedEntity), }); } - setLastAddedPoint(pos); + setLastAddedPoint(alignedPoint); } if (toolState.selected === 'eraser') { const lastLinePoint = getLastPointOfLastLineOfEntity(selectedEntity, toolState.selected); + const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); if (e.evt.shiftKey && lastLinePoint) { // Create a straight line from the last line point if (selectedEntityAdapter.getDrawingBuffer()) { @@ -229,8 +238,8 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { // The last point of the last line is already normalized to the entity's coordinates lastLinePoint.x, lastLinePoint.y, - pos.x - selectedEntity.position.x, - pos.y - selectedEntity.position.y, + alignedPoint.x, + alignedPoint.y, ], strokeWidth: toolState.eraser.width, clip: getClip(selectedEntity), @@ -242,12 +251,12 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.setDrawingBuffer({ id: getObjectId('eraser_line', true), type: 'eraser_line', - points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y], + points: [alignedPoint.x, alignedPoint.y], strokeWidth: toolState.eraser.width, clip: getClip(selectedEntity), }); } - setLastAddedPoint(pos); + setLastAddedPoint(alignedPoint); } if (toolState.selected === 'rect') { @@ -257,8 +266,8 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.setDrawingBuffer({ id: getObjectId('rect_shape', true), type: 'rect_shape', - x: pos.x - selectedEntity.position.x, - y: pos.y - selectedEntity.position.y, + x: Math.round(normalizedPoint.x), + y: Math.round(normalizedPoint.y), width: 0, height: 0, color: getCurrentFill(), @@ -340,12 +349,11 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (drawingBuffer?.type === 'brush_line') { const nextPoint = getNextPoint(pos, toolState, getLastPointOfLine(drawingBuffer.points)); if (nextPoint) { - drawingBuffer.points.push( - nextPoint.x - selectedEntity.position.x, - nextPoint.y - selectedEntity.position.y - ); + const normalizedPoint = offsetCoord(nextPoint, selectedEntity.position); + const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); + drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); - setLastAddedPoint(nextPoint); + setLastAddedPoint(alignedPoint); } } else { await selectedEntityAdapter.setDrawingBuffer(null); @@ -354,15 +362,17 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (selectedEntityAdapter.getDrawingBuffer()) { await selectedEntityAdapter.finalizeDrawingBuffer(); } + const normalizedPoint = offsetCoord(pos, selectedEntity.position); + const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); await selectedEntityAdapter.setDrawingBuffer({ id: getObjectId('brush_line', true), type: 'brush_line', - points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y], + points: [alignedPoint.x, alignedPoint.y], strokeWidth: toolState.brush.width, color: getCurrentFill(), clip: getClip(selectedEntity), }); - setLastAddedPoint(pos); + setLastAddedPoint(alignedPoint); } } @@ -372,12 +382,11 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (drawingBuffer.type === 'eraser_line') { const nextPoint = getNextPoint(pos, toolState, getLastPointOfLine(drawingBuffer.points)); if (nextPoint) { - drawingBuffer.points.push( - nextPoint.x - selectedEntity.position.x, - nextPoint.y - selectedEntity.position.y - ); + const normalizedPoint = offsetCoord(nextPoint, selectedEntity.position); + const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); + drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); - setLastAddedPoint(nextPoint); + setLastAddedPoint(alignedPoint); } } else { await selectedEntityAdapter.setDrawingBuffer(null); @@ -386,14 +395,16 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (selectedEntityAdapter.getDrawingBuffer()) { await selectedEntityAdapter.finalizeDrawingBuffer(); } + const normalizedPoint = offsetCoord(pos, selectedEntity.position); + const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); await selectedEntityAdapter.setDrawingBuffer({ id: getObjectId('eraser_line', true), type: 'eraser_line', - points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y], + points: [alignedPoint.x, alignedPoint.y], strokeWidth: toolState.eraser.width, clip: getClip(selectedEntity), }); - setLastAddedPoint(pos); + setLastAddedPoint(alignedPoint); } } @@ -401,8 +412,9 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); if (drawingBuffer) { if (drawingBuffer.type === 'rect_shape') { - drawingBuffer.width = pos.x - selectedEntity.position.x - drawingBuffer.x; - drawingBuffer.height = pos.y - selectedEntity.position.y - drawingBuffer.y; + const normalizedPoint = offsetCoord(pos, selectedEntity.position); + drawingBuffer.width = Math.round(normalizedPoint.x - drawingBuffer.x); + drawingBuffer.height = Math.round(normalizedPoint.y - drawingBuffer.y); await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); } else { await selectedEntityAdapter.setDrawingBuffer(null); @@ -432,17 +444,20 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { getIsPrimaryMouseDown(e) ) { const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); + const normalizedPoint = offsetCoord(pos, selectedEntity.position); if (toolState.selected === 'brush' && drawingBuffer?.type === 'brush_line') { - drawingBuffer.points.push(pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y); + const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); + drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); await selectedEntityAdapter.finalizeDrawingBuffer(); } else if (toolState.selected === 'eraser' && drawingBuffer?.type === 'eraser_line') { - drawingBuffer.points.push(pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y); + const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); + drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); await selectedEntityAdapter.finalizeDrawingBuffer(); } else if (toolState.selected === 'rect' && drawingBuffer?.type === 'rect_shape') { - drawingBuffer.width = pos.x - selectedEntity.position.x - drawingBuffer.x; - drawingBuffer.height = pos.y - selectedEntity.position.y - drawingBuffer.y; + drawingBuffer.width = Math.round(normalizedPoint.x - drawingBuffer.x); + drawingBuffer.height = Math.round(normalizedPoint.y - drawingBuffer.y); await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); await selectedEntityAdapter.finalizeDrawingBuffer(); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index 71f66151c4d..07f8601cce0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -1,7 +1,7 @@ import { getImageDataTransparency } from 'common/util/arrayBuffer'; import { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { GenerationMode, Rect, RenderableObject, RgbaColor } from 'features/controlLayers/store/types'; +import type { Coordinate, GenerationMode, Rect, RenderableObject, RgbaColor } from 'features/controlLayers/store/types'; import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers'; import Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; @@ -41,6 +41,39 @@ export const getScaledCursorPosition = (stage: Konva.Stage): Vector2d | null => return stageTransform.invert().point(pointerPosition); }; +/** + * Aligns a coordinate to the nearest integer. When the tool width is odd, an offset is added to align the edges + * of the tool to the grid. Without this alignment, the edges of the tool will be 0.5px off. + * @param coord The coordinate to align + * @param toolWidth The width of the tool + * @returns The aligned coordinate + */ +export const alignCoordForTool = (coord: Coordinate, toolWidth: number): Coordinate => { + const roundedX = Math.round(coord.x); + const roundedY = Math.round(coord.y); + const deltaX = coord.x - roundedX; + const deltaY = coord.y - roundedY; + const offset = (toolWidth / 2) % 1; + const point = { + x: roundedX + Math.sign(deltaX) * offset, + y: roundedY + Math.sign(deltaY) * offset, + }; + return point; +}; + +/** + * Offsets a point by the given offset. The offset is subtracted from the point. + * @param coord The coordinate to offset + * @param offset The offset to apply + * @returns + */ +export const offsetCoord = (coord: Coordinate, offset: Coordinate): Coordinate => { + return { + x: coord.x - offset.x, + y: coord.y - offset.y, + }; +}; + /** * Snaps a position to the edge of the stage if within a threshold of the edge * @param pos The position to snap From cd76a2d2178d7247554c7282f2c056a9155c7af3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 2 Aug 2024 13:26:28 +1000 Subject: [PATCH 280/678] tidy(ui): consolidate getLoggingContext builders --- .../controlLayers/konva/CanvasBrushLine.ts | 2 +- .../controlLayers/konva/CanvasEraserLine.ts | 2 +- .../controlLayers/konva/CanvasImage.ts | 2 +- .../controlLayers/konva/CanvasLayer.ts | 2 +- .../controlLayers/konva/CanvasManager.ts | 41 +++++++++++-------- .../controlLayers/konva/CanvasRect.ts | 2 +- .../controlLayers/konva/CanvasStagingArea.ts | 2 +- .../controlLayers/konva/CanvasTransformer.ts | 2 +- 8 files changed, 31 insertions(+), 24 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts index d81a413f83d..5b8df77e8d9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts @@ -30,7 +30,7 @@ export class CanvasBrushLine { this.parent = parent; this.manager = parent.manager; - this.getLoggingContext = this.manager.buildObjectGetLoggingContext(this); + this.getLoggingContext = this.manager.buildGetLoggingContext(this); this.log = this.manager.buildLogger(this.getLoggingContext); this.log.trace({ state }, 'Creating brush line'); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts index cf54722bf1c..fabaf77e700 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts @@ -29,7 +29,7 @@ export class CanvasEraserLine { this.id = id; this.parent = parent; this.manager = parent.manager; - this.getLoggingContext = this.manager.buildObjectGetLoggingContext(this); + this.getLoggingContext = this.manager.buildGetLoggingContext(this); this.log = this.manager.buildLogger(this.getLoggingContext); this.log.trace({ state }, 'Creating eraser line'); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts index 8e69b7b41d4..c3b33a38782 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -39,7 +39,7 @@ export class CanvasImage { this.id = id; this.parent = parent; this.manager = parent.manager; - this.getLoggingContext = this.manager.buildObjectGetLoggingContext(this); + this.getLoggingContext = this.manager.buildGetLoggingContext(this); this.log = this.manager.buildLogger(this.getLoggingContext); this.log.trace({ state }, 'Creating image'); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 4892df4df8d..62977aaf9c0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -60,7 +60,7 @@ export class CanvasLayer { constructor(state: LayerEntity, manager: CanvasManager) { this.id = state.id; this.manager = manager; - this.getLoggingContext = this.manager.buildEntityGetLoggingContext(this); + this.getLoggingContext = this.manager.buildGetLoggingContext(this); this.log = this.manager.buildLogger(this.getLoggingContext); this.log.debug({ state }, 'Creating layer'); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 69e69b161aa..177236b88a4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -591,26 +591,33 @@ export class CanvasManager { }); } - buildObjectGetLoggingContext = ( - instance: CanvasBrushLine | CanvasEraserLine | CanvasRect | CanvasImage | CanvasTransformer + buildGetLoggingContext = ( + instance: + | CanvasBrushLine + | CanvasEraserLine + | CanvasRect + | CanvasImage + | CanvasTransformer + | CanvasLayer + | CanvasStagingArea ): GetLoggingContext => { - return (extra?: JSONObject): JSONObject => { - return { - ...instance.parent.getLoggingContext(), - objectId: instance.id, - ...extra, + if (instance instanceof CanvasLayer || instance instanceof CanvasStagingArea) { + return (extra?: JSONObject): JSONObject => { + return { + ...instance.manager.getLoggingContext(), + entityId: instance.id, + ...extra, + }; }; - }; - }; - - buildEntityGetLoggingContext = (instance: CanvasLayer | CanvasStagingArea): GetLoggingContext => { - return (extra?: JSONObject): JSONObject => { - return { - ...instance.manager.getLoggingContext(), - entityId: instance.id, - ...extra, + } else { + return (extra?: JSONObject): JSONObject => { + return { + ...instance.parent.getLoggingContext(), + objectId: instance.id, + ...extra, + }; }; - }; + } }; logDebugInfo() { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts index a0453048be6..101a64b9235 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts @@ -28,7 +28,7 @@ export class CanvasRect { this.id = id; this.parent = parent; this.manager = parent.manager; - this.getLoggingContext = this.manager.buildObjectGetLoggingContext(this); + this.getLoggingContext = this.manager.buildGetLoggingContext(this); this.log = this.manager.buildLogger(this.getLoggingContext); this.log.trace({ state }, 'Creating rect'); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts index 278e3dbfc02..b881b5f7f91 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts @@ -22,7 +22,7 @@ export class CanvasStagingArea { constructor(manager: CanvasManager) { this.id = getPrefixedId(CanvasStagingArea.TYPE); this.manager = manager; - this.getLoggingContext = this.manager.buildEntityGetLoggingContext(this); + this.getLoggingContext = this.manager.buildGetLoggingContext(this); this.log = this.manager.buildLogger(this.getLoggingContext); this.log.debug('Creating staging area'); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index 68ceaf00377..303f60ecd2c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -63,7 +63,7 @@ export class CanvasTransformer { this.manager = parent.manager; this.transformTarget = transformTarget; - this.getLoggingContext = this.manager.buildObjectGetLoggingContext(this); + this.getLoggingContext = this.manager.buildGetLoggingContext(this); this.log = this.manager.buildLogger(this.getLoggingContext); this.subscriptions = []; From edeb706d19574ca139abf15d39dcdd994179f143 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 2 Aug 2024 14:00:51 +1000 Subject: [PATCH 281/678] tidy(ui): rename canvas stuff --- .../addCommitStagingAreaImageListener.ts | 4 +- .../ControlAdapter/CAImagePreview.tsx | 4 +- .../controlLayers/konva/CanvasBrushLine.ts | 8 +- .../konva/CanvasControlAdapter.ts | 8 +- .../controlLayers/konva/CanvasEraserLine.ts | 8 +- .../controlLayers/konva/CanvasImage.ts | 8 +- .../controlLayers/konva/CanvasInpaintMask.ts | 20 ++-- .../controlLayers/konva/CanvasLayer.ts | 26 ++--- .../controlLayers/konva/CanvasRect.ts | 8 +- .../controlLayers/konva/CanvasRegion.ts | 20 ++-- .../controlLayers/konva/CanvasStateApi.ts | 12 +- .../controlLayers/konva/entityBbox.ts | 12 +- .../features/controlLayers/konva/events.ts | 20 ++-- .../src/features/controlLayers/konva/util.ts | 4 +- .../store/controlAdaptersReducers.ts | 12 +- .../store/inpaintMaskReducers.ts | 18 +-- .../controlLayers/store/ipAdaptersReducers.ts | 6 +- .../controlLayers/store/layersReducers.ts | 26 ++--- .../controlLayers/store/regionsReducers.ts | 22 ++-- .../src/features/controlLayers/store/types.ts | 108 +++++++++--------- .../metadata/components/MetadataLayers.tsx | 8 +- .../src/features/metadata/util/handlers.ts | 6 +- .../web/src/features/metadata/util/parsers.ts | 34 +++--- .../src/features/metadata/util/recallers.ts | 20 ++-- .../src/features/metadata/util/validators.ts | 8 +- .../graph/generation/addControlAdapters.ts | 16 +-- .../util/graph/generation/addIPAdapters.ts | 10 +- .../nodes/util/graph/generation/addLayers.ts | 4 +- .../nodes/util/graph/generation/addRegions.ts | 10 +- 29 files changed, 235 insertions(+), 235 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts index 6917c83a217..98ed8071cd3 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts @@ -6,7 +6,7 @@ import { sessionStagingAreaImageAccepted, sessionStagingAreaReset, } from 'features/controlLayers/store/canvasV2Slice'; -import type { LayerEntity } from 'features/controlLayers/store/types'; +import type { CanvasLayerState } from 'features/controlLayers/store/types'; import { imageDTOToImageObject } from 'features/controlLayers/store/types'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; @@ -62,7 +62,7 @@ export const addStagingListeners = (startAppListening: AppStartListening) => { const { imageDTO, offsetX, offsetY } = stagingAreaImage; const imageObject = imageDTOToImageObject(imageDTO); - const overrides: Partial = { + const overrides: Partial = { position: { x: x + offsetX, y: y + offsetY }, objects: [imageObject], }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx index b1c862bab18..c8a998b5738 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx @@ -5,7 +5,7 @@ import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; -import type { ControlAdapterEntity } from 'features/controlLayers/store/types'; +import type { CanvasControlAdapterState } from 'features/controlLayers/store/types'; import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; import { memo, useCallback, useEffect, useMemo, useState } from 'react'; @@ -20,7 +20,7 @@ import { import type { ImageDTO, PostUploadAction } from 'services/api/types'; type Props = { - controlAdapter: ControlAdapterEntity; + controlAdapter: CanvasControlAdapterState; onChangeImage: (imageDTO: ImageDTO | null) => void; droppableData: TypesafeDroppableData; postUploadAction: PostUploadAction; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts index 5b8df77e8d9..85704cd80d1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts @@ -3,7 +3,7 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { BrushLine } from 'features/controlLayers/store/types'; +import type { CanvasBrushLineState } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; @@ -18,13 +18,13 @@ export class CanvasBrushLine { log: Logger; getLoggingContext: (extra?: JSONObject) => JSONObject; - state: BrushLine; + state: CanvasBrushLineState; konva: { group: Konva.Group; line: Konva.Line; }; - constructor(state: BrushLine, parent: CanvasLayer) { + constructor(state: CanvasBrushLineState, parent: CanvasLayer) { const { id, strokeWidth, clip, color, points } = state; this.id = id; this.parent = parent; @@ -59,7 +59,7 @@ export class CanvasBrushLine { this.state = state; } - update(state: BrushLine, force?: boolean): boolean { + update(state: CanvasBrushLineState, force?: boolean): boolean { if (force || this.state !== state) { this.log.trace({ state }, 'Updating brush line'); const { points, color, clip, strokeWidth } = state; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts index 0065d876d3c..64f65cfb8f8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts @@ -2,7 +2,7 @@ import { CanvasEntity } from 'features/controlLayers/konva/CanvasEntity'; import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; -import { type ControlAdapterEntity, isDrawingTool } from 'features/controlLayers/store/types'; +import { type CanvasControlAdapterState, isDrawingTool } from 'features/controlLayers/store/types'; import Konva from 'konva'; export class CanvasControlAdapter extends CanvasEntity { @@ -13,7 +13,7 @@ export class CanvasControlAdapter extends CanvasEntity { static OBJECT_GROUP_NAME = `${CanvasControlAdapter.NAME_PREFIX}_object-group`; type = 'control_adapter'; - _state: ControlAdapterEntity; + _state: CanvasControlAdapterState; konva: { layer: Konva.Layer; @@ -24,7 +24,7 @@ export class CanvasControlAdapter extends CanvasEntity { image: CanvasImage | null; transformer: CanvasTransformer; - constructor(state: ControlAdapterEntity, manager: CanvasManager) { + constructor(state: CanvasControlAdapterState, manager: CanvasManager) { super(state.id, manager); this.konva = { layer: new Konva.Layer({ @@ -47,7 +47,7 @@ export class CanvasControlAdapter extends CanvasEntity { this._state = state; } - async render(state: ControlAdapterEntity) { + async render(state: CanvasControlAdapterState) { this._state = state; // Update the layer's position and listening state diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts index fabaf77e700..fdff175b741 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts @@ -2,7 +2,7 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { EraserLine, GetLoggingContext } from 'features/controlLayers/store/types'; +import type { CanvasEraserLineState, GetLoggingContext } from 'features/controlLayers/store/types'; import { RGBA_RED } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; @@ -18,13 +18,13 @@ export class CanvasEraserLine { log: Logger; getLoggingContext: GetLoggingContext; - state: EraserLine; + state: CanvasEraserLineState; konva: { group: Konva.Group; line: Konva.Line; }; - constructor(state: EraserLine, parent: CanvasLayer) { + constructor(state: CanvasEraserLineState, parent: CanvasLayer) { const { id, strokeWidth, clip, points } = state; this.id = id; this.parent = parent; @@ -58,7 +58,7 @@ export class CanvasEraserLine { this.state = state; } - update(state: EraserLine, force?: boolean): boolean { + update(state: CanvasEraserLineState, force?: boolean): boolean { if (force || this.state !== state) { this.log.trace({ state }, 'Updating eraser line'); const { points, clip, strokeWidth } = state; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts index c3b33a38782..500a9fa208d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -4,7 +4,7 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasStagingArea } from 'features/controlLayers/konva/CanvasStagingArea'; import { FILTER_MAP } from 'features/controlLayers/konva/filters'; import { loadImage } from 'features/controlLayers/konva/util'; -import type { GetLoggingContext, ImageObject } from 'features/controlLayers/store/types'; +import type { GetLoggingContext, CanvasImageState } from 'features/controlLayers/store/types'; import { t } from 'i18next'; import Konva from 'konva'; import type { Logger } from 'roarr'; @@ -24,7 +24,7 @@ export class CanvasImage { log: Logger; getLoggingContext: GetLoggingContext; - state: ImageObject; + state: CanvasImageState; konva: { group: Konva.Group; placeholder: { group: Konva.Group; rect: Konva.Rect; text: Konva.Text }; @@ -34,7 +34,7 @@ export class CanvasImage { isLoading: boolean; isError: boolean; - constructor(state: ImageObject, parent: CanvasLayer | CanvasStagingArea) { + constructor(state: CanvasImageState, parent: CanvasLayer | CanvasStagingArea) { const { id, width, height, x, y } = state; this.id = id; this.parent = parent; @@ -138,7 +138,7 @@ export class CanvasImage { } } - async update(state: ImageObject, force?: boolean): Promise { + async update(state: CanvasImageState, force?: boolean): Promise { if (this.state !== state || force) { this.log.trace({ state }, 'Updating image'); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts index e03133b78cb..965c7fc5df6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts @@ -5,7 +5,7 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox'; import { mapId } from 'features/controlLayers/konva/util'; -import type { BrushLine, EraserLine, InpaintMaskEntity, RectShape } from 'features/controlLayers/store/types'; +import type { CanvasBrushLineState, CanvasEraserLineState, CanvasInpaintMaskState, CanvasRectState } from 'features/controlLayers/store/types'; import { isDrawingTool, RGBA_RED } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { assert } from 'tsafe'; @@ -18,8 +18,8 @@ export class CanvasInpaintMask { static OBJECT_GROUP_NAME = `${CanvasInpaintMask.NAME_PREFIX}_object-group`; static COMPOSITING_RECT_NAME = `${CanvasInpaintMask.NAME_PREFIX}_compositing-rect`; - private drawingBuffer: BrushLine | EraserLine | RectShape | null; - private state: InpaintMaskEntity; + private drawingBuffer: CanvasBrushLineState | CanvasEraserLineState | CanvasRectState | null; + private state: CanvasInpaintMaskState; id = 'inpaint_mask'; manager: CanvasManager; @@ -33,7 +33,7 @@ export class CanvasInpaintMask { }; objects: Map; - constructor(state: InpaintMaskEntity, manager: CanvasManager) { + constructor(state: CanvasInpaintMaskState, manager: CanvasManager) { this.manager = manager; this.konva = { @@ -87,12 +87,12 @@ export class CanvasInpaintMask { return this.drawingBuffer; } - async setDrawingBuffer(obj: BrushLine | EraserLine | RectShape | null) { + async setDrawingBuffer(obj: CanvasBrushLineState | CanvasEraserLineState | CanvasRectState | null) { this.drawingBuffer = obj; if (this.drawingBuffer) { if (this.drawingBuffer.type === 'brush_line') { this.drawingBuffer.color = RGBA_RED; - } else if (this.drawingBuffer.type === 'rect_shape') { + } else if (this.drawingBuffer.type === 'rect') { this.drawingBuffer.color = RGBA_RED; } @@ -109,13 +109,13 @@ export class CanvasInpaintMask { this.manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: this.drawingBuffer }, 'inpaint_mask'); } else if (this.drawingBuffer.type === 'eraser_line') { this.manager.stateApi.onEraserLineAdded({ id: this.id, eraserLine: this.drawingBuffer }, 'inpaint_mask'); - } else if (this.drawingBuffer.type === 'rect_shape') { + } else if (this.drawingBuffer.type === 'rect') { this.manager.stateApi.onRectShapeAdded({ id: this.id, rectShape: this.drawingBuffer }, 'inpaint_mask'); } this.setDrawingBuffer(null); } - async render(state: InpaintMaskEntity) { + async render(state: CanvasInpaintMaskState) { this.state = state; // Update the layer's position and listening state @@ -153,7 +153,7 @@ export class CanvasInpaintMask { this.updateGroup(didDraw); } - private async renderObject(obj: InpaintMaskEntity['objects'][number], force = false): Promise { + private async renderObject(obj: CanvasInpaintMaskState['objects'][number], force = false): Promise { if (obj.type === 'brush_line') { let brushLine = this.objects.get(obj.id); assert(brushLine instanceof CanvasBrushLine || brushLine === undefined); @@ -182,7 +182,7 @@ export class CanvasInpaintMask { return true; } } - } else if (obj.type === 'rect_shape') { + } else if (obj.type === 'rect') { let rect = this.objects.get(obj.id); assert(rect instanceof CanvasRect || rect === undefined); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 62977aaf9c0..e9279095300 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -9,14 +9,14 @@ import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransforme import { getPrefixedId, konvaNodeToBlob, mapId, previewBlob } from 'features/controlLayers/konva/util'; import { layerRasterized } from 'features/controlLayers/store/canvasV2Slice'; import type { - BrushLine, + CanvasBrushLineState, + CanvasEraserLineState, + CanvasLayerState, + CanvasRectState, CanvasV2State, Coordinate, - EraserLine, GetLoggingContext, - LayerEntity, Rect, - RectShape, } from 'features/controlLayers/store/types'; import { imageDTOToImageObject } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -39,8 +39,8 @@ export class CanvasLayer { log: Logger; getLoggingContext: GetLoggingContext; - drawingBuffer: BrushLine | EraserLine | RectShape | null; - state: LayerEntity; + drawingBuffer: CanvasBrushLineState | CanvasEraserLineState | CanvasRectState | null; + state: CanvasLayerState; konva: { layer: Konva.Layer; @@ -57,7 +57,7 @@ export class CanvasLayer { rect: Rect; bbox: Rect; - constructor(state: LayerEntity, manager: CanvasManager) { + constructor(state: CanvasLayerState, manager: CanvasManager) { this.id = state.id; this.manager = manager; this.getLoggingContext = this.manager.buildGetLoggingContext(this); @@ -104,7 +104,7 @@ export class CanvasLayer { return this.drawingBuffer; }; - setDrawingBuffer = async (obj: BrushLine | EraserLine | RectShape | null) => { + setDrawingBuffer = async (obj: CanvasBrushLineState | CanvasEraserLineState | CanvasRectState | null) => { if (obj) { this.drawingBuffer = obj; await this._renderObject(this.drawingBuffer, true); @@ -129,13 +129,13 @@ export class CanvasLayer { } else if (drawingBuffer.type === 'eraser_line') { drawingBuffer.id = getPrefixedId('brush_line'); this.manager.stateApi.onEraserLineAdded({ id: this.id, eraserLine: drawingBuffer }, 'layer'); - } else if (drawingBuffer.type === 'rect_shape') { + } else if (drawingBuffer.type === 'rect') { drawingBuffer.id = getPrefixedId('brush_line'); this.manager.stateApi.onRectShapeAdded({ id: this.id, rectShape: drawingBuffer }, 'layer'); } }; - update = async (arg?: { state: LayerEntity; toolState: CanvasV2State['tool']; isSelected: boolean }) => { + update = async (arg?: { state: CanvasLayerState; toolState: CanvasV2State['tool']; isSelected: boolean }) => { const state = get(arg, 'state', this.state); const toolState = get(arg, 'toolState', this.manager.stateApi.getToolState()); const isSelected = get(arg, 'isSelected', this.manager.stateApi.getIsSelected(this.id)); @@ -191,7 +191,7 @@ export class CanvasLayer { this.transformer.update(position, this.bbox); }; - updateObjects = async (arg?: { objects: LayerEntity['objects'] }) => { + updateObjects = async (arg?: { objects: CanvasLayerState['objects'] }) => { this.log.trace('Updating objects'); const objects = get(arg, 'objects', this.state.objects); @@ -297,7 +297,7 @@ export class CanvasLayer { }); }; - _renderObject = async (obj: LayerEntity['objects'][number], force = false): Promise => { + _renderObject = async (obj: CanvasLayerState['objects'][number], force = false): Promise => { if (obj.type === 'brush_line') { let brushLine = this.objects.get(obj.id); assert(brushLine instanceof CanvasBrushLine || brushLine === undefined); @@ -324,7 +324,7 @@ export class CanvasLayer { return true; } } - } else if (obj.type === 'rect_shape') { + } else if (obj.type === 'rect') { let rect = this.objects.get(obj.id); assert(rect instanceof CanvasRect || rect === undefined); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts index 101a64b9235..c714a601448 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts @@ -2,7 +2,7 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { GetLoggingContext, RectShape } from 'features/controlLayers/store/types'; +import type { GetLoggingContext, CanvasRectState } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; @@ -17,13 +17,13 @@ export class CanvasRect { log: Logger; getLoggingContext: GetLoggingContext; - state: RectShape; + state: CanvasRectState; konva: { group: Konva.Group; rect: Konva.Rect; }; - constructor(state: RectShape, parent: CanvasLayer) { + constructor(state: CanvasRectState, parent: CanvasLayer) { const { id, x, y, width, height, color } = state; this.id = id; this.parent = parent; @@ -48,7 +48,7 @@ export class CanvasRect { this.state = state; } - update(state: RectShape, force?: boolean): boolean { + update(state: CanvasRectState, force?: boolean): boolean { if (this.state !== state || force) { this.log.trace({ state }, 'Updating rect'); const { x, y, width, height, color } = state; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts index 1e3fa446fbc..30851a79e40 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts @@ -5,7 +5,7 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox'; import { mapId } from 'features/controlLayers/konva/util'; -import type { BrushLine, EraserLine, RectShape, RegionEntity } from 'features/controlLayers/store/types'; +import type { CanvasBrushLineState, CanvasEraserLineState, CanvasRectState, CanvasRegionalGuidanceState } from 'features/controlLayers/store/types'; import { isDrawingTool, RGBA_RED } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { assert } from 'tsafe'; @@ -18,8 +18,8 @@ export class CanvasRegion { static OBJECT_GROUP_NAME = `${CanvasRegion.NAME_PREFIX}_object-group`; static COMPOSITING_RECT_NAME = `${CanvasRegion.NAME_PREFIX}_compositing-rect`; - private drawingBuffer: BrushLine | EraserLine | RectShape | null; - private state: RegionEntity; + private drawingBuffer: CanvasBrushLineState | CanvasEraserLineState | CanvasRectState | null; + private state: CanvasRegionalGuidanceState; id: string; manager: CanvasManager; @@ -34,7 +34,7 @@ export class CanvasRegion { objects: Map; - constructor(state: RegionEntity, manager: CanvasManager) { + constructor(state: CanvasRegionalGuidanceState, manager: CanvasManager) { this.id = state.id; this.manager = manager; @@ -86,12 +86,12 @@ export class CanvasRegion { return this.drawingBuffer; } - async setDrawingBuffer(obj: BrushLine | EraserLine | RectShape | null) { + async setDrawingBuffer(obj: CanvasBrushLineState | CanvasEraserLineState | CanvasRectState | null) { this.drawingBuffer = obj; if (this.drawingBuffer) { if (this.drawingBuffer.type === 'brush_line') { this.drawingBuffer.color = RGBA_RED; - } else if (this.drawingBuffer.type === 'rect_shape') { + } else if (this.drawingBuffer.type === 'rect') { this.drawingBuffer.color = RGBA_RED; } @@ -108,13 +108,13 @@ export class CanvasRegion { this.manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: this.drawingBuffer }, 'regional_guidance'); } else if (this.drawingBuffer.type === 'eraser_line') { this.manager.stateApi.onEraserLineAdded({ id: this.id, eraserLine: this.drawingBuffer }, 'regional_guidance'); - } else if (this.drawingBuffer.type === 'rect_shape') { + } else if (this.drawingBuffer.type === 'rect') { this.manager.stateApi.onRectShapeAdded({ id: this.id, rectShape: this.drawingBuffer }, 'regional_guidance'); } this.setDrawingBuffer(null); } - async render(state: RegionEntity) { + async render(state: CanvasRegionalGuidanceState) { this.state = state; // Update the layer's position and listening state @@ -152,7 +152,7 @@ export class CanvasRegion { this.updateGroup(didDraw); } - private async renderObject(obj: RegionEntity['objects'][number], force = false): Promise { + private async renderObject(obj: CanvasRegionalGuidanceState['objects'][number], force = false): Promise { if (obj.type === 'brush_line') { let brushLine = this.objects.get(obj.id); assert(brushLine instanceof CanvasBrushLine || brushLine === undefined); @@ -181,7 +181,7 @@ export class CanvasRegion { return true; } } - } else if (obj.type === 'rect_shape') { + } else if (obj.type === 'rect') { let rect = this.objects.get(obj.id); assert(rect instanceof CanvasRect || rect === undefined); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index f1cb1733c6b..4983b4c9af0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -46,11 +46,11 @@ import { } from 'features/controlLayers/store/canvasV2Slice'; import type { BboxChangedArg, - BrushLine, + CanvasBrushLineState, CanvasEntity, - EraserLine, + CanvasEraserLineState, PositionChangedArg, - RectShape, + CanvasRectState, ScaleChangedArg, Tool, } from 'features/controlLayers/store/types'; @@ -111,7 +111,7 @@ export class CanvasStateApi { this.store.dispatch(imBboxChanged(arg)); } }; - onBrushLineAdded = (arg: { id: string; brushLine: BrushLine }, entityType: CanvasEntity['type']) => { + onBrushLineAdded = (arg: { id: string; brushLine: CanvasBrushLineState }, entityType: CanvasEntity['type']) => { log.debug('Brush line added'); if (entityType === 'layer') { this.store.dispatch(layerBrushLineAdded(arg)); @@ -121,7 +121,7 @@ export class CanvasStateApi { this.store.dispatch(imBrushLineAdded(arg)); } }; - onEraserLineAdded = (arg: { id: string; eraserLine: EraserLine }, entityType: CanvasEntity['type']) => { + onEraserLineAdded = (arg: { id: string; eraserLine: CanvasEraserLineState }, entityType: CanvasEntity['type']) => { log.debug('Eraser line added'); if (entityType === 'layer') { this.store.dispatch(layerEraserLineAdded(arg)); @@ -131,7 +131,7 @@ export class CanvasStateApi { this.store.dispatch(imEraserLineAdded(arg)); } }; - onRectShapeAdded = (arg: { id: string; rectShape: RectShape }, entityType: CanvasEntity['type']) => { + onRectShapeAdded = (arg: { id: string; rectShape: CanvasRectState }, entityType: CanvasEntity['type']) => { log.debug('Rect shape added'); if (entityType === 'layer') { this.store.dispatch(layerRectShapeAdded(arg)); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts index 6a55ae4f1b7..c4e8bfe9df3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts @@ -4,9 +4,9 @@ import { imageDataToDataURL } from 'features/controlLayers/konva/util'; import type { BboxChangedArg, CanvasEntity, - ControlAdapterEntity, - LayerEntity, - RegionEntity, + CanvasControlAdapterState, + CanvasLayerState, + CanvasRegionalGuidanceState, } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; @@ -198,9 +198,9 @@ const filterCAChildren = (node: Konva.Node): boolean => true; */ export const updateBboxes = ( stage: Konva.Stage, - layers: LayerEntity[], - controlAdapters: ControlAdapterEntity[], - regions: RegionEntity[], + layers: CanvasLayerState[], + controlAdapters: CanvasControlAdapterState[], + regions: CanvasRegionalGuidanceState[], onBboxChanged: (arg: BboxChangedArg, entityType: CanvasEntity['type']) => void ): void => { for (const entityState of [...layers, ...controlAdapters, ...regions]) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 89d0ac6a754..e4b2ee7d3f4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -8,9 +8,9 @@ import { import type { CanvasV2State, Coordinate, - InpaintMaskEntity, - LayerEntity, - RegionEntity, + CanvasInpaintMaskState, + CanvasLayerState, + CanvasRegionalGuidanceState, Tool, } from 'features/controlLayers/store/types'; import { isDrawableEntity, isDrawableEntityAdapter } from 'features/controlLayers/store/types'; @@ -81,7 +81,7 @@ const getLastPointOfLine = (points: number[]): Coordinate | null => { }; const getLastPointOfLastLineOfEntity = ( - entity: LayerEntity | RegionEntity | InpaintMaskEntity, + entity: CanvasLayerState | CanvasRegionalGuidanceState | CanvasInpaintMaskState, tool: Tool ): Coordinate | null => { const lastObject = entity.objects[entity.objects.length - 1]; @@ -138,7 +138,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { return e.evt.buttons === 1; } - function getClip(entity: RegionEntity | LayerEntity | InpaintMaskEntity) { + function getClip(entity: CanvasRegionalGuidanceState | CanvasLayerState | CanvasInpaintMaskState) { const settings = getSettings(); const bboxRect = getBbox().rect; @@ -264,8 +264,8 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntityAdapter.finalizeDrawingBuffer(); } await selectedEntityAdapter.setDrawingBuffer({ - id: getObjectId('rect_shape', true), - type: 'rect_shape', + id: getObjectId('rect', true), + type: 'rect', x: Math.round(normalizedPoint.x), y: Math.round(normalizedPoint.y), width: 0, @@ -314,7 +314,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (toolState.selected === 'rect') { const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); - if (drawingBuffer?.type === 'rect_shape') { + if (drawingBuffer?.type === 'rect') { await selectedEntityAdapter.finalizeDrawingBuffer(); } else { await selectedEntityAdapter.setDrawingBuffer(null); @@ -411,7 +411,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (toolState.selected === 'rect') { const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); if (drawingBuffer) { - if (drawingBuffer.type === 'rect_shape') { + if (drawingBuffer.type === 'rect') { const normalizedPoint = offsetCoord(pos, selectedEntity.position); drawingBuffer.width = Math.round(normalizedPoint.x - drawingBuffer.x); drawingBuffer.height = Math.round(normalizedPoint.y - drawingBuffer.y); @@ -455,7 +455,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); await selectedEntityAdapter.finalizeDrawingBuffer(); - } else if (toolState.selected === 'rect' && drawingBuffer?.type === 'rect_shape') { + } else if (toolState.selected === 'rect' && drawingBuffer?.type === 'rect') { drawingBuffer.width = Math.round(normalizedPoint.x - drawingBuffer.x); drawingBuffer.height = Math.round(normalizedPoint.y - drawingBuffer.y); await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index 07f8601cce0..b382e43127f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -1,7 +1,7 @@ import { getImageDataTransparency } from 'common/util/arrayBuffer'; import { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { Coordinate, GenerationMode, Rect, RenderableObject, RgbaColor } from 'features/controlLayers/store/types'; +import type { Coordinate, GenerationMode, Rect, CanvasObjectState, RgbaColor } from 'features/controlLayers/store/types'; import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers'; import Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; @@ -618,7 +618,7 @@ export function getPrefixedId(prefix: string): string { return `${prefix}:${nanoid()}`; } -export function getObjectId(type: RenderableObject['type'], isBuffer?: boolean): string { +export function getObjectId(type: CanvasObjectState['type'], isBuffer?: boolean): string { if (isBuffer) { return getPrefixedId(`buffer_${type}`); } else { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts index 94596d246b6..aa77102263f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts @@ -9,16 +9,16 @@ import { v4 as uuidv4 } from 'uuid'; import type { CanvasV2State, - ControlAdapterEntity, + CanvasControlAdapterState, ControlModeV2, ControlNetConfig, - ControlNetData, + CanvasControlNetState, Filter, PositionChangedArg, ProcessorConfig, ScaleChangedArg, T2IAdapterConfig, - T2IAdapterData, + CanvasT2IAdapterState, } from './types'; import { buildControlAdapterProcessorV2, imageDTOToImageObject } from './types'; @@ -51,7 +51,7 @@ export const controlAdaptersReducers = { payload: { id: uuidv4(), ...payload }, }), }, - caRecalled: (state, action: PayloadAction<{ data: ControlAdapterEntity }>) => { + caRecalled: (state, action: PayloadAction<{ data: CanvasControlAdapterState }>) => { const { data } = action.payload; state.controlAdapters.entities.push(data); state.selectedEntityIdentifier = { type: 'control_adapter', id: data.id }; @@ -217,11 +217,11 @@ export const controlAdaptersReducers = { // We may need to convert the CA to match the model if (ca.adapterType === 't2i_adapter' && ca.model.type === 'controlnet') { - const convertedCA: ControlNetData = { ...ca, adapterType: 'controlnet', controlMode: 'balanced' }; + const convertedCA: CanvasControlNetState = { ...ca, adapterType: 'controlnet', controlMode: 'balanced' }; state.controlAdapters.entities.splice(state.controlAdapters.entities.indexOf(ca), 1, convertedCA); } else if (ca.adapterType === 'controlnet' && ca.model.type === 't2i_adapter') { const { controlMode: _, ...rest } = ca; - const convertedCA: T2IAdapterData = { ...rest, adapterType: 't2i_adapter' }; + const convertedCA: CanvasT2IAdapterState = { ...rest, adapterType: 't2i_adapter' }; state.controlAdapters.entities.splice(state.controlAdapters.entities.indexOf(ca), 1, convertedCA); } }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts index e49552cf2ac..ac12068505a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts @@ -1,11 +1,11 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import type { - BrushLine, + CanvasBrushLineState, CanvasV2State, Coordinate, - EraserLine, - InpaintMaskEntity, - RectShape, + CanvasEraserLineState, + CanvasInpaintMaskState, + CanvasRectState, ScaleChangedArg, } from 'features/controlLayers/store/types'; import { imageDTOToImageWithDims } from 'features/controlLayers/store/types'; @@ -21,7 +21,7 @@ export const inpaintMaskReducers = { state.inpaintMask.bboxNeedsUpdate = false; state.inpaintMask.imageCache = null; }, - imRecalled: (state, action: PayloadAction<{ data: InpaintMaskEntity }>) => { + imRecalled: (state, action: PayloadAction<{ data: CanvasInpaintMaskState }>) => { const { data } = action.payload; state.inpaintMask = data; state.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id }; @@ -42,7 +42,7 @@ export const inpaintMaskReducers = { } else if (obj.type === 'eraser_line') { obj.points = obj.points.map((point) => point * scale); obj.strokeWidth *= scale; - } else if (obj.type === 'rect_shape') { + } else if (obj.type === 'rect') { obj.x *= scale; obj.y *= scale; obj.height *= scale; @@ -66,19 +66,19 @@ export const inpaintMaskReducers = { const { imageDTO } = action.payload; state.inpaintMask.imageCache = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; }, - imBrushLineAdded: (state, action: PayloadAction<{ brushLine: BrushLine }>) => { + imBrushLineAdded: (state, action: PayloadAction<{ brushLine: CanvasBrushLineState }>) => { const { brushLine } = action.payload; state.inpaintMask.objects.push(brushLine); state.inpaintMask.bboxNeedsUpdate = true; state.layers.imageCache = null; }, - imEraserLineAdded: (state, action: PayloadAction<{ eraserLine: EraserLine }>) => { + imEraserLineAdded: (state, action: PayloadAction<{ eraserLine: CanvasEraserLineState }>) => { const { eraserLine } = action.payload; state.inpaintMask.objects.push(eraserLine); state.inpaintMask.bboxNeedsUpdate = true; state.layers.imageCache = null; }, - imRectShapeAdded: (state, action: PayloadAction<{ rectShape: RectShape }>) => { + imRectShapeAdded: (state, action: PayloadAction<{ rectShape: CanvasRectState }>) => { const { rectShape } = action.payload; state.inpaintMask.objects.push(rectShape); state.inpaintMask.bboxNeedsUpdate = true; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts index 60c4c78d08f..561a769880c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts @@ -4,7 +4,7 @@ import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; -import type { CanvasV2State, CLIPVisionModelV2, IPAdapterConfig, IPAdapterEntity, IPMethodV2 } from './types'; +import type { CanvasV2State, CLIPVisionModelV2, IPAdapterConfig, CanvasIPAdapterState, IPMethodV2 } from './types'; import { imageDTOToImageObject } from './types'; export const selectIPA = (state: CanvasV2State, id: string) => state.ipAdapters.entities.find((ipa) => ipa.id === id); @@ -18,7 +18,7 @@ export const ipAdaptersReducers = { ipaAdded: { reducer: (state, action: PayloadAction<{ id: string; config: IPAdapterConfig }>) => { const { id, config } = action.payload; - const layer: IPAdapterEntity = { + const layer: CanvasIPAdapterState = { id, type: 'ip_adapter', isEnabled: true, @@ -29,7 +29,7 @@ export const ipAdaptersReducers = { }, prepare: (payload: { config: IPAdapterConfig }) => ({ payload: { id: uuidv4(), ...payload } }), }, - ipaRecalled: (state, action: PayloadAction<{ data: IPAdapterEntity }>) => { + ipaRecalled: (state, action: PayloadAction<{ data: CanvasIPAdapterState }>) => { const { data } = action.payload; state.ipAdapters.entities.push(data); state.selectedEntityIdentifier = { type: 'ip_adapter', id: data.id }; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index e58c2294a80..e1253e726ad 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -7,15 +7,15 @@ import type { ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; import type { - BrushLine, + CanvasBrushLineState, CanvasV2State, Coordinate, - EraserLine, - ImageObject, + CanvasEraserLineState, + CanvasImageState, ImageObjectAddedArg, - LayerEntity, + CanvasLayerState, PositionChangedArg, - RectShape, + CanvasRectState, } from './types'; import { imageDTOToImageObject, imageDTOToImageWithDims } from './types'; @@ -28,9 +28,9 @@ export const selectLayerOrThrow = (state: CanvasV2State, id: string) => { export const layersReducers = { layerAdded: { - reducer: (state, action: PayloadAction<{ id: string; overrides?: Partial }>) => { + reducer: (state, action: PayloadAction<{ id: string; overrides?: Partial }>) => { const { id } = action.payload; - const layer: LayerEntity = { + const layer: CanvasLayerState = { id, type: 'layer', isEnabled: true, @@ -43,11 +43,11 @@ export const layersReducers = { state.selectedEntityIdentifier = { type: 'layer', id }; state.layers.imageCache = null; }, - prepare: (payload: { overrides?: Partial }) => ({ + prepare: (payload: { overrides?: Partial }) => ({ payload: { ...payload, id: getPrefixedId('layer') }, }), }, - layerRecalled: (state, action: PayloadAction<{ data: LayerEntity }>) => { + layerRecalled: (state, action: PayloadAction<{ data: CanvasLayerState }>) => { const { data } = action.payload; state.layers.entities.push(data); state.selectedEntityIdentifier = { type: 'layer', id: data.id }; @@ -148,7 +148,7 @@ export const layersReducers = { moveToStart(state.layers.entities, layer); state.layers.imageCache = null; }, - layerBrushLineAdded: (state, action: PayloadAction<{ id: string; brushLine: BrushLine }>) => { + layerBrushLineAdded: (state, action: PayloadAction<{ id: string; brushLine: CanvasBrushLineState }>) => { const { id, brushLine } = action.payload; const layer = selectLayer(state, id); if (!layer) { @@ -158,7 +158,7 @@ export const layersReducers = { layer.objects.push(brushLine); state.layers.imageCache = null; }, - layerEraserLineAdded: (state, action: PayloadAction<{ id: string; eraserLine: EraserLine }>) => { + layerEraserLineAdded: (state, action: PayloadAction<{ id: string; eraserLine: CanvasEraserLineState }>) => { const { id, eraserLine } = action.payload; const layer = selectLayer(state, id); if (!layer) { @@ -168,7 +168,7 @@ export const layersReducers = { layer.objects.push(eraserLine); state.layers.imageCache = null; }, - layerRectShapeAdded: (state, action: PayloadAction<{ id: string; rectShape: RectShape }>) => { + layerRectShapeAdded: (state, action: PayloadAction<{ id: string; rectShape: CanvasRectState }>) => { const { id, rectShape } = action.payload; const layer = selectLayer(state, id); if (!layer) { @@ -199,7 +199,7 @@ export const layersReducers = { const { imageDTO } = action.payload; state.layers.imageCache = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; }, - layerRasterized: (state, action: PayloadAction<{ id: string; imageObject: ImageObject; position: Coordinate }>) => { + layerRasterized: (state, action: PayloadAction<{ id: string; imageObject: CanvasImageState; position: Coordinate }>) => { const { id, imageObject, position } = action.payload; const layer = selectLayer(state, id); if (!layer) { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index 5a93dcd8d43..4a82f586c41 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -1,13 +1,13 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import type { - BrushLine, + CanvasBrushLineState, CanvasV2State, CLIPVisionModelV2, - EraserLine, + CanvasEraserLineState, IPMethodV2, PositionChangedArg, - RectShape, + CanvasRectState, ScaleChangedArg, } from 'features/controlLayers/store/types'; import { imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/types'; @@ -19,7 +19,7 @@ import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; -import type { IPAdapterEntity, RegionEntity, RgbColor } from './types'; +import type { CanvasIPAdapterState, CanvasRegionalGuidanceState, RgbColor } from './types'; export const selectRG = (state: CanvasV2State, id: string) => state.regions.entities.find((rg) => rg.id === id); export const selectRGOrThrow = (state: CanvasV2State, id: string) => { @@ -54,7 +54,7 @@ export const regionsReducers = { rgAdded: { reducer: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; - const rg: RegionEntity = { + const rg: CanvasRegionalGuidanceState = { id, type: 'regional_guidance', isEnabled: true, @@ -85,7 +85,7 @@ export const regionsReducers = { rg.bboxNeedsUpdate = false; rg.imageCache = null; }, - rgRecalled: (state, action: PayloadAction<{ data: RegionEntity }>) => { + rgRecalled: (state, action: PayloadAction<{ data: CanvasRegionalGuidanceState }>) => { const { data } = action.payload; state.regions.entities.push(data); state.selectedEntityIdentifier = { type: 'regional_guidance', id: data.id }; @@ -117,7 +117,7 @@ export const regionsReducers = { } else if (obj.type === 'eraser_line') { obj.points = obj.points.map((point) => point * scale); obj.strokeWidth *= scale; - } else if (obj.type === 'rect_shape') { + } else if (obj.type === 'rect') { obj.x *= scale; obj.y *= scale; obj.height *= scale; @@ -215,7 +215,7 @@ export const regionsReducers = { } rg.autoNegative = autoNegative; }, - rgIPAdapterAdded: (state, action: PayloadAction<{ id: string; ipAdapter: IPAdapterEntity }>) => { + rgIPAdapterAdded: (state, action: PayloadAction<{ id: string; ipAdapter: CanvasIPAdapterState }>) => { const { id, ipAdapter } = action.payload; const rg = selectRG(state, id); if (!rg) { @@ -328,7 +328,7 @@ export const regionsReducers = { } ipa.clipVisionModel = clipVisionModel; }, - rgBrushLineAdded: (state, action: PayloadAction<{ id: string; brushLine: BrushLine }>) => { + rgBrushLineAdded: (state, action: PayloadAction<{ id: string; brushLine: CanvasBrushLineState }>) => { const { id, brushLine } = action.payload; const rg = selectRG(state, id); if (!rg) { @@ -339,7 +339,7 @@ export const regionsReducers = { rg.bboxNeedsUpdate = true; state.layers.imageCache = null; }, - rgEraserLineAdded: (state, action: PayloadAction<{ id: string; eraserLine: EraserLine }>) => { + rgEraserLineAdded: (state, action: PayloadAction<{ id: string; eraserLine: CanvasEraserLineState }>) => { const { id, eraserLine } = action.payload; const rg = selectRG(state, id); if (!rg) { @@ -350,7 +350,7 @@ export const regionsReducers = { rg.bboxNeedsUpdate = true; state.layers.imageCache = null; }, - rgRectShapeAdded: (state, action: PayloadAction<{ id: string; rectShape: RectShape }>) => { + rgRectShapeAdded: (state, action: PayloadAction<{ id: string; rectShape: CanvasRectState }>) => { const { id, rectShape } = action.payload; const rg = selectRG(state, id); if (!rg) { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 6b4874d27fe..353527d5980 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -527,7 +527,7 @@ const zRect = z.object({ }); export type Rect = z.infer; -const zBrushLine = z.object({ +const zCanvasBrushLineState = z.object({ id: zId, type: z.literal('brush_line'), strokeWidth: z.number().min(1), @@ -535,32 +535,32 @@ const zBrushLine = z.object({ color: zRgbaColor, clip: zRect.nullable(), }); -export type BrushLine = z.infer; +export type CanvasBrushLineState = z.infer; -const zEraserline = z.object({ +const zCanvasEraserLineState = z.object({ id: zId, type: z.literal('eraser_line'), strokeWidth: z.number().min(1), points: zPoints, clip: zRect.nullable(), }); -export type EraserLine = z.infer; +export type CanvasEraserLineState = z.infer; -const zRectShape = z.object({ +const zCanvasRectState = z.object({ id: zId, - type: z.literal('rect_shape'), + type: z.literal('rect'), x: z.number(), y: z.number(), width: z.number().min(1), height: z.number().min(1), color: zRgbaColor, }); -export type RectShape = z.infer; +export type CanvasRectState = z.infer; const zFilter = z.enum(['LightnessToAlphaFilter']); export type Filter = z.infer; -const zImageObject = z.object({ +const zCanvasImageState = z.object({ id: zId, type: z.literal('image'), image: zImageWithDims, @@ -570,46 +570,46 @@ const zImageObject = z.object({ height: z.number().min(1), filters: z.array(zFilter), }); -export type ImageObject = z.infer; +export type CanvasImageState = z.infer; -const zRenderableObject = z.discriminatedUnion('type', [zImageObject, zBrushLine, zEraserline, zRectShape]); -export type RenderableObject = z.infer; +const zCanvasObjectState = z.discriminatedUnion('type', [zCanvasImageState, zCanvasBrushLineState, zCanvasEraserLineState, zCanvasRectState]); +export type CanvasObjectState = z.infer; -export const zLayerEntity = z.object({ +export const zCanvasLayerState = z.object({ id: zId, type: z.literal('layer'), isEnabled: z.boolean(), position: zCoordinate, opacity: zOpacity, - objects: z.array(zRenderableObject), + objects: z.array(zCanvasObjectState), }); -export type LayerEntity = z.infer; +export type CanvasLayerState = z.infer; -export const zIPAdapterEntity = z.object({ +export const zCanvasIPAdapterState = z.object({ id: zId, type: z.literal('ip_adapter'), isEnabled: z.boolean(), weight: z.number().gte(-1).lte(2), method: zIPMethodV2, - imageObject: zImageObject.nullable(), + imageObject: zCanvasImageState.nullable(), model: zModelIdentifierField.nullable(), clipVisionModel: zCLIPVisionModelV2, beginEndStepPct: zBeginEndStepPct, }); -export type IPAdapterEntity = z.infer; +export type CanvasIPAdapterState = z.infer; export type IPAdapterConfig = Pick< - IPAdapterEntity, + CanvasIPAdapterState, 'weight' | 'imageObject' | 'beginEndStepPct' | 'model' | 'clipVisionModel' | 'method' >; const zMaskObject = z - .discriminatedUnion('type', [zOLD_VectorMaskLine, zOLD_VectorMaskRect, zBrushLine, zEraserline, zRectShape]) + .discriminatedUnion('type', [zOLD_VectorMaskLine, zOLD_VectorMaskRect, zCanvasBrushLineState, zCanvasEraserLineState, zCanvasRectState]) .transform((val) => { // Migrate old vector mask objects to new format if (val.type === 'vector_mask_line') { const { tool, ...rest } = val; if (tool === 'brush') { - const asBrushline: BrushLine = { + const asBrushline: CanvasBrushLineState = { ...rest, type: 'brush_line', color: { r: 255, g: 255, b: 255, a: 1 }, @@ -617,7 +617,7 @@ const zMaskObject = z }; return asBrushline; } else if (tool === 'eraser') { - const asEraserLine: EraserLine = { + const asEraserLine: CanvasEraserLineState = { ...rest, type: 'eraser_line', clip: null, @@ -625,9 +625,9 @@ const zMaskObject = z return asEraserLine; } } else if (val.type === 'vector_mask_rect') { - const asRectShape: RectShape = { + const asRectShape: CanvasRectState = { ...val, - type: 'rect_shape', + type: 'rect', color: { r: 255, g: 255, b: 255, a: 1 }, }; return asRectShape; @@ -635,9 +635,9 @@ const zMaskObject = z return val; } }) - .pipe(z.discriminatedUnion('type', [zBrushLine, zEraserline, zRectShape])); + .pipe(z.discriminatedUnion('type', [zCanvasBrushLineState, zCanvasEraserLineState, zCanvasRectState])); -export const zRegionEntity = z.object({ +export const zCanvasRegionalGuidanceState = z.object({ id: zId, type: z.literal('regional_guidance'), isEnabled: z.boolean(), @@ -647,12 +647,12 @@ export const zRegionEntity = z.object({ objects: z.array(zMaskObject), positivePrompt: zParameterPositivePrompt.nullable(), negativePrompt: zParameterNegativePrompt.nullable(), - ipAdapters: z.array(zIPAdapterEntity), + ipAdapters: z.array(zCanvasIPAdapterState), fill: zRgbColor, autoNegative: zAutoNegative, imageCache: zImageWithDims.nullable(), }); -export type RegionEntity = z.infer; +export type CanvasRegionalGuidanceState = z.infer; const zColorFill = z.object({ type: z.literal('color_fill'), @@ -663,7 +663,7 @@ const zImageFill = z.object({ src: z.string(), }); const zFill = z.discriminatedUnion('type', [zColorFill, zImageFill]); -const zInpaintMaskEntity = z.object({ +const zCanvasInpaintMaskState = z.object({ id: z.literal('inpaint_mask'), type: z.literal('inpaint_mask'), isEnabled: z.boolean(), @@ -674,7 +674,7 @@ const zInpaintMaskEntity = z.object({ fill: zRgbColor, imageCache: zImageWithDims.nullable(), }); -export type InpaintMaskEntity = z.infer; +export type CanvasInpaintMaskState = z.infer; const zInitialImageEntity = z.object({ id: z.literal('initial_image'), @@ -682,11 +682,11 @@ const zInitialImageEntity = z.object({ isEnabled: z.boolean(), bbox: zRect.nullable(), bboxNeedsUpdate: z.boolean(), - imageObject: zImageObject.nullable(), + imageObject: zCanvasImageState.nullable(), }); export type InitialImageEntity = z.infer; -const zControlAdapterEntityBase = z.object({ +const zCanvasControlAdapterStateBase = z.object({ id: zId, type: z.literal('control_adapter'), isEnabled: z.boolean(), @@ -696,27 +696,27 @@ const zControlAdapterEntityBase = z.object({ opacity: zOpacity, filters: z.array(zFilter), weight: z.number().gte(-1).lte(2), - imageObject: zImageObject.nullable(), - processedImageObject: zImageObject.nullable(), + imageObject: zCanvasImageState.nullable(), + processedImageObject: zCanvasImageState.nullable(), processorConfig: zProcessorConfig.nullable(), processorPendingBatchId: z.string().nullable().default(null), beginEndStepPct: zBeginEndStepPct, model: zModelIdentifierField.nullable(), }); -const zControlNetEntity = zControlAdapterEntityBase.extend({ +const zCanvasControlNetState = zCanvasControlAdapterStateBase.extend({ adapterType: z.literal('controlnet'), controlMode: zControlModeV2, }); -export type ControlNetData = z.infer; -const zT2IAdapterEntity = zControlAdapterEntityBase.extend({ +export type CanvasControlNetState = z.infer; +const zCanvasT2IAdapteState = zCanvasControlAdapterStateBase.extend({ adapterType: z.literal('t2i_adapter'), }); -export type T2IAdapterData = z.infer; +export type CanvasT2IAdapterState = z.infer; -export const zControlAdapterEntity = z.discriminatedUnion('adapterType', [zControlNetEntity, zT2IAdapterEntity]); -export type ControlAdapterEntity = z.infer; +export const zCanvasControlAdapterState = z.discriminatedUnion('adapterType', [zCanvasControlNetState, zCanvasT2IAdapteState]); +export type CanvasControlAdapterState = z.infer; export type ControlNetConfig = Pick< - ControlNetData, + CanvasControlNetState, | 'adapterType' | 'weight' | 'imageObject' @@ -727,7 +727,7 @@ export type ControlNetConfig = Pick< | 'controlMode' >; export type T2IAdapterConfig = Pick< - T2IAdapterData, + CanvasT2IAdapterState, 'adapterType' | 'weight' | 'imageObject' | 'processedImageObject' | 'processorConfig' | 'beginEndStepPct' | 'model' >; @@ -778,7 +778,7 @@ export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO) height, }); -export const imageDTOToImageObject = (imageDTO: ImageDTO, overrides?: Partial): ImageObject => { +export const imageDTOToImageObject = (imageDTO: ImageDTO, overrides?: Partial): CanvasImageState => { const { width, height, image_name } = imageDTO; return { id: getObjectId('image'), @@ -803,11 +803,11 @@ export const isBoundingBoxScaleMethod = (v: unknown): v is BoundingBoxScaleMetho zBoundingBoxScaleMethod.safeParse(v).success; export type CanvasEntity = - | LayerEntity - | ControlAdapterEntity - | RegionEntity - | InpaintMaskEntity - | IPAdapterEntity + | CanvasLayerState + | CanvasControlAdapterState + | CanvasRegionalGuidanceState + | CanvasInpaintMaskState + | CanvasIPAdapterState | InitialImageEntity; export type CanvasEntityIdentifier = Pick; @@ -827,14 +827,14 @@ export type StagingAreaImage = { export type CanvasV2State = { _version: 3; selectedEntityIdentifier: CanvasEntityIdentifier | null; - inpaintMask: InpaintMaskEntity; + inpaintMask: CanvasInpaintMaskState; layers: { imageCache: ImageWithDims | null; - entities: LayerEntity[]; + entities: CanvasLayerState[]; }; - controlAdapters: { entities: ControlAdapterEntity[] }; - ipAdapters: { entities: IPAdapterEntity[] }; - regions: { entities: RegionEntity[] }; + controlAdapters: { entities: CanvasControlAdapterState[] }; + ipAdapters: { entities: CanvasIPAdapterState[] }; + regions: { entities: CanvasRegionalGuidanceState[] }; loras: LoRA[]; initialImage: InitialImageEntity; tool: { @@ -932,7 +932,7 @@ export type RectShapeAddedArg = { id: string; rect: IRect; color: RgbaColor }; export type ImageObjectAddedArg = { id: string; imageDTO: ImageDTO; position?: Coordinate }; //#region Type guards -export const isLine = (obj: RenderableObject): obj is BrushLine | EraserLine => { +export const isLine = (obj: CanvasObjectState): obj is CanvasBrushLineState | CanvasEraserLineState => { return obj.type === 'brush_line' || obj.type === 'eraser_line'; }; @@ -949,7 +949,7 @@ export type RemoveIndexString = { export type GenerationMode = 'txt2img' | 'img2img' | 'inpaint' | 'outpaint'; -export function isDrawableEntity(entity: CanvasEntity): entity is LayerEntity | RegionEntity | InpaintMaskEntity { +export function isDrawableEntity(entity: CanvasEntity): entity is CanvasLayerState | CanvasRegionalGuidanceState | CanvasInpaintMaskState { return entity.type === 'layer' || entity.type === 'regional_guidance' || entity.type === 'inpaint_mask'; } diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx index 38db77b9974..740a1a2200b 100644 --- a/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx +++ b/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx @@ -1,4 +1,4 @@ -import type { LayerEntity } from 'features/controlLayers/store/types'; +import type { CanvasLayerState } from 'features/controlLayers/store/types'; import { MetadataItemView } from 'features/metadata/components/MetadataItemView'; import type { MetadataHandlers } from 'features/metadata/types'; import { handlers } from 'features/metadata/util/handlers'; @@ -9,7 +9,7 @@ type Props = { }; export const MetadataLayers = ({ metadata }: Props) => { - const [layers, setLayers] = useState([]); + const [layers, setLayers] = useState([]); useEffect(() => { const parse = async () => { @@ -40,8 +40,8 @@ const MetadataViewLayer = ({ handlers, }: { label: string; - layer: LayerEntity; - handlers: MetadataHandlers; + layer: CanvasLayerState; + handlers: MetadataHandlers; }) => { const onRecall = useCallback(() => { if (!handlers.recallItem) { diff --git a/invokeai/frontend/web/src/features/metadata/util/handlers.ts b/invokeai/frontend/web/src/features/metadata/util/handlers.ts index 187ad8f9695..887b1fbd2c9 100644 --- a/invokeai/frontend/web/src/features/metadata/util/handlers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/handlers.ts @@ -2,7 +2,7 @@ import { getStore } from 'app/store/nanostores/store'; import { deepClone } from 'common/util/deepClone'; import { objectKeys } from 'common/util/objectKeys'; import { shouldConcatPromptsChanged } from 'features/controlLayers/store/canvasV2Slice'; -import type { LayerEntity, LoRA } from 'features/controlLayers/store/types'; +import type { CanvasLayerState, LoRA } from 'features/controlLayers/store/types'; import type { AnyControlAdapterConfigMetadata, BuildMetadataHandlers, @@ -48,7 +48,7 @@ const renderControlAdapterValue: MetadataRenderValueFunc = async (layer) => { +const renderLayerValue: MetadataRenderValueFunc = async (layer) => { if (layer.type === 'initial_image_layer') { let rendered = t('controlLayers.globalInitialImageLayer'); if (layer.image) { @@ -88,7 +88,7 @@ const renderLayerValue: MetadataRenderValueFunc = async (layer) => } assert(false, 'Unknown layer type'); }; -const renderLayersValue: MetadataRenderValueFunc = async (layers) => { +const renderLayersValue: MetadataRenderValueFunc = async (layers) => { return `${layers.length} ${t('controlLayers.layers', { count: layers.length })}`; }; diff --git a/invokeai/frontend/web/src/features/metadata/util/parsers.ts b/invokeai/frontend/web/src/features/metadata/util/parsers.ts index 5363133ff01..0d85d90af5b 100644 --- a/invokeai/frontend/web/src/features/metadata/util/parsers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/parsers.ts @@ -1,6 +1,6 @@ import { getCAId, getImageObjectId, getIPAId, getLayerId } from 'features/controlLayers/konva/naming'; import { defaultLoRAConfig } from 'features/controlLayers/store/lorasReducers'; -import type { ControlAdapterEntity, IPAdapterEntity, LayerEntity, LoRA } from 'features/controlLayers/store/types'; +import type { CanvasControlAdapterState, CanvasIPAdapterState, CanvasLayerState, LoRA } from 'features/controlLayers/store/types'; import { CA_PROCESSOR_DATA, imageDTOToImageWithDims, @@ -8,7 +8,7 @@ import { initialIPAdapterV2, initialT2IAdapterV2, isProcessorTypeV2, - zLayerEntity, + zCanvasLayerState, } from 'features/controlLayers/store/types'; import type { ControlNetConfigMetadata, @@ -424,22 +424,22 @@ const parseAllIPAdapters: MetadataParseFunc = async ( }; //#region Control Layers -const parseLayer: MetadataParseFunc = async (metadataItem) => zLayerEntity.parseAsync(metadataItem); +const parseLayer: MetadataParseFunc = async (metadataItem) => zCanvasLayerState.parseAsync(metadataItem); -const parseLayers: MetadataParseFunc = async (metadata) => { +const parseLayers: MetadataParseFunc = async (metadata) => { // We need to support recalling pre-Control Layers metadata into Control Layers. A separate set of parsers handles // taking pre-CL metadata and parsing it into layers. It doesn't always map 1-to-1, so this is best-effort. For // example, CL Control Adapters don't support resize mode, so we simply omit that property. try { - const layers: LayerEntity[] = []; + const layers: CanvasLayerState[] = []; try { const control_layers = await getProperty(metadata, 'control_layers'); const controlLayersRaw = await getProperty(control_layers, 'layers', isArray); const controlLayersParseResults = await Promise.allSettled(controlLayersRaw.map(parseLayer)); const controlLayers = controlLayersParseResults - .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') .map((result) => result.value); layers.push(...controlLayers); } catch { @@ -452,7 +452,7 @@ const parseLayers: MetadataParseFunc = async (metadata) => { controlNetsRaw.map(async (cn) => await parseControlNetToControlAdapterLayer(cn)) ); const controlNetsAsLayers = controlNetsParseResults - .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') .map((result) => result.value); layers.push(...controlNetsAsLayers); } catch { @@ -465,7 +465,7 @@ const parseLayers: MetadataParseFunc = async (metadata) => { t2iAdaptersRaw.map(async (cn) => await parseT2IAdapterToControlAdapterLayer(cn)) ); const t2iAdaptersAsLayers = t2iAdaptersParseResults - .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') .map((result) => result.value); layers.push(...t2iAdaptersAsLayers); } catch { @@ -478,7 +478,7 @@ const parseLayers: MetadataParseFunc = async (metadata) => { ipAdaptersRaw.map(async (cn) => await parseIPAdapterToIPAdapterLayer(cn)) ); const ipAdaptersAsLayers = ipAdaptersParseResults - .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') .map((result) => result.value); layers.push(...ipAdaptersAsLayers); } catch { @@ -498,14 +498,14 @@ const parseLayers: MetadataParseFunc = async (metadata) => { } }; -const parseInitialImageToInitialImageLayer: MetadataParseFunc = async (metadata) => { +const parseInitialImageToInitialImageLayer: MetadataParseFunc = async (metadata) => { // TODO(psyche): recall denoise strength // const denoisingStrength = await getProperty(metadata, 'strength', isParameterStrength); const imageName = await getProperty(metadata, 'init_image', isString); const imageDTO = await getImageDTO(imageName); assert(imageDTO, 'ImageDTO is null'); const id = getLayerId(uuidv4()); - const layer: LayerEntity = { + const layer: CanvasLayerState = { id, type: 'layer', bbox: null, @@ -529,7 +529,7 @@ const parseInitialImageToInitialImageLayer: MetadataParseFunc = asy return layer; }; -const parseControlNetToControlAdapterLayer: MetadataParseFunc = async (metadataItem) => { +const parseControlNetToControlAdapterLayer: MetadataParseFunc = async (metadataItem) => { const control_model = await getProperty(metadataItem, 'control_model'); const key = await getModelKey(control_model, 'controlnet'); const controlNetModel = await fetchModelConfigWithTypeGuard(key, isControlNetModelConfig); @@ -569,7 +569,7 @@ const parseControlNetToControlAdapterLayer: MetadataParseFunc = async (metadataItem) => { +const parseT2IAdapterToControlAdapterLayer: MetadataParseFunc = async (metadataItem) => { const t2i_adapter_model = await getProperty(metadataItem, 't2i_adapter_model'); const key = await getModelKey(t2i_adapter_model, 't2i_adapter'); const t2iAdapterModel = await fetchModelConfigWithTypeGuard(key, isT2IAdapterModelConfig); @@ -630,7 +630,7 @@ const parseT2IAdapterToControlAdapterLayer: MetadataParseFunc = async (metadataItem) => { +const parseIPAdapterToIPAdapterLayer: MetadataParseFunc = async (metadataItem) => { const ip_adapter_model = await getProperty(metadataItem, 'ip_adapter_model'); const key = await getModelKey(ip_adapter_model, 'ip_adapter'); const ipAdapterModel = await fetchModelConfigWithTypeGuard(key, isIPAdapterModelConfig); @@ -685,7 +685,7 @@ const parseIPAdapterToIPAdapterLayer: MetadataParseFunc = async ]; const imageDTO = image ? await getImageDTO(image.image_name) : null; - const layer: IPAdapterEntity = { + const layer: CanvasIPAdapterState = { id: getIPAId(uuidv4()), type: 'ip_adapter', isEnabled: true, diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index 94d19c57a89..fbf5fb172c2 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -40,11 +40,11 @@ import { vaeSelected, } from 'features/controlLayers/store/canvasV2Slice'; import type { - ControlAdapterEntity, - IPAdapterEntity, - LayerEntity, + CanvasControlAdapterState, + CanvasIPAdapterState, + CanvasLayerState, LoRA, - RegionEntity, + CanvasRegionalGuidanceState, } from 'features/controlLayers/store/types'; import { setHrfEnabled, setHrfMethod, setHrfStrength } from 'features/hrf/store/hrfSlice'; import type { @@ -246,7 +246,7 @@ const recallIPAdapters: MetadataRecallFunc = (ipAdapt }); }; -const recallCA: MetadataRecallFunc = async (ca) => { +const recallCA: MetadataRecallFunc = async (ca) => { const { dispatch } = getStore(); const clone = deepClone(ca); if (clone.image) { @@ -275,7 +275,7 @@ const recallCA: MetadataRecallFunc = async (ca) => { return; }; -const recallIPA: MetadataRecallFunc = async (ipa) => { +const recallIPA: MetadataRecallFunc = async (ipa) => { const { dispatch } = getStore(); const clone = deepClone(ipa); if (clone.imageObject) { @@ -298,7 +298,7 @@ const recallIPA: MetadataRecallFunc = async (ipa) => { return; }; -const recallRG: MetadataRecallFunc = async (rg) => { +const recallRG: MetadataRecallFunc = async (rg) => { const { dispatch } = getStore(); const clone = deepClone(rg); // Strip out the uploaded mask image property - this is an intermediate image @@ -328,7 +328,7 @@ const recallRG: MetadataRecallFunc = async (rg) => { }; //#region Control Layers -const recallLayer: MetadataRecallFunc = async (layer) => { +const recallLayer: MetadataRecallFunc = async (layer) => { const { dispatch } = getStore(); const clone = deepClone(layer); const invalidObjects: string[] = []; @@ -348,7 +348,7 @@ const recallLayer: MetadataRecallFunc = async (layer) => { obj.id = getEraserLineId(clone.id, uuidv4()); } else if (obj.type === 'image') { obj.id = getImageObjectId(clone.id, uuidv4()); - } else if (obj.type === 'rect_shape') { + } else if (obj.type === 'rect') { obj.id = getRectShapeId(clone.id, uuidv4()); } else { logger('metadata').error(`Unknown object type ${obj.type}`); @@ -359,7 +359,7 @@ const recallLayer: MetadataRecallFunc = async (layer) => { return; }; -const recallLayers: MetadataRecallFunc = (layers) => { +const recallLayers: MetadataRecallFunc = (layers) => { const { dispatch } = getStore(); dispatch(layerAllDeleted()); for (const l of layers) { diff --git a/invokeai/frontend/web/src/features/metadata/util/validators.ts b/invokeai/frontend/web/src/features/metadata/util/validators.ts index f3eff83b377..5423a7e3595 100644 --- a/invokeai/frontend/web/src/features/metadata/util/validators.ts +++ b/invokeai/frontend/web/src/features/metadata/util/validators.ts @@ -1,5 +1,5 @@ import { getStore } from 'app/store/nanostores/store'; -import type { LayerEntity, LoRA } from 'features/controlLayers/store/types'; +import type { CanvasLayerState, LoRA } from 'features/controlLayers/store/types'; import type { ControlNetConfigMetadata, IPAdapterConfigMetadata, @@ -109,7 +109,7 @@ const validateIPAdapters: MetadataValidateFunc = (ipA return new Promise((resolve) => resolve(validatedIPAdapters)); }; -const validateLayer: MetadataValidateFunc = async (layer) => { +const validateLayer: MetadataValidateFunc = async (layer) => { if (layer.type === 'control_adapter_layer') { const model = layer.controlAdapter.model; assert(model, 'Control Adapter layer missing model'); @@ -131,8 +131,8 @@ const validateLayer: MetadataValidateFunc = async (layer) => { return layer; }; -const validateLayers: MetadataValidateFunc = async (layers) => { - const validatedLayers: LayerEntity[] = []; +const validateLayers: MetadataValidateFunc = async (layers) => { + const validatedLayers: CanvasLayerState[] = []; for (const l of layers) { try { const validated = await validateLayer(l); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts index 7e55beca0a0..dfc7eb0aa10 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts @@ -1,11 +1,11 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { - ControlAdapterEntity, - ControlNetData, + CanvasControlAdapterState, + CanvasControlNetState, ImageWithDims, ProcessorConfig, Rect, - T2IAdapterData, + CanvasT2IAdapterState, } from 'features/controlLayers/store/types'; import type { ImageField } from 'features/nodes/types/common'; import { CONTROL_NET_COLLECT, T2I_ADAPTER_COLLECT } from 'features/nodes/util/graph/constants'; @@ -15,12 +15,12 @@ import { assert } from 'tsafe'; export const addControlAdapters = async ( manager: CanvasManager, - controlAdapters: ControlAdapterEntity[], + controlAdapters: CanvasControlAdapterState[], g: Graph, bbox: Rect, denoise: Invocation<'denoise_latents'>, base: BaseModelType -): Promise => { +): Promise => { const validControlAdapters = controlAdapters.filter((ca) => isValidControlAdapter(ca, base)); for (const ca of validControlAdapters) { if (ca.adapterType === 'controlnet') { @@ -51,7 +51,7 @@ const addControlNetCollectorSafe = (g: Graph, denoise: Invocation<'denoise_laten const addControlNetToGraph = async ( manager: CanvasManager, - ca: ControlNetData, + ca: CanvasControlNetState, g: Graph, bbox: Rect, denoise: Invocation<'denoise_latents'> @@ -96,7 +96,7 @@ const addT2IAdapterCollectorSafe = (g: Graph, denoise: Invocation<'denoise_laten const addT2IAdapterToGraph = async ( manager: CanvasManager, - ca: T2IAdapterData, + ca: CanvasT2IAdapterState, g: Graph, bbox: Rect, denoise: Invocation<'denoise_latents'> @@ -140,7 +140,7 @@ const buildControlImage = ( assert(false, 'Attempted to add unprocessed control image'); }; -const isValidControlAdapter = (ca: ControlAdapterEntity, base: BaseModelType): boolean => { +const isValidControlAdapter = (ca: CanvasControlAdapterState, base: BaseModelType): boolean => { // Must be have a model that matches the current base and must have a control image const hasModel = Boolean(ca.model); const modelMatchesBase = ca.model?.base === base; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts index 2eb501fad92..2bfd97786da 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts @@ -1,15 +1,15 @@ -import type { IPAdapterEntity } from 'features/controlLayers/store/types'; +import type { CanvasIPAdapterState } from 'features/controlLayers/store/types'; import { IP_ADAPTER_COLLECT } from 'features/nodes/util/graph/constants'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import type { BaseModelType, Invocation } from 'services/api/types'; import { assert } from 'tsafe'; export const addIPAdapters = ( - ipAdapters: IPAdapterEntity[], + ipAdapters: CanvasIPAdapterState[], g: Graph, denoise: Invocation<'denoise_latents'>, base: BaseModelType -): IPAdapterEntity[] => { +): CanvasIPAdapterState[] => { const validIPAdapters = ipAdapters.filter((ipa) => isValidIPAdapter(ipa, base)); for (const ipa of validIPAdapters) { addIPAdapter(ipa, g, denoise); @@ -33,7 +33,7 @@ export const addIPAdapterCollectorSafe = (g: Graph, denoise: Invocation<'denoise } }; -const addIPAdapter = (ipa: IPAdapterEntity, g: Graph, denoise: Invocation<'denoise_latents'>) => { +const addIPAdapter = (ipa: CanvasIPAdapterState, g: Graph, denoise: Invocation<'denoise_latents'>) => { const { id, weight, model, clipVisionModel, method, beginEndStepPct, imageObject } = ipa; assert(imageObject, 'IP Adapter image is required'); assert(model, 'IP Adapter model is required'); @@ -55,7 +55,7 @@ const addIPAdapter = (ipa: IPAdapterEntity, g: Graph, denoise: Invocation<'denoi g.addEdge(ipAdapter, 'ip_adapter', ipAdapterCollect, 'item'); }; -export const isValidIPAdapter = (ipa: IPAdapterEntity, base: BaseModelType): boolean => { +export const isValidIPAdapter = (ipa: CanvasIPAdapterState, base: BaseModelType): boolean => { // Must be have a model that matches the current base and must have a control image const hasModel = Boolean(ipa.model); const modelMatchesBase = ipa.model?.base === base; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts index 64cf8593832..157e0e96dc7 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts @@ -1,6 +1,6 @@ -import type { LayerEntity } from 'features/controlLayers/store/types'; +import type { CanvasLayerState } from 'features/controlLayers/store/types'; -export const isValidLayer = (entity: LayerEntity) => { +export const isValidLayer = (entity: CanvasLayerState) => { return ( entity.isEnabled && // Boolean(entity.bbox) && TODO(psyche): Re-enable this check when we have a way to calculate bbox for all layers diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts index 55b283a7f1c..4fd328dc96a 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts @@ -1,6 +1,6 @@ import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { IPAdapterEntity, Rect, RegionEntity } from 'features/controlLayers/store/types'; +import type { CanvasIPAdapterState, Rect, CanvasRegionalGuidanceState } from 'features/controlLayers/store/types'; import { PROMPT_REGION_INVERT_TENSOR_MASK_PREFIX, PROMPT_REGION_MASK_TO_TENSOR_PREFIX, @@ -28,7 +28,7 @@ import { assert } from 'tsafe'; export const addRegions = async ( manager: CanvasManager, - regions: RegionEntity[], + regions: CanvasRegionalGuidanceState[], g: Graph, bbox: Rect, base: BaseModelType, @@ -37,7 +37,7 @@ export const addRegions = async ( negCond: Invocation<'compel'> | Invocation<'sdxl_compel_prompt'>, posCondCollect: Invocation<'collect'>, negCondCollect: Invocation<'collect'> -): Promise => { +): Promise => { const isSDXL = base === 'sdxl'; const validRegions = regions.filter((rg) => isValidRegion(rg, base)); @@ -173,7 +173,7 @@ export const addRegions = async ( } } - const validRGIPAdapters: IPAdapterEntity[] = region.ipAdapters.filter((ipa) => isValidIPAdapter(ipa, base)); + const validRGIPAdapters: CanvasIPAdapterState[] = region.ipAdapters.filter((ipa) => isValidIPAdapter(ipa, base)); for (const ipa of validRGIPAdapters) { const ipAdapterCollect = addIPAdapterCollectorSafe(g, denoise); @@ -205,7 +205,7 @@ export const addRegions = async ( return validRegions; }; -export const isValidRegion = (rg: RegionEntity, base: BaseModelType) => { +export const isValidRegion = (rg: CanvasRegionalGuidanceState, base: BaseModelType) => { const hasTextPrompt = Boolean(rg.positivePrompt || rg.negativePrompt); const hasIPAdapter = rg.ipAdapters.filter((ipa) => isValidIPAdapter(ipa, base)).length > 0; return hasTextPrompt || hasIPAdapter; From b60dcf80369258fef00ac8841b27a846e7e2314e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 2 Aug 2024 18:32:04 +1000 Subject: [PATCH 282/678] fix(ui): unable to hold shit while transforming to retain ratio --- .../src/features/controlLayers/konva/CanvasTransformer.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index 303f60ecd2c..81711a479d2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -150,6 +150,11 @@ export class CanvasTransformer { return { x: scaledTargetX, y: scaledTargetY }; }, boundBoxFunc: (oldBoundBox, newBoundBox) => { + // Bail if we are not rotating, we don't need to do anything. + if (this.konva.transformer.getActiveAnchor() !== 'rotater') { + return newBoundBox; + } + // This transform constraint operates on the bounding box of the transformer. This box has x, y, width, and // height in stage coordinates, and rotation in radians. This can be used to snap the transformer rotation to // the nearest 45 degrees when shift is held. From 2a2dadb952e6f0c94e2bdd09760a6eac0cf499c3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 2 Aug 2024 18:35:31 +1000 Subject: [PATCH 283/678] feat(ui): split out object renderer --- .../controlLayers/konva/CanvasBrushLine.ts | 28 ++- .../konva/CanvasControlAdapter.ts | 6 +- .../controlLayers/konva/CanvasEraserLine.ts | 28 ++- .../controlLayers/konva/CanvasImage.ts | 65 +++--- .../controlLayers/konva/CanvasInitialImage.ts | 6 +- .../controlLayers/konva/CanvasInpaintMask.ts | 20 +- .../controlLayers/konva/CanvasLayer.ts | 210 ++--------------- .../controlLayers/konva/CanvasManager.ts | 26 ++- .../konva/CanvasObjectRenderer.ts | 215 ++++++++++++++++++ .../controlLayers/konva/CanvasRect.ts | 30 +-- .../controlLayers/konva/CanvasRegion.ts | 20 +- .../controlLayers/konva/CanvasStagingArea.ts | 6 +- .../controlLayers/konva/CanvasTransformer.ts | 14 +- .../features/controlLayers/konva/events.ts | 96 ++++---- .../src/features/controlLayers/konva/util.ts | 5 +- .../src/features/controlLayers/store/types.ts | 27 ++- 16 files changed, 444 insertions(+), 358 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts index 85704cd80d1..d842efff11e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts @@ -1,19 +1,19 @@ import type { JSONObject } from 'common/types'; import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; -import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import type { CanvasBrushLineState } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; -export class CanvasBrushLine { +export class CanvasBrushLineRenderer { static TYPE = 'brush_line'; - static GROUP_NAME = `${CanvasBrushLine.TYPE}_group`; - static LINE_NAME = `${CanvasBrushLine.TYPE}_line`; + static GROUP_NAME = `${CanvasBrushLineRenderer.TYPE}_group`; + static LINE_NAME = `${CanvasBrushLineRenderer.TYPE}_line`; id: string; - parent: CanvasLayer; + parent: CanvasObjectRenderer; manager: CanvasManager; log: Logger; getLoggingContext: (extra?: JSONObject) => JSONObject; @@ -23,8 +23,9 @@ export class CanvasBrushLine { group: Konva.Group; line: Konva.Line; }; + isFirstRender: boolean = false; - constructor(state: CanvasBrushLineState, parent: CanvasLayer) { + constructor(state: CanvasBrushLineState, parent: CanvasObjectRenderer) { const { id, strokeWidth, clip, color, points } = state; this.id = id; this.parent = parent; @@ -37,12 +38,12 @@ export class CanvasBrushLine { this.konva = { group: new Konva.Group({ - name: CanvasBrushLine.GROUP_NAME, + name: CanvasBrushLineRenderer.GROUP_NAME, clip, listening: false, }), line: new Konva.Line({ - name: CanvasBrushLine.LINE_NAME, + name: CanvasBrushLineRenderer.LINE_NAME, listening: false, shadowForStrokeEnabled: false, strokeWidth, @@ -59,8 +60,10 @@ export class CanvasBrushLine { this.state = state; } - update(state: CanvasBrushLineState, force?: boolean): boolean { + update(state: CanvasBrushLineState, force = this.isFirstRender): 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({ @@ -72,9 +75,9 @@ export class CanvasBrushLine { }); this.state = state; return true; - } else { - return false; } + + return false; } destroy() { @@ -90,8 +93,9 @@ export class CanvasBrushLine { repr() { return { id: this.id, - type: CanvasBrushLine.TYPE, + type: CanvasBrushLineRenderer.TYPE, parent: this.parent.id, + isFirstRender: this.isFirstRender, state: deepClone(this.state), }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts index 64f65cfb8f8..679dad49df9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts @@ -1,5 +1,5 @@ import { CanvasEntity } from 'features/controlLayers/konva/CanvasEntity'; -import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; +import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; import { type CanvasControlAdapterState, isDrawingTool } from 'features/controlLayers/store/types'; @@ -21,7 +21,7 @@ export class CanvasControlAdapter extends CanvasEntity { objectGroup: Konva.Group; }; - image: CanvasImage | null; + image: CanvasImageRenderer | null; transformer: CanvasTransformer; constructor(state: CanvasControlAdapterState, manager: CanvasManager) { @@ -68,7 +68,7 @@ export class CanvasControlAdapter extends CanvasEntity { didDraw = true; } } else if (!this.image) { - this.image = new CanvasImage(imageObject, this); + this.image = new CanvasImageRenderer(imageObject, this); this.updateGroup(true); this.konva.objectGroup.add(this.image.konva.group); await this.image.updateImageSource(imageObject.image.name); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts index fdff175b741..1f4679807d5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts @@ -1,30 +1,31 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; -import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import type { CanvasEraserLineState, GetLoggingContext } from 'features/controlLayers/store/types'; import { RGBA_RED } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; -export class CanvasEraserLine { +export class CanvasEraserLineRenderer { static TYPE = 'eraser_line'; - static GROUP_NAME = `${CanvasEraserLine.TYPE}_group`; - static LINE_NAME = `${CanvasEraserLine.TYPE}_line`; + static GROUP_NAME = `${CanvasEraserLineRenderer.TYPE}_group`; + static LINE_NAME = `${CanvasEraserLineRenderer.TYPE}_line`; id: string; - parent: CanvasLayer; + parent: CanvasObjectRenderer; manager: CanvasManager; log: Logger; getLoggingContext: GetLoggingContext; + isFirstRender: boolean = false; state: CanvasEraserLineState; konva: { group: Konva.Group; line: Konva.Line; }; - constructor(state: CanvasEraserLineState, parent: CanvasLayer) { + constructor(state: CanvasEraserLineState, parent: CanvasObjectRenderer) { const { id, strokeWidth, clip, points } = state; this.id = id; this.parent = parent; @@ -36,12 +37,12 @@ export class CanvasEraserLine { this.konva = { group: new Konva.Group({ - name: CanvasEraserLine.GROUP_NAME, + name: CanvasEraserLineRenderer.GROUP_NAME, clip, listening: false, }), line: new Konva.Line({ - name: CanvasEraserLine.LINE_NAME, + name: CanvasEraserLineRenderer.LINE_NAME, listening: false, shadowForStrokeEnabled: false, strokeWidth, @@ -58,8 +59,10 @@ export class CanvasEraserLine { this.state = state; } - update(state: CanvasEraserLineState, force?: boolean): boolean { + update(state: CanvasEraserLineState, force = this.isFirstRender): 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({ @@ -70,9 +73,9 @@ export class CanvasEraserLine { }); this.state = state; return true; - } else { - return false; } + + return false; } destroy() { @@ -88,8 +91,9 @@ export class CanvasEraserLine { repr() { return { id: this.id, - type: CanvasEraserLine.TYPE, + 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 500a9fa208d..77ec5a2a682 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -1,25 +1,24 @@ import { deepClone } from 'common/util/deepClone'; -import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { CanvasStagingArea } from 'features/controlLayers/konva/CanvasStagingArea'; +import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { FILTER_MAP } from 'features/controlLayers/konva/filters'; import { loadImage } from 'features/controlLayers/konva/util'; -import type { GetLoggingContext, CanvasImageState } from 'features/controlLayers/store/types'; +import type { CanvasImageState, GetLoggingContext } from 'features/controlLayers/store/types'; import { t } from 'i18next'; import Konva from 'konva'; import type { Logger } from 'roarr'; import { getImageDTO } from 'services/api/endpoints/images'; -export class CanvasImage { +export class CanvasImageRenderer { static TYPE = 'image'; - static GROUP_NAME = `${CanvasImage.TYPE}_group`; - static IMAGE_NAME = `${CanvasImage.TYPE}_image`; - static PLACEHOLDER_GROUP_NAME = `${CanvasImage.TYPE}_placeholder-group`; - static PLACEHOLDER_RECT_NAME = `${CanvasImage.TYPE}_placeholder-rect`; - static PLACEHOLDER_TEXT_NAME = `${CanvasImage.TYPE}_placeholder-text`; + static GROUP_NAME = `${CanvasImageRenderer.TYPE}_group`; + static IMAGE_NAME = `${CanvasImageRenderer.TYPE}_image`; + static PLACEHOLDER_GROUP_NAME = `${CanvasImageRenderer.TYPE}_placeholder-group`; + static PLACEHOLDER_RECT_NAME = `${CanvasImageRenderer.TYPE}_placeholder-rect`; + static PLACEHOLDER_TEXT_NAME = `${CanvasImageRenderer.TYPE}_placeholder-text`; id: string; - parent: CanvasLayer | CanvasStagingArea; + parent: CanvasObjectRenderer; manager: CanvasManager; log: Logger; getLoggingContext: GetLoggingContext; @@ -33,8 +32,9 @@ export class CanvasImage { imageName: string | null; isLoading: boolean; isError: boolean; + isFirstRender: boolean = true; - constructor(state: CanvasImageState, parent: CanvasLayer | CanvasStagingArea) { + constructor(state: CanvasImageState, parent: CanvasObjectRenderer) { const { id, width, height, x, y } = state; this.id = id; this.parent = parent; @@ -45,18 +45,18 @@ export class CanvasImage { this.log.trace({ state }, 'Creating image'); this.konva = { - group: new Konva.Group({ name: CanvasImage.GROUP_NAME, listening: false, x, y }), + group: new Konva.Group({ name: CanvasImageRenderer.GROUP_NAME, listening: false, x, y }), placeholder: { - group: new Konva.Group({ name: CanvasImage.PLACEHOLDER_GROUP_NAME, listening: false }), + group: new Konva.Group({ name: CanvasImageRenderer.PLACEHOLDER_GROUP_NAME, listening: false }), rect: new Konva.Rect({ - name: CanvasImage.PLACEHOLDER_RECT_NAME, + name: CanvasImageRenderer.PLACEHOLDER_RECT_NAME, fill: 'hsl(220 12% 45% / 1)', // 'base.500' width, height, listening: false, }), text: new Konva.Text({ - name: CanvasImage.PLACEHOLDER_TEXT_NAME, + name: CanvasImageRenderer.PLACEHOLDER_TEXT_NAME, fill: 'hsl(220 12% 10% / 1)', // 'base.900' width, height, @@ -81,7 +81,7 @@ export class CanvasImage { this.state = state; } - async updateImageSource(imageName: string) { + updateImageSource = async (imageName: string) => { try { this.log.trace({ imageName }, 'Updating image source'); @@ -106,7 +106,7 @@ export class CanvasImage { }); } else { this.konva.image = new Konva.Image({ - name: CanvasImage.IMAGE_NAME, + name: CanvasImageRenderer.IMAGE_NAME, listening: false, image: imageEl, width: this.state.width, @@ -136,14 +136,16 @@ export class CanvasImage { this.konva.placeholder.text.text(t('common.imageFailedToLoad', 'Image Failed to Load')); this.konva.placeholder.group.visible(true); } - } + }; + + update = async (state: CanvasImageState, force = this.isFirstRender): Promise => { + if (force || this.state !== state) { + this.isFirstRender = false; - async update(state: CanvasImageState, force?: boolean): Promise { - if (this.state !== state || force) { this.log.trace({ state }, 'Updating image'); const { width, height, x, y, image, filters } = state; - if (this.state.image.name !== image.name || force) { + if (force || (this.state.image.name !== image.name && !this.isLoading)) { await this.updateImageSource(image.name); } this.konva.image?.setAttrs({ x, y, width, height }); @@ -158,30 +160,31 @@ export class CanvasImage { this.konva.placeholder.text.setAttrs({ width, height, fontSize: width / 16 }); this.state = state; return true; - } else { - return false; } - } - destroy() { + return false; + }; + + destroy = () => { this.log.trace('Destroying image'); this.konva.group.destroy(); - } + }; - setVisibility(isVisible: boolean): void { + setVisibility = (isVisible: boolean): void => { this.log.trace({ isVisible }, 'Setting image visibility'); this.konva.group.visible(isVisible); - } + }; - repr() { + repr = () => { return { id: this.id, - type: CanvasImage.TYPE, + type: CanvasImageRenderer.TYPE, parent: this.parent.id, 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/CanvasInitialImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts index 37fe66c96b9..52bb4a398a7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts @@ -1,4 +1,4 @@ -import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; +import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { InitialImageEntity } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -20,7 +20,7 @@ export class CanvasInitialImage { objectGroup: Konva.Group; }; - image: CanvasImage | null; + image: CanvasImageRenderer | null; constructor(state: InitialImageEntity, manager: CanvasManager) { this.manager = manager; @@ -45,7 +45,7 @@ export class CanvasInitialImage { } if (!this.image) { - this.image = new CanvasImage(this.state.imageObject); + this.image = new CanvasImageRenderer(this.state.imageObject); this.konva.objectGroup.add(this.image.konva.group); await this.image.update(this.state.imageObject, true); } else if (!this.image.isLoading && !this.image.isError) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts index 965c7fc5df6..681ded2bb9b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts @@ -1,8 +1,8 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; -import { CanvasBrushLine } from 'features/controlLayers/konva/CanvasBrushLine'; -import { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine'; +import { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine'; +import { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; +import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox'; import { mapId } from 'features/controlLayers/konva/util'; import type { CanvasBrushLineState, CanvasEraserLineState, CanvasInpaintMaskState, CanvasRectState } from 'features/controlLayers/store/types'; @@ -31,7 +31,7 @@ export class CanvasInpaintMask { transformer: Konva.Transformer; compositingRect: Konva.Rect; }; - objects: Map; + objects: Map; constructor(state: CanvasInpaintMaskState, manager: CanvasManager) { this.manager = manager; @@ -156,10 +156,10 @@ export class CanvasInpaintMask { private async renderObject(obj: CanvasInpaintMaskState['objects'][number], force = false): Promise { if (obj.type === 'brush_line') { let brushLine = this.objects.get(obj.id); - assert(brushLine instanceof CanvasBrushLine || brushLine === undefined); + assert(brushLine instanceof CanvasBrushLineRenderer || brushLine === undefined); if (!brushLine) { - brushLine = new CanvasBrushLine(obj); + brushLine = new CanvasBrushLineRenderer(obj); this.objects.set(brushLine.id, brushLine); this.konva.objectGroup.add(brushLine.konva.group); return true; @@ -170,10 +170,10 @@ export class CanvasInpaintMask { } } else if (obj.type === 'eraser_line') { let eraserLine = this.objects.get(obj.id); - assert(eraserLine instanceof CanvasEraserLine || eraserLine === undefined); + assert(eraserLine instanceof CanvasEraserLineRenderer || eraserLine === undefined); if (!eraserLine) { - eraserLine = new CanvasEraserLine(obj); + eraserLine = new CanvasEraserLineRenderer(obj); this.objects.set(eraserLine.id, eraserLine); this.konva.objectGroup.add(eraserLine.konva.group); return true; @@ -184,10 +184,10 @@ export class CanvasInpaintMask { } } else if (obj.type === 'rect') { let rect = this.objects.get(obj.id); - assert(rect instanceof CanvasRect || rect === undefined); + assert(rect instanceof CanvasRectRenderer || rect === undefined); if (!rect) { - rect = new CanvasRect(obj); + rect = new CanvasRectRenderer(obj); this.objects.set(rect.id, rect); this.konva.objectGroup.add(rect.konva.group); return true; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index e9279095300..684f7b2d6e8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -1,18 +1,12 @@ import { getStore } from 'app/store/nanostores/store'; import { deepClone } from 'common/util/deepClone'; -import { CanvasBrushLine } from 'features/controlLayers/konva/CanvasBrushLine'; -import { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine'; -import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; import { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; +import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; -import { getPrefixedId, konvaNodeToBlob, mapId, previewBlob } from 'features/controlLayers/konva/util'; +import { konvaNodeToBlob, previewBlob } from 'features/controlLayers/konva/util'; import { layerRasterized } from 'features/controlLayers/store/canvasV2Slice'; import type { - CanvasBrushLineState, - CanvasEraserLineState, CanvasLayerState, - CanvasRectState, CanvasV2State, Coordinate, GetLoggingContext, @@ -23,34 +17,28 @@ import Konva from 'konva'; import { debounce, get } from 'lodash-es'; import type { Logger } from 'roarr'; import { uploadImage } from 'services/api/endpoints/images'; -import { assert } from 'tsafe'; export class CanvasLayer { static TYPE = 'layer'; - static LAYER_NAME = `${CanvasLayer.TYPE}_layer`; - static TRANSFORMER_NAME = `${CanvasLayer.TYPE}_transformer`; - static INTERACTION_RECT_NAME = `${CanvasLayer.TYPE}_interaction-rect`; - static GROUP_NAME = `${CanvasLayer.TYPE}_group`; - static OBJECT_GROUP_NAME = `${CanvasLayer.TYPE}_object-group`; - static BBOX_NAME = `${CanvasLayer.TYPE}_bbox`; + static KONVA_LAYER_NAME = `${CanvasLayer.TYPE}_layer`; + static KONVA_OBJECT_GROUP_NAME = `${CanvasLayer.TYPE}_object-group`; id: string; manager: CanvasManager; log: Logger; getLoggingContext: GetLoggingContext; - drawingBuffer: CanvasBrushLineState | CanvasEraserLineState | CanvasRectState | null; state: CanvasLayerState; konva: { layer: Konva.Layer; objectGroup: Konva.Group; }; - objects: Map; transformer: CanvasTransformer; + renderer: CanvasObjectRenderer; + isFirstRender: boolean = true; bboxNeedsUpdate: boolean; - isFirstRender: boolean; isTransforming: boolean; isPendingBboxCalculation: boolean; @@ -67,26 +55,24 @@ export class CanvasLayer { this.konva = { layer: new Konva.Layer({ id: this.id, - name: CanvasLayer.LAYER_NAME, + name: CanvasLayer.KONVA_LAYER_NAME, listening: false, imageSmoothingEnabled: false, }), - objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }), + objectGroup: new Konva.Group({ name: CanvasLayer.KONVA_OBJECT_GROUP_NAME, listening: false }), }; - this.transformer = new CanvasTransformer(this, this.konva.objectGroup); + this.transformer = new CanvasTransformer(this); + this.renderer = new CanvasObjectRenderer(this); this.konva.layer.add(this.konva.objectGroup); this.konva.layer.add(...this.transformer.getNodes()); - this.objects = new Map(); - this.drawingBuffer = null; this.state = state; this.rect = this.getDefaultRect(); this.bbox = this.getDefaultRect(); this.bboxNeedsUpdate = true; this.isTransforming = false; - this.isFirstRender = true; this.isPendingBboxCalculation = false; } @@ -94,47 +80,10 @@ export class CanvasLayer { this.log.debug('Destroying layer'); // We need to call the destroy method on all children so they can do their own cleanup. this.transformer.destroy(); - for (const obj of this.objects.values()) { - obj.destroy(); - } + this.renderer.destroy(); this.konva.layer.destroy(); }; - getDrawingBuffer = () => { - return this.drawingBuffer; - }; - - setDrawingBuffer = async (obj: CanvasBrushLineState | CanvasEraserLineState | CanvasRectState | null) => { - if (obj) { - this.drawingBuffer = obj; - await this._renderObject(this.drawingBuffer, true); - } else { - this.drawingBuffer = null; - } - }; - - finalizeDrawingBuffer = async () => { - if (!this.drawingBuffer) { - return; - } - const drawingBuffer = this.drawingBuffer; - await this.setDrawingBuffer(null); - - // We need to give the objects a fresh ID else they will be considered the same object when they are re-rendered as - // a non-buffer object, and we won't trigger things like bbox calculation - - if (drawingBuffer.type === 'brush_line') { - drawingBuffer.id = getPrefixedId('brush_line'); - this.manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: drawingBuffer }, 'layer'); - } else if (drawingBuffer.type === 'eraser_line') { - drawingBuffer.id = getPrefixedId('brush_line'); - this.manager.stateApi.onEraserLineAdded({ id: this.id, eraserLine: drawingBuffer }, 'layer'); - } else if (drawingBuffer.type === 'rect') { - drawingBuffer.id = getPrefixedId('brush_line'); - this.manager.stateApi.onRectShapeAdded({ id: this.id, rectShape: drawingBuffer }, 'layer'); - } - }; - update = async (arg?: { state: CanvasLayerState; toolState: CanvasV2State['tool']; isSelected: boolean }) => { const state = get(arg, 'state', this.state); const toolState = get(arg, 'toolState', this.manager.stateApi.getToolState()); @@ -173,8 +122,7 @@ export class CanvasLayer { updateVisibility = (arg?: { isEnabled: boolean }) => { this.log.trace('Updating visibility'); const isEnabled = get(arg, 'isEnabled', this.state.isEnabled); - const hasObjects = this.objects.size > 0 || this.drawingBuffer !== null; - this.konva.layer.visible(isEnabled && hasObjects); + this.konva.layer.visible(isEnabled && this.renderer.hasObjects()); }; updatePosition = (arg?: { position: Coordinate }) => { @@ -196,30 +144,7 @@ export class CanvasLayer { const objects = get(arg, 'objects', this.state.objects); - const objectIds = objects.map(mapId); - - let didUpdate = false; - - // Destroy any objects that are no longer in state - for (const object of this.objects.values()) { - if (!objectIds.includes(object.id) && object.id !== this.drawingBuffer?.id) { - this.objects.delete(object.id); - object.destroy(); - didUpdate = true; - } - } - - for (const obj of objects) { - if (await this._renderObject(obj)) { - didUpdate = true; - } - } - - if (this.drawingBuffer) { - if (await this._renderObject(this.drawingBuffer)) { - didUpdate = true; - } - } + const didUpdate = await this.renderer.render(objects); if (didUpdate) { this.calculateBbox(); @@ -240,7 +165,7 @@ export class CanvasLayer { const toolState = get(arg, 'toolState', this.manager.stateApi.getToolState()); const isSelected = get(arg, 'isSelected', this.manager.stateApi.getIsSelected(this.id)); - if (this.objects.size === 0) { + if (!this.renderer.hasObjects()) { // The layer is totally empty, we can just disable the layer this.konva.layer.listening(false); this.transformer.setMode('off'); @@ -279,7 +204,7 @@ export class CanvasLayer { // eraser lines, fully clipped brush lines or if it has been fully erased. if (this.bbox.width === 0 || this.bbox.height === 0) { // We shouldn't reset on the first render - the bbox will be calculated on the next render - if (!this.isFirstRender && this.objects.size > 0) { + if (!this.isFirstRender && !this.renderer.hasObjects()) { // The layer is fully transparent but has objects - reset it this.manager.stateApi.onEntityReset({ id: this.id }, 'layer'); } @@ -297,67 +222,6 @@ export class CanvasLayer { }); }; - _renderObject = async (obj: CanvasLayerState['objects'][number], force = false): Promise => { - if (obj.type === 'brush_line') { - let brushLine = this.objects.get(obj.id); - assert(brushLine instanceof CanvasBrushLine || brushLine === undefined); - - if (!brushLine) { - brushLine = new CanvasBrushLine(obj, this); - this.objects.set(brushLine.id, brushLine); - this.konva.objectGroup.add(brushLine.konva.group); - return true; - } else { - return await brushLine.update(obj, force); - } - } else if (obj.type === 'eraser_line') { - let eraserLine = this.objects.get(obj.id); - assert(eraserLine instanceof CanvasEraserLine || eraserLine === undefined); - - if (!eraserLine) { - eraserLine = new CanvasEraserLine(obj, this); - this.objects.set(eraserLine.id, eraserLine); - this.konva.objectGroup.add(eraserLine.konva.group); - return true; - } else { - if (await eraserLine.update(obj, force)) { - return true; - } - } - } else if (obj.type === 'rect') { - let rect = this.objects.get(obj.id); - assert(rect instanceof CanvasRect || rect === undefined); - - if (!rect) { - rect = new CanvasRect(obj, this); - this.objects.set(rect.id, rect); - this.konva.objectGroup.add(rect.konva.group); - return true; - } else { - if (await rect.update(obj, force)) { - return true; - } - } - } else if (obj.type === 'image') { - let image = this.objects.get(obj.id); - assert(image instanceof CanvasImage || image === undefined); - - if (!image) { - image = new CanvasImage(obj, this); - this.objects.set(image.id, image); - this.konva.objectGroup.add(image.konva.group); - await image.updateImageSource(obj.image.name); - return true; - } else { - if (await image.update(obj, force)) { - return true; - } - } - } - - return false; - }; - startTransform = () => { this.log.debug('Starting transform'); this.isTransforming = true; @@ -365,9 +229,8 @@ export class CanvasLayer { // When transforming, we want the stage to still be movable if the view tool is selected. If the transformer or // interaction rect are listening, it will interrupt the stage's drag events. So we should disable listening // when the view tool is selected - const listening = this.manager.stateApi.getToolState().selected !== 'view'; - - this.konva.layer.listening(listening); + const shouldListen = this.manager.stateApi.getToolState().selected !== 'view'; + this.konva.layer.listening(shouldListen); this.transformer.setMode('transform'); }; @@ -395,12 +258,8 @@ export class CanvasLayer { const imageDTO = await uploadImage(blob, `${this.id}_rasterized.png`, 'other', true); const { dispatch } = getStore(); const imageObject = imageDTOToImageObject(imageDTO); - await this._renderObject(imageObject, true); - for (const obj of this.objects.values()) { - if (obj.id !== imageObject.id) { - obj.konva.group.visible(false); - } - } + 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) } })); }; @@ -424,7 +283,7 @@ export class CanvasLayer { this.isPendingBboxCalculation = true; - if (this.objects.size === 0) { + if (!this.renderer.hasObjects()) { this.log.trace('No objects, resetting bbox'); this.rect = this.getDefaultRect(); this.bbox = this.getDefaultRect(); @@ -435,30 +294,7 @@ export class CanvasLayer { const rect = this.konva.objectGroup.getClientRect({ skipTransform: true }); - /** - * In some cases, we can use konva's getClientRect as the bbox, but there are some cases where we need to calculate - * the bbox using pixel data: - * - * - Eraser lines are normal lines, except they composite as transparency. Konva's getClientRect includes them when - * calculating the bbox. - * - Clipped portions of lines will be included in the client rect. - * - Images have transparency, so they will be included in the client rect. - * - * TODO(psyche): Using pixel data is slow. Is it possible to be clever and somehow subtract the eraser lines and - * clipped areas from the client rect? - */ - let needsPixelBbox = false; - for (const obj of this.objects.values()) { - const isEraserLine = obj instanceof CanvasEraserLine; - const isImage = obj instanceof CanvasImage; - const hasClip = obj instanceof CanvasBrushLine && obj.state.clip; - if (isEraserLine || hasClip || isImage) { - needsPixelBbox = true; - break; - } - } - - if (!needsPixelBbox) { + if (!this.renderer.needsPixelBbox()) { this.rect = deepClone(rect); this.bbox = deepClone(rect); this.isPendingBboxCalculation = false; @@ -508,10 +344,10 @@ export class CanvasLayer { rect: deepClone(this.rect), bbox: deepClone(this.bbox), bboxNeedsUpdate: this.bboxNeedsUpdate, - isFirstRender: this.isFirstRender, isTransforming: this.isTransforming, isPendingBboxCalculation: this.isPendingBboxCalculation, - objects: Array.from(this.objects.values()).map((obj) => obj.repr()), + transformer: this.transformer.repr(), + renderer: this.renderer.repr(), }; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 177236b88a4..ffb1678376f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -2,12 +2,13 @@ import type { Store } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import type { JSONObject } from 'common/types'; -import type { CanvasBrushLine } from 'features/controlLayers/konva/CanvasBrushLine'; -import type { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine'; -import type { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; +import type { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine'; +import type { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine'; +import type { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; import { CanvasInitialImage } from 'features/controlLayers/konva/CanvasInitialImage'; +import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { CanvasProgressPreview } from 'features/controlLayers/konva/CanvasProgressPreview'; -import type { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; +import type { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; import type { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; import { getCompositeLayerImage, @@ -593,11 +594,12 @@ export class CanvasManager { buildGetLoggingContext = ( instance: - | CanvasBrushLine - | CanvasEraserLine - | CanvasRect - | CanvasImage + | CanvasBrushLineRenderer + | CanvasEraserLineRenderer + | CanvasRectRenderer + | CanvasImageRenderer | CanvasTransformer + | CanvasObjectRenderer | CanvasLayer | CanvasStagingArea ): GetLoggingContext => { @@ -609,6 +611,14 @@ export class CanvasManager { ...extra, }; }; + } else if (instance instanceof CanvasObjectRenderer) { + return (extra?: JSONObject): JSONObject => { + return { + ...instance.parent.getLoggingContext(), + rendererId: instance.id, + ...extra, + }; + }; } else { return (extra?: JSONObject): JSONObject => { return { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts new file mode 100644 index 00000000000..559f21cd90d --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -0,0 +1,215 @@ +import type { JSONObject } from 'common/types'; +import { deepClone } from 'common/util/deepClone'; +import { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine'; +import { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine'; +import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; +import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; +import { getPrefixedId } from 'features/controlLayers/konva/util'; +import type { + CanvasBrushLineState, + CanvasEraserLineState, + CanvasImageState, + CanvasRectState, +} from 'features/controlLayers/store/types'; +import type { Logger } from 'roarr'; +import { assert } from 'tsafe'; + +type AnyObjectRenderer = CanvasBrushLineRenderer | CanvasEraserLineRenderer | CanvasRectRenderer | CanvasImageRenderer; +type AnyObjectState = CanvasBrushLineState | CanvasEraserLineState | CanvasImageState | CanvasRectState; + +export class CanvasObjectRenderer { + static TYPE = 'object_renderer'; + static OBJECT_GROUP_NAME = `${CanvasObjectRenderer.TYPE}_group`; + + id: string; + parent: CanvasLayer; + manager: CanvasManager; + log: Logger; + getLoggingContext: (extra?: JSONObject) => JSONObject; + + isFirstRender: boolean = true; + isRendering: boolean = false; + buffer: AnyObjectState | null = null; + renderers: Map = new Map(); + + constructor(parent: CanvasLayer) { + this.id = getPrefixedId(CanvasObjectRenderer.TYPE); + this.parent = parent; + this.manager = parent.manager; + this.getLoggingContext = this.manager.buildGetLoggingContext(this); + this.log = this.manager.buildLogger(this.getLoggingContext); + this.log.trace('Creating object renderer'); + } + + render = async (objectStates: AnyObjectState[]): Promise => { + this.isRendering = true; + let didRender = false; + const objectIds = objectStates.map((objectState) => objectState.id); + + for (const renderer of this.renderers.values()) { + if (!objectIds.includes(renderer.id) && renderer.id !== this.buffer?.id) { + this.renderers.delete(renderer.id); + renderer.destroy(); + didRender = true; + } + } + + for (const objectState of objectStates) { + didRender = (await this.renderObject(objectState)) || didRender; + } + + if (this.buffer) { + didRender = (await this.renderObject(this.buffer)) || didRender; + } + + this.isRendering = false; + this.isFirstRender = false; + + return didRender; + }; + + renderObject = async (objectState: AnyObjectState, force?: boolean): Promise => { + let didRender = false; + + if (objectState.type === 'brush_line') { + let renderer = this.renderers.get(objectState.id); + assert(renderer instanceof CanvasBrushLineRenderer || renderer === undefined); + + if (!renderer) { + renderer = new CanvasBrushLineRenderer(objectState, this); + this.renderers.set(renderer.id, renderer); + this.parent.konva.objectGroup.add(renderer.konva.group); + } + + didRender = renderer.update(objectState, force); + } else if (objectState.type === 'eraser_line') { + let renderer = this.renderers.get(objectState.id); + assert(renderer instanceof CanvasEraserLineRenderer || renderer === undefined); + + if (!renderer) { + renderer = new CanvasEraserLineRenderer(objectState, this); + this.renderers.set(renderer.id, renderer); + this.parent.konva.objectGroup.add(renderer.konva.group); + } + + didRender = renderer.update(objectState, force); + } else if (objectState.type === 'rect') { + let renderer = this.renderers.get(objectState.id); + assert(renderer instanceof CanvasRectRenderer || renderer === undefined); + + if (!renderer) { + renderer = new CanvasRectRenderer(objectState, this); + this.renderers.set(renderer.id, renderer); + this.parent.konva.objectGroup.add(renderer.konva.group); + } + + didRender = renderer.update(objectState, force); + } else if (objectState.type === 'image') { + let renderer = this.renderers.get(objectState.id); + assert(renderer instanceof CanvasImageRenderer || renderer === undefined); + + if (!renderer) { + renderer = new CanvasImageRenderer(objectState, this); + this.renderers.set(renderer.id, renderer); + this.parent.konva.objectGroup.add(renderer.konva.group); + } + didRender = await renderer.update(objectState, force); + } + + this.isFirstRender = false; + return didRender; + }; + + hasBuffer = (): boolean => { + return this.buffer !== null; + }; + + setBuffer = async (objectState: AnyObjectState): Promise => { + this.buffer = objectState; + return await this.renderObject(this.buffer, true); + }; + + clearBuffer = () => { + this.buffer = null; + }; + + commitBuffer = () => { + if (!this.buffer) { + return; + } + + // We need to give the objects a fresh ID else they will be considered the same object when they are re-rendered as + // a non-buffer object, and we won't trigger things like bbox calculation + this.buffer.id = getPrefixedId(this.buffer.type); + + if (this.buffer.type === 'brush_line') { + this.manager.stateApi.onBrushLineAdded({ id: this.parent.id, brushLine: this.buffer }, 'layer'); + } else if (this.buffer.type === 'eraser_line') { + this.manager.stateApi.onEraserLineAdded({ id: this.parent.id, eraserLine: this.buffer }, 'layer'); + } else if (this.buffer.type === 'rect') { + this.manager.stateApi.onRectShapeAdded({ id: this.parent.id, rectShape: this.buffer }, 'layer'); + } else { + this.log.warn({ buffer: this.buffer }, 'Invalid buffer object type'); + } + + this.buffer = null; + }; + + /** + * Determines if the objects in the renderer require a pixel bbox calculation. + * + * In some cases, we can use Konva's getClientRect as the bbox, but it is not always accurate. It includes + * these visually transparent shapes in its calculation: + * + * - Eraser lines, which are normal lines with a globalCompositeOperation of 'destination-out'. + * - Clipped portions of any shape. + * - Images, which may have transparent areas. + */ + needsPixelBbox = (): boolean => { + let needsPixelBbox = false; + for (const renderer of this.renderers.values()) { + const isEraserLine = renderer instanceof CanvasEraserLineRenderer; + const isImage = renderer instanceof CanvasImageRenderer; + const hasClip = renderer instanceof CanvasBrushLineRenderer && renderer.state.clip; + if (isEraserLine || hasClip || isImage) { + needsPixelBbox = true; + break; + } + } + return needsPixelBbox; + }; + + 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); + } + } + }; + + destroy = () => { + this.log.trace('Destroying object renderer'); + for (const renderer of this.renderers.values()) { + renderer.destroy(); + } + this.renderers.clear(); + }; + + repr = () => { + return { + id: this.id, + type: CanvasObjectRenderer.TYPE, + 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 c714a601448..32a91a6a9a8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts @@ -1,18 +1,18 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; -import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { GetLoggingContext, CanvasRectState } from 'features/controlLayers/store/types'; +import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; +import type { CanvasRectState, GetLoggingContext } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; -export class CanvasRect { +export class CanvasRectRenderer { static TYPE = 'rect'; - static GROUP_NAME = `${CanvasRect.TYPE}_group`; - static RECT_NAME = `${CanvasRect.TYPE}_rect`; + static GROUP_NAME = `${CanvasRectRenderer.TYPE}_group`; + static RECT_NAME = `${CanvasRectRenderer.TYPE}_rect`; id: string; - parent: CanvasLayer; + parent: CanvasObjectRenderer; manager: CanvasManager; log: Logger; getLoggingContext: GetLoggingContext; @@ -22,8 +22,9 @@ export class CanvasRect { group: Konva.Group; rect: Konva.Rect; }; + isFirstRender: boolean = false; - constructor(state: CanvasRectState, parent: CanvasLayer) { + constructor(state: CanvasRectState, parent: CanvasObjectRenderer) { const { id, x, y, width, height, color } = state; this.id = id; this.parent = parent; @@ -33,9 +34,9 @@ export class CanvasRect { this.log.trace({ state }, 'Creating rect'); this.konva = { - group: new Konva.Group({ name: CanvasRect.GROUP_NAME, listening: false }), + group: new Konva.Group({ name: CanvasRectRenderer.GROUP_NAME, listening: false }), rect: new Konva.Rect({ - name: CanvasRect.RECT_NAME, + name: CanvasRectRenderer.RECT_NAME, x, y, width, @@ -48,8 +49,10 @@ export class CanvasRect { this.state = state; } - update(state: CanvasRectState, force?: boolean): boolean { + update(state: CanvasRectState, force = this.isFirstRender): boolean { if (this.state !== state || force) { + this.isFirstRender = false; + this.log.trace({ state }, 'Updating rect'); const { x, y, width, height, color } = state; this.konva.rect.setAttrs({ @@ -61,9 +64,9 @@ export class CanvasRect { }); this.state = state; return true; - } else { - return false; } + + return false; } destroy() { @@ -79,8 +82,9 @@ export class CanvasRect { repr() { return { id: this.id, - type: CanvasRect.TYPE, + type: CanvasRectRenderer.TYPE, parent: this.parent.id, + isFirstRender: this.isFirstRender, state: deepClone(this.state), }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts index 30851a79e40..c48f95d851c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts @@ -1,8 +1,8 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; -import { CanvasBrushLine } from 'features/controlLayers/konva/CanvasBrushLine'; -import { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine'; +import { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine'; +import { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; +import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox'; import { mapId } from 'features/controlLayers/konva/util'; import type { CanvasBrushLineState, CanvasEraserLineState, CanvasRectState, CanvasRegionalGuidanceState } from 'features/controlLayers/store/types'; @@ -32,7 +32,7 @@ export class CanvasRegion { transformer: Konva.Transformer; }; - objects: Map; + objects: Map; constructor(state: CanvasRegionalGuidanceState, manager: CanvasManager) { this.id = state.id; @@ -155,10 +155,10 @@ export class CanvasRegion { private async renderObject(obj: CanvasRegionalGuidanceState['objects'][number], force = false): Promise { if (obj.type === 'brush_line') { let brushLine = this.objects.get(obj.id); - assert(brushLine instanceof CanvasBrushLine || brushLine === undefined); + assert(brushLine instanceof CanvasBrushLineRenderer || brushLine === undefined); if (!brushLine) { - brushLine = new CanvasBrushLine(obj); + brushLine = new CanvasBrushLineRenderer(obj); this.objects.set(brushLine.id, brushLine); this.konva.objectGroup.add(brushLine.konva.group); return true; @@ -169,10 +169,10 @@ export class CanvasRegion { } } else if (obj.type === 'eraser_line') { let eraserLine = this.objects.get(obj.id); - assert(eraserLine instanceof CanvasEraserLine || eraserLine === undefined); + assert(eraserLine instanceof CanvasEraserLineRenderer || eraserLine === undefined); if (!eraserLine) { - eraserLine = new CanvasEraserLine(obj); + eraserLine = new CanvasEraserLineRenderer(obj); this.objects.set(eraserLine.id, eraserLine); this.konva.objectGroup.add(eraserLine.konva.group); return true; @@ -183,10 +183,10 @@ export class CanvasRegion { } } else if (obj.type === 'rect') { let rect = this.objects.get(obj.id); - assert(rect instanceof CanvasRect || rect === undefined); + assert(rect instanceof CanvasRectRenderer || rect === undefined); if (!rect) { - rect = new CanvasRect(obj); + rect = new CanvasRectRenderer(obj); this.objects.set(rect.id, rect); this.konva.objectGroup.add(rect.konva.group); return true; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts index b881b5f7f91..64e763aa6c4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts @@ -1,4 +1,4 @@ -import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; +import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { GetLoggingContext, StagingAreaImage } from 'features/controlLayers/store/types'; @@ -16,7 +16,7 @@ export class CanvasStagingArea { konva: { group: Konva.Group }; - image: CanvasImage | null; + image: CanvasImageRenderer | null; selectedImage: StagingAreaImage | null; constructor(manager: CanvasManager) { @@ -43,7 +43,7 @@ export class CanvasStagingArea { if (!this.image) { const { image_name, width, height } = imageDTO; - this.image = new CanvasImage( + this.image = new CanvasImageRenderer( { id: 'staging-area-image', type: 'image', diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index 81711a479d2..25563bce23d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -46,22 +46,16 @@ export class CanvasTransformer { */ isTransformEnabled: boolean; - /** - * The konva group that the transformer will manipulate. - */ - transformTarget: Konva.Group; - konva: { transformer: Konva.Transformer; proxyRect: Konva.Rect; bboxOutline: Konva.Rect; }; - constructor(parent: CanvasLayer, transformTarget: Konva.Group) { + constructor(parent: CanvasLayer) { this.id = getPrefixedId(CanvasTransformer.TYPE); this.parent = parent; this.manager = parent.manager; - this.transformTarget = transformTarget; this.getLoggingContext = this.manager.buildGetLoggingContext(this); this.log = this.manager.buildLogger(this.getLoggingContext); @@ -192,7 +186,7 @@ export class CanvasTransformer { // This is called when a transform anchor is dragged. By this time, the transform constraints in the above // callbacks have been enforced, and the transformer has updated its nodes' attributes. We need to pass the // updated attributes to the object group, propagating the transformation on down. - this.transformTarget.setAttrs({ + this.parent.konva.objectGroup.setAttrs({ x: this.konva.proxyRect.x(), y: this.konva.proxyRect.y(), scaleX: this.konva.proxyRect.scaleX(), @@ -234,7 +228,7 @@ export class CanvasTransformer { scaleX: snappedScaleX, scaleY: snappedScaleY, }); - this.transformTarget.setAttrs({ + this.parent.konva.objectGroup.setAttrs({ x: snappedX, y: snappedY, scaleX: snappedScaleX, @@ -278,7 +272,7 @@ export class CanvasTransformer { // The object group is translated by the difference between the interaction rect's new and old positions (which is // stored as this.bbox) - this.transformTarget.setAttrs({ + this.parent.konva.objectGroup.setAttrs({ x: this.konva.proxyRect.x(), y: this.konva.proxyRect.y(), }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index e4b2ee7d3f4..9c07676a6bb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -6,11 +6,11 @@ import { offsetCoord, } from 'features/controlLayers/konva/util'; import type { - CanvasV2State, - Coordinate, CanvasInpaintMaskState, CanvasLayerState, CanvasRegionalGuidanceState, + CanvasV2State, + Coordinate, Tool, } from 'features/controlLayers/store/types'; import { isDrawableEntity, isDrawableEntityAdapter } from 'features/controlLayers/store/types'; @@ -189,11 +189,11 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); if (e.evt.shiftKey && lastLinePoint) { // Create a straight line from the last line point - if (selectedEntityAdapter.getDrawingBuffer()) { - await selectedEntityAdapter.finalizeDrawingBuffer(); + if (selectedEntityAdapter.renderer.buffer) { + await selectedEntityAdapter.renderer.commitBuffer(); } - await selectedEntityAdapter.setDrawingBuffer({ + await selectedEntityAdapter.renderer.setBuffer({ id: getObjectId('brush_line', true), type: 'brush_line', points: [ @@ -208,10 +208,10 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { clip: getClip(selectedEntity), }); } else { - if (selectedEntityAdapter.getDrawingBuffer()) { - await selectedEntityAdapter.finalizeDrawingBuffer(); + if (selectedEntityAdapter.renderer.buffer) { + await selectedEntityAdapter.renderer.commitBuffer(); } - await selectedEntityAdapter.setDrawingBuffer({ + await selectedEntityAdapter.renderer.setBuffer({ id: getObjectId('brush_line', true), type: 'brush_line', points: [alignedPoint.x, alignedPoint.y], @@ -228,10 +228,10 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); if (e.evt.shiftKey && lastLinePoint) { // Create a straight line from the last line point - if (selectedEntityAdapter.getDrawingBuffer()) { - await selectedEntityAdapter.finalizeDrawingBuffer(); + if (selectedEntityAdapter.renderer.buffer) { + await selectedEntityAdapter.renderer.commitBuffer(); } - await selectedEntityAdapter.setDrawingBuffer({ + await selectedEntityAdapter.renderer.setBuffer({ id: getObjectId('eraser_line', true), type: 'eraser_line', points: [ @@ -245,10 +245,10 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { clip: getClip(selectedEntity), }); } else { - if (selectedEntityAdapter.getDrawingBuffer()) { - await selectedEntityAdapter.finalizeDrawingBuffer(); + if (selectedEntityAdapter.renderer.buffer) { + await selectedEntityAdapter.renderer.commitBuffer(); } - await selectedEntityAdapter.setDrawingBuffer({ + await selectedEntityAdapter.renderer.setBuffer({ id: getObjectId('eraser_line', true), type: 'eraser_line', points: [alignedPoint.x, alignedPoint.y], @@ -260,10 +260,10 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { } if (toolState.selected === 'rect') { - if (selectedEntityAdapter.getDrawingBuffer()) { - await selectedEntityAdapter.finalizeDrawingBuffer(); + if (selectedEntityAdapter.renderer.buffer) { + await selectedEntityAdapter.renderer.commitBuffer(); } - await selectedEntityAdapter.setDrawingBuffer({ + await selectedEntityAdapter.renderer.setBuffer({ id: getObjectId('rect', true), type: 'rect', x: Math.round(normalizedPoint.x), @@ -295,29 +295,29 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { const toolState = getToolState(); if (toolState.selected === 'brush') { - const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); + const drawingBuffer = selectedEntityAdapter.renderer.buffer; if (drawingBuffer?.type === 'brush_line') { - await selectedEntityAdapter.finalizeDrawingBuffer(); + await selectedEntityAdapter.renderer.commitBuffer(); } else { - await selectedEntityAdapter.setDrawingBuffer(null); + await selectedEntityAdapter.renderer.clearBuffer(); } } if (toolState.selected === 'eraser') { - const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); + const drawingBuffer = selectedEntityAdapter.renderer.buffer; if (drawingBuffer?.type === 'eraser_line') { - await selectedEntityAdapter.finalizeDrawingBuffer(); + await selectedEntityAdapter.renderer.commitBuffer(); } else { - await selectedEntityAdapter.setDrawingBuffer(null); + await selectedEntityAdapter.renderer.clearBuffer(); } } if (toolState.selected === 'rect') { - const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); + const drawingBuffer = selectedEntityAdapter.renderer.buffer; if (drawingBuffer?.type === 'rect') { - await selectedEntityAdapter.finalizeDrawingBuffer(); + await selectedEntityAdapter.renderer.commitBuffer(); } else { - await selectedEntityAdapter.setDrawingBuffer(null); + await selectedEntityAdapter.renderer.clearBuffer(); } } @@ -344,7 +344,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { getIsPrimaryMouseDown(e) ) { if (toolState.selected === 'brush') { - const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); + const drawingBuffer = selectedEntityAdapter.renderer.buffer; if (drawingBuffer) { if (drawingBuffer?.type === 'brush_line') { const nextPoint = getNextPoint(pos, toolState, getLastPointOfLine(drawingBuffer.points)); @@ -352,19 +352,19 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { const normalizedPoint = offsetCoord(nextPoint, selectedEntity.position); const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); - await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); + await selectedEntityAdapter.renderer.setBuffer(drawingBuffer); setLastAddedPoint(alignedPoint); } } else { - await selectedEntityAdapter.setDrawingBuffer(null); + await selectedEntityAdapter.renderer.clearBuffer(); } } else { - if (selectedEntityAdapter.getDrawingBuffer()) { - await selectedEntityAdapter.finalizeDrawingBuffer(); + if (selectedEntityAdapter.renderer.buffer) { + await selectedEntityAdapter.renderer.commitBuffer(); } const normalizedPoint = offsetCoord(pos, selectedEntity.position); const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); - await selectedEntityAdapter.setDrawingBuffer({ + await selectedEntityAdapter.renderer.setBuffer({ id: getObjectId('brush_line', true), type: 'brush_line', points: [alignedPoint.x, alignedPoint.y], @@ -377,7 +377,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { } if (toolState.selected === 'eraser') { - const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); + const drawingBuffer = selectedEntityAdapter.renderer.buffer; if (drawingBuffer) { if (drawingBuffer.type === 'eraser_line') { const nextPoint = getNextPoint(pos, toolState, getLastPointOfLine(drawingBuffer.points)); @@ -385,19 +385,19 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { const normalizedPoint = offsetCoord(nextPoint, selectedEntity.position); const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); - await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); + await selectedEntityAdapter.renderer.setBuffer(drawingBuffer); setLastAddedPoint(alignedPoint); } } else { - await selectedEntityAdapter.setDrawingBuffer(null); + await selectedEntityAdapter.renderer.clearBuffer(); } } else { - if (selectedEntityAdapter.getDrawingBuffer()) { - await selectedEntityAdapter.finalizeDrawingBuffer(); + if (selectedEntityAdapter.renderer.buffer) { + await selectedEntityAdapter.renderer.commitBuffer(); } const normalizedPoint = offsetCoord(pos, selectedEntity.position); const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); - await selectedEntityAdapter.setDrawingBuffer({ + await selectedEntityAdapter.renderer.setBuffer({ id: getObjectId('eraser_line', true), type: 'eraser_line', points: [alignedPoint.x, alignedPoint.y], @@ -409,15 +409,15 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { } if (toolState.selected === 'rect') { - const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); + const drawingBuffer = selectedEntityAdapter.renderer.buffer; if (drawingBuffer) { if (drawingBuffer.type === 'rect') { const normalizedPoint = offsetCoord(pos, selectedEntity.position); drawingBuffer.width = Math.round(normalizedPoint.x - drawingBuffer.x); drawingBuffer.height = Math.round(normalizedPoint.y - drawingBuffer.y); - await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); + await selectedEntityAdapter.renderer.setBuffer(drawingBuffer); } else { - await selectedEntityAdapter.setDrawingBuffer(null); + await selectedEntityAdapter.renderer.clearBuffer(); } } } @@ -443,23 +443,23 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { !getSpaceKey() && getIsPrimaryMouseDown(e) ) { - const drawingBuffer = selectedEntityAdapter.getDrawingBuffer(); + const drawingBuffer = selectedEntityAdapter.renderer.buffer; const normalizedPoint = offsetCoord(pos, selectedEntity.position); if (toolState.selected === 'brush' && drawingBuffer?.type === 'brush_line') { const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); - await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); - await selectedEntityAdapter.finalizeDrawingBuffer(); + await selectedEntityAdapter.renderer.setBuffer(drawingBuffer); + await selectedEntityAdapter.renderer.commitBuffer(); } else if (toolState.selected === 'eraser' && drawingBuffer?.type === 'eraser_line') { const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); - await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); - await selectedEntityAdapter.finalizeDrawingBuffer(); + await selectedEntityAdapter.renderer.setBuffer(drawingBuffer); + await selectedEntityAdapter.renderer.commitBuffer(); } else if (toolState.selected === 'rect' && drawingBuffer?.type === 'rect') { drawingBuffer.width = Math.round(normalizedPoint.x - drawingBuffer.x); drawingBuffer.height = Math.round(normalizedPoint.y - drawingBuffer.y); - await selectedEntityAdapter.setDrawingBuffer(drawingBuffer); - await selectedEntityAdapter.finalizeDrawingBuffer(); + await selectedEntityAdapter.renderer.setBuffer(drawingBuffer); + await selectedEntityAdapter.renderer.commitBuffer(); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index b382e43127f..350c9981352 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -1,7 +1,6 @@ import { getImageDataTransparency } from 'common/util/arrayBuffer'; -import { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { Coordinate, GenerationMode, Rect, CanvasObjectState, RgbaColor } from 'features/controlLayers/store/types'; +import type { CanvasObjectState, Coordinate, GenerationMode, Rect, RgbaColor } from 'features/controlLayers/store/types'; import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers'; import Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; @@ -414,8 +413,6 @@ export function getCompositeLayerStageClone(arg: { manager: CanvasManager }): Ko if (!layer) { console.log('deleting', konvaLayer); toDelete.push(konvaLayer); - } else { - konvaLayer.findOne(`.${CanvasLayer.GROUP_NAME}`)?.findOne(`.${CanvasLayer.BBOX_NAME}`)?.destroy(); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 353527d5980..a77e6255cd7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -572,8 +572,16 @@ const zCanvasImageState = z.object({ }); export type CanvasImageState = z.infer; -const zCanvasObjectState = z.discriminatedUnion('type', [zCanvasImageState, zCanvasBrushLineState, zCanvasEraserLineState, zCanvasRectState]); +const zCanvasObjectState = z.discriminatedUnion('type', [ + zCanvasImageState, + zCanvasBrushLineState, + zCanvasEraserLineState, + zCanvasRectState, +]); export type CanvasObjectState = z.infer; +export function isCanvasBrushLineState(obj: CanvasObjectState): obj is CanvasBrushLineState { + return obj.type === 'brush_line'; +} export const zCanvasLayerState = z.object({ id: zId, @@ -603,7 +611,13 @@ export type IPAdapterConfig = Pick< >; const zMaskObject = z - .discriminatedUnion('type', [zOLD_VectorMaskLine, zOLD_VectorMaskRect, zCanvasBrushLineState, zCanvasEraserLineState, zCanvasRectState]) + .discriminatedUnion('type', [ + zOLD_VectorMaskLine, + zOLD_VectorMaskRect, + zCanvasBrushLineState, + zCanvasEraserLineState, + zCanvasRectState, + ]) .transform((val) => { // Migrate old vector mask objects to new format if (val.type === 'vector_mask_line') { @@ -713,7 +727,10 @@ const zCanvasT2IAdapteState = zCanvasControlAdapterStateBase.extend({ }); export type CanvasT2IAdapterState = z.infer; -export const zCanvasControlAdapterState = z.discriminatedUnion('adapterType', [zCanvasControlNetState, zCanvasT2IAdapteState]); +export const zCanvasControlAdapterState = z.discriminatedUnion('adapterType', [ + zCanvasControlNetState, + zCanvasT2IAdapteState, +]); export type CanvasControlAdapterState = z.infer; export type ControlNetConfig = Pick< CanvasControlNetState, @@ -949,7 +966,9 @@ export type RemoveIndexString = { export type GenerationMode = 'txt2img' | 'img2img' | 'inpaint' | 'outpaint'; -export function isDrawableEntity(entity: CanvasEntity): entity is CanvasLayerState | CanvasRegionalGuidanceState | CanvasInpaintMaskState { +export function isDrawableEntity( + entity: CanvasEntity +): entity is CanvasLayerState | CanvasRegionalGuidanceState | CanvasInpaintMaskState { return entity.type === 'layer' || entity.type === 'regional_guidance' || entity.type === 'inpaint_mask'; } From 7395c94d00ae422abce7ea20a7caf84e6c00af80 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 284/678] 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 d842efff11e..1b53344cca1 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 1f4679807d5..26540ae9740 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 77ec5a2a682..1e574217cc5 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 684f7b2d6e8..14acffcf092 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 559f21cd90d..ab2ae038445 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 32a91a6a9a8..8748379da48 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'); From cab1ba89702fc5ac42e9267c239b672c428b5def Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:39:00 +1000 Subject: [PATCH 285/678] feat(ui): add simple pubsub --- .../web/src/common/util/PubSub/PubSub.test.ts | 117 ++++++++++++++++++ .../web/src/common/util/PubSub/PubSub.ts | 76 ++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 invokeai/frontend/web/src/common/util/PubSub/PubSub.test.ts create mode 100644 invokeai/frontend/web/src/common/util/PubSub/PubSub.ts diff --git a/invokeai/frontend/web/src/common/util/PubSub/PubSub.test.ts b/invokeai/frontend/web/src/common/util/PubSub/PubSub.test.ts new file mode 100644 index 00000000000..108d68ff88f --- /dev/null +++ b/invokeai/frontend/web/src/common/util/PubSub/PubSub.test.ts @@ -0,0 +1,117 @@ +import { PubSub } from 'common/util/PubSub/PubSub'; +import { describe, expect, it, vi } from 'vitest'; + +describe('PubSub', () => { + it('should call listener when value is published and value changes', () => { + const pubsub = new PubSub(1); + const listener = vi.fn(); + + pubsub.subscribe(listener); + pubsub.publish(42); + + expect(listener).toHaveBeenCalledWith(42, 1); + }); + + it('should not call listener if value does not change', () => { + const pubsub = new PubSub(42); + const listener = vi.fn(); + + pubsub.subscribe(listener); + pubsub.publish(42); + + expect(listener).not.toHaveBeenCalled(); + }); + + it('should handle non-primitive values', () => { + const pubsub = new PubSub<{ foo: string }>({ foo: 'bar' }); + const listener = vi.fn(); + + pubsub.subscribe(listener); + pubsub.publish({ foo: 'bar' }); + + expect(listener).toHaveBeenCalled(); + }); + + it('should call listener with old and new value', () => { + const pubsub = new PubSub(1); + const listener = vi.fn(); + + pubsub.subscribe(listener); + pubsub.publish(2); + + expect(listener).toHaveBeenCalledWith(2, 1); + }); + + it('should allow unsubscribing', () => { + const pubsub = new PubSub(1); + const listener1 = vi.fn(); + const listener2 = vi.fn(); + + const unsubscribe = pubsub.subscribe(listener1); + pubsub.subscribe(listener2); + unsubscribe(); + pubsub.publish(42); + + expect(listener1).not.toHaveBeenCalled(); + expect(listener2).toHaveBeenCalled(); + expect(pubsub.getListeners().size).toBe(1); + }); + + it('should clear all listeners', () => { + const pubsub = new PubSub(1); + const listener1 = vi.fn(); + const listener2 = vi.fn(); + + pubsub.subscribe(listener1); + pubsub.subscribe(listener2); + pubsub.off(); + pubsub.publish(42); + + expect(listener1).not.toHaveBeenCalled(); + expect(listener2).not.toHaveBeenCalled(); + expect(pubsub.getListeners().size).toBe(0); + }); + + it('should use custom compareFn', () => { + const compareFn = (a: number, b: number) => Math.abs(a) === Math.abs(b); + const pubsub = new PubSub(1, compareFn); + const listener = vi.fn(); + + pubsub.subscribe(listener); + pubsub.publish(-1); + + expect(listener).not.toHaveBeenCalled(); + }); + + it('should handle multiple listeners', () => { + const pubsub = new PubSub(1); + const listener1 = vi.fn(); + const listener2 = vi.fn(); + + pubsub.subscribe(listener1); + pubsub.subscribe(listener2); + pubsub.publish(42); + + expect(listener1).toHaveBeenCalledWith(42, 1); + expect(listener2).toHaveBeenCalledWith(42, 1); + expect(pubsub.getListeners().size).toBe(2); + }); + + it('should get the current value', () => { + const pubsub = new PubSub(42); + expect(pubsub.getValue()).toBe(42); + pubsub.publish(43); + expect(pubsub.getValue()).toBe(43); + }); + + it('should get the listeners', () => { + const pubsub = new PubSub(1); + const listener1 = vi.fn(); + const listener2 = vi.fn(); + + pubsub.subscribe(listener1); + pubsub.subscribe(listener2); + + expect(pubsub.getListeners()).toEqual(new Set([listener1, listener2])); + }); +}); diff --git a/invokeai/frontend/web/src/common/util/PubSub/PubSub.ts b/invokeai/frontend/web/src/common/util/PubSub/PubSub.ts new file mode 100644 index 00000000000..5ae6779b744 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/PubSub/PubSub.ts @@ -0,0 +1,76 @@ +export type Listener = (newValue: T, oldValue: T) => void; +export type CompareFn = (a: T, b: T) => boolean; + +/** + * A simple PubSub implementation. + * + * @template T The type of the value to be published. + * @param initialValue The initial value to publish. + */ +export class PubSub { + private _listeners: Set> = new Set(); + private _oldValue: T; + private _compareFn: CompareFn; + + public constructor(initialValue: T, compareFn?: CompareFn) { + this._oldValue = initialValue; + this._compareFn = compareFn || ((a, b) => a === b); + } + + /** + * Subscribes to the PubSub. + * @param listener The listener to be called when the value is published. + * @returns A function that can be called to unsubscribe the listener. + */ + public subscribe = (listener: Listener): (() => void) => { + this._listeners.add(listener); + + return () => { + this.unsubscribe(listener); + }; + }; + + /** + * Unsubscribes a listener from the PubSub. + * @param listener The listener to unsubscribe. + */ + public unsubscribe = (listener: Listener): void => { + this._listeners.delete(listener); + }; + + /** + * Publishes a new value to the PubSub. + * @param newValue The new value to publish. + */ + public publish = (newValue: T): void => { + if (!this._compareFn(this._oldValue, newValue)) { + for (const listener of this._listeners) { + listener(newValue, this._oldValue); + } + this._oldValue = newValue; + } + }; + + /** + * Clears all listeners from the PubSub. + */ + public off = (): void => { + this._listeners.clear(); + }; + + /** + * Gets the current value of the PubSub. + * @returns The current value of the PubSub. + */ + public getValue = (): T | undefined => { + return this._oldValue; + }; + + /** + * Gets the listeners of the PubSub. + * @returns The listeners of the PubSub. + */ + public getListeners = (): Set> => { + return this._listeners; + }; +} From 227c3197b42f5cd6be22a983620ba039f23309f3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:42:02 +1000 Subject: [PATCH 286/678] feat(ui): revised event pubsub, transformer logic split out --- .../controlLayers/konva/CanvasBbox.ts | 14 +- .../konva/CanvasControlAdapter.ts | 3 +- .../controlLayers/konva/CanvasInpaintMask.ts | 12 +- .../controlLayers/konva/CanvasLayer.ts | 98 ++------ .../controlLayers/konva/CanvasManager.ts | 150 +++++++++--- .../konva/CanvasObjectRenderer.ts | 3 + .../controlLayers/konva/CanvasRegion.ts | 9 +- .../controlLayers/konva/CanvasStagingArea.ts | 4 +- .../controlLayers/konva/CanvasStateApi.ts | 181 +++++--------- .../controlLayers/konva/CanvasTool.ts | 16 +- .../controlLayers/konva/CanvasTransformer.ts | 208 ++++++++++++---- .../features/controlLayers/konva/events.ts | 231 ++++++++---------- .../src/features/controlLayers/konva/util.ts | 31 +-- 13 files changed, 505 insertions(+), 455 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts index d60896ae9d4..6f7ff99593e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts @@ -90,7 +90,7 @@ export class CanvasBbox { assert(stage, 'Stage must exist'); // We need to snap the anchors to the grid. If the user is holding ctrl/meta, we use the finer 8px grid. - const gridSize = this.manager.stateApi.getCtrlKey() || this.manager.stateApi.getMetaKey() ? 8 : 64; + const gridSize = this.manager.stateApi.$ctrlKey.get() || this.manager.stateApi.$metaKey.get() ? 8 : 64; // Because we are working in absolute coordinates, we need to scale the grid size by the stage scale. const scaledGridSize = gridSize * stage.scaleX(); // To snap the anchor to the grid, we need to calculate an offset from the stage's absolute position. @@ -107,7 +107,7 @@ export class CanvasBbox { }), }; this.konva.rect.on('dragmove', () => { - const gridSize = this.manager.stateApi.getCtrlKey() || this.manager.stateApi.getMetaKey() ? 8 : 64; + const gridSize = this.manager.stateApi.$ctrlKey.get() || this.manager.stateApi.$metaKey.get() ? 8 : 64; const bbox = this.manager.stateApi.getBbox(); const bboxRect: Rect = { ...bbox.rect, @@ -129,10 +129,10 @@ export class CanvasBbox { return; } - const alt = this.manager.stateApi.getAltKey(); - const ctrl = this.manager.stateApi.getCtrlKey(); - const meta = this.manager.stateApi.getMetaKey(); - const shift = this.manager.stateApi.getShiftKey(); + const alt = this.manager.stateApi.$altKey.get(); + const ctrl = this.manager.stateApi.$ctrlKey.get(); + const meta = this.manager.stateApi.$metaKey.get(); + const shift = this.manager.stateApi.$shiftKey.get(); // Grid size depends on the modifier keys let gridSize = ctrl || meta ? 8 : 64; @@ -141,7 +141,7 @@ export class CanvasBbox { // new dimensions so that each size scales in the correct increments and doesn't mis-place the bbox. For example, if // we snapped the width and height to 8px increments, the bbox would be mis-placed by 4px in the x and y axes. // Doubling the grid size ensures the bbox's coords remain aligned to the 8px/64px grid. - if (this.manager.stateApi.getAltKey()) { + if (this.manager.stateApi.$altKey.get()) { gridSize = gridSize * 2; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts index 679dad49df9..a03e1eb9e13 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts @@ -11,8 +11,9 @@ export class CanvasControlAdapter extends CanvasEntity { static TRANSFORMER_NAME = `${CanvasControlAdapter.NAME_PREFIX}_transformer`; static GROUP_NAME = `${CanvasControlAdapter.NAME_PREFIX}_group`; static OBJECT_GROUP_NAME = `${CanvasControlAdapter.NAME_PREFIX}_object-group`; + static TYPE = 'control_adapter' as const; - type = 'control_adapter'; + type = CanvasControlAdapter.TYPE; _state: CanvasControlAdapterState; konva: { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts index 681ded2bb9b..5d354ca1693 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts @@ -5,7 +5,12 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox'; import { mapId } from 'features/controlLayers/konva/util'; -import type { CanvasBrushLineState, CanvasEraserLineState, CanvasInpaintMaskState, CanvasRectState } from 'features/controlLayers/store/types'; +import type { + CanvasBrushLineState, + CanvasEraserLineState, + CanvasInpaintMaskState, + CanvasRectState, +} from 'features/controlLayers/store/types'; import { isDrawingTool, RGBA_RED } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { assert } from 'tsafe'; @@ -17,11 +22,12 @@ export class CanvasInpaintMask { static GROUP_NAME = `${CanvasInpaintMask.NAME_PREFIX}_group`; static OBJECT_GROUP_NAME = `${CanvasInpaintMask.NAME_PREFIX}_object-group`; static COMPOSITING_RECT_NAME = `${CanvasInpaintMask.NAME_PREFIX}_compositing-rect`; - + static TYPE = 'inpaint_mask' as const; private drawingBuffer: CanvasBrushLineState | CanvasEraserLineState | CanvasRectState | null; private state: CanvasInpaintMaskState; - id = 'inpaint_mask'; + id = CanvasInpaintMask.TYPE; + type = CanvasInpaintMask.TYPE; manager: CanvasManager; konva: { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 14acffcf092..c865aca7c3a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -3,7 +3,7 @@ import { deepClone } from 'common/util/deepClone'; import { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; -import { konvaNodeToBlob, previewBlob } from 'features/controlLayers/konva/util'; +import { getEmptyRect, konvaNodeToBlob, previewBlob } from 'features/controlLayers/konva/util'; import { layerRasterized } from 'features/controlLayers/store/canvasV2Slice'; import type { CanvasLayerState, @@ -19,11 +19,12 @@ import type { Logger } from 'roarr'; import { uploadImage } from 'services/api/endpoints/images'; export class CanvasLayer { - static TYPE = 'layer'; + static TYPE = 'layer' as const; static KONVA_LAYER_NAME = `${CanvasLayer.TYPE}_layer`; static KONVA_OBJECT_GROUP_NAME = `${CanvasLayer.TYPE}_object-group`; id: string; + type = CanvasLayer.TYPE; manager: CanvasManager; log: Logger; getLoggingContext: GetLoggingContext; @@ -38,12 +39,11 @@ export class CanvasLayer { renderer: CanvasObjectRenderer; isFirstRender: boolean = true; - bboxNeedsUpdate: boolean; - isTransforming: boolean; - isPendingBboxCalculation: boolean; + bboxNeedsUpdate: boolean = true; + isPendingBboxCalculation: boolean = false; - rect: Rect; - bbox: Rect; + rect: Rect = getEmptyRect(); + bbox: Rect = getEmptyRect(); constructor(state: CanvasLayerState, manager: CanvasManager) { this.id = state.id; @@ -69,11 +69,6 @@ export class CanvasLayer { this.konva.layer.add(...this.transformer.getNodes()); this.state = state; - this.rect = this.getDefaultRect(); - this.bbox = this.getDefaultRect(); - this.bboxNeedsUpdate = true; - this.isTransforming = false; - this.isPendingBboxCalculation = false; } destroy = (): void => { @@ -86,8 +81,6 @@ export class CanvasLayer { update = async (arg?: { state: CanvasLayerState; toolState: CanvasV2State['tool']; isSelected: boolean }) => { const state = get(arg, 'state', this.state); - const toolState = get(arg, 'toolState', this.manager.stateApi.getToolState()); - const isSelected = get(arg, 'isSelected', this.manager.stateApi.getIsSelected(this.id)); if (!this.isFirstRender && state === this.state) { this.log.trace('State unchanged, skipping update'); @@ -109,7 +102,7 @@ export class CanvasLayer { if (this.isFirstRender || isEnabled !== this.state.isEnabled) { await this.updateVisibility({ isEnabled }); } - await this.updateInteraction({ toolState, isSelected }); + // this.transformer.syncInteractionState(); if (this.isFirstRender) { await this.updateBbox(); @@ -159,40 +152,6 @@ export class CanvasLayer { this.konva.objectGroup.opacity(opacity); }; - updateInteraction = (arg?: { toolState: CanvasV2State['tool']; isSelected: boolean }) => { - this.log.trace('Updating interaction'); - - const toolState = get(arg, 'toolState', this.manager.stateApi.getToolState()); - const isSelected = get(arg, 'isSelected', this.manager.stateApi.getIsSelected(this.id)); - - if (!this.renderer.hasObjects()) { - // The layer is totally empty, we can just disable the layer - this.konva.layer.listening(false); - this.transformer.setMode('off'); - return; - } - - if (isSelected && !this.isTransforming && toolState.selected === 'move') { - // We are moving this layer, it must be listening - this.konva.layer.listening(true); - this.transformer.setMode('drag'); - } else if (isSelected && this.isTransforming) { - // When transforming, we want the stage to still be movable if the view tool is selected. If the transformer is - // active, it will interrupt the stage drag events. So we should disable listening when the view tool is selected. - if (toolState.selected !== 'view') { - this.konva.layer.listening(true); - this.transformer.setMode('transform'); - } else { - this.konva.layer.listening(false); - this.transformer.setMode('off'); - } - } else { - // The layer is not selected, or we are using a tool that doesn't need the layer to be listening - disable interaction stuff - this.konva.layer.listening(false); - this.transformer.setMode('off'); - } - }; - updateBbox = () => { this.log.trace('Updating bbox'); @@ -208,11 +167,11 @@ export class CanvasLayer { // The layer is fully transparent but has objects - reset it this.manager.stateApi.onEntityReset({ id: this.id }, 'layer'); } - this.transformer.setMode('off'); + this.transformer.syncInteractionState(); return; } - this.transformer.setMode('drag'); + this.transformer.syncInteractionState(); this.transformer.update(this.state.position, this.bbox); this.konva.objectGroup.setAttrs({ x: this.state.position.x + this.bbox.x, @@ -222,18 +181,6 @@ export class CanvasLayer { }); }; - startTransform = () => { - this.log.debug('Starting transform'); - this.isTransforming = true; - - // When transforming, we want the stage to still be movable if the view tool is selected. If the transformer or - // interaction rect are listening, it will interrupt the stage's drag events. So we should disable listening - // when the view tool is selected - const shouldListen = this.manager.stateApi.getToolState().selected !== 'view'; - this.konva.layer.listening(shouldListen); - this.transformer.setMode('transform'); - }; - resetScale = () => { const attrs = { scaleX: 1, @@ -245,7 +192,7 @@ export class CanvasLayer { this.transformer.konva.proxyRect.setAttrs(attrs); }; - rasterizeLayer = async () => { + rasterize = async () => { this.log.debug('Rasterizing layer'); const objectGroupClone = this.konva.objectGroup.clone(); @@ -263,20 +210,6 @@ export class CanvasLayer { dispatch(layerRasterized({ id: this.id, imageObject, position: { x: Math.round(rect.x), y: Math.round(rect.y) } })); }; - stopTransform = () => { - this.log.debug('Stopping transform'); - - this.isTransforming = false; - this.resetScale(); - this.updatePosition(); - this.updateBbox(); - this.updateInteraction(); - }; - - getDefaultRect = (): Rect => { - return { x: 0, y: 0, width: 0, height: 0 }; - }; - calculateBbox = debounce(() => { this.log.debug('Calculating bbox'); @@ -284,8 +217,8 @@ export class CanvasLayer { if (!this.renderer.hasObjects()) { this.log.trace('No objects, resetting bbox'); - this.rect = this.getDefaultRect(); - this.bbox = this.getDefaultRect(); + this.rect = getEmptyRect(); + this.bbox = getEmptyRect(); this.isPendingBboxCalculation = false; this.updateBbox(); return; @@ -324,8 +257,8 @@ export class CanvasLayer { height: maxY - minY, }; } else { - this.bbox = this.getDefaultRect(); - this.rect = this.getDefaultRect(); + this.bbox = getEmptyRect(); + this.rect = getEmptyRect(); } this.isPendingBboxCalculation = false; this.log.trace({ bbox: this.bbox, rect: this.rect, extents }, `Got bbox from worker`); @@ -343,7 +276,6 @@ export class CanvasLayer { rect: deepClone(this.rect), bbox: deepClone(this.bbox), bboxNeedsUpdate: this.bboxNeedsUpdate, - isTransforming: this.isTransforming, isPendingBboxCalculation: this.isPendingBboxCalculation, transformer: this.transformer.repr(), renderer: this.renderer.repr(), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index ffb1678376f..0f932534544 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -2,6 +2,7 @@ import type { Store } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import type { JSONObject } from 'common/types'; +import { PubSub } from 'common/util/PubSub/PubSub'; import type { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine'; import type { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine'; import type { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; @@ -22,7 +23,19 @@ import { } from 'features/controlLayers/konva/util'; import type { Extents, ExtentsResult, GetBboxTask, WorkerLogMessage } from 'features/controlLayers/konva/worker'; import { $lastProgressEvent, $shouldShowStagedImage } from 'features/controlLayers/store/canvasV2Slice'; -import type { CanvasV2State, Coordinate, GenerationMode, GetLoggingContext } from 'features/controlLayers/store/types'; +import type { + CanvasControlAdapterState, + CanvasEntity, + CanvasEntityIdentifier, + CanvasInpaintMaskState, + CanvasLayerState, + CanvasRegionalGuidanceState, + CanvasV2State, + Coordinate, + GenerationMode, + GetLoggingContext, + RgbaColor, +} from 'features/controlLayers/store/types'; import type Konva from 'konva'; import { atom } from 'nanostores'; import type { Logger } from 'roarr'; @@ -70,6 +83,24 @@ type Util = { ) => Promise; }; +type EntityStateAndAdapter = + | { + state: CanvasLayerState; + adapter: CanvasLayer; + } + | { + state: CanvasInpaintMaskState; + adapter: CanvasInpaintMask; + } + | { + state: CanvasControlAdapterState; + adapter: CanvasControlAdapter; + } + | { + state: CanvasRegionalGuidanceState; + adapter: CanvasRegion; + }; + export const $canvasManager = atom(null); export class CanvasManager { @@ -101,6 +132,11 @@ export class CanvasManager { _worker: Worker; _tasks: Map void }>; + toolState: PubSub; + currentFill: PubSub; + selectedEntity: PubSub; + selectedEntityIdentifier: PubSub; + constructor( stage: Konva.Stage, container: HTMLDivElement, @@ -111,7 +147,7 @@ export class CanvasManager { this.stage = stage; this.container = container; this._store = store; - this.stateApi = new CanvasStateApi(this._store); + this.stateApi = new CanvasStateApi(this._store, this); this._prevState = this.stateApi.getState(); this._isFirstRender = true; @@ -178,6 +214,17 @@ export class CanvasManager { }; this.onTransform = null; this._isDebugging = false; + + this.toolState = new PubSub(this.stateApi.getToolState()); + this.currentFill = new PubSub(this.getCurrentFill()); + this.selectedEntityIdentifier = new PubSub( + this.stateApi.getState().selectedEntityIdentifier, + (a, b) => a?.id === b?.id + ); + this.selectedEntity = new PubSub( + this.getSelectedEntity(), + (a, b) => a?.state === b?.state && a?.adapter === b?.adapter + ); } enableDebugging() { @@ -226,7 +273,7 @@ export class CanvasManager { } async renderProgressPreview() { - await this.preview.progressPreview.render(this.stateApi.getLastProgressEvent()); + await this.preview.progressPreview.render(this.stateApi.$lastProgressEvent.get()); } async renderInpaintMask() { @@ -279,7 +326,7 @@ export class CanvasManager { fitStageToContainer() { this.stage.width(this.container.offsetWidth); this.stage.height(this.container.offsetHeight); - this.stateApi.setStageAttrs({ + this.stateApi.$stageAttrs.set({ position: { x: this.stage.x(), y: this.stage.y() }, dimensions: { width: this.stage.width(), height: this.stage.height() }, scale: this.stage.scaleX(), @@ -287,8 +334,57 @@ export class CanvasManager { this.background.render(); } + getEntity(identifier: CanvasEntityIdentifier): EntityStateAndAdapter | null { + const state = this.stateApi.getState(); + + let entityState: CanvasEntity | null = null; + let entityAdapter: CanvasLayer | CanvasRegion | CanvasControlAdapter | CanvasInpaintMask | null = null; + + if (identifier.type === 'layer') { + entityState = state.layers.entities.find((i) => i.id === identifier.id) ?? null; + entityAdapter = this.layers.get(identifier.id) ?? null; + } else if (identifier.type === 'control_adapter') { + entityState = state.controlAdapters.entities.find((i) => i.id === identifier.id) ?? null; + entityAdapter = this.controlAdapters.get(identifier.id) ?? null; + } else if (identifier.type === 'regional_guidance') { + entityState = state.regions.entities.find((i) => i.id === identifier.id) ?? null; + entityAdapter = this.regions.get(identifier.id) ?? null; + } else if (identifier.type === 'inpaint_mask') { + entityState = state.inpaintMask; + entityAdapter = this.inpaintMask; + } + + if (entityState && entityAdapter && entityState.type === entityAdapter.type) { + return { state: entityState, adapter: entityAdapter } as EntityStateAndAdapter; + } + + return null; + } + + getSelectedEntity = () => { + const state = this.stateApi.getState(); + if (state.selectedEntityIdentifier) { + return this.getEntity(state.selectedEntityIdentifier); + } + return null; + }; + + getCurrentFill = () => { + const state = this.stateApi.getState(); + let currentFill: RgbaColor = state.tool.fill; + const selectedEntity = this.getSelectedEntity(); + if (selectedEntity) { + if (selectedEntity.state.type === 'regional_guidance') { + currentFill = { ...selectedEntity.state.fill, a: state.settings.maskOpacity }; + } else if (selectedEntity.state.type === 'inpaint_mask') { + currentFill = { ...state.inpaintMask.fill, a: state.settings.maskOpacity }; + } + } + return currentFill; + }; + getTransformingLayer() { - return Array.from(this.layers.values()).find((layer) => layer.isTransforming); + return Array.from(this.layers.values()).find((layer) => layer.transformer.isTransforming); } getIsTransforming() { @@ -299,17 +395,17 @@ export class CanvasManager { if (this.getIsTransforming()) { return; } - const layer = this.getSelectedEntityAdapter(); - assert(layer instanceof CanvasLayer, 'No selected layer'); - layer.startTransform(); + const layer = this.getSelectedEntity(); + // TODO(psyche): Support other entity types + assert(layer?.adapter instanceof CanvasLayer, 'No selected layer'); + layer.adapter.transformer.startTransform(); this.onTransform?.(true); } async applyTransform() { const layer = this.getTransformingLayer(); if (layer) { - await layer.rasterizeLayer(); - layer.stopTransform(); + await layer.transformer.applyTransform(); } this.onTransform?.(false); } @@ -317,7 +413,7 @@ export class CanvasManager { cancelTransform() { const layer = this.getTransformingLayer(); if (layer) { - layer.stopTransform(); + layer.transformer.stopTransform(); } this.onTransform?.(false); } @@ -355,16 +451,10 @@ export class CanvasManager { } } - if ( - this._isFirstRender || - state.tool.selected !== this._prevState.tool.selected || - state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id - ) { - this.log.debug('Updating interaction'); - for (const layer of this.layers.values()) { - layer.updateInteraction({ toolState: state.tool, isSelected: state.selectedEntityIdentifier?.id === layer.id }); - } - } + this.toolState.publish(state.tool); + this.selectedEntityIdentifier.publish(state.selectedEntityIdentifier); + this.selectedEntity.publish(this.getSelectedEntity()); + this.currentFill.publish(this.getCurrentFill()); if ( this._isFirstRender || @@ -521,24 +611,6 @@ export class CanvasManager { return CanvasManager.BBOX_PADDING_PX; } - getSelectedEntityAdapter = (): CanvasLayer | CanvasRegion | CanvasControlAdapter | CanvasInpaintMask | null => { - const state = this.stateApi.getState(); - const identifier = state.selectedEntityIdentifier; - if (!identifier) { - return null; - } else if (identifier.type === 'layer') { - return this.layers.get(identifier.id) ?? null; - } else if (identifier.type === 'control_adapter') { - return this.controlAdapters.get(identifier.id) ?? null; - } else if (identifier.type === 'regional_guidance') { - return this.regions.get(identifier.id) ?? null; - } else if (identifier.type === 'inpaint_mask') { - return this.inpaintMask; - } else { - return null; - } - }; - getGenerationMode(): GenerationMode { const session = this.stateApi.getSession(); if (session.isActive) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index ab2ae038445..303864e0253 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -25,6 +25,9 @@ type AnyObjectRenderer = CanvasBrushLineRenderer | CanvasEraserLineRenderer | Ca */ type AnyObjectState = CanvasBrushLineState | CanvasEraserLineState | CanvasImageState | CanvasRectState; +/** + * Handles rendering of objects for a canvas entity. + */ export class CanvasObjectRenderer { static TYPE = 'object_renderer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts index c48f95d851c..833314ae022 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts @@ -5,7 +5,12 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox'; import { mapId } from 'features/controlLayers/konva/util'; -import type { CanvasBrushLineState, CanvasEraserLineState, CanvasRectState, CanvasRegionalGuidanceState } from 'features/controlLayers/store/types'; +import type { + CanvasBrushLineState, + CanvasEraserLineState, + CanvasRectState, + CanvasRegionalGuidanceState, +} from 'features/controlLayers/store/types'; import { isDrawingTool, RGBA_RED } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { assert } from 'tsafe'; @@ -17,11 +22,13 @@ export class CanvasRegion { static GROUP_NAME = `${CanvasRegion.NAME_PREFIX}_group`; static OBJECT_GROUP_NAME = `${CanvasRegion.NAME_PREFIX}_object-group`; static COMPOSITING_RECT_NAME = `${CanvasRegion.NAME_PREFIX}_compositing-rect`; + static TYPE = 'regional_guidance' as const; private drawingBuffer: CanvasBrushLineState | CanvasEraserLineState | CanvasRectState | null; private state: CanvasRegionalGuidanceState; id: string; + type = CanvasRegion.TYPE; manager: CanvasManager; konva: { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts index 64e763aa6c4..47b679ed870 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts @@ -34,7 +34,7 @@ export class CanvasStagingArea { render = async () => { const session = this.manager.stateApi.getSession(); const bboxRect = this.manager.stateApi.getBbox().rect; - const shouldShowStagedImage = this.manager.stateApi.getShouldShowStagedImage(); + const shouldShowStagedImage = this.manager.stateApi.$shouldShowStagedImage.get(); this.selectedImage = session.stagedImages[session.selectedStagedImageIndex] ?? null; @@ -69,7 +69,7 @@ export class CanvasStagingArea { this.image.konva.group.x(bboxRect.x + offsetX); this.image.konva.group.y(bboxRect.y + offsetY); await this.image.updateImageSource(imageDTO.image_name); - this.manager.stateApi.resetLastProgressEvent(); + this.manager.stateApi.$lastProgressEvent.set(null); } this.image.konva.group.visible(shouldShowStagedImage); } else { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index 4983b4c9af0..6b4be9ac6dd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -2,7 +2,7 @@ import { $alt, $ctrl, $meta, $shift } from '@invoke-ai/ui-library'; import type { Store } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; -import { buildSubscribe } from 'features/controlLayers/konva/util'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { $isDrawing, $isMouseDown, @@ -49,173 +49,143 @@ import type { CanvasBrushLineState, CanvasEntity, CanvasEraserLineState, - PositionChangedArg, CanvasRectState, + PositionChangedArg, ScaleChangedArg, Tool, } from 'features/controlLayers/store/types'; import type { IRect } from 'konva/lib/types'; -import type { RgbaColor } from 'react-colorful'; import type { ImageDTO } from 'services/api/types'; const log = logger('canvas'); + export class CanvasStateApi { - private store: Store; + _store: Store; + manager: CanvasManager; + + + constructor(store: Store, manager: CanvasManager) { + this._store = store; + this.manager = manager; - constructor(store: Store) { - this.store = store; } // Reminder - use arrow functions to avoid binding issues getState = () => { - return this.store.getState().canvasV2; + return this._store.getState().canvasV2; }; onEntityReset = (arg: { id: string }, entityType: CanvasEntity['type']) => { log.debug('onEntityReset'); if (entityType === 'layer') { - this.store.dispatch(layerReset(arg)); + this._store.dispatch(layerReset(arg)); } }; onPosChanged = (arg: PositionChangedArg, entityType: CanvasEntity['type']) => { log.debug('onPosChanged'); if (entityType === 'layer') { - this.store.dispatch(layerTranslated(arg)); + this._store.dispatch(layerTranslated(arg)); } else if (entityType === 'regional_guidance') { - this.store.dispatch(rgTranslated(arg)); + this._store.dispatch(rgTranslated(arg)); } else if (entityType === 'inpaint_mask') { - this.store.dispatch(imTranslated(arg)); + this._store.dispatch(imTranslated(arg)); } else if (entityType === 'control_adapter') { - this.store.dispatch(caTranslated(arg)); + this._store.dispatch(caTranslated(arg)); } }; onScaleChanged = (arg: ScaleChangedArg, entityType: CanvasEntity['type']) => { log.debug('onScaleChanged'); if (entityType === 'inpaint_mask') { - this.store.dispatch(imScaled(arg)); + this._store.dispatch(imScaled(arg)); } else if (entityType === 'regional_guidance') { - this.store.dispatch(rgScaled(arg)); + this._store.dispatch(rgScaled(arg)); } else if (entityType === 'control_adapter') { - this.store.dispatch(caScaled(arg)); + this._store.dispatch(caScaled(arg)); } }; onBboxChanged = (arg: BboxChangedArg, entityType: CanvasEntity['type']) => { log.debug('Entity bbox changed'); if (entityType === 'layer') { - this.store.dispatch(layerBboxChanged(arg)); + this._store.dispatch(layerBboxChanged(arg)); } else if (entityType === 'control_adapter') { - this.store.dispatch(caBboxChanged(arg)); + this._store.dispatch(caBboxChanged(arg)); } else if (entityType === 'regional_guidance') { - this.store.dispatch(rgBboxChanged(arg)); + this._store.dispatch(rgBboxChanged(arg)); } else if (entityType === 'inpaint_mask') { - this.store.dispatch(imBboxChanged(arg)); + this._store.dispatch(imBboxChanged(arg)); } }; onBrushLineAdded = (arg: { id: string; brushLine: CanvasBrushLineState }, entityType: CanvasEntity['type']) => { log.debug('Brush line added'); if (entityType === 'layer') { - this.store.dispatch(layerBrushLineAdded(arg)); + this._store.dispatch(layerBrushLineAdded(arg)); } else if (entityType === 'regional_guidance') { - this.store.dispatch(rgBrushLineAdded(arg)); + this._store.dispatch(rgBrushLineAdded(arg)); } else if (entityType === 'inpaint_mask') { - this.store.dispatch(imBrushLineAdded(arg)); + this._store.dispatch(imBrushLineAdded(arg)); } }; onEraserLineAdded = (arg: { id: string; eraserLine: CanvasEraserLineState }, entityType: CanvasEntity['type']) => { log.debug('Eraser line added'); if (entityType === 'layer') { - this.store.dispatch(layerEraserLineAdded(arg)); + this._store.dispatch(layerEraserLineAdded(arg)); } else if (entityType === 'regional_guidance') { - this.store.dispatch(rgEraserLineAdded(arg)); + this._store.dispatch(rgEraserLineAdded(arg)); } else if (entityType === 'inpaint_mask') { - this.store.dispatch(imEraserLineAdded(arg)); + this._store.dispatch(imEraserLineAdded(arg)); } }; onRectShapeAdded = (arg: { id: string; rectShape: CanvasRectState }, entityType: CanvasEntity['type']) => { log.debug('Rect shape added'); if (entityType === 'layer') { - this.store.dispatch(layerRectShapeAdded(arg)); + this._store.dispatch(layerRectShapeAdded(arg)); } else if (entityType === 'regional_guidance') { - this.store.dispatch(rgRectShapeAdded(arg)); + this._store.dispatch(rgRectShapeAdded(arg)); } else if (entityType === 'inpaint_mask') { - this.store.dispatch(imRectShapeAdded(arg)); + this._store.dispatch(imRectShapeAdded(arg)); } }; onEntitySelected = (arg: { id: string; type: CanvasEntity['type'] }) => { log.debug('Entity selected'); - this.store.dispatch(entitySelected(arg)); + this._store.dispatch(entitySelected(arg)); }; onBboxTransformed = (bbox: IRect) => { log.debug('Generation bbox transformed'); - this.store.dispatch(bboxChanged(bbox)); + this._store.dispatch(bboxChanged(bbox)); }; onBrushWidthChanged = (width: number) => { log.debug('Brush width changed'); - this.store.dispatch(brushWidthChanged(width)); + this._store.dispatch(brushWidthChanged(width)); }; onEraserWidthChanged = (width: number) => { log.debug('Eraser width changed'); - this.store.dispatch(eraserWidthChanged(width)); + this._store.dispatch(eraserWidthChanged(width)); }; onRegionMaskImageCached = (id: string, imageDTO: ImageDTO) => { log.debug('Region mask image cached'); - this.store.dispatch(rgImageCacheChanged({ id, imageDTO })); + this._store.dispatch(rgImageCacheChanged({ id, imageDTO })); }; onInpaintMaskImageCached = (imageDTO: ImageDTO) => { log.debug('Inpaint mask image cached'); - this.store.dispatch(imImageCacheChanged({ imageDTO })); + this._store.dispatch(imImageCacheChanged({ imageDTO })); }; onLayerImageCached = (imageDTO: ImageDTO) => { log.debug('Layer image cached'); - this.store.dispatch(layerImageCacheChanged({ imageDTO })); + this._store.dispatch(layerImageCacheChanged({ imageDTO })); }; setTool = (tool: Tool) => { log.debug('Tool selection changed'); - this.store.dispatch(toolChanged(tool)); + this._store.dispatch(toolChanged(tool)); }; setToolBuffer = (toolBuffer: Tool | null) => { log.debug('Tool buffer changed'); - this.store.dispatch(toolBufferChanged(toolBuffer)); + this._store.dispatch(toolBufferChanged(toolBuffer)); }; - getSelectedEntity = (): CanvasEntity | null => { - const state = this.getState(); - const identifier = state.selectedEntityIdentifier; - if (!identifier) { - return null; - } else if (identifier.type === 'layer') { - return state.layers.entities.find((i) => i.id === identifier.id) ?? null; - } else if (identifier.type === 'control_adapter') { - return state.controlAdapters.entities.find((i) => i.id === identifier.id) ?? null; - } else if (identifier.type === 'ip_adapter') { - return state.ipAdapters.entities.find((i) => i.id === identifier.id) ?? null; - } else if (identifier.type === 'regional_guidance') { - return state.regions.entities.find((i) => i.id === identifier.id) ?? null; - } else if (identifier.type === 'inpaint_mask') { - return state.inpaintMask; - } else { - return null; - } - }; - - getCurrentFill = () => { - const state = this.getState(); - const selectedEntity = this.getSelectedEntity(); - let currentFill: RgbaColor = state.tool.fill; - if (selectedEntity) { - if (selectedEntity.type === 'regional_guidance') { - currentFill = { ...selectedEntity.fill, a: state.settings.maskOpacity }; - } else if (selectedEntity.type === 'inpaint_mask') { - currentFill = { ...state.inpaintMask.fill, a: state.settings.maskOpacity }; - } - } else { - currentFill = state.tool.fill; - } - return currentFill; - }; getBbox = () => { return this.getState().bbox; }; + getToolState = () => { return this.getState().tool; }; @@ -244,61 +214,24 @@ export class CanvasStateApi { return this.getState().session; }; getIsSelected = (id: string) => { - return this.getSelectedEntity()?.id === id; + return this.getState().selectedEntityIdentifier?.id === id; }; getLogLevel = () => { - return this.store.getState().system.consoleLogLevel; - }; - - // Read-only state, derived from nanostores - resetLastProgressEvent = () => { - $lastProgressEvent.set(null); + return this._store.getState().system.consoleLogLevel; }; // Read-write state, ephemeral interaction state - getIsDrawing = $isDrawing.get; - setIsDrawing = $isDrawing.set; - onIsDrawingChanged = $isDrawing.subscribe; - - getIsMouseDown = $isMouseDown.get; - setIsMouseDown = $isMouseDown.set; - onIsMouseDownChanged = $isMouseDown.subscribe; - - getLastAddedPoint = $lastAddedPoint.get; - setLastAddedPoint = $lastAddedPoint.set; - onLastAddedPointChanged = $lastAddedPoint.subscribe; - - getLastMouseDownPos = $lastMouseDownPos.get; - setLastMouseDownPos = $lastMouseDownPos.set; - onLastMouseDownPosChanged = $lastMouseDownPos.subscribe; - - getLastCursorPos = $lastCursorPos.get; - setLastCursorPos = $lastCursorPos.set; - onLastCursorPosChanged = $lastCursorPos.subscribe; - - getSpaceKey = $spaceKey.get; - setSpaceKey = $spaceKey.set; - onSpaceKeyChanged = $spaceKey.subscribe; - - getLastProgressEvent = $lastProgressEvent.get; - setLastProgressEvent = $lastProgressEvent.set; - onLastProgressEventChanged = $lastProgressEvent.subscribe; - - getAltKey = $alt.get; - onAltChanged = $alt.subscribe; - - getCtrlKey = $ctrl.get; - onCtrlChanged = $ctrl.subscribe; - - getMetaKey = $meta.get; - onMetaChanged = $meta.subscribe; - - getShiftKey = $shift.get; - onShiftChanged = buildSubscribe($shift.subscribe, 'onShiftChanged'); - - getShouldShowStagedImage = $shouldShowStagedImage.get; - onGetShouldShowStagedImageChanged = $shouldShowStagedImage.subscribe; - - setStageAttrs = $stageAttrs.set; - onStageAttrsChanged = buildSubscribe($stageAttrs.subscribe, 'onStageAttrsChanged'); + $isDrawing = $isDrawing; + $isMouseDown = $isMouseDown; + $lastAddedPoint = $lastAddedPoint; + $lastMouseDownPos = $lastMouseDownPos; + $lastCursorPos = $lastCursorPos; + $lastProgressEvent = $lastProgressEvent; + $spaceKey = $spaceKey; + $altKey = $alt; + $ctrlKey = $ctrl; + $metaKey = $meta; + $shiftKey = $shift; + $shouldShowStagedImage = $shouldShowStagedImage; + $stageAttrs = $stageAttrs; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts index 9d92f707146..742ac5ad80e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts @@ -139,17 +139,17 @@ export class CanvasTool { const stage = this.manager.stage; const renderedEntityCount: number = 1; // TODO(psyche): this.manager should be renderable entity count const toolState = this.manager.stateApi.getToolState(); - const currentFill = this.manager.stateApi.getCurrentFill(); - const selectedEntity = this.manager.stateApi.getSelectedEntity(); - const cursorPos = this.manager.stateApi.getLastCursorPos(); - const isDrawing = this.manager.stateApi.getIsDrawing(); - const isMouseDown = this.manager.stateApi.getIsMouseDown(); + const currentFill = this.manager.getCurrentFill(); + const selectedEntity = this.manager.getSelectedEntity(); + const cursorPos = this.manager.stateApi.$lastCursorPos.get(); + const isDrawing = this.manager.stateApi.$isDrawing.get(); + const isMouseDown = this.manager.stateApi.$isMouseDown.get(); const tool = toolState.selected; const isDrawableEntity = - selectedEntity?.type === 'regional_guidance' || - selectedEntity?.type === 'layer' || - selectedEntity?.type === 'inpaint_mask'; + selectedEntity?.state.type === 'regional_guidance' || + selectedEntity?.state.type === 'layer' || + selectedEntity?.state.type === 'inpaint_mask'; // Update the stage's pointer style if (tool === 'view') { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index 25563bce23d..4e8ab49b2e4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -1,6 +1,5 @@ import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { Subscription } from 'features/controlLayers/konva/util'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { Coordinate, GetLoggingContext, Rect } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -19,32 +18,50 @@ export class CanvasTransformer { static TRANSFORMER_NAME = `${CanvasTransformer.TYPE}:transformer`; static PROXY_RECT_NAME = `${CanvasTransformer.TYPE}:proxy_rect`; static BBOX_OUTLINE_NAME = `${CanvasTransformer.TYPE}:bbox_outline`; - static STROKE_COLOR = 'hsl(200deg 76% 59%)'; // `invokeBlue.400 + static STROKE_COLOR = 'hsl(200 76% 50% / 1)'; // invokeBlue.500 + static ANCHOR_FILL_COLOR = CanvasTransformer.STROKE_COLOR; + static ANCHOR_STROKE_COLOR = 'hsl(200 76% 77% / 1)'; // invokeBlue.200 + static RESIZE_ANCHOR_SIZE = 8; + static ROTATE_ANCHOR_FILL_COLOR = 'hsl(200 76% 95% / 1)'; // invokeBlue.50 + static ROTATE_ANCHOR_STROKE_COLOR = 'hsl(200 76% 40% / 1)'; // invokeBlue.700 + static ROTATE_ANCHOR_SIZE = 12; + static ANCHOR_CORNER_RADIUS_RATIO = 0.5; + static ANCHOR_STROKE_WIDTH = 2; + static ANCHOR_HIT_PADDING = 10; id: string; parent: CanvasLayer; manager: CanvasManager; log: Logger; getLoggingContext: GetLoggingContext; - subscriptions: Subscription[]; /** - * The current mode of the transformer: - * - 'transform': The entity can be moved, resized, and rotated - * - 'drag': The entity can only be moved - * - 'off': The transformer is disabled + * A list of subscriptions that should be cleaned up when the transformer is destroyed. */ - mode: 'transform' | 'drag' | 'off'; + subscriptions: (() => void)[] = []; /** - * Whether dragging is enabled. Dragging is enabled in both 'transform' and 'drag' modes. + * Whether the transformer is currently transforming the entity. */ - isDragEnabled: boolean; + isTransforming: boolean = false; /** - * Whether transforming is enabled. Transforming is enabled only in 'transform' mode. + * The current interaction mode of the transformer: + * - 'all': The entity can be moved, resized, and rotated. + * - 'drag': The entity can be moved. + * - 'off': The transformer is not interactable. */ - isTransformEnabled: boolean; + interactionMode: 'all' | 'drag' | 'off' = 'off'; + + /** + * Whether dragging is enabled. Dragging is enabled in both 'all' and 'drag' interaction modes. + */ + isDragEnabled: boolean = false; + + /** + * Whether transforming is enabled. Transforming is enabled only in 'all' interaction mode. + */ + isTransformEnabled: boolean = false; konva: { transformer: Konva.Transformer; @@ -59,11 +76,6 @@ export class CanvasTransformer { this.getLoggingContext = this.manager.buildGetLoggingContext(this); this.log = this.manager.buildLogger(this.getLoggingContext); - this.subscriptions = []; - - this.mode = 'off'; - this.isDragEnabled = false; - this.isTransformEnabled = false; this.konva = { bboxOutline: new Konva.Rect({ @@ -89,6 +101,35 @@ export class CanvasTransformer { padding: this.manager.getTransformerPadding(), // This is `invokeBlue.400` stroke: CanvasTransformer.STROKE_COLOR, + anchorFill: CanvasTransformer.ANCHOR_FILL_COLOR, + anchorStroke: CanvasTransformer.ANCHOR_STROKE_COLOR, + anchorStrokeWidth: CanvasTransformer.ANCHOR_STROKE_WIDTH, + anchorSize: CanvasTransformer.RESIZE_ANCHOR_SIZE, + anchorCornerRadius: CanvasTransformer.RESIZE_ANCHOR_SIZE * CanvasTransformer.ANCHOR_CORNER_RADIUS_RATIO, + anchorStyleFunc: (anchor) => { + if (anchor.hasName('rotater')) { + anchor.setAttrs({ + height: CanvasTransformer.ROTATE_ANCHOR_SIZE, + width: CanvasTransformer.ROTATE_ANCHOR_SIZE, + cornerRadius: CanvasTransformer.ROTATE_ANCHOR_SIZE * CanvasTransformer.ANCHOR_CORNER_RADIUS_RATIO, + fill: CanvasTransformer.ROTATE_ANCHOR_FILL_COLOR, + stroke: CanvasTransformer.ANCHOR_FILL_COLOR, + offsetX: CanvasTransformer.ROTATE_ANCHOR_SIZE / 2, + offsetY: CanvasTransformer.ROTATE_ANCHOR_SIZE / 2, + }); + } + anchor.hitFunc((context) => { + context.beginPath(); + context.rect( + -CanvasTransformer.ANCHOR_HIT_PADDING, + -CanvasTransformer.ANCHOR_HIT_PADDING, + anchor.width() + CanvasTransformer.ANCHOR_HIT_PADDING * 2, + anchor.height() + CanvasTransformer.ANCHOR_HIT_PADDING * 2 + ); + context.closePath(); + context.fillStrokeShape(anchor); + }); + }, // TODO(psyche): The konva Vector2D type is is apparently not compatible with the JSONObject type that the log // function expects. The in-house Coordinate type is functionally the same - `{x: number; y: number}` - and // TypeScript is happy with it. @@ -152,7 +193,7 @@ export class CanvasTransformer { // This transform constraint operates on the bounding box of the transformer. This box has x, y, width, and // height in stage coordinates, and rotation in radians. This can be used to snap the transformer rotation to // the nearest 45 degrees when shift is held. - if (this.manager.stateApi.getShiftKey()) { + if (this.manager.stateApi.$shiftKey.get()) { if (Math.abs(newBoundBox.rotation % (Math.PI / 4)) > 0) { return oldBoundBox; } @@ -278,9 +319,9 @@ export class CanvasTransformer { }); }); this.konva.proxyRect.on('dragend', () => { - if (this.parent.isTransforming) { - // When the user cancels the transformation, we need to reset the layer, so we should not update the layer's - // positition while we are transforming - bail out early. + if (this.isTransforming) { + // If we are transforming the entity, we should not push the new position to the state. This will trigger a + // re-render of the entity and bork the transformation. return; } @@ -296,9 +337,9 @@ export class CanvasTransformer { this.subscriptions.push( // When the stage scale changes, we may need to re-scale some of the transformer's components. For example, // the bbox outline should always be 1 screen pixel wide, so we need to update its stroke width. - this.manager.stateApi.onStageAttrsChanged((newAttrs, oldAttrs) => { - if (newAttrs.scale !== oldAttrs?.scale) { - this.scale(); + this.manager.stateApi.$stageAttrs.listen((newVal, oldVal) => { + if (newVal.scale !== oldVal.scale) { + this.syncScale(); } }) ); @@ -306,8 +347,24 @@ export class CanvasTransformer { this.subscriptions.push( // While the user holds shift, we want to snap rotation to 45 degree increments. Listen for the shift key state // and update the snap angles accordingly. - this.manager.stateApi.onShiftChanged((isPressed) => { - this.konva.transformer.rotationSnaps(isPressed ? [0, 45, 90, 135, 180, 225, 270, 315] : []); + this.manager.stateApi.$shiftKey.listen((newVal) => { + this.konva.transformer.rotationSnaps(newVal ? [0, 45, 90, 135, 180, 225, 270, 315] : []); + }) + ); + + this.subscriptions.push( + // When the selected tool changes, we need to update the transformer's interaction state. + this.manager.toolState.subscribe((newVal, oldVal) => { + if (newVal.selected !== oldVal.selected) { + this.syncInteractionState(); + } + }) + ); + + this.subscriptions.push( + // When the selected entity changes, we need to update the transformer's interaction state. + this.manager.selectedEntityIdentifier.subscribe(() => { + this.syncInteractionState(); }) ); } @@ -336,10 +393,48 @@ export class CanvasTransformer { }); }; + /** + * Syncs the transformer's interaction state with the application and entity's states. This is called when the entity + * is selected or deselected, or when the user changes the selected tool. + */ + syncInteractionState = () => { + this.log.trace('Syncing interaction state'); + + const toolState = this.manager.stateApi.getToolState(); + const isSelected = this.manager.stateApi.getIsSelected(this.parent.id); + + if (!this.parent.renderer.hasObjects()) { + // The layer is totally empty, we can just disable the layer + this.parent.konva.layer.listening(false); + this.setInteractionMode('off'); + return; + } + + if (isSelected && !this.isTransforming && toolState.selected === 'move') { + // We are moving this layer, it must be listening + this.parent.konva.layer.listening(true); + this.setInteractionMode('drag'); + } else if (isSelected && this.isTransforming) { + // When transforming, we want the stage to still be movable if the view tool is selected. If the transformer is + // active, it will interrupt the stage drag events. So we should disable listening when the view tool is selected. + if (toolState.selected !== 'view') { + this.parent.konva.layer.listening(true); + this.setInteractionMode('all'); + } else { + this.parent.konva.layer.listening(false); + this.setInteractionMode('off'); + } + } else { + // The layer is not selected, or we are using a tool that doesn't need the layer to be listening - disable interaction stuff + this.parent.konva.layer.listening(false); + this.setInteractionMode('off'); + } + }; + /** * Updates the transformer's scale. This is called when the stage is scaled. */ - scale = () => { + syncScale = () => { const onePixel = this.manager.getScaledPixel(); const bboxPadding = this.manager.getScaledBboxPadding(); @@ -353,24 +448,53 @@ export class CanvasTransformer { this.konva.transformer.forceUpdate(); }; + startTransform = () => { + this.log.debug('Starting transform'); + this.isTransforming = true; + + // When transforming, we want the stage to still be movable if the view tool is selected. If the transformer or + // interaction rect are listening, it will interrupt the stage's drag events. So we should disable listening + // when the view tool is selected + const shouldListen = this.manager.stateApi.getToolState().selected !== 'view'; + this.parent.konva.layer.listening(shouldListen); + this.setInteractionMode('all'); + }; + + applyTransform = async () => { + this.log.debug('Applying transform'); + await this.parent.rasterize(); + this.stopTransform(); + }; + + stopTransform = () => { + this.log.debug('Stopping transform'); + + this.isTransforming = false; + this.setInteractionMode('off'); + this.parent.resetScale(); + this.parent.updatePosition(); + this.parent.updateBbox(); + this.syncInteractionState(); + }; + /** - * Sets the transformer to a specific mode. - * @param mode The mode to set the transformer to. The transformer can be in one of three modes: - * - 'transform': The entity can be moved, resized, and rotated - * - 'drag': The entity can only be moved - * - 'off': The transformer is disabled + * Sets the transformer to a specific interaction mode. + * @param interactionMode The mode to set the transformer to. The transformer can be in one of three modes: + * - 'all': The entity can be moved, resized, and rotated. + * - 'drag': The entity can be moved. + * - 'off': The transformer is not interactable. */ - setMode = (mode: 'transform' | 'drag' | 'off') => { - this.mode = mode; - if (mode === 'drag') { + setInteractionMode = (interactionMode: 'all' | 'drag' | 'off') => { + this.interactionMode = interactionMode; + if (interactionMode === 'drag') { this._enableDrag(); this._disableTransform(); this._showBboxOutline(); - } else if (mode === 'transform') { + } else if (interactionMode === 'all') { this._enableDrag(); this._enableTransform(); this._hideBboxOutline(); - } else if (mode === 'off') { + } else if (interactionMode === 'off') { this._disableDrag(); this._disableTransform(); this._hideBboxOutline(); @@ -411,13 +535,13 @@ export class CanvasTransformer { this.konva.bboxOutline.visible(false); }; - getNodes = () => [this.konva.transformer, this.konva.proxyRect, this.konva.bboxOutline]; + getNodes = () => [this.konva.bboxOutline, this.konva.proxyRect, this.konva.transformer]; repr = () => { return { id: this.id, type: CanvasTransformer.TYPE, - mode: this.mode, + mode: this.interactionMode, isTransformEnabled: this.isTransformEnabled, isDragEnabled: this.isDragEnabled, }; @@ -425,9 +549,9 @@ export class CanvasTransformer { destroy = () => { this.log.trace('Destroying transformer'); - for (const { name, unsubscribe } of this.subscriptions) { - this.log.trace({ name }, 'Cleaning up listener'); - unsubscribe(); + for (const cleanup of this.subscriptions) { + this.log.trace('Cleaning up listener'); + cleanup(); } this.konva.bboxOutline.destroy(); this.konva.transformer.destroy(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 9c07676a6bb..3355052aaab 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -26,7 +26,10 @@ import { BRUSH_SPACING_TARGET_SCALE, CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANV * @param stage The konva stage * @param setLastCursorPos The callback to store the cursor pos */ -const updateLastCursorPos = (stage: Konva.Stage, setLastCursorPos: CanvasManager['stateApi']['setLastCursorPos']) => { +const updateLastCursorPos = ( + stage: Konva.Stage, + setLastCursorPos: CanvasManager['stateApi']['$lastCursorPos']['set'] +) => { const pos = getScaledCursorPosition(stage); if (!pos) { return null; @@ -112,22 +115,17 @@ const getLastPointOfLastLineOfEntity = ( }; export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { - const { stage, stateApi, getSelectedEntityAdapter } = manager; + const { stage, stateApi, getCurrentFill, getSelectedEntity } = manager; const { getToolState, - getCurrentFill, setTool, setToolBuffer, - setIsMouseDown, - setLastMouseDownPos, - getLastCursorPos, - setLastCursorPos, - // getLastAddedPoint, - setLastAddedPoint, - setStageAttrs, - getSelectedEntity, - getSpaceKey, - setSpaceKey, + $isMouseDown, + $lastMouseDownPos, + $lastCursorPos, + $lastAddedPoint, + $stageAttrs, + $spaceKey, getBbox, getSettings, onBrushWidthChanged, @@ -166,34 +164,31 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { //#region mousedown stage.on('mousedown', async (e) => { - setIsMouseDown(true); + $isMouseDown.set(true); const toolState = getToolState(); - const pos = updateLastCursorPos(stage, setLastCursorPos); + const pos = updateLastCursorPos(stage, $lastCursorPos.set); const selectedEntity = getSelectedEntity(); - const selectedEntityAdapter = getSelectedEntityAdapter(); if ( pos && selectedEntity && - isDrawableEntity(selectedEntity) && - selectedEntityAdapter && - isDrawableEntityAdapter(selectedEntityAdapter) && - !getSpaceKey() && + isDrawableEntity(selectedEntity.state) && + !$spaceKey.get() && getIsPrimaryMouseDown(e) ) { - setLastMouseDownPos(pos); - const normalizedPoint = offsetCoord(pos, selectedEntity.position); + $lastMouseDownPos.set(pos); + const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); if (toolState.selected === 'brush') { - const lastLinePoint = getLastPointOfLastLineOfEntity(selectedEntity, toolState.selected); + const lastLinePoint = getLastPointOfLastLineOfEntity(selectedEntity.state, toolState.selected); const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); if (e.evt.shiftKey && lastLinePoint) { // Create a straight line from the last line point - if (selectedEntityAdapter.renderer.buffer) { - await selectedEntityAdapter.renderer.commitBuffer(); + if (selectedEntity.adapter.renderer.buffer) { + await selectedEntity.adapter.renderer.commitBuffer(); } - await selectedEntityAdapter.renderer.setBuffer({ + await selectedEntity.adapter.renderer.setBuffer({ id: getObjectId('brush_line', true), type: 'brush_line', points: [ @@ -205,33 +200,33 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { ], strokeWidth: toolState.brush.width, color: getCurrentFill(), - clip: getClip(selectedEntity), + clip: getClip(selectedEntity.state), }); } else { - if (selectedEntityAdapter.renderer.buffer) { - await selectedEntityAdapter.renderer.commitBuffer(); + if (selectedEntity.adapter.renderer.buffer) { + await selectedEntity.adapter.renderer.commitBuffer(); } - await selectedEntityAdapter.renderer.setBuffer({ + await selectedEntity.adapter.renderer.setBuffer({ id: getObjectId('brush_line', true), type: 'brush_line', points: [alignedPoint.x, alignedPoint.y], strokeWidth: toolState.brush.width, color: getCurrentFill(), - clip: getClip(selectedEntity), + clip: getClip(selectedEntity.state), }); } - setLastAddedPoint(alignedPoint); + $lastAddedPoint.set(alignedPoint); } if (toolState.selected === 'eraser') { - const lastLinePoint = getLastPointOfLastLineOfEntity(selectedEntity, toolState.selected); + const lastLinePoint = getLastPointOfLastLineOfEntity(selectedEntity.state, toolState.selected); const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); if (e.evt.shiftKey && lastLinePoint) { // Create a straight line from the last line point - if (selectedEntityAdapter.renderer.buffer) { - await selectedEntityAdapter.renderer.commitBuffer(); + if (selectedEntity.adapter.renderer.buffer) { + await selectedEntity.adapter.renderer.commitBuffer(); } - await selectedEntityAdapter.renderer.setBuffer({ + await selectedEntity.adapter.renderer.setBuffer({ id: getObjectId('eraser_line', true), type: 'eraser_line', points: [ @@ -242,28 +237,28 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { alignedPoint.y, ], strokeWidth: toolState.eraser.width, - clip: getClip(selectedEntity), + clip: getClip(selectedEntity.state), }); } else { - if (selectedEntityAdapter.renderer.buffer) { - await selectedEntityAdapter.renderer.commitBuffer(); + if (selectedEntity.adapter.renderer.buffer) { + await selectedEntity.adapter.renderer.commitBuffer(); } - await selectedEntityAdapter.renderer.setBuffer({ + await selectedEntity.adapter.renderer.setBuffer({ id: getObjectId('eraser_line', true), type: 'eraser_line', points: [alignedPoint.x, alignedPoint.y], strokeWidth: toolState.eraser.width, - clip: getClip(selectedEntity), + clip: getClip(selectedEntity.state), }); } - setLastAddedPoint(alignedPoint); + $lastAddedPoint.set(alignedPoint); } if (toolState.selected === 'rect') { - if (selectedEntityAdapter.renderer.buffer) { - await selectedEntityAdapter.renderer.commitBuffer(); + if (selectedEntity.adapter.renderer.buffer) { + await selectedEntity.adapter.renderer.commitBuffer(); } - await selectedEntityAdapter.renderer.setBuffer({ + await selectedEntity.adapter.renderer.setBuffer({ id: getObjectId('rect', true), type: 'rect', x: Math.round(normalizedPoint.x), @@ -279,49 +274,41 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { //#region mouseup stage.on('mouseup', async () => { - setIsMouseDown(false); - const pos = getLastCursorPos(); + $isMouseDown.set(false); + const pos = $lastCursorPos.get(); const selectedEntity = getSelectedEntity(); - const selectedEntityAdapter = getSelectedEntityAdapter(); - if ( - pos && - selectedEntity && - isDrawableEntity(selectedEntity) && - selectedEntityAdapter && - isDrawableEntityAdapter(selectedEntityAdapter) && - !getSpaceKey() - ) { + if (pos && selectedEntity && isDrawableEntity(selectedEntity.state) && !$spaceKey.get()) { const toolState = getToolState(); if (toolState.selected === 'brush') { - const drawingBuffer = selectedEntityAdapter.renderer.buffer; + const drawingBuffer = selectedEntity.adapter.renderer.buffer; if (drawingBuffer?.type === 'brush_line') { - await selectedEntityAdapter.renderer.commitBuffer(); + await selectedEntity.adapter.renderer.commitBuffer(); } else { - await selectedEntityAdapter.renderer.clearBuffer(); + await selectedEntity.adapter.renderer.clearBuffer(); } } if (toolState.selected === 'eraser') { - const drawingBuffer = selectedEntityAdapter.renderer.buffer; + const drawingBuffer = selectedEntity.adapter.renderer.buffer; if (drawingBuffer?.type === 'eraser_line') { - await selectedEntityAdapter.renderer.commitBuffer(); + await selectedEntity.adapter.renderer.commitBuffer(); } else { - await selectedEntityAdapter.renderer.clearBuffer(); + await selectedEntity.adapter.renderer.clearBuffer(); } } if (toolState.selected === 'rect') { - const drawingBuffer = selectedEntityAdapter.renderer.buffer; + const drawingBuffer = selectedEntity.adapter.renderer.buffer; if (drawingBuffer?.type === 'rect') { - await selectedEntityAdapter.renderer.commitBuffer(); + await selectedEntity.adapter.renderer.commitBuffer(); } else { - await selectedEntityAdapter.renderer.clearBuffer(); + await selectedEntity.adapter.renderer.clearBuffer(); } } - setLastMouseDownPos(null); + $lastMouseDownPos.set(null); } manager.preview.tool.render(); @@ -330,94 +317,93 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { //#region mousemove stage.on('mousemove', async (e) => { const toolState = getToolState(); - const pos = updateLastCursorPos(stage, setLastCursorPos); + const pos = updateLastCursorPos(stage, $lastCursorPos.set); const selectedEntity = getSelectedEntity(); - const selectedEntityAdapter = getSelectedEntityAdapter(); if ( pos && selectedEntity && - isDrawableEntity(selectedEntity) && - selectedEntityAdapter && - isDrawableEntityAdapter(selectedEntityAdapter) && - !getSpaceKey() && + isDrawableEntity(selectedEntity.state) && + selectedEntity.adapter && + isDrawableEntityAdapter(selectedEntity.adapter) && + !$spaceKey.get() && getIsPrimaryMouseDown(e) ) { if (toolState.selected === 'brush') { - const drawingBuffer = selectedEntityAdapter.renderer.buffer; + const drawingBuffer = selectedEntity.adapter.renderer.buffer; if (drawingBuffer) { if (drawingBuffer?.type === 'brush_line') { const nextPoint = getNextPoint(pos, toolState, getLastPointOfLine(drawingBuffer.points)); if (nextPoint) { - const normalizedPoint = offsetCoord(nextPoint, selectedEntity.position); + const normalizedPoint = offsetCoord(nextPoint, selectedEntity.state.position); const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); - await selectedEntityAdapter.renderer.setBuffer(drawingBuffer); - setLastAddedPoint(alignedPoint); + await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); + $lastAddedPoint.set(alignedPoint); } } else { - await selectedEntityAdapter.renderer.clearBuffer(); + await selectedEntity.adapter.renderer.clearBuffer(); } } else { - if (selectedEntityAdapter.renderer.buffer) { - await selectedEntityAdapter.renderer.commitBuffer(); + if (selectedEntity.adapter.renderer.buffer) { + await selectedEntity.adapter.renderer.commitBuffer(); } - const normalizedPoint = offsetCoord(pos, selectedEntity.position); + const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); - await selectedEntityAdapter.renderer.setBuffer({ + await selectedEntity.adapter.renderer.setBuffer({ id: getObjectId('brush_line', true), type: 'brush_line', points: [alignedPoint.x, alignedPoint.y], strokeWidth: toolState.brush.width, color: getCurrentFill(), - clip: getClip(selectedEntity), + clip: getClip(selectedEntity.state), }); - setLastAddedPoint(alignedPoint); + $lastAddedPoint.set(alignedPoint); } } if (toolState.selected === 'eraser') { - const drawingBuffer = selectedEntityAdapter.renderer.buffer; + const drawingBuffer = selectedEntity.adapter.renderer.buffer; if (drawingBuffer) { if (drawingBuffer.type === 'eraser_line') { const nextPoint = getNextPoint(pos, toolState, getLastPointOfLine(drawingBuffer.points)); if (nextPoint) { - const normalizedPoint = offsetCoord(nextPoint, selectedEntity.position); + const normalizedPoint = offsetCoord(nextPoint, selectedEntity.state.position); const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); - await selectedEntityAdapter.renderer.setBuffer(drawingBuffer); - setLastAddedPoint(alignedPoint); + await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); + $lastAddedPoint.set(alignedPoint); } } else { - await selectedEntityAdapter.renderer.clearBuffer(); + await selectedEntity.adapter.renderer.clearBuffer(); } } else { - if (selectedEntityAdapter.renderer.buffer) { - await selectedEntityAdapter.renderer.commitBuffer(); + if (selectedEntity.adapter.renderer.buffer) { + await selectedEntity.adapter.renderer.commitBuffer(); } - const normalizedPoint = offsetCoord(pos, selectedEntity.position); + const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); - await selectedEntityAdapter.renderer.setBuffer({ + await selectedEntity.adapter.renderer.setBuffer({ id: getObjectId('eraser_line', true), type: 'eraser_line', points: [alignedPoint.x, alignedPoint.y], strokeWidth: toolState.eraser.width, - clip: getClip(selectedEntity), + clip: getClip(selectedEntity.state), }); - setLastAddedPoint(alignedPoint); + $lastAddedPoint.set(alignedPoint); } } if (toolState.selected === 'rect') { - const drawingBuffer = selectedEntityAdapter.renderer.buffer; + const drawingBuffer = selectedEntity.adapter.renderer.buffer; if (drawingBuffer) { if (drawingBuffer.type === 'rect') { - const normalizedPoint = offsetCoord(pos, selectedEntity.position); + const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); drawingBuffer.width = Math.round(normalizedPoint.x - drawingBuffer.x); drawingBuffer.height = Math.round(normalizedPoint.y - drawingBuffer.y); - await selectedEntityAdapter.renderer.setBuffer(drawingBuffer); + await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); } else { - await selectedEntityAdapter.renderer.clearBuffer(); + await selectedEntity.adapter.renderer.clearBuffer(); } } } @@ -427,39 +413,36 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { //#region mouseleave stage.on('mouseleave', async (e) => { - const pos = updateLastCursorPos(stage, setLastCursorPos); - setLastCursorPos(null); - setLastMouseDownPos(null); + const pos = updateLastCursorPos(stage, $lastCursorPos.set); + $lastCursorPos.set(null); + $lastMouseDownPos.set(null); const selectedEntity = getSelectedEntity(); - const selectedEntityAdapter = getSelectedEntityAdapter(); const toolState = getToolState(); if ( pos && selectedEntity && - isDrawableEntity(selectedEntity) && - selectedEntityAdapter && - isDrawableEntityAdapter(selectedEntityAdapter) && - !getSpaceKey() && + isDrawableEntity(selectedEntity.state) && + !$spaceKey.get() && getIsPrimaryMouseDown(e) ) { - const drawingBuffer = selectedEntityAdapter.renderer.buffer; - const normalizedPoint = offsetCoord(pos, selectedEntity.position); + const drawingBuffer = selectedEntity.adapter.renderer.buffer; + const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); if (toolState.selected === 'brush' && drawingBuffer?.type === 'brush_line') { const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); - await selectedEntityAdapter.renderer.setBuffer(drawingBuffer); - await selectedEntityAdapter.renderer.commitBuffer(); + await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); + await selectedEntity.adapter.renderer.commitBuffer(); } else if (toolState.selected === 'eraser' && drawingBuffer?.type === 'eraser_line') { const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); - await selectedEntityAdapter.renderer.setBuffer(drawingBuffer); - await selectedEntityAdapter.renderer.commitBuffer(); + await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); + await selectedEntity.adapter.renderer.commitBuffer(); } else if (toolState.selected === 'rect' && drawingBuffer?.type === 'rect') { drawingBuffer.width = Math.round(normalizedPoint.x - drawingBuffer.x); drawingBuffer.height = Math.round(normalizedPoint.y - drawingBuffer.y); - await selectedEntityAdapter.renderer.setBuffer(drawingBuffer); - await selectedEntityAdapter.renderer.commitBuffer(); + await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); + await selectedEntity.adapter.renderer.commitBuffer(); } } @@ -503,7 +486,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { stage.scaleX(newScale); stage.scaleY(newScale); stage.position(newPos); - setStageAttrs({ + $stageAttrs.set({ position: newPos, dimensions: { width: stage.width(), height: stage.height() }, scale: newScale, @@ -516,7 +499,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { //#region dragmove stage.on('dragmove', () => { - setStageAttrs({ + $stageAttrs.set({ position: { x: Math.floor(stage.x()), y: Math.floor(stage.y()) }, dimensions: { width: stage.width(), height: stage.height() }, scale: stage.scaleX(), @@ -528,7 +511,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { //#region dragend stage.on('dragend', () => { // Stage position should always be an integer, else we get fractional pixels which are blurry - setStageAttrs({ + $stageAttrs.set({ position: { x: Math.floor(stage.x()), y: Math.floor(stage.y()) }, dimensions: { width: stage.width(), height: stage.height() }, scale: stage.scaleX(), @@ -546,17 +529,17 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { } if (e.key === 'Escape') { // Cancel shape drawing on escape - setLastMouseDownPos(null); + $lastMouseDownPos.set(null); } else if (e.key === ' ') { // Select the view tool on space key down setToolBuffer(getToolState().selected); setTool('view'); - setSpaceKey(true); - setLastCursorPos(null); - setLastMouseDownPos(null); + $spaceKey.set(true); + $lastCursorPos.set(null); + $lastMouseDownPos.set(null); } else if (e.key === 'r') { - setLastCursorPos(null); - setLastMouseDownPos(null); + $lastCursorPos.set(null); + $lastMouseDownPos.set(null); manager.background.render(); // TODO(psyche): restore some kind of fit } @@ -576,7 +559,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { const toolBuffer = getToolState().selectedBuffer; setTool(toolBuffer ?? 'move'); setToolBuffer(null); - setSpaceKey(false); + $spaceKey.set(false); } manager.preview.tool.render(); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index 350c9981352..120799d2fad 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -1,12 +1,17 @@ import { getImageDataTransparency } from 'common/util/arrayBuffer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { CanvasObjectState, Coordinate, GenerationMode, Rect, RgbaColor } from 'features/controlLayers/store/types'; +import type { + CanvasObjectState, + Coordinate, + GenerationMode, + Rect, + RgbaColor, +} from 'features/controlLayers/store/types'; import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers'; import Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; import type { Vector2d } from 'konva/lib/types'; import { customAlphabet } from 'nanoid'; -import type { WritableAtom } from 'nanostores'; import type { ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; @@ -64,7 +69,7 @@ export const alignCoordForTool = (coord: Coordinate, toolWidth: number): Coordin * Offsets a point by the given offset. The offset is subtracted from the point. * @param coord The coordinate to offset * @param offset The offset to apply - * @returns + * @returns */ export const offsetCoord = (coord: Coordinate, offset: Coordinate): Coordinate => { return { @@ -623,22 +628,6 @@ export function getObjectId(type: CanvasObjectState['type'], isBuffer?: boolean) } } -export type Subscription = { - name: string; - unsubscribe: () => void; -}; - -/** - * Builds a subscribe function for a nanostores atom. - * @param subscribe The subscribe function of the atom - * @param name The name of the atom - * @returns A subscribe function that returns an object with the name and unsubscribe function - */ -export const buildSubscribe = (subscribe: WritableAtom['subscribe'], name: string) => { - return (cb: Parameters['subscribe']>[0]): Subscription => { - return { - name, - unsubscribe: subscribe(cb), - }; - }; +export const getEmptyRect = (): Rect => { + return { x: 0, y: 0, width: 0, height: 0 }; }; From 7b7d722baecab2c49604c818f6621d0e482c3094 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:56:19 +1000 Subject: [PATCH 287/678] docs(ui): update transformer docstrings --- .../controlLayers/konva/CanvasTransformer.ts | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index 4e8ab49b2e4..d31af5c264a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -106,7 +106,9 @@ export class CanvasTransformer { anchorStrokeWidth: CanvasTransformer.ANCHOR_STROKE_WIDTH, anchorSize: CanvasTransformer.RESIZE_ANCHOR_SIZE, anchorCornerRadius: CanvasTransformer.RESIZE_ANCHOR_SIZE * CanvasTransformer.ANCHOR_CORNER_RADIUS_RATIO, + // This function is called for each anchor to style it (and do anything else you might want to do). anchorStyleFunc: (anchor) => { + // Give the rotater special styling if (anchor.hasName('rotater')) { anchor.setAttrs({ height: CanvasTransformer.ROTATE_ANCHOR_SIZE, @@ -118,6 +120,7 @@ export class CanvasTransformer { offsetY: CanvasTransformer.ROTATE_ANCHOR_SIZE / 2, }); } + // Add some padding to the hit area of the anchors anchor.hitFunc((context) => { context.beginPath(); context.rect( @@ -344,16 +347,16 @@ export class CanvasTransformer { }) ); + // While the user holds shift, we want to snap rotation to 45 degree increments. Listen for the shift key state + // and update the snap angles accordingly. this.subscriptions.push( - // While the user holds shift, we want to snap rotation to 45 degree increments. Listen for the shift key state - // and update the snap angles accordingly. this.manager.stateApi.$shiftKey.listen((newVal) => { this.konva.transformer.rotationSnaps(newVal ? [0, 45, 90, 135, 180, 225, 270, 315] : []); }) ); + // When the selected tool changes, we need to update the transformer's interaction state. this.subscriptions.push( - // When the selected tool changes, we need to update the transformer's interaction state. this.manager.toolState.subscribe((newVal, oldVal) => { if (newVal.selected !== oldVal.selected) { this.syncInteractionState(); @@ -361,8 +364,8 @@ export class CanvasTransformer { }) ); + // When the selected entity changes, we need to update the transformer's interaction state. this.subscriptions.push( - // When the selected entity changes, we need to update the transformer's interaction state. this.manager.selectedEntityIdentifier.subscribe(() => { this.syncInteractionState(); }) @@ -448,6 +451,9 @@ export class CanvasTransformer { this.konva.transformer.forceUpdate(); }; + /** + * Starts the transformation of the entity. + */ startTransform = () => { this.log.debug('Starting transform'); this.isTransforming = true; @@ -460,12 +466,19 @@ export class CanvasTransformer { this.setInteractionMode('all'); }; + /** + * Applies the transformation of the entity. + */ applyTransform = async () => { this.log.debug('Applying transform'); await this.parent.rasterize(); this.stopTransform(); }; + /** + * Stops the transformation of the entity. If the transformation is in progress, the entity will be reset to its + * original state. + */ stopTransform = () => { this.log.debug('Stopping transform'); @@ -535,8 +548,15 @@ export class CanvasTransformer { this.konva.bboxOutline.visible(false); }; + /** + * Gets the nodes that make up the transformer, in the order they should be added to the layer. + * @returns The nodes that make up the transformer. + */ getNodes = () => [this.konva.bboxOutline, this.konva.proxyRect, this.konva.transformer]; + /** + * Gets a JSON-serializable object that describes the transformer. + */ repr = () => { return { id: this.id, @@ -547,6 +567,9 @@ export class CanvasTransformer { }; }; + /** + * Destroys the transformer, cleaning up any subscriptions. + */ destroy = () => { this.log.trace('Destroying transformer'); for (const cleanup of this.subscriptions) { From 92b8c68b940dc31fa02e510c1de4cbf72b5ef748 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:56:45 +1000 Subject: [PATCH 288/678] feat(ui): use pubsub for isTransforming on manager --- .../components/TransformToolButton.tsx | 5 +--- .../controlLayers/konva/CanvasManager.ts | 28 +++++++++---------- .../controlLayers/konva/CanvasTool.ts | 2 +- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx index cf70f59ee91..14ac66706f9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx @@ -19,10 +19,7 @@ export const TransformToolButton = memo(() => { if (!canvasManager) { return; } - canvasManager.onTransform = setIsTransforming; - return () => { - canvasManager.onTransform = null; - }; + return canvasManager.isTransforming.subscribe(setIsTransforming); }, [canvasManager]); const onTransform = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 0f932534544..b4236cb0435 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -124,7 +124,7 @@ export class CanvasManager { _isDebugging: boolean; - onTransform: ((isTransforming: boolean) => void) | null; + isTransforming: PubSub; _store: Store; _isFirstRender: boolean; @@ -212,16 +212,16 @@ export class CanvasManager { this._worker.onmessageerror = () => { this.log.error('Worker message error'); }; - this.onTransform = null; this._isDebugging = false; - this.toolState = new PubSub(this.stateApi.getToolState()); - this.currentFill = new PubSub(this.getCurrentFill()); - this.selectedEntityIdentifier = new PubSub( + this.isTransforming = new PubSub(false); + this.toolState = new PubSub(this.stateApi.getToolState()); + this.currentFill = new PubSub(this.getCurrentFill()); + this.selectedEntityIdentifier = new PubSub( this.stateApi.getState().selectedEntityIdentifier, (a, b) => a?.id === b?.id ); - this.selectedEntity = new PubSub( + this.selectedEntity = new PubSub( this.getSelectedEntity(), (a, b) => a?.state === b?.state && a?.adapter === b?.adapter ); @@ -399,7 +399,7 @@ export class CanvasManager { // TODO(psyche): Support other entity types assert(layer?.adapter instanceof CanvasLayer, 'No selected layer'); layer.adapter.transformer.startTransform(); - this.onTransform?.(true); + this.isTransforming.publish(true); } async applyTransform() { @@ -407,7 +407,7 @@ export class CanvasManager { if (layer) { await layer.transformer.applyTransform(); } - this.onTransform?.(false); + this.isTransforming.publish(false); } cancelTransform() { @@ -415,7 +415,7 @@ export class CanvasManager { if (layer) { layer.transformer.stopTransform(); } - this.onTransform?.(false); + this.isTransforming.publish(false); } render = async () => { @@ -451,11 +451,6 @@ export class CanvasManager { } } - this.toolState.publish(state.tool); - this.selectedEntityIdentifier.publish(state.selectedEntityIdentifier); - this.selectedEntity.publish(this.getSelectedEntity()); - this.currentFill.publish(this.getCurrentFill()); - if ( this._isFirstRender || state.initialImage !== this._prevState.initialImage || @@ -499,6 +494,11 @@ export class CanvasManager { await this.renderControlAdapters(); } + this.toolState.publish(state.tool); + this.selectedEntityIdentifier.publish(state.selectedEntityIdentifier); + this.selectedEntity.publish(this.getSelectedEntity()); + this.currentFill.publish(this.getCurrentFill()); + if ( this._isFirstRender || state.bbox !== this._prevState.bbox || diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts index 742ac5ad80e..ff71ebfda20 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts @@ -161,7 +161,7 @@ export class CanvasTool { } else if (!isDrawableEntity) { // Non-drawable layers don't have tools stage.container().style.cursor = 'not-allowed'; - } else if (tool === 'move' || toolState.isTransforming) { + } else if (tool === 'move' || this.manager.isTransforming.getValue()) { // Move tool gets a pointer stage.container().style.cursor = 'default'; } else if (tool === 'rect') { From 7c7117c39a5df54c6ce6abe3f9506f6581fe1add Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Aug 2024 18:02:11 +1000 Subject: [PATCH 289/678] tidy(ui): remove unused code in CanvasTool --- .../controlLayers/konva/CanvasTool.ts | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts index ff71ebfda20..1dd26767669 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts @@ -102,18 +102,6 @@ export class CanvasTool { this.konva.eraser.group.add(this.konva.eraser.innerBorderCircle); this.konva.eraser.group.add(this.konva.eraser.outerBorderCircle); this.konva.group.add(this.konva.eraser.group); - - // // Create the rect preview - this is a rectangle drawn from the last mouse down position to the current cursor position - // this.rect = { - // group: new Konva.Group(), - // fillRect: new Konva.Rect({ - // id: PREVIEW_RECT_ID, - // listening: false, - // strokeEnabled: false, - // }), - // }; - // this.rect.group.add(this.rect.fillRect); - // this.konva.group.add(this.rect.group); } scaleTool = () => { @@ -210,7 +198,6 @@ export class CanvasTool { this.konva.brush.group.visible(true); this.konva.eraser.group.visible(false); - // this.rect.group.visible(false); } else if (cursorPos && tool === 'eraser') { const alignedCursorPos = alignCoordForTool(cursorPos, toolState.eraser.width); @@ -238,23 +225,9 @@ export class CanvasTool { this.konva.brush.group.visible(false); this.konva.eraser.group.visible(true); - // this.rect.group.visible(false); - // } else if (cursorPos && lastMouseDownPos && tool === 'rect') { - // this.rect.fillRect.setAttrs({ - // x: Math.min(cursorPos.x, lastMouseDownPos.x), - // y: Math.min(cursorPos.y, lastMouseDownPos.y), - // width: Math.abs(cursorPos.x - lastMouseDownPos.x), - // height: Math.abs(cursorPos.y - lastMouseDownPos.y), - // fill: rgbaColorToString(currentFill), - // visible: true, - // }); - // this.konva.brush.group.visible(false); - // this.konva.eraser.group.visible(false); - // this.rect.group.visible(true); } else { this.konva.brush.group.visible(false); this.konva.eraser.group.visible(false); - // this.rect.group.visible(false); } } } From 3f359f105986304c329e60548e13c59aed19de2c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Aug 2024 18:41:50 +1000 Subject: [PATCH 290/678] tidy(ui): clean up worker tasks when complete --- .../features/controlLayers/konva/CanvasManager.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index b4236cb0435..176455ea035 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -122,15 +122,15 @@ export class CanvasManager { log: Logger; workerLog: Logger; - _isDebugging: boolean; - isTransforming: PubSub; _store: Store; - _isFirstRender: boolean; _prevState: CanvasV2State; - _worker: Worker; - _tasks: Map void }>; + _isFirstRender: boolean = true; + _isDebugging: boolean = false; + + _worker: Worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module', name: 'worker' }); + _tasks: Map void }> = new Map(); toolState: PubSub; currentFill: PubSub; @@ -149,7 +149,6 @@ export class CanvasManager { this._store = store; this.stateApi = new CanvasStateApi(this._store, this); this._prevState = this.stateApi.getState(); - this._isFirstRender = true; this.log = logger('canvas').child((message) => { return { @@ -188,8 +187,6 @@ export class CanvasManager { this.initialImage = new CanvasInitialImage(this.stateApi.getInitialImageState(), this); this.stage.add(this.initialImage.konva.layer); - this._worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module', name: 'worker' }); - this._tasks = new Map(); this._worker.onmessage = (event: MessageEvent) => { const { type, data } = event.data; if (type === 'log') { @@ -204,6 +201,7 @@ export class CanvasManager { return; } task.onComplete(data.extents); + this._tasks.delete(data.id); } }; this._worker.onerror = (event) => { @@ -212,7 +210,6 @@ export class CanvasManager { this._worker.onmessageerror = () => { this.log.error('Worker message error'); }; - this._isDebugging = false; this.isTransforming = new PubSub(false); this.toolState = new PubSub(this.stateApi.getToolState()); From b86645c1a772a685ec03f2972712988457811926 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Aug 2024 18:43:00 +1000 Subject: [PATCH 291/678] feat(ui): use set for transformer subscriptions --- .../features/controlLayers/konva/CanvasTransformer.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index d31af5c264a..432f9c4ca25 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -38,7 +38,7 @@ export class CanvasTransformer { /** * A list of subscriptions that should be cleaned up when the transformer is destroyed. */ - subscriptions: (() => void)[] = []; + subscriptions: Set<() => void> = new Set(); /** * Whether the transformer is currently transforming the entity. @@ -337,7 +337,7 @@ export class CanvasTransformer { this.manager.stateApi.onPosChanged({ id: this.parent.id, position }, 'layer'); }); - this.subscriptions.push( + this.subscriptions.add( // When the stage scale changes, we may need to re-scale some of the transformer's components. For example, // the bbox outline should always be 1 screen pixel wide, so we need to update its stroke width. this.manager.stateApi.$stageAttrs.listen((newVal, oldVal) => { @@ -349,14 +349,14 @@ export class CanvasTransformer { // While the user holds shift, we want to snap rotation to 45 degree increments. Listen for the shift key state // and update the snap angles accordingly. - this.subscriptions.push( + this.subscriptions.add( this.manager.stateApi.$shiftKey.listen((newVal) => { this.konva.transformer.rotationSnaps(newVal ? [0, 45, 90, 135, 180, 225, 270, 315] : []); }) ); // When the selected tool changes, we need to update the transformer's interaction state. - this.subscriptions.push( + this.subscriptions.add( this.manager.toolState.subscribe((newVal, oldVal) => { if (newVal.selected !== oldVal.selected) { this.syncInteractionState(); @@ -365,7 +365,7 @@ export class CanvasTransformer { ); // When the selected entity changes, we need to update the transformer's interaction state. - this.subscriptions.push( + this.subscriptions.add( this.manager.selectedEntityIdentifier.subscribe(() => { this.syncInteractionState(); }) From 79f216a491d9335b205d29edbdf1dafa552b1e14 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Aug 2024 18:43:42 +1000 Subject: [PATCH 292/678] feat(ui): move bbox calculation to transformer --- .../controlLayers/konva/CanvasLayer.ts | 115 ++------------- .../controlLayers/konva/CanvasTransformer.ts | 134 +++++++++++++++++- 2 files changed, 137 insertions(+), 112 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index c865aca7c3a..70fe8d41079 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -1,20 +1,19 @@ import { getStore } from 'app/store/nanostores/store'; import { deepClone } from 'common/util/deepClone'; -import { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; -import { getEmptyRect, konvaNodeToBlob, previewBlob } from 'features/controlLayers/konva/util'; +import { konvaNodeToBlob, previewBlob } from 'features/controlLayers/konva/util'; import { layerRasterized } from 'features/controlLayers/store/canvasV2Slice'; import type { CanvasLayerState, CanvasV2State, Coordinate, GetLoggingContext, - Rect, } from 'features/controlLayers/store/types'; import { imageDTOToImageObject } from 'features/controlLayers/store/types'; import Konva from 'konva'; -import { debounce, get } from 'lodash-es'; +import { get } from 'lodash-es'; import type { Logger } from 'roarr'; import { uploadImage } from 'services/api/endpoints/images'; @@ -40,10 +39,6 @@ export class CanvasLayer { isFirstRender: boolean = true; bboxNeedsUpdate: boolean = true; - isPendingBboxCalculation: boolean = false; - - rect: Rect = getEmptyRect(); - bbox: Rect = getEmptyRect(); constructor(state: CanvasLayerState, manager: CanvasManager) { this.id = state.id; @@ -105,7 +100,7 @@ export class CanvasLayer { // this.transformer.syncInteractionState(); if (this.isFirstRender) { - await this.updateBbox(); + await this.transformer.updateBbox(); } this.state = state; @@ -123,13 +118,13 @@ export class CanvasLayer { const position = get(arg, 'position', this.state.position); this.konva.objectGroup.setAttrs({ - x: position.x + this.bbox.x, - y: position.y + this.bbox.y, - offsetX: this.bbox.x, - offsetY: this.bbox.y, + x: position.x + this.transformer.pixelRect.x, + y: position.y + this.transformer.pixelRect.y, + offsetX: this.transformer.pixelRect.x, + offsetY: this.transformer.pixelRect.y, }); - this.transformer.update(position, this.bbox); + this.transformer.update(position, this.transformer.pixelRect); }; updateObjects = async (arg?: { objects: CanvasLayerState['objects'] }) => { @@ -140,7 +135,7 @@ export class CanvasLayer { const didUpdate = await this.renderer.render(objects); if (didUpdate) { - this.calculateBbox(); + this.transformer.requestRectCalculation(); } this.isFirstRender = false; @@ -152,35 +147,6 @@ export class CanvasLayer { this.konva.objectGroup.opacity(opacity); }; - updateBbox = () => { - this.log.trace('Updating bbox'); - - if (this.isPendingBboxCalculation) { - return; - } - - // If the bbox has no width or height, that means the layer is fully transparent. This can happen if it is only - // eraser lines, fully clipped brush lines or if it has been fully erased. - if (this.bbox.width === 0 || this.bbox.height === 0) { - // We shouldn't reset on the first render - the bbox will be calculated on the next render - if (!this.isFirstRender && !this.renderer.hasObjects()) { - // The layer is fully transparent but has objects - reset it - this.manager.stateApi.onEntityReset({ id: this.id }, 'layer'); - } - this.transformer.syncInteractionState(); - return; - } - - this.transformer.syncInteractionState(); - this.transformer.update(this.state.position, this.bbox); - this.konva.objectGroup.setAttrs({ - x: this.state.position.x + this.bbox.x, - y: this.state.position.y + this.bbox.y, - offsetX: this.bbox.x, - offsetY: this.bbox.y, - }); - }; - resetScale = () => { const attrs = { scaleX: 1, @@ -210,73 +176,12 @@ export class CanvasLayer { dispatch(layerRasterized({ id: this.id, imageObject, position: { x: Math.round(rect.x), y: Math.round(rect.y) } })); }; - calculateBbox = debounce(() => { - this.log.debug('Calculating bbox'); - - this.isPendingBboxCalculation = true; - - if (!this.renderer.hasObjects()) { - this.log.trace('No objects, resetting bbox'); - this.rect = getEmptyRect(); - this.bbox = getEmptyRect(); - this.isPendingBboxCalculation = false; - this.updateBbox(); - return; - } - - const rect = this.konva.objectGroup.getClientRect({ skipTransform: true }); - - if (!this.renderer.needsPixelBbox()) { - this.rect = deepClone(rect); - this.bbox = deepClone(rect); - this.isPendingBboxCalculation = false; - this.log.trace({ bbox: this.bbox, rect: this.rect }, 'Got bbox from client rect'); - this.updateBbox(); - return; - } - - // We have eraser strokes - we must calculate the bbox using pixel data - - const clone = this.konva.objectGroup.clone(); - const canvas = clone.toCanvas(); - const ctx = canvas.getContext('2d'); - if (!ctx) { - return; - } - const imageData = ctx.getImageData(0, 0, rect.width, rect.height); - this.manager.requestBbox( - { buffer: imageData.data.buffer, width: imageData.width, height: imageData.height }, - (extents) => { - if (extents) { - const { minX, minY, maxX, maxY } = extents; - this.rect = deepClone(rect); - this.bbox = { - x: rect.x + minX, - y: rect.y + minY, - width: maxX - minX, - height: maxY - minY, - }; - } else { - this.bbox = getEmptyRect(); - this.rect = getEmptyRect(); - } - this.isPendingBboxCalculation = false; - this.log.trace({ bbox: this.bbox, rect: this.rect, extents }, `Got bbox from worker`); - this.updateBbox(); - clone.destroy(); - } - ); - }, CanvasManager.BBOX_DEBOUNCE_MS); - repr = () => { return { id: this.id, type: CanvasLayer.TYPE, state: deepClone(this.state), - rect: deepClone(this.rect), - bbox: deepClone(this.bbox), bboxNeedsUpdate: this.bboxNeedsUpdate, - isPendingBboxCalculation: this.isPendingBboxCalculation, transformer: this.transformer.repr(), renderer: this.renderer.repr(), }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index 432f9c4ca25..5911a36b87b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -1,8 +1,9 @@ import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; -import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { getPrefixedId } from 'features/controlLayers/konva/util'; +import { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { getEmptyRect, getPrefixedId } from 'features/controlLayers/konva/util'; import type { Coordinate, GetLoggingContext, Rect } from 'features/controlLayers/store/types'; import Konva from 'konva'; +import { debounce } from 'lodash-es'; import type { Logger } from 'roarr'; /** @@ -36,7 +37,26 @@ export class CanvasTransformer { getLoggingContext: GetLoggingContext; /** - * A list of subscriptions that should be cleaned up when the transformer is destroyed. + * The rect of the parent, _including_ transparent regions. + * It is calculated via Konva's getClientRect method, which is fast but includes transparent regions. + */ + nodeRect = getEmptyRect(); + + /** + * The rect of the parent, _excluding_ transparent regions. + * If the parent's nodes have no possibility of transparent regions, this will be calculated the same way as nodeRect. + * If the parent's nodes may have transparent regions, this will be calculated manually by rasterizing the parent and + * checking the pixel data. + */ + pixelRect = getEmptyRect(); + + /** + * Whether the transformer is currently calculating the rect of the parent. + */ + isPendingRectCalculation: boolean = false; + + /** + * A set of subscriptions that should be cleaned up when the transformer is destroyed. */ subscriptions: Set<() => void> = new Set(); @@ -315,7 +335,7 @@ export class CanvasTransformer { }); // The object group is translated by the difference between the interaction rect's new and old positions (which is - // stored as this.bbox) + // stored as this.pixelRect) this.parent.konva.objectGroup.setAttrs({ x: this.konva.proxyRect.x(), y: this.konva.proxyRect.y(), @@ -329,8 +349,8 @@ export class CanvasTransformer { } const position = { - x: this.konva.proxyRect.x() - this.parent.bbox.x, - y: this.konva.proxyRect.y() - this.parent.bbox.y, + x: this.konva.proxyRect.x() - this.pixelRect.x, + y: this.konva.proxyRect.y() - this.pixelRect.y, }; this.log.trace({ position }, 'Position changed'); @@ -403,6 +423,13 @@ export class CanvasTransformer { syncInteractionState = () => { this.log.trace('Syncing interaction state'); + if (this.isPendingRectCalculation || this.pixelRect.width === 0 || this.pixelRect.height === 0) { + // If the rect is being calculated, or if the rect has no width or height, we can't interact with the transformer + this.parent.konva.layer.listening(false); + this.setInteractionMode('off'); + return; + } + const toolState = this.manager.stateApi.getToolState(); const isSelected = this.manager.stateApi.getIsSelected(this.parent.id); @@ -486,7 +513,7 @@ export class CanvasTransformer { this.setInteractionMode('off'); this.parent.resetScale(); this.parent.updatePosition(); - this.parent.updateBbox(); + this.updateBbox(); this.syncInteractionState(); }; @@ -514,6 +541,99 @@ export class CanvasTransformer { } }; + updateBbox = () => { + this.log.trace('Updating bbox'); + + if (this.isPendingRectCalculation) { + this.syncInteractionState(); + return; + } + + // If the bbox has no width or height, that means the layer is fully transparent. This can happen if it is only + // eraser lines, fully clipped brush lines or if it has been fully erased. + if (this.pixelRect.width === 0 || this.pixelRect.height === 0) { + // We shouldn't reset on the first render - the bbox will be calculated on the next render + if (!this.parent.renderer.hasObjects()) { + // The layer is fully transparent but has objects - reset it + this.manager.stateApi.onEntityReset({ id: this.parent.id }, this.parent.type); + } + this.syncInteractionState(); + return; + } + + this.syncInteractionState(); + this.update(this.parent.state.position, this.pixelRect); + this.parent.konva.objectGroup.setAttrs({ + x: this.parent.state.position.x + this.pixelRect.x, + y: this.parent.state.position.y + this.pixelRect.y, + offsetX: this.pixelRect.x, + offsetY: this.pixelRect.y, + }); + }; + + calculateRect = debounce(() => { + this.log.debug('Calculating bbox'); + + this.isPendingRectCalculation = true; + + if (!this.parent.renderer.hasObjects()) { + this.log.trace('No objects, resetting bbox'); + this.nodeRect = getEmptyRect(); + this.pixelRect = getEmptyRect(); + this.isPendingRectCalculation = false; + this.updateBbox(); + return; + } + + const rect = this.parent.konva.objectGroup.getClientRect({ skipTransform: true }); + + if (!this.parent.renderer.needsPixelBbox()) { + this.nodeRect = { ...rect }; + this.pixelRect = { ...rect }; + this.isPendingRectCalculation = false; + this.log.trace({ nodeRect: this.nodeRect, pixelRect: this.pixelRect }, 'Got bbox from client rect'); + this.updateBbox(); + return; + } + + // We have eraser strokes - we must calculate the bbox using pixel data + + const clone = this.parent.konva.objectGroup.clone(); + const canvas = clone.toCanvas(); + const ctx = canvas.getContext('2d'); + if (!ctx) { + return; + } + const imageData = ctx.getImageData(0, 0, rect.width, rect.height); + this.manager.requestBbox( + { buffer: imageData.data.buffer, width: imageData.width, height: imageData.height }, + (extents) => { + if (extents) { + const { minX, minY, maxX, maxY } = extents; + this.nodeRect = { ...rect }; + this.pixelRect = { + x: rect.x + minX, + y: rect.y + minY, + width: maxX - minX, + height: maxY - minY, + }; + } else { + this.nodeRect = getEmptyRect(); + this.pixelRect = getEmptyRect(); + } + this.isPendingRectCalculation = false; + this.log.trace({ nodeRect: this.nodeRect, pixelRect: this.pixelRect, extents }, `Got bbox from worker`); + this.updateBbox(); + clone.destroy(); + } + ); + }, CanvasManager.BBOX_DEBOUNCE_MS); + + requestRectCalculation = () => { + this.isPendingRectCalculation = true; + this.calculateRect(); + }; + _enableTransform = () => { this.isTransformEnabled = true; this.konva.transformer.visible(true); From 84d15b0c7fadb24d12fd1542fa8e8cc7e6c4d0a4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Aug 2024 18:53:34 +1000 Subject: [PATCH 293/678] fix(ui): request rect calc immediately on transform, hiding rect --- .../web/src/features/controlLayers/konva/CanvasTransformer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index 5911a36b87b..d73f7ed2b12 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -499,6 +499,7 @@ export class CanvasTransformer { applyTransform = async () => { this.log.debug('Applying transform'); await this.parent.rasterize(); + this.requestRectCalculation(); this.stopTransform(); }; From e5a7e932cfd9266ba690685a34b93eac21992fce Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Aug 2024 20:09:28 +1000 Subject: [PATCH 294/678] tidy(ui): rename union CanvasEntity -> CanvasEntityState --- .../src/common/hooks/useIsReadyToEnqueue.ts | 4 ++-- .../controlLayers/konva/CanvasManager.ts | 4 ++-- .../controlLayers/konva/CanvasStateApi.ts | 24 +++++++++---------- .../controlLayers/konva/entityBbox.ts | 6 ++--- .../src/features/controlLayers/store/types.ts | 8 +++---- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index 289132c717c..bf8bf22d08c 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; -import type { CanvasEntity } from 'features/controlLayers/store/types'; +import type { CanvasEntityState } from 'features/controlLayers/store/types'; import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; @@ -18,7 +18,7 @@ import { forEach, upperFirst } from 'lodash-es'; import { useMemo } from 'react'; import { getConnectedEdges } from 'reactflow'; -const LAYER_TYPE_TO_TKEY: Record = { +const LAYER_TYPE_TO_TKEY: Record = { control_adapter: 'controlLayers.globalControlAdapter', ip_adapter: 'controlLayers.globalIPAdapter', regional_guidance: 'controlLayers.regionalGuidance', diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 176455ea035..c9924ce3833 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -25,8 +25,8 @@ import type { Extents, ExtentsResult, GetBboxTask, WorkerLogMessage } from 'feat import { $lastProgressEvent, $shouldShowStagedImage } from 'features/controlLayers/store/canvasV2Slice'; import type { CanvasControlAdapterState, - CanvasEntity, CanvasEntityIdentifier, + CanvasEntityState, CanvasInpaintMaskState, CanvasLayerState, CanvasRegionalGuidanceState, @@ -334,7 +334,7 @@ export class CanvasManager { getEntity(identifier: CanvasEntityIdentifier): EntityStateAndAdapter | null { const state = this.stateApi.getState(); - let entityState: CanvasEntity | null = null; + let entityState: CanvasEntityState | null = null; let entityAdapter: CanvasLayer | CanvasRegion | CanvasControlAdapter | CanvasInpaintMask | null = null; if (identifier.type === 'layer') { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index 6b4be9ac6dd..581d0f9a5a6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -47,7 +47,7 @@ import { import type { BboxChangedArg, CanvasBrushLineState, - CanvasEntity, + CanvasEntityState, CanvasEraserLineState, CanvasRectState, PositionChangedArg, @@ -59,29 +59,26 @@ import type { ImageDTO } from 'services/api/types'; const log = logger('canvas'); - export class CanvasStateApi { _store: Store; manager: CanvasManager; - constructor(store: Store, manager: CanvasManager) { this._store = store; this.manager = manager; - } // Reminder - use arrow functions to avoid binding issues getState = () => { return this._store.getState().canvasV2; }; - onEntityReset = (arg: { id: string }, entityType: CanvasEntity['type']) => { + onEntityReset = (arg: { id: string }, entityType: CanvasEntityState['type']) => { log.debug('onEntityReset'); if (entityType === 'layer') { this._store.dispatch(layerReset(arg)); } }; - onPosChanged = (arg: PositionChangedArg, entityType: CanvasEntity['type']) => { + onPosChanged = (arg: PositionChangedArg, entityType: CanvasEntityState['type']) => { log.debug('onPosChanged'); if (entityType === 'layer') { this._store.dispatch(layerTranslated(arg)); @@ -93,7 +90,7 @@ export class CanvasStateApi { this._store.dispatch(caTranslated(arg)); } }; - onScaleChanged = (arg: ScaleChangedArg, entityType: CanvasEntity['type']) => { + onScaleChanged = (arg: ScaleChangedArg, entityType: CanvasEntityState['type']) => { log.debug('onScaleChanged'); if (entityType === 'inpaint_mask') { this._store.dispatch(imScaled(arg)); @@ -103,7 +100,7 @@ export class CanvasStateApi { this._store.dispatch(caScaled(arg)); } }; - onBboxChanged = (arg: BboxChangedArg, entityType: CanvasEntity['type']) => { + onBboxChanged = (arg: BboxChangedArg, entityType: CanvasEntityState['type']) => { log.debug('Entity bbox changed'); if (entityType === 'layer') { this._store.dispatch(layerBboxChanged(arg)); @@ -115,7 +112,7 @@ export class CanvasStateApi { this._store.dispatch(imBboxChanged(arg)); } }; - onBrushLineAdded = (arg: { id: string; brushLine: CanvasBrushLineState }, entityType: CanvasEntity['type']) => { + onBrushLineAdded = (arg: { id: string; brushLine: CanvasBrushLineState }, entityType: CanvasEntityState['type']) => { log.debug('Brush line added'); if (entityType === 'layer') { this._store.dispatch(layerBrushLineAdded(arg)); @@ -125,7 +122,10 @@ export class CanvasStateApi { this._store.dispatch(imBrushLineAdded(arg)); } }; - onEraserLineAdded = (arg: { id: string; eraserLine: CanvasEraserLineState }, entityType: CanvasEntity['type']) => { + onEraserLineAdded = ( + arg: { id: string; eraserLine: CanvasEraserLineState }, + entityType: CanvasEntityState['type'] + ) => { log.debug('Eraser line added'); if (entityType === 'layer') { this._store.dispatch(layerEraserLineAdded(arg)); @@ -135,7 +135,7 @@ export class CanvasStateApi { this._store.dispatch(imEraserLineAdded(arg)); } }; - onRectShapeAdded = (arg: { id: string; rectShape: CanvasRectState }, entityType: CanvasEntity['type']) => { + onRectShapeAdded = (arg: { id: string; rectShape: CanvasRectState }, entityType: CanvasEntityState['type']) => { log.debug('Rect shape added'); if (entityType === 'layer') { this._store.dispatch(layerRectShapeAdded(arg)); @@ -145,7 +145,7 @@ export class CanvasStateApi { this._store.dispatch(imRectShapeAdded(arg)); } }; - onEntitySelected = (arg: { id: string; type: CanvasEntity['type'] }) => { + onEntitySelected = (arg: { id: string; type: CanvasEntityState['type'] }) => { log.debug('Entity selected'); this._store.dispatch(entitySelected(arg)); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts index c4e8bfe9df3..8e8763460bc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts @@ -3,8 +3,8 @@ import { getLayerBboxId } from 'features/controlLayers/konva/naming'; import { imageDataToDataURL } from 'features/controlLayers/konva/util'; import type { BboxChangedArg, - CanvasEntity, CanvasControlAdapterState, + CanvasEntityState, CanvasLayerState, CanvasRegionalGuidanceState, } from 'features/controlLayers/store/types'; @@ -17,7 +17,7 @@ import { assert } from 'tsafe'; * @param entity The layer state for the layer to create the bounding box for * @param konvaLayer The konva layer to attach the bounding box to */ -export const createBboxRect = (entity: CanvasEntity, konvaLayer: Konva.Layer): Konva.Rect => { +export const createBboxRect = (entity: CanvasEntityState, konvaLayer: Konva.Layer): Konva.Rect => { const rect = new Konva.Rect({ id: getLayerBboxId(entity.id), name: 'bbox', @@ -201,7 +201,7 @@ export const updateBboxes = ( layers: CanvasLayerState[], controlAdapters: CanvasControlAdapterState[], regions: CanvasRegionalGuidanceState[], - onBboxChanged: (arg: BboxChangedArg, entityType: CanvasEntity['type']) => void + onBboxChanged: (arg: BboxChangedArg, entityType: CanvasEntityState['type']) => void ): void => { for (const entityState of [...layers, ...controlAdapters, ...regions]) { const konvaLayer = stage.findOne(`#${entityState.id}`); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index a77e6255cd7..00a1bbaf6ad 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -819,14 +819,14 @@ export type BoundingBoxScaleMethod = z.infer; export const isBoundingBoxScaleMethod = (v: unknown): v is BoundingBoxScaleMethod => zBoundingBoxScaleMethod.safeParse(v).success; -export type CanvasEntity = +export type CanvasEntityState = | CanvasLayerState | CanvasControlAdapterState | CanvasRegionalGuidanceState | CanvasInpaintMaskState | CanvasIPAdapterState | InitialImageEntity; -export type CanvasEntityIdentifier = Pick; +export type CanvasEntityIdentifier = Pick; export type LoRA = { id: string; @@ -967,7 +967,7 @@ export type RemoveIndexString = { export type GenerationMode = 'txt2img' | 'img2img' | 'inpaint' | 'outpaint'; export function isDrawableEntity( - entity: CanvasEntity + entity: CanvasEntityState ): entity is CanvasLayerState | CanvasRegionalGuidanceState | CanvasInpaintMaskState { return entity.type === 'layer' || entity.type === 'regional_guidance' || entity.type === 'inpaint_mask'; } @@ -979,7 +979,7 @@ export function isDrawableEntityAdapter( } export function isDrawableEntityType( - entityType: CanvasEntity['type'] + entityType: CanvasEntityState['type'] ): entityType is 'layer' | 'regional_guidance' | 'inpaint_mask' { return entityType === 'layer' || entityType === 'regional_guidance' || entityType === 'inpaint_mask'; } From 3c9e9fa746ea202766e529397965509a4ae7859d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Aug 2024 20:16:59 +1000 Subject: [PATCH 295/678] fix(ui): sync transformer when requesting bbox calc --- .../web/src/features/controlLayers/konva/CanvasTransformer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index d73f7ed2b12..02577a91808 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -632,6 +632,7 @@ export class CanvasTransformer { requestRectCalculation = () => { this.isPendingRectCalculation = true; + this.syncInteractionState(); this.calculateRect(); }; From 25158d4d924cbf4fade4a7be18745ccb816934ca Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Aug 2024 20:23:36 +1000 Subject: [PATCH 296/678] fix(ui): commit drawing buffer on tool change, fixing bbox not calculating --- .../konva/CanvasObjectRenderer.ts | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index 303864e0253..ee211eb6265 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -37,6 +37,11 @@ export class CanvasObjectRenderer { log: Logger; getLoggingContext: (extra?: JSONObject) => JSONObject; + /** + * A set of subscriptions that should be cleaned up when the transformer is destroyed. + */ + subscriptions: Set<() => void> = new Set(); + /** * 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 @@ -56,6 +61,14 @@ export class CanvasObjectRenderer { this.getLoggingContext = this.manager.buildGetLoggingContext(this); this.log = this.manager.buildLogger(this.getLoggingContext); this.log.trace('Creating object renderer'); + + this.subscriptions.add( + this.manager.toolState.subscribe((newVal, oldVal) => { + if (newVal.selected !== oldVal.selected) { + this.commitBuffer(); + } + }) + ); } /** @@ -160,6 +173,8 @@ export class CanvasObjectRenderer { * @returns A promise that resolves to a boolean, indicating if the object was rendered. */ setBuffer = async (objectState: AnyObjectState): Promise => { + this.log.trace('Setting buffer object'); + this.buffer = objectState; return await this.renderObject(this.buffer, true); }; @@ -168,6 +183,8 @@ export class CanvasObjectRenderer { * Clears the buffer object state. */ clearBuffer = () => { + this.log.trace('Clearing buffer object'); + this.buffer = null; }; @@ -176,10 +193,12 @@ export class CanvasObjectRenderer { */ commitBuffer = () => { if (!this.buffer) { - this.log.warn('No buffer object to commit'); + this.log.trace('No buffer object to commit'); return; } + this.log.trace('Committing buffer object'); + // We need to give the objects a fresh ID else they will be considered the same object when they are re-rendered as // a non-buffer object, and we won't trigger things like bbox calculation this.buffer.id = getPrefixedId(this.buffer.type); @@ -234,6 +253,10 @@ export class CanvasObjectRenderer { */ destroy = () => { this.log.trace('Destroying object renderer'); + for (const cleanup of this.subscriptions) { + this.log.trace('Cleaning up listener'); + cleanup(); + } for (const renderer of this.renderers.values()) { renderer.destroy(); } From 5a2b00fb95fd49960a188fd218594f30a8c8a017 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Aug 2024 20:32:09 +1000 Subject: [PATCH 297/678] tidy(ui): use imperative names for setters in stateapi --- .../controlLayers/konva/CanvasBbox.ts | 4 +- .../controlLayers/konva/CanvasInpaintMask.ts | 6 +- .../konva/CanvasObjectRenderer.ts | 4 +- .../controlLayers/konva/CanvasRegion.ts | 6 +- .../controlLayers/konva/CanvasStateApi.ts | 88 ++++++------------- .../controlLayers/konva/CanvasTransformer.ts | 4 +- .../features/controlLayers/konva/events.ts | 4 +- .../src/features/controlLayers/konva/util.ts | 6 +- 8 files changed, 44 insertions(+), 78 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts index 6f7ff99593e..c9331df1908 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts @@ -116,7 +116,7 @@ export class CanvasBbox { }; this.konva.rect.setAttrs(bboxRect); if (bbox.rect.x !== bboxRect.x || bbox.rect.y !== bboxRect.y) { - this.manager.stateApi.onBboxTransformed(bboxRect); + this.manager.stateApi.setGenerationBbox(bboxRect); } }); @@ -196,7 +196,7 @@ export class CanvasBbox { this.konva.rect.setAttrs({ ...bboxRect, scaleX: 1, scaleY: 1 }); // Update the bbox in internal state. - this.manager.stateApi.onBboxTransformed(bboxRect); + this.manager.stateApi.setGenerationBbox(bboxRect); // Update the aspect ratio buffer whenever the shift key is not held - this allows for a nice UX where you can start // a transform, get the right aspect ratio, then hold shift to lock it in. diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts index 5d354ca1693..dcfd9f63d2d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts @@ -72,7 +72,7 @@ export class CanvasInpaintMask { ); }); this.konva.transformer.on('dragend', () => { - this.manager.stateApi.onPosChanged( + this.manager.stateApi.setEntityPosition( { id: this.id, position: { x: this.konva.group.x(), y: this.konva.group.y() } }, 'inpaint_mask' ); @@ -114,9 +114,9 @@ export class CanvasInpaintMask { if (this.drawingBuffer.type === 'brush_line') { this.manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: this.drawingBuffer }, 'inpaint_mask'); } else if (this.drawingBuffer.type === 'eraser_line') { - this.manager.stateApi.onEraserLineAdded({ id: this.id, eraserLine: this.drawingBuffer }, 'inpaint_mask'); + this.manager.stateApi.addEraserLine({ id: this.id, eraserLine: this.drawingBuffer }, 'inpaint_mask'); } else if (this.drawingBuffer.type === 'rect') { - this.manager.stateApi.onRectShapeAdded({ id: this.id, rectShape: this.drawingBuffer }, 'inpaint_mask'); + this.manager.stateApi.addRect({ id: this.id, rectShape: this.drawingBuffer }, 'inpaint_mask'); } this.setDrawingBuffer(null); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index ee211eb6265..af0963baf19 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -206,9 +206,9 @@ export class CanvasObjectRenderer { if (this.buffer.type === 'brush_line') { this.manager.stateApi.onBrushLineAdded({ id: this.parent.id, brushLine: this.buffer }, 'layer'); } else if (this.buffer.type === 'eraser_line') { - this.manager.stateApi.onEraserLineAdded({ id: this.parent.id, eraserLine: this.buffer }, 'layer'); + this.manager.stateApi.addEraserLine({ id: this.parent.id, eraserLine: this.buffer }, 'layer'); } else if (this.buffer.type === 'rect') { - this.manager.stateApi.onRectShapeAdded({ id: this.parent.id, rectShape: this.buffer }, 'layer'); + this.manager.stateApi.addRect({ id: this.parent.id, rectShape: this.buffer }, 'layer'); } else { this.log.warn({ buffer: this.buffer }, 'Invalid buffer object type'); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts index 833314ae022..927ee87c91a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts @@ -73,7 +73,7 @@ export class CanvasRegion { ); }); this.konva.transformer.on('dragend', () => { - this.manager.stateApi.onPosChanged( + this.manager.stateApi.setEntityPosition( { id: this.id, position: { x: this.konva.group.x(), y: this.konva.group.y() } }, 'regional_guidance' ); @@ -114,9 +114,9 @@ export class CanvasRegion { if (this.drawingBuffer.type === 'brush_line') { this.manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: this.drawingBuffer }, 'regional_guidance'); } else if (this.drawingBuffer.type === 'eraser_line') { - this.manager.stateApi.onEraserLineAdded({ id: this.id, eraserLine: this.drawingBuffer }, 'regional_guidance'); + this.manager.stateApi.addEraserLine({ id: this.id, eraserLine: this.drawingBuffer }, 'regional_guidance'); } else if (this.drawingBuffer.type === 'rect') { - this.manager.stateApi.onRectShapeAdded({ id: this.id, rectShape: this.drawingBuffer }, 'regional_guidance'); + this.manager.stateApi.addRect({ id: this.id, rectShape: this.drawingBuffer }, 'regional_guidance'); } this.setDrawingBuffer(null); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index 581d0f9a5a6..b1e615f0410 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -15,46 +15,37 @@ import { $stageAttrs, bboxChanged, brushWidthChanged, - caBboxChanged, - caScaled, caTranslated, entitySelected, eraserWidthChanged, - imBboxChanged, imBrushLineAdded, imEraserLineAdded, imImageCacheChanged, imRectShapeAdded, - imScaled, imTranslated, - layerBboxChanged, layerBrushLineAdded, layerEraserLineAdded, layerImageCacheChanged, layerRectShapeAdded, layerReset, layerTranslated, - rgBboxChanged, rgBrushLineAdded, rgEraserLineAdded, rgImageCacheChanged, rgRectShapeAdded, - rgScaled, rgTranslated, toolBufferChanged, toolChanged, } from 'features/controlLayers/store/canvasV2Slice'; import type { - BboxChangedArg, CanvasBrushLineState, CanvasEntityState, CanvasEraserLineState, CanvasRectState, PositionChangedArg, - ScaleChangedArg, + Rect, Tool, } from 'features/controlLayers/store/types'; -import type { IRect } from 'konva/lib/types'; import type { ImageDTO } from 'services/api/types'; const log = logger('canvas'); @@ -72,14 +63,14 @@ export class CanvasStateApi { getState = () => { return this._store.getState().canvasV2; }; - onEntityReset = (arg: { id: string }, entityType: CanvasEntityState['type']) => { - log.debug('onEntityReset'); + resetEntity = (arg: { id: string }, entityType: CanvasEntityState['type']) => { + log.trace({ arg, entityType }, 'Resetting entity'); if (entityType === 'layer') { this._store.dispatch(layerReset(arg)); } }; - onPosChanged = (arg: PositionChangedArg, entityType: CanvasEntityState['type']) => { - log.debug('onPosChanged'); + setEntityPosition = (arg: PositionChangedArg, entityType: CanvasEntityState['type']) => { + log.trace({ arg, entityType }, 'Setting entity position'); if (entityType === 'layer') { this._store.dispatch(layerTranslated(arg)); } else if (entityType === 'regional_guidance') { @@ -90,30 +81,8 @@ export class CanvasStateApi { this._store.dispatch(caTranslated(arg)); } }; - onScaleChanged = (arg: ScaleChangedArg, entityType: CanvasEntityState['type']) => { - log.debug('onScaleChanged'); - if (entityType === 'inpaint_mask') { - this._store.dispatch(imScaled(arg)); - } else if (entityType === 'regional_guidance') { - this._store.dispatch(rgScaled(arg)); - } else if (entityType === 'control_adapter') { - this._store.dispatch(caScaled(arg)); - } - }; - onBboxChanged = (arg: BboxChangedArg, entityType: CanvasEntityState['type']) => { - log.debug('Entity bbox changed'); - if (entityType === 'layer') { - this._store.dispatch(layerBboxChanged(arg)); - } else if (entityType === 'control_adapter') { - this._store.dispatch(caBboxChanged(arg)); - } else if (entityType === 'regional_guidance') { - this._store.dispatch(rgBboxChanged(arg)); - } else if (entityType === 'inpaint_mask') { - this._store.dispatch(imBboxChanged(arg)); - } - }; - onBrushLineAdded = (arg: { id: string; brushLine: CanvasBrushLineState }, entityType: CanvasEntityState['type']) => { - log.debug('Brush line added'); + addBrushLine = (arg: { id: string; brushLine: CanvasBrushLineState }, entityType: CanvasEntityState['type']) => { + log.trace({ arg, entityType }, 'Adding brush line'); if (entityType === 'layer') { this._store.dispatch(layerBrushLineAdded(arg)); } else if (entityType === 'regional_guidance') { @@ -122,11 +91,8 @@ export class CanvasStateApi { this._store.dispatch(imBrushLineAdded(arg)); } }; - onEraserLineAdded = ( - arg: { id: string; eraserLine: CanvasEraserLineState }, - entityType: CanvasEntityState['type'] - ) => { - log.debug('Eraser line added'); + addEraserLine = (arg: { id: string; eraserLine: CanvasEraserLineState }, entityType: CanvasEntityState['type']) => { + log.trace({ arg, entityType }, 'Adding eraser line'); if (entityType === 'layer') { this._store.dispatch(layerEraserLineAdded(arg)); } else if (entityType === 'regional_guidance') { @@ -135,8 +101,8 @@ export class CanvasStateApi { this._store.dispatch(imEraserLineAdded(arg)); } }; - onRectShapeAdded = (arg: { id: string; rectShape: CanvasRectState }, entityType: CanvasEntityState['type']) => { - log.debug('Rect shape added'); + addRect = (arg: { id: string; rectShape: CanvasRectState }, entityType: CanvasEntityState['type']) => { + log.trace({ arg, entityType }, 'Adding rect'); if (entityType === 'layer') { this._store.dispatch(layerRectShapeAdded(arg)); } else if (entityType === 'regional_guidance') { @@ -145,40 +111,40 @@ export class CanvasStateApi { this._store.dispatch(imRectShapeAdded(arg)); } }; - onEntitySelected = (arg: { id: string; type: CanvasEntityState['type'] }) => { - log.debug('Entity selected'); + setSelectedEntity = (arg: { id: string; type: CanvasEntityState['type'] }) => { + log.trace({ arg }, 'Setting selected entity'); this._store.dispatch(entitySelected(arg)); }; - onBboxTransformed = (bbox: IRect) => { - log.debug('Generation bbox transformed'); + setGenerationBbox = (bbox: Rect) => { + log.trace({ bbox }, 'Setting generation bbox'); this._store.dispatch(bboxChanged(bbox)); }; - onBrushWidthChanged = (width: number) => { - log.debug('Brush width changed'); + setBrushWidth = (width: number) => { + log.trace({ width }, 'Setting brush width'); this._store.dispatch(brushWidthChanged(width)); }; - onEraserWidthChanged = (width: number) => { - log.debug('Eraser width changed'); + setEraserWidth = (width: number) => { + log.trace({ width }, 'Setting eraser width'); this._store.dispatch(eraserWidthChanged(width)); }; - onRegionMaskImageCached = (id: string, imageDTO: ImageDTO) => { - log.debug('Region mask image cached'); + setRegionMaskImageCache = (id: string, imageDTO: ImageDTO) => { + log.trace({ id, imageDTO }, 'Setting region mask image cache'); this._store.dispatch(rgImageCacheChanged({ id, imageDTO })); }; - onInpaintMaskImageCached = (imageDTO: ImageDTO) => { - log.debug('Inpaint mask image cached'); + setInpaintMaskImageCache = (imageDTO: ImageDTO) => { + log.trace({ imageDTO }, 'Setting inpaint mask image cache'); this._store.dispatch(imImageCacheChanged({ imageDTO })); }; - onLayerImageCached = (imageDTO: ImageDTO) => { - log.debug('Layer image cached'); + setLayerImageCache = (imageDTO: ImageDTO) => { + log.trace({ imageDTO }, 'Setting layer image cache'); this._store.dispatch(layerImageCacheChanged({ imageDTO })); }; setTool = (tool: Tool) => { - log.debug('Tool selection changed'); + log.trace({ tool }, 'Setting tool'); this._store.dispatch(toolChanged(tool)); }; setToolBuffer = (toolBuffer: Tool | null) => { - log.debug('Tool buffer changed'); + log.trace({ toolBuffer }, 'Setting tool buffer'); this._store.dispatch(toolBufferChanged(toolBuffer)); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index 02577a91808..ad4b82b6635 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -354,7 +354,7 @@ export class CanvasTransformer { }; this.log.trace({ position }, 'Position changed'); - this.manager.stateApi.onPosChanged({ id: this.parent.id, position }, 'layer'); + this.manager.stateApi.setEntityPosition({ id: this.parent.id, position }, 'layer'); }); this.subscriptions.add( @@ -556,7 +556,7 @@ export class CanvasTransformer { // We shouldn't reset on the first render - the bbox will be calculated on the next render if (!this.parent.renderer.hasObjects()) { // The layer is fully transparent but has objects - reset it - this.manager.stateApi.onEntityReset({ id: this.parent.id }, this.parent.type); + this.manager.stateApi.resetEntity({ id: this.parent.id }, this.parent.type); } this.syncInteractionState(); return; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 3355052aaab..992e4cbbf5e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -128,8 +128,8 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { $spaceKey, getBbox, getSettings, - onBrushWidthChanged, - onEraserWidthChanged, + setBrushWidth: onBrushWidthChanged, + setEraserWidth: onEraserWidthChanged, } = stateApi; function getIsPrimaryMouseDown(e: KonvaEventObject) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index 120799d2fad..ff663244553 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -480,7 +480,7 @@ export async function getRegionMaskImage(arg: { layerClone.destroy(); const imageDTO = await manager.util.uploadImage(blob, `${region.id}_mask.png`, 'mask', true); - manager.stateApi.onRegionMaskImageCached(region.id, imageDTO); + manager.stateApi.setRegionMaskImageCache(region.id, imageDTO); return imageDTO; } @@ -568,7 +568,7 @@ export async function getInpaintMaskImage(arg: { layerClone.destroy(); const imageDTO = await manager.util.uploadImage(blob, 'inpaint_mask.png', 'mask', true); - manager.stateApi.onInpaintMaskImageCached(imageDTO); + manager.stateApi.setInpaintMaskImageCache(imageDTO); return imageDTO; } @@ -598,7 +598,7 @@ export async function getCompositeLayerImage(arg: { stageClone.destroy(); const imageDTO = await manager.util.uploadImage(blob, 'base_layer.png', 'general', true); - manager.stateApi.onLayerImageCached(imageDTO); + manager.stateApi.setLayerImageCache(imageDTO); return imageDTO; } From 8f780e79e76506b3f575827ecdf51fe1fab3c6d0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Aug 2024 20:46:00 +1000 Subject: [PATCH 298/678] tidy(ui): more imperative naming --- .../controlLayers/konva/CanvasInpaintMask.ts | 4 +-- .../controlLayers/konva/CanvasLayer.ts | 8 +++--- .../konva/CanvasObjectRenderer.ts | 4 +-- .../controlLayers/konva/CanvasRegion.ts | 4 +-- .../controlLayers/konva/CanvasStateApi.ts | 28 +++++++++++++------ .../controlLayers/store/canvasV2Slice.ts | 6 ++-- .../store/inpaintMaskReducers.ts | 10 +++---- .../controlLayers/store/layersReducers.ts | 17 ++++++----- .../controlLayers/store/regionsReducers.ts | 10 +++---- .../src/features/controlLayers/store/types.ts | 15 ++++------ 10 files changed, 56 insertions(+), 50 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts index dcfd9f63d2d..cede2c9928f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts @@ -112,11 +112,11 @@ export class CanvasInpaintMask { return; } if (this.drawingBuffer.type === 'brush_line') { - this.manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: this.drawingBuffer }, 'inpaint_mask'); + this.manager.stateApi.addBrushLine({ id: this.id, brushLine: this.drawingBuffer }, 'inpaint_mask'); } else if (this.drawingBuffer.type === 'eraser_line') { this.manager.stateApi.addEraserLine({ id: this.id, eraserLine: this.drawingBuffer }, 'inpaint_mask'); } else if (this.drawingBuffer.type === 'rect') { - this.manager.stateApi.addRect({ id: this.id, rectShape: this.drawingBuffer }, 'inpaint_mask'); + this.manager.stateApi.addRect({ id: this.id, rect: this.drawingBuffer }, 'inpaint_mask'); } this.setDrawingBuffer(null); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 70fe8d41079..c2e0a344ecd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -1,10 +1,8 @@ -import { getStore } from 'app/store/nanostores/store'; import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; import { konvaNodeToBlob, previewBlob } from 'features/controlLayers/konva/util'; -import { layerRasterized } from 'features/controlLayers/store/canvasV2Slice'; import type { CanvasLayerState, CanvasV2State, @@ -169,11 +167,13 @@ export class CanvasLayer { previewBlob(blob, 'Rasterized layer'); } const imageDTO = await uploadImage(blob, `${this.id}_rasterized.png`, 'other', true); - const { dispatch } = getStore(); const imageObject = imageDTOToImageObject(imageDTO); await this.renderer.renderObject(imageObject, true); this.resetScale(); - dispatch(layerRasterized({ id: this.id, imageObject, position: { x: Math.round(rect.x), y: Math.round(rect.y) } })); + this.manager.stateApi.rasterizeEntity( + { id: this.id, imageObject, position: { x: Math.round(rect.x), y: Math.round(rect.y) } }, + this.type + ); }; repr = () => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index af0963baf19..7e6f7ac34a1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -204,11 +204,11 @@ export class CanvasObjectRenderer { this.buffer.id = getPrefixedId(this.buffer.type); if (this.buffer.type === 'brush_line') { - this.manager.stateApi.onBrushLineAdded({ id: this.parent.id, brushLine: this.buffer }, 'layer'); + this.manager.stateApi.addBrushLine({ id: this.parent.id, brushLine: this.buffer }, 'layer'); } else if (this.buffer.type === 'eraser_line') { this.manager.stateApi.addEraserLine({ id: this.parent.id, eraserLine: this.buffer }, 'layer'); } else if (this.buffer.type === 'rect') { - this.manager.stateApi.addRect({ id: this.parent.id, rectShape: this.buffer }, 'layer'); + this.manager.stateApi.addRect({ id: this.parent.id, rect: this.buffer }, 'layer'); } else { this.log.warn({ buffer: this.buffer }, 'Invalid buffer object type'); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts index 927ee87c91a..74a0cdd6de2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts @@ -112,11 +112,11 @@ export class CanvasRegion { return; } if (this.drawingBuffer.type === 'brush_line') { - this.manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: this.drawingBuffer }, 'regional_guidance'); + this.manager.stateApi.addBrushLine({ id: this.id, brushLine: this.drawingBuffer }, 'regional_guidance'); } else if (this.drawingBuffer.type === 'eraser_line') { this.manager.stateApi.addEraserLine({ id: this.id, eraserLine: this.drawingBuffer }, 'regional_guidance'); } else if (this.drawingBuffer.type === 'rect') { - this.manager.stateApi.addRect({ id: this.id, rectShape: this.drawingBuffer }, 'regional_guidance'); + this.manager.stateApi.addRect({ id: this.id, rect: this.drawingBuffer }, 'regional_guidance'); } this.setDrawingBuffer(null); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index b1e615f0410..0ed2359aadc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -21,32 +21,36 @@ import { imBrushLineAdded, imEraserLineAdded, imImageCacheChanged, - imRectShapeAdded, + imRectAdded, imTranslated, layerBrushLineAdded, layerEraserLineAdded, layerImageCacheChanged, - layerRectShapeAdded, + layerRasterized, + layerRectAdded, layerReset, layerTranslated, rgBrushLineAdded, rgEraserLineAdded, rgImageCacheChanged, - rgRectShapeAdded, + rgRectAdded, rgTranslated, toolBufferChanged, toolChanged, } from 'features/controlLayers/store/canvasV2Slice'; import type { CanvasBrushLineState, + CanvasEntityIdentifier, CanvasEntityState, CanvasEraserLineState, CanvasRectState, + EntityRasterizedArg, PositionChangedArg, Rect, Tool, } from 'features/controlLayers/store/types'; import type { ImageDTO } from 'services/api/types'; +import { assert } from 'tsafe'; const log = logger('canvas'); @@ -101,17 +105,25 @@ export class CanvasStateApi { this._store.dispatch(imEraserLineAdded(arg)); } }; - addRect = (arg: { id: string; rectShape: CanvasRectState }, entityType: CanvasEntityState['type']) => { + addRect = (arg: { id: string; rect: CanvasRectState }, entityType: CanvasEntityState['type']) => { log.trace({ arg, entityType }, 'Adding rect'); if (entityType === 'layer') { - this._store.dispatch(layerRectShapeAdded(arg)); + this._store.dispatch(layerRectAdded(arg)); } else if (entityType === 'regional_guidance') { - this._store.dispatch(rgRectShapeAdded(arg)); + this._store.dispatch(rgRectAdded(arg)); } else if (entityType === 'inpaint_mask') { - this._store.dispatch(imRectShapeAdded(arg)); + this._store.dispatch(imRectAdded(arg)); } }; - setSelectedEntity = (arg: { id: string; type: CanvasEntityState['type'] }) => { + rasterizeEntity = (arg: EntityRasterizedArg, entityType: CanvasEntityState['type']) => { + log.trace({ arg, entityType }, 'Rasterizing entity'); + if (entityType === 'layer') { + this._store.dispatch(layerRasterized(arg)); + } else { + assert(false, 'Rasterizing not supported for this entity type'); + } + }; + setSelectedEntity = (arg: CanvasEntityIdentifier) => { log.trace({ arg }, 'Setting selected entity'); this._store.dispatch(entitySelected(arg)); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 9435358d927..c217ed06ac1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -222,7 +222,7 @@ export const { layerImageCacheChanged, layerBrushLineAdded, layerEraserLineAdded, - layerRectShapeAdded, + layerRectAdded, layerRasterized, // IP Adapters ipaAdded, @@ -288,7 +288,7 @@ export const { rgScaled, rgBrushLineAdded, rgEraserLineAdded, - rgRectShapeAdded, + rgRectAdded, // Compositing setInfillMethod, setInfillTileSize, @@ -344,7 +344,7 @@ export const { imScaled, imBrushLineAdded, imEraserLineAdded, - imRectShapeAdded, + imRectAdded, // Staging sessionStarted, sessionStartedStaging, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts index ac12068505a..8a94e523d86 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts @@ -1,11 +1,11 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import type { CanvasBrushLineState, - CanvasV2State, - Coordinate, CanvasEraserLineState, CanvasInpaintMaskState, CanvasRectState, + CanvasV2State, + Coordinate, ScaleChangedArg, } from 'features/controlLayers/store/types'; import { imageDTOToImageWithDims } from 'features/controlLayers/store/types'; @@ -78,9 +78,9 @@ export const inpaintMaskReducers = { state.inpaintMask.bboxNeedsUpdate = true; state.layers.imageCache = null; }, - imRectShapeAdded: (state, action: PayloadAction<{ rectShape: CanvasRectState }>) => { - const { rectShape } = action.payload; - state.inpaintMask.objects.push(rectShape); + imRectAdded: (state, action: PayloadAction<{ rect: CanvasRectState }>) => { + const { rect } = action.payload; + state.inpaintMask.objects.push(rect); state.inpaintMask.bboxNeedsUpdate = true; state.layers.imageCache = null; }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index e1253e726ad..4a00aa821fa 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -8,14 +8,13 @@ import { assert } from 'tsafe'; import type { CanvasBrushLineState, - CanvasV2State, - Coordinate, CanvasEraserLineState, - CanvasImageState, - ImageObjectAddedArg, CanvasLayerState, - PositionChangedArg, CanvasRectState, + CanvasV2State, + EntityRasterizedArg, + ImageObjectAddedArg, + PositionChangedArg, } from './types'; import { imageDTOToImageObject, imageDTOToImageWithDims } from './types'; @@ -168,14 +167,14 @@ export const layersReducers = { layer.objects.push(eraserLine); state.layers.imageCache = null; }, - layerRectShapeAdded: (state, action: PayloadAction<{ id: string; rectShape: CanvasRectState }>) => { - const { id, rectShape } = action.payload; + layerRectAdded: (state, action: PayloadAction<{ id: string; rect: CanvasRectState }>) => { + const { id, rect } = action.payload; const layer = selectLayer(state, id); if (!layer) { return; } - layer.objects.push(rectShape); + layer.objects.push(rect); state.layers.imageCache = null; }, layerImageAdded: ( @@ -199,7 +198,7 @@ export const layersReducers = { const { imageDTO } = action.payload; state.layers.imageCache = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; }, - layerRasterized: (state, action: PayloadAction<{ id: string; imageObject: CanvasImageState; position: Coordinate }>) => { + layerRasterized: (state, action: PayloadAction) => { const { id, imageObject, position } = action.payload; const layer = selectLayer(state, id); if (!layer) { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index 4a82f586c41..b847a24e86f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -2,12 +2,12 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import type { CanvasBrushLineState, + CanvasEraserLineState, + CanvasRectState, CanvasV2State, CLIPVisionModelV2, - CanvasEraserLineState, IPMethodV2, PositionChangedArg, - CanvasRectState, ScaleChangedArg, } from 'features/controlLayers/store/types'; import { imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/types'; @@ -350,14 +350,14 @@ export const regionsReducers = { rg.bboxNeedsUpdate = true; state.layers.imageCache = null; }, - rgRectShapeAdded: (state, action: PayloadAction<{ id: string; rectShape: CanvasRectState }>) => { - const { id, rectShape } = action.payload; + rgRectAdded: (state, action: PayloadAction<{ id: string; rect: CanvasRectState }>) => { + const { id, rect } = action.payload; const rg = selectRG(state, id); if (!rg) { return; } - rg.objects.push(rectShape); + rg.objects.push(rect); rg.bboxNeedsUpdate = true; state.layers.imageCache = null; }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 00a1bbaf6ad..54e046c5e0c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -32,7 +32,6 @@ import { zParameterNegativePrompt, zParameterPositivePrompt, } from 'features/parameters/types/parameterSchemas'; -import type { IRect } from 'konva/lib/types'; import type { AnyInvocation, BaseModelType, @@ -937,16 +936,12 @@ export type StageAttrs = { position: Coordinate; dimensions: Dimensions; scale: export type PositionChangedArg = { id: string; position: Coordinate }; export type ScaleChangedArg = { id: string; scale: Coordinate; position: Coordinate }; export type BboxChangedArg = { id: string; bbox: Rect | null }; -export type EraserLineAddedArg = { - id: string; - points: [number, number, number, number]; - width: number; - clip: Rect | null; -}; -export type BrushLineAddedArg = EraserLineAddedArg & { color: RgbaColor }; -export type PointAddedToLineArg = { id: string; point: [number, number] }; -export type RectShapeAddedArg = { id: string; rect: IRect; color: RgbaColor }; + +export type BrushLineAddedArg = { id: string; brushLine: CanvasBrushLineState }; +export type EraserLineAddedArg = { id: string; eraserLine: CanvasEraserLineState }; +export type RectAddedArg = { id: string; rect: CanvasRectState }; export type ImageObjectAddedArg = { id: string; imageDTO: ImageDTO; position?: Coordinate }; +export type EntityRasterizedArg = { id: string; imageObject: CanvasImageState; position: Coordinate }; //#region Type guards export const isLine = (obj: CanvasObjectState): obj is CanvasBrushLineState | CanvasEraserLineState => { From 7b7f0a7380de00a9c6c79a10fa2e68c55d51cf7e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Aug 2024 20:52:16 +1000 Subject: [PATCH 299/678] feat(ui): move resetScale to transformer --- .../controlLayers/konva/CanvasLayer.ts | 12 ---------- .../controlLayers/konva/CanvasTransformer.ts | 22 ++++++++++++++++++- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index c2e0a344ecd..142584e7c87 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -145,17 +145,6 @@ export class CanvasLayer { this.konva.objectGroup.opacity(opacity); }; - resetScale = () => { - const attrs = { - scaleX: 1, - scaleY: 1, - rotation: 0, - }; - this.konva.objectGroup.setAttrs(attrs); - this.transformer.konva.bboxOutline.setAttrs(attrs); - this.transformer.konva.proxyRect.setAttrs(attrs); - }; - rasterize = async () => { this.log.debug('Rasterizing layer'); @@ -169,7 +158,6 @@ export class CanvasLayer { const imageDTO = await uploadImage(blob, `${this.id}_rasterized.png`, 'other', true); const imageObject = imageDTOToImageObject(imageDTO); await this.renderer.renderObject(imageObject, true); - this.resetScale(); this.manager.stateApi.rasterizeEntity( { id: this.id, imageObject, position: { x: Math.round(rect.x), y: Math.round(rect.y) } }, this.type diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index ad4b82b6635..93854f6b7b5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -512,12 +512,32 @@ export class CanvasTransformer { this.isTransforming = false; this.setInteractionMode('off'); - this.parent.resetScale(); + + // Reset the scale of the the entity. We've either replaced the transformed objects with a rasterized image, or + // canceled a transformation. In either case, the scale should be reset. + this.resetScale(); + this.parent.updatePosition(); this.updateBbox(); this.syncInteractionState(); }; + /** + * Resets the scale of the transformer and the entity. + * When the entity is transformed, it's scale and rotation are modified by the transformer. After canceling or applying + * a transformation, the scale and rotation should be reset to the original values. + */ + resetScale = () => { + const attrs = { + scaleX: 1, + scaleY: 1, + rotation: 0, + }; + this.parent.konva.objectGroup.setAttrs(attrs); + this.konva.bboxOutline.setAttrs(attrs); + this.konva.proxyRect.setAttrs(attrs); + }; + /** * Sets the transformer to a specific interaction mode. * @param interactionMode The mode to set the transformer to. The transformer can be in one of three modes: From 6dddad01fc76be302aba289539dc380529c491d7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Aug 2024 20:56:47 +1000 Subject: [PATCH 300/678] feat(ui): move updatePosition to transformer --- .../controlLayers/konva/CanvasLayer.ts | 17 +------------- .../controlLayers/konva/CanvasTransformer.ts | 22 +++++++++++++++++-- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 142584e7c87..c6029a76cf0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -6,7 +6,6 @@ import { konvaNodeToBlob, previewBlob } from 'features/controlLayers/konva/util' import type { CanvasLayerState, CanvasV2State, - Coordinate, GetLoggingContext, } from 'features/controlLayers/store/types'; import { imageDTOToImageObject } from 'features/controlLayers/store/types'; @@ -87,7 +86,7 @@ export class CanvasLayer { await this.updateObjects({ objects }); } if (this.isFirstRender || position !== this.state.position) { - await this.updatePosition({ position }); + await this.transformer.updatePosition({ position }); } if (this.isFirstRender || opacity !== this.state.opacity) { await this.updateOpacity({ opacity }); @@ -111,20 +110,6 @@ export class CanvasLayer { this.konva.layer.visible(isEnabled && this.renderer.hasObjects()); }; - updatePosition = (arg?: { position: Coordinate }) => { - this.log.trace('Updating position'); - const position = get(arg, 'position', this.state.position); - - this.konva.objectGroup.setAttrs({ - x: position.x + this.transformer.pixelRect.x, - y: position.y + this.transformer.pixelRect.y, - offsetX: this.transformer.pixelRect.x, - offsetY: this.transformer.pixelRect.y, - }); - - this.transformer.update(position, this.transformer.pixelRect); - }; - updateObjects = async (arg?: { objects: CanvasLayerState['objects'] }) => { this.log.trace('Updating objects'); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index 93854f6b7b5..bc89f069950 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -3,7 +3,7 @@ import { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getEmptyRect, getPrefixedId } from 'features/controlLayers/konva/util'; import type { Coordinate, GetLoggingContext, Rect } from 'features/controlLayers/store/types'; import Konva from 'konva'; -import { debounce } from 'lodash-es'; +import { debounce, get } from 'lodash-es'; import type { Logger } from 'roarr'; /** @@ -517,7 +517,7 @@ export class CanvasTransformer { // canceled a transformation. In either case, the scale should be reset. this.resetScale(); - this.parent.updatePosition(); + this.updatePosition(); this.updateBbox(); this.syncInteractionState(); }; @@ -538,6 +538,24 @@ export class CanvasTransformer { this.konva.proxyRect.setAttrs(attrs); }; + /** + * Updates the position of the transformer and the entity. + * @param arg The position to update to. If omitted, the parent's last stored position will be used. + */ + updatePosition = (arg?: { position: Coordinate }) => { + this.log.trace('Updating position'); + const position = get(arg, 'position', this.parent.state.position); + + this.parent.konva.objectGroup.setAttrs({ + x: position.x + this.pixelRect.x, + y: position.y + this.pixelRect.y, + offsetX: this.pixelRect.x, + offsetY: this.pixelRect.y, + }); + + this.update(position, this.pixelRect); + }; + /** * Sets the transformer to a specific interaction mode. * @param interactionMode The mode to set the transformer to. The transformer can be in one of three modes: From 8816d6c0c59c21b5121a04b721277b9fc19686f0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Aug 2024 22:01:06 +1000 Subject: [PATCH 301/678] feat(ui): wip inpaint mask uses new API --- .../controlLayers/konva/CanvasInpaintMask.ts | 318 +++++------------- .../controlLayers/konva/CanvasManager.ts | 34 +- .../konva/CanvasObjectRenderer.ts | 69 +++- .../controlLayers/konva/CanvasTransformer.ts | 7 +- .../features/controlLayers/konva/events.ts | 8 +- 5 files changed, 168 insertions(+), 268 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts index cede2c9928f..d2ce4518dfb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts @@ -1,294 +1,128 @@ -import { rgbColorToString } from 'common/util/colorCodeTransformers'; -import { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine'; -import { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; -import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox'; -import { mapId } from 'features/controlLayers/konva/util'; -import type { - CanvasBrushLineState, - CanvasEraserLineState, - CanvasInpaintMaskState, - CanvasRectState, -} from 'features/controlLayers/store/types'; -import { isDrawingTool, RGBA_RED } from 'features/controlLayers/store/types'; +import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; +import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; +import type { CanvasInpaintMaskState, CanvasV2State, GetLoggingContext } from 'features/controlLayers/store/types'; import Konva from 'konva'; +import { get } from 'lodash-es'; +import type { Logger } from 'roarr'; import { assert } from 'tsafe'; export class CanvasInpaintMask { + static TYPE = 'inpaint_mask' as const; static NAME_PREFIX = 'inpaint-mask'; - static LAYER_NAME = `${CanvasInpaintMask.NAME_PREFIX}_layer`; - static TRANSFORMER_NAME = `${CanvasInpaintMask.NAME_PREFIX}_transformer`; - static GROUP_NAME = `${CanvasInpaintMask.NAME_PREFIX}_group`; + static KONVA_LAYER_NAME = `${CanvasInpaintMask.NAME_PREFIX}_layer`; static OBJECT_GROUP_NAME = `${CanvasInpaintMask.NAME_PREFIX}_object-group`; - static COMPOSITING_RECT_NAME = `${CanvasInpaintMask.NAME_PREFIX}_compositing-rect`; - static TYPE = 'inpaint_mask' as const; - private drawingBuffer: CanvasBrushLineState | CanvasEraserLineState | CanvasRectState | null; - private state: CanvasInpaintMaskState; id = CanvasInpaintMask.TYPE; type = CanvasInpaintMask.TYPE; manager: CanvasManager; + log: Logger; + getLoggingContext: GetLoggingContext; + + state: CanvasInpaintMaskState; + + transformer: CanvasTransformer; + renderer: CanvasObjectRenderer; + + isFirstRender: boolean = true; konva: { layer: Konva.Layer; - group: Konva.Group; objectGroup: Konva.Group; - transformer: Konva.Transformer; - compositingRect: Konva.Rect; }; - objects: Map; constructor(state: CanvasInpaintMaskState, manager: CanvasManager) { this.manager = manager; + this.getLoggingContext = this.manager.buildGetLoggingContext(this); + this.log = this.manager.buildLogger(this.getLoggingContext); + this.log.debug({ state }, 'Creating inpaint mask'); this.konva = { - layer: new Konva.Layer({ name: CanvasInpaintMask.LAYER_NAME }), - group: new Konva.Group({ name: CanvasInpaintMask.GROUP_NAME, listening: false }), - objectGroup: new Konva.Group({ name: CanvasInpaintMask.OBJECT_GROUP_NAME, listening: false }), - transformer: new Konva.Transformer({ - name: CanvasInpaintMask.TRANSFORMER_NAME, - shouldOverdrawWholeArea: true, - draggable: true, - dragDistance: 0, - enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], - rotateEnabled: false, - flipEnabled: false, + layer: new Konva.Layer({ + name: CanvasInpaintMask.KONVA_LAYER_NAME, + listening: false, + imageSmoothingEnabled: false, }), - compositingRect: new Konva.Rect({ name: CanvasInpaintMask.COMPOSITING_RECT_NAME, listening: false }), + objectGroup: new Konva.Group({ name: CanvasInpaintMask.OBJECT_GROUP_NAME, listening: false }), }; - this.konva.group.add(this.konva.objectGroup); - this.konva.layer.add(this.konva.group); + this.transformer = new CanvasTransformer(this); + this.renderer = new CanvasObjectRenderer(this, true); + assert(this.renderer.konva.compositingRect, 'Compositing rect must be set'); - this.konva.transformer.on('transformend', () => { - this.manager.stateApi.onScaleChanged( - { - id: this.id, - scale: this.konva.group.scaleX(), - position: { x: this.konva.group.x(), y: this.konva.group.y() }, - }, - 'inpaint_mask' - ); - }); - this.konva.transformer.on('dragend', () => { - this.manager.stateApi.setEntityPosition( - { id: this.id, position: { x: this.konva.group.x(), y: this.konva.group.y() } }, - 'inpaint_mask' - ); - }); - this.konva.layer.add(this.konva.transformer); + this.konva.layer.add(this.konva.objectGroup); + this.konva.layer.add(this.renderer.konva.compositingRect); + this.konva.layer.add(...this.transformer.getNodes()); - this.konva.group.add(this.konva.compositingRect); - this.objects = new Map(); - this.drawingBuffer = null; this.state = state; } - destroy(): void { + destroy = (): void => { + this.log.debug('Destroying inpaint mask'); + // We need to call the destroy method on all children so they can do their own cleanup. + this.transformer.destroy(); + this.renderer.destroy(); this.konva.layer.destroy(); - } - - getDrawingBuffer() { - return this.drawingBuffer; - } - - async setDrawingBuffer(obj: CanvasBrushLineState | CanvasEraserLineState | CanvasRectState | null) { - this.drawingBuffer = obj; - if (this.drawingBuffer) { - if (this.drawingBuffer.type === 'brush_line') { - this.drawingBuffer.color = RGBA_RED; - } else if (this.drawingBuffer.type === 'rect') { - this.drawingBuffer.color = RGBA_RED; - } + }; - await this.renderObject(this.drawingBuffer, true); - this.updateGroup(true); - } - } + update = async (arg?: { state: CanvasInpaintMaskState; toolState: CanvasV2State['tool']; isSelected: boolean }) => { + const state = get(arg, 'state', this.state); - finalizeDrawingBuffer() { - if (!this.drawingBuffer) { + if (!this.isFirstRender && state === this.state) { + this.log.trace('State unchanged, skipping update'); return; } - if (this.drawingBuffer.type === 'brush_line') { - this.manager.stateApi.addBrushLine({ id: this.id, brushLine: this.drawingBuffer }, 'inpaint_mask'); - } else if (this.drawingBuffer.type === 'eraser_line') { - this.manager.stateApi.addEraserLine({ id: this.id, eraserLine: this.drawingBuffer }, 'inpaint_mask'); - } else if (this.drawingBuffer.type === 'rect') { - this.manager.stateApi.addRect({ id: this.id, rect: this.drawingBuffer }, 'inpaint_mask'); - } - this.setDrawingBuffer(null); - } - async render(state: CanvasInpaintMaskState) { - this.state = state; - - // Update the layer's position and listening state - this.konva.group.setAttrs({ - x: state.position.x, - y: state.position.y, - scaleX: 1, - scaleY: 1, - }); + // const maskOpacity = this.manager.stateApi.getMaskOpacity() - let didDraw = false; + this.log.debug('Updating'); + const { position, objects, isEnabled } = state; - const objectIds = state.objects.map(mapId); - // Destroy any objects that are no longer in state - for (const object of this.objects.values()) { - if (!objectIds.includes(object.id)) { - this.objects.delete(object.id); - object.destroy(); - didDraw = true; - } + if (this.isFirstRender || objects !== this.state.objects) { + await this.updateObjects({ objects }); } - - for (const obj of state.objects) { - if (await this.renderObject(obj)) { - didDraw = true; - } + if (this.isFirstRender || position !== this.state.position) { + await this.transformer.updatePosition({ position }); } - - if (this.drawingBuffer) { - if (await this.renderObject(this.drawingBuffer)) { - didDraw = true; - } + // if (this.isFirstRender || opacity !== this.state.opacity) { + // await this.updateOpacity({ opacity }); + // } + if (this.isFirstRender || isEnabled !== this.state.isEnabled) { + await this.updateVisibility({ isEnabled }); } + // this.transformer.syncInteractionState(); - this.updateGroup(didDraw); - } - - private async renderObject(obj: CanvasInpaintMaskState['objects'][number], force = false): Promise { - if (obj.type === 'brush_line') { - let brushLine = this.objects.get(obj.id); - assert(brushLine instanceof CanvasBrushLineRenderer || brushLine === undefined); - - if (!brushLine) { - brushLine = new CanvasBrushLineRenderer(obj); - this.objects.set(brushLine.id, brushLine); - this.konva.objectGroup.add(brushLine.konva.group); - return true; - } else { - if (brushLine.update(obj, force)) { - return true; - } - } - } else if (obj.type === 'eraser_line') { - let eraserLine = this.objects.get(obj.id); - assert(eraserLine instanceof CanvasEraserLineRenderer || eraserLine === undefined); - - if (!eraserLine) { - eraserLine = new CanvasEraserLineRenderer(obj); - this.objects.set(eraserLine.id, eraserLine); - this.konva.objectGroup.add(eraserLine.konva.group); - return true; - } else { - if (eraserLine.update(obj, force)) { - return true; - } - } - } else if (obj.type === 'rect') { - let rect = this.objects.get(obj.id); - assert(rect instanceof CanvasRectRenderer || rect === undefined); - - if (!rect) { - rect = new CanvasRectRenderer(obj); - this.objects.set(rect.id, rect); - this.konva.objectGroup.add(rect.konva.group); - return true; - } else { - if (rect.update(obj, force)) { - return true; - } - } + if (this.isFirstRender) { + await this.transformer.updateBbox(); } - return false; - } - - updateGroup(didDraw: boolean) { - this.konva.layer.visible(this.state.isEnabled); + this.state = state; + this.isFirstRender = false; + }; - // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work - this.konva.group.opacity(1); + updateObjects = async (arg?: { objects: CanvasInpaintMaskState['objects'] }) => { + this.log.trace('Updating objects'); - if (didDraw) { - // Convert the color to a string, stripping the alpha - the object group will handle opacity. - const rgbColor = rgbColorToString(this.state.fill); - const maskOpacity = this.manager.stateApi.getMaskOpacity(); + const objects = get(arg, 'objects', this.state.objects); - this.konva.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 - ...getNodeBboxFast(this.konva.objectGroup), - fill: rgbColor, - opacity: maskOpacity, - // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) - globalCompositeOperation: 'source-in', - visible: true, - // This rect must always be on top of all other shapes - zIndex: this.objects.size + 1, - }); - } + const didUpdate = await this.renderer.render(objects); - const isSelected = this.manager.stateApi.getIsSelected(this.id); - const selectedTool = this.manager.stateApi.getToolState().selected; - - if (this.objects.size === 0) { - // If the layer is totally empty, reset the cache and bail out. - this.konva.layer.listening(false); - this.konva.transformer.nodes([]); - if (this.konva.group.isCached()) { - this.konva.group.clearCache(); - } - return; + if (didUpdate) { + this.transformer.requestRectCalculation(); } - if (isSelected && selectedTool === 'move') { - // When the layer is selected and being moved, we should always cache it. - // We should update the cache if we drew to the layer. - if (!this.konva.group.isCached() || didDraw) { - // this.konva.group.cache(); - } - // Activate the transformer - this.konva.layer.listening(true); - this.konva.transformer.nodes([this.konva.group]); - this.konva.transformer.forceUpdate(); - return; - } - - if (isSelected && selectedTool !== 'move') { - // If the layer is selected but not using the move tool, we don't want the layer to be listening. - this.konva.layer.listening(false); - // The transformer also does not need to be active. - this.konva.transformer.nodes([]); - if (isDrawingTool(selectedTool)) { - // We are using a drawing tool (brush, eraser, rect). These tools change the layer's rendered appearance, so we - // should never be cached. - if (this.konva.group.isCached()) { - this.konva.group.clearCache(); - } - } else { - // We are using a non-drawing tool (move, view, bbox), so we should cache the layer. - // We should update the cache if we drew to the layer. - if (!this.konva.group.isCached() || didDraw) { - // this.konva.group.cache(); - } - } - return; - } + this.isFirstRender = false; + }; - if (!isSelected) { - // Unselected layers should not be listening - this.konva.layer.listening(false); - // The transformer also does not need to be active. - this.konva.transformer.nodes([]); - // Update the layer's cache if it's not already cached or we drew to it. - if (!this.konva.group.isCached() || didDraw) { - // this.konva.group.cache(); - } + // updateOpacity = (arg?: { opacity: number }) => { + // this.log.trace('Updating opacity'); + // const opacity = get(arg, 'opacity', this.state.opacity); + // this.konva.objectGroup.opacity(opacity); + // }; - return; - } - } + updateVisibility = (arg?: { isEnabled: boolean }) => { + this.log.trace('Updating visibility'); + const isEnabled = get(arg, 'isEnabled', this.state.isEnabled); + this.konva.layer.visible(isEnabled && this.renderer.hasObjects()); + }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index c9924ce3833..6d80a455406 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -177,9 +177,6 @@ export class CanvasManager { this.background = new CanvasBackground(this); this.stage.add(this.background.konva.layer); - this.inpaintMask = new CanvasInpaintMask(this.stateApi.getInpaintMaskState(), this); - this.stage.add(this.inpaintMask.konva.layer); - this.layers = new Map(); this.regions = new Map(); this.controlAdapters = new Map(); @@ -222,6 +219,9 @@ export class CanvasManager { this.getSelectedEntity(), (a, b) => a?.state === b?.state && a?.adapter === b?.adapter ); + + this.inpaintMask = new CanvasInpaintMask(this.stateApi.getInpaintMaskState(), this); + this.stage.add(this.inpaintMask.konva.layer); } enableDebugging() { @@ -273,11 +273,6 @@ export class CanvasManager { await this.preview.progressPreview.render(this.stateApi.$lastProgressEvent.get()); } - async renderInpaintMask() { - const inpaintMaskState = this.stateApi.getInpaintMaskState(); - await this.inpaintMask.render(inpaintMaskState); - } - async renderControlAdapters() { const { entities } = this.stateApi.getControlAdaptersState(); @@ -372,9 +367,9 @@ export class CanvasManager { const selectedEntity = this.getSelectedEntity(); if (selectedEntity) { if (selectedEntity.state.type === 'regional_guidance') { - currentFill = { ...selectedEntity.state.fill, a: state.settings.maskOpacity }; + currentFill = { ...selectedEntity.state.fill, a: 1 }; } else if (selectedEntity.state.type === 'inpaint_mask') { - currentFill = { ...state.inpaintMask.fill, a: state.settings.maskOpacity }; + currentFill = { ...state.inpaintMask.fill, a: 1 }; } } return currentFill; @@ -394,7 +389,10 @@ export class CanvasManager { } const layer = this.getSelectedEntity(); // TODO(psyche): Support other entity types - assert(layer?.adapter instanceof CanvasLayer, 'No selected layer'); + assert( + layer && (layer.adapter instanceof CanvasLayer || layer.adapter instanceof CanvasInpaintMask), + 'No selected layer' + ); layer.adapter.transformer.startTransform(); this.isTransforming.publish(true); } @@ -472,13 +470,16 @@ export class CanvasManager { if ( this._isFirstRender || - state.inpaintMask !== this._prevState.inpaintMask || state.settings.maskOpacity !== this._prevState.settings.maskOpacity || state.tool.selected !== this._prevState.tool.selected || state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id ) { this.log.debug('Rendering inpaint mask'); - await this.renderInpaintMask(); + await this.inpaintMask.update({ + state: state.inpaintMask, + toolState: state.tool, + isSelected: state.selectedEntityIdentifier?.id === state.inpaintMask.id, + }); } if ( @@ -670,9 +671,14 @@ export class CanvasManager { | CanvasTransformer | CanvasObjectRenderer | CanvasLayer + | CanvasInpaintMask | CanvasStagingArea ): GetLoggingContext => { - if (instance instanceof CanvasLayer || instance instanceof CanvasStagingArea) { + if ( + instance instanceof CanvasLayer || + instance instanceof CanvasStagingArea || + instance instanceof CanvasInpaintMask + ) { return (extra?: JSONObject): JSONObject => { return { ...instance.manager.getLoggingContext(), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index 7e6f7ac34a1..35ae6209d52 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -1,8 +1,10 @@ import type { JSONObject } from 'common/types'; +import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; import { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine'; import { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine'; import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; +import type { CanvasInpaintMask } from 'features/controlLayers/konva/CanvasInpaintMask'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; @@ -13,6 +15,7 @@ import type { CanvasImageState, CanvasRectState, } from 'features/controlLayers/store/types'; +import Konva from 'konva'; import type { Logger } from 'roarr'; import { assert } from 'tsafe'; @@ -30,9 +33,10 @@ type AnyObjectState = CanvasBrushLineState | CanvasEraserLineState | CanvasImage */ export class CanvasObjectRenderer { static TYPE = 'object_renderer'; + static KONVA_COMPOSITING_RECT_NAME = 'compositing-rect'; id: string; - parent: CanvasLayer; + parent: CanvasLayer | CanvasInpaintMask; manager: CanvasManager; log: Logger; getLoggingContext: (extra?: JSONObject) => JSONObject; @@ -54,7 +58,11 @@ export class CanvasObjectRenderer { */ renderers: Map = new Map(); - constructor(parent: CanvasLayer) { + konva: { + compositingRect: Konva.Rect | null; + }; + + constructor(parent: CanvasLayer | CanvasInpaintMask, withCompositingRect: boolean = false) { this.id = getPrefixedId(CanvasObjectRenderer.TYPE); this.parent = parent; this.manager = parent.manager; @@ -62,6 +70,18 @@ export class CanvasObjectRenderer { this.log = this.manager.buildLogger(this.getLoggingContext); this.log.trace('Creating object renderer'); + this.konva = { + compositingRect: null, + }; + + if (withCompositingRect) { + this.konva.compositingRect = new Konva.Rect({ + name: CanvasObjectRenderer.KONVA_COMPOSITING_RECT_NAME, + listening: false, + }); + this.parent.konva.objectGroup.add(this.konva.compositingRect); + } + this.subscriptions.add( this.manager.toolState.subscribe((newVal, oldVal) => { if (newVal.selected !== oldVal.selected) { @@ -69,6 +89,21 @@ export class CanvasObjectRenderer { } }) ); + + // The compositing rect must cover the whole stage at all times. When the stage is scaled, moved or resized, we + // need to update the compositing rect to match the stage. + this.subscriptions.add( + this.manager.stateApi.$stageAttrs.listen((attrs) => { + if (this.konva.compositingRect) { + this.konva.compositingRect.setAttrs({ + x: -attrs.position.x / attrs.scale, + y: -attrs.position.y / attrs.scale, + width: attrs.dimensions.width / attrs.scale, + height: attrs.dimensions.height / attrs.scale, + }); + } + }) + ); } /** @@ -96,6 +131,24 @@ export class CanvasObjectRenderer { didRender = (await this.renderObject(this.buffer)) || didRender; } + if (didRender && this.parent.type === 'inpaint_mask') { + assert(this.konva.compositingRect, 'Compositing rect must exist for inpaint mask'); + + // Convert the color to a string, stripping the alpha - the object group will handle opacity. + const rgbColor = rgbColorToString(this.parent.state.fill); + const maskOpacity = this.manager.stateApi.getMaskOpacity(); + + this.konva.compositingRect.setAttrs({ + fill: rgbColor, + opacity: maskOpacity, + // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) + globalCompositeOperation: 'source-in', + visible: true, + // This rect must always be on top of all other shapes + // zIndex: this.renderers.size + 1, + }); + } + return didRender; }; @@ -177,6 +230,12 @@ export class CanvasObjectRenderer { this.buffer = objectState; return await this.renderObject(this.buffer, true); + + // const didDraw = await this.renderObject(this.buffer, true); + // if (didDraw && this.konva.compositingRect) { + // this.konva.compositingRect.zIndex(this.renderers.size + 1); + // } + // return didDraw; }; /** @@ -204,11 +263,11 @@ export class CanvasObjectRenderer { this.buffer.id = getPrefixedId(this.buffer.type); if (this.buffer.type === 'brush_line') { - this.manager.stateApi.addBrushLine({ id: this.parent.id, brushLine: this.buffer }, 'layer'); + this.manager.stateApi.addBrushLine({ id: this.parent.id, brushLine: this.buffer }, this.parent.type); } else if (this.buffer.type === 'eraser_line') { - this.manager.stateApi.addEraserLine({ id: this.parent.id, eraserLine: this.buffer }, 'layer'); + this.manager.stateApi.addEraserLine({ id: this.parent.id, eraserLine: this.buffer }, this.parent.type); } else if (this.buffer.type === 'rect') { - this.manager.stateApi.addRect({ id: this.parent.id, rect: this.buffer }, 'layer'); + this.manager.stateApi.addRect({ id: this.parent.id, rect: this.buffer }, this.parent.type); } else { this.log.warn({ buffer: this.buffer }, 'Invalid buffer object type'); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index bc89f069950..ad99a5d7ce8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -1,3 +1,4 @@ +import type { CanvasInpaintMask } from 'features/controlLayers/konva/CanvasInpaintMask'; import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getEmptyRect, getPrefixedId } from 'features/controlLayers/konva/util'; @@ -31,7 +32,7 @@ export class CanvasTransformer { static ANCHOR_HIT_PADDING = 10; id: string; - parent: CanvasLayer; + parent: CanvasLayer | CanvasInpaintMask; manager: CanvasManager; log: Logger; getLoggingContext: GetLoggingContext; @@ -89,7 +90,7 @@ export class CanvasTransformer { bboxOutline: Konva.Rect; }; - constructor(parent: CanvasLayer) { + constructor(parent: CanvasLayer | CanvasInpaintMask) { this.id = getPrefixedId(CanvasTransformer.TYPE); this.parent = parent; this.manager = parent.manager; @@ -354,7 +355,7 @@ export class CanvasTransformer { }; this.log.trace({ position }, 'Position changed'); - this.manager.stateApi.setEntityPosition({ id: this.parent.id, position }, 'layer'); + this.manager.stateApi.setEntityPosition({ id: this.parent.id, position }, this.parent.type); }); this.subscriptions.add( diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 992e4cbbf5e..75b8ac9b17c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -128,8 +128,8 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { $spaceKey, getBbox, getSettings, - setBrushWidth: onBrushWidthChanged, - setEraserWidth: onEraserWidthChanged, + setBrushWidth, + setEraserWidth, } = stateApi; function getIsPrimaryMouseDown(e: KonvaEventObject) { @@ -461,9 +461,9 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { } // Holding ctrl or meta while scrolling changes the brush size if (toolState.selected === 'brush') { - onBrushWidthChanged(calculateNewBrushSize(toolState.brush.width, delta)); + setBrushWidth(calculateNewBrushSize(toolState.brush.width, delta)); } else if (toolState.selected === 'eraser') { - onEraserWidthChanged(calculateNewBrushSize(toolState.eraser.width, delta)); + setEraserWidth(calculateNewBrushSize(toolState.eraser.width, delta)); } } else { // We need the absolute cursor position - not the scaled position From 9f2dc4d6d104e97caf06e9992fce9d1dc5dc7c52 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 6 Aug 2024 12:41:17 +1000 Subject: [PATCH 302/678] fix(ui): inpaint mask rendering --- .../controlLayers/components/MaskOpacity.tsx | 6 +-- .../controlLayers/konva/CanvasInpaintMask.ts | 29 ++++++---- .../controlLayers/konva/CanvasManager.ts | 2 + .../konva/CanvasObjectRenderer.ts | 54 ++++++++++--------- 4 files changed, 53 insertions(+), 38 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/MaskOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/MaskOpacity.tsx index 58c659c65bc..d3372f4faea 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/MaskOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/MaskOpacity.tsx @@ -13,7 +13,7 @@ export const MaskOpacity = memo(() => { const opacity = useAppSelector((s) => Math.round(s.canvasV2.settings.maskOpacity * 100)); const onChange = useCallback( (v: number) => { - dispatch(maskOpacityChanged(v / 100)); + dispatch(maskOpacityChanged(Math.max(v / 100, 0.25))); }, [dispatch] ); @@ -22,7 +22,7 @@ export const MaskOpacity = memo(() => { {t('controlLayers.globalMaskOpacity')} { minW={48} /> { @@ -67,14 +69,18 @@ export class CanvasInpaintMask { update = async (arg?: { state: CanvasInpaintMaskState; toolState: CanvasV2State['tool']; isSelected: boolean }) => { const state = get(arg, 'state', this.state); - - if (!this.isFirstRender && state === this.state) { + const maskOpacity = this.manager.stateApi.getMaskOpacity(); + + if ( + !this.isFirstRender && + state === this.state && + state.fill === this.state.fill && + maskOpacity === this.maskOpacity + ) { this.log.trace('State unchanged, skipping update'); return; } - // const maskOpacity = this.manager.stateApi.getMaskOpacity() - this.log.debug('Updating'); const { position, objects, isEnabled } = state; @@ -82,18 +88,23 @@ export class CanvasInpaintMask { await this.updateObjects({ objects }); } if (this.isFirstRender || position !== this.state.position) { - await this.transformer.updatePosition({ position }); + this.transformer.updatePosition({ position }); } // if (this.isFirstRender || opacity !== this.state.opacity) { // await this.updateOpacity({ opacity }); // } if (this.isFirstRender || isEnabled !== this.state.isEnabled) { - await this.updateVisibility({ isEnabled }); + this.updateVisibility({ isEnabled }); + } + + if (this.isFirstRender || state.fill !== this.state.fill || maskOpacity !== this.maskOpacity) { + this.renderer.updateCompositingRect(state.fill, maskOpacity); + this.maskOpacity = maskOpacity; } // this.transformer.syncInteractionState(); if (this.isFirstRender) { - await this.transformer.updateBbox(); + this.transformer.updateBbox(); } this.state = state; @@ -110,8 +121,6 @@ export class CanvasInpaintMask { if (didUpdate) { this.transformer.requestRectCalculation(); } - - this.isFirstRender = false; }; // updateOpacity = (arg?: { opacity: number }) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 6d80a455406..571eac133bb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -366,6 +366,7 @@ export class CanvasManager { let currentFill: RgbaColor = state.tool.fill; const selectedEntity = this.getSelectedEntity(); if (selectedEntity) { + // These two entity types use a compositing rect for opacity. Their alpha is always 1. if (selectedEntity.state.type === 'regional_guidance') { currentFill = { ...selectedEntity.state.fill, a: 1 }; } else if (selectedEntity.state.type === 'inpaint_mask') { @@ -470,6 +471,7 @@ export class CanvasManager { if ( this._isFirstRender || + state.inpaintMask !== this._prevState.inpaintMask || state.settings.maskOpacity !== this._prevState.settings.maskOpacity || state.tool.selected !== this._prevState.tool.selected || state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index 35ae6209d52..c62d2a3d68c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -14,6 +14,7 @@ import type { CanvasEraserLineState, CanvasImageState, CanvasRectState, + RgbColor, } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; @@ -58,11 +59,26 @@ export class CanvasObjectRenderer { */ renderers: Map = new Map(); + /** + * A object containing singleton Konva nodes. + */ konva: { + /** + * The compositing rect is used to draw the inpaint mask as a single shape with a given opacity. + * + * When drawing multiple transparent shapes on a canvas, overlapping regions will be more opaque. This doesn't + * match the expectation for a mask, where all shapes should have the same opacity, even if they overlap. + * + * To prevent this, we use a trick. Instead of drawing all shapes at the desired opacity, we draw them at opacity of 1. + * Then we draw a single rect that covers the entire canvas at the desired opacity, with a globalCompositeOperation + * of 'source-in'. The shapes effectively become a mask for the "compositing rect". + * + * This node is only added when the parent of the renderer is an inpaint mask or region, which require this behavior. + */ compositingRect: Konva.Rect | null; }; - constructor(parent: CanvasLayer | CanvasInpaintMask, withCompositingRect: boolean = false) { + constructor(parent: CanvasLayer | CanvasInpaintMask) { this.id = getPrefixedId(CanvasObjectRenderer.TYPE); this.parent = parent; this.manager = parent.manager; @@ -74,10 +90,11 @@ export class CanvasObjectRenderer { compositingRect: null, }; - if (withCompositingRect) { + if (this.parent.type === 'inpaint_mask') { this.konva.compositingRect = new Konva.Rect({ name: CanvasObjectRenderer.KONVA_COMPOSITING_RECT_NAME, listening: false, + globalCompositeOperation: 'source-in', }); this.parent.konva.objectGroup.add(this.konva.compositingRect); } @@ -131,25 +148,18 @@ export class CanvasObjectRenderer { didRender = (await this.renderObject(this.buffer)) || didRender; } - if (didRender && this.parent.type === 'inpaint_mask') { - assert(this.konva.compositingRect, 'Compositing rect must exist for inpaint mask'); + return didRender; + }; - // Convert the color to a string, stripping the alpha - the object group will handle opacity. - const rgbColor = rgbColorToString(this.parent.state.fill); - const maskOpacity = this.manager.stateApi.getMaskOpacity(); + updateCompositingRect = (fill: RgbColor, opacity: number) => { + this.log.trace('Updating compositing rect'); + assert(this.konva.compositingRect, 'Missing compositing rect'); - this.konva.compositingRect.setAttrs({ - fill: rgbColor, - opacity: maskOpacity, - // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) - globalCompositeOperation: 'source-in', - visible: true, - // This rect must always be on top of all other shapes - // zIndex: this.renderers.size + 1, - }); - } - - return didRender; + const rgbColor = rgbColorToString(fill); + this.konva.compositingRect.setAttrs({ + fill: rgbColor, + opacity, + }); }; /** @@ -230,12 +240,6 @@ export class CanvasObjectRenderer { this.buffer = objectState; return await this.renderObject(this.buffer, true); - - // const didDraw = await this.renderObject(this.buffer, true); - // if (didDraw && this.konva.compositingRect) { - // this.konva.compositingRect.zIndex(this.renderers.size + 1); - // } - // return didDraw; }; /** From b006edf9c465a453fdc9b8fc180150a56d31d550 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 6 Aug 2024 12:41:41 +1000 Subject: [PATCH 303/678] fix(ui): layer accidental early set isFirstRender=false --- .../features/controlLayers/konva/CanvasLayer.ts | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index c6029a76cf0..d32834053d0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -3,11 +3,7 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; import { konvaNodeToBlob, previewBlob } from 'features/controlLayers/konva/util'; -import type { - CanvasLayerState, - CanvasV2State, - GetLoggingContext, -} from 'features/controlLayers/store/types'; +import type { CanvasLayerState, CanvasV2State, GetLoggingContext } from 'features/controlLayers/store/types'; import { imageDTOToImageObject } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { get } from 'lodash-es'; @@ -86,18 +82,18 @@ export class CanvasLayer { await this.updateObjects({ objects }); } if (this.isFirstRender || position !== this.state.position) { - await this.transformer.updatePosition({ position }); + this.transformer.updatePosition({ position }); } if (this.isFirstRender || opacity !== this.state.opacity) { - await this.updateOpacity({ opacity }); + this.updateOpacity({ opacity }); } if (this.isFirstRender || isEnabled !== this.state.isEnabled) { - await this.updateVisibility({ isEnabled }); + this.updateVisibility({ isEnabled }); } // this.transformer.syncInteractionState(); if (this.isFirstRender) { - await this.transformer.updateBbox(); + this.transformer.updateBbox(); } this.state = state; @@ -120,8 +116,6 @@ export class CanvasLayer { if (didUpdate) { this.transformer.requestRectCalculation(); } - - this.isFirstRender = false; }; updateOpacity = (arg?: { opacity: number }) => { From c172221038511f30140c2a9ca8798dcac87290f9 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 6 Aug 2024 13:25:26 +1000 Subject: [PATCH 304/678] feat(ui): inpaint mask transform --- .../components/TransformToolButton.tsx | 4 +- .../controlLayers/konva/CanvasInpaintMask.ts | 15 ----- .../controlLayers/konva/CanvasLayer.ts | 47 ++++------------ .../controlLayers/konva/CanvasManager.ts | 27 ++++++--- .../konva/CanvasObjectRenderer.ts | 55 +++++++++++++++---- .../controlLayers/konva/CanvasStateApi.ts | 3 + .../controlLayers/konva/CanvasTool.ts | 2 +- .../controlLayers/konva/CanvasTransformer.ts | 28 +++++----- .../controlLayers/store/canvasV2Slice.ts | 1 + .../store/inpaintMaskReducers.ts | 7 +++ .../src/features/controlLayers/store/types.ts | 2 +- 11 files changed, 102 insertions(+), 89 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx index 14ac66706f9..2909174214e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx @@ -19,7 +19,9 @@ export const TransformToolButton = memo(() => { if (!canvasManager) { return; } - return canvasManager.isTransforming.subscribe(setIsTransforming); + return canvasManager.transformingEntity.subscribe((newValue) => { + setIsTransforming(Boolean(newValue)); + }); }, [canvasManager]); const onTransform = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts index 8861e45fe9c..bbc80b0f8cd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts @@ -5,13 +5,11 @@ import type { CanvasInpaintMaskState, CanvasV2State, GetLoggingContext } from 'f import Konva from 'konva'; import { get } from 'lodash-es'; import type { Logger } from 'roarr'; -import { assert } from 'tsafe'; export class CanvasInpaintMask { static TYPE = 'inpaint_mask' as const; static NAME_PREFIX = 'inpaint-mask'; static KONVA_LAYER_NAME = `${CanvasInpaintMask.NAME_PREFIX}_layer`; - static OBJECT_GROUP_NAME = `${CanvasInpaintMask.NAME_PREFIX}_object-group`; id = CanvasInpaintMask.TYPE; type = CanvasInpaintMask.TYPE; @@ -29,7 +27,6 @@ export class CanvasInpaintMask { konva: { layer: Konva.Layer; - objectGroup: Konva.Group; }; constructor(state: CanvasInpaintMaskState, manager: CanvasManager) { @@ -44,16 +41,10 @@ export class CanvasInpaintMask { listening: false, imageSmoothingEnabled: false, }), - objectGroup: new Konva.Group({ name: CanvasInpaintMask.OBJECT_GROUP_NAME, listening: false }), }; this.transformer = new CanvasTransformer(this); this.renderer = new CanvasObjectRenderer(this); - assert(this.renderer.konva.compositingRect, 'Compositing rect must be set'); - - this.konva.layer.add(this.konva.objectGroup); - this.konva.layer.add(this.renderer.konva.compositingRect); - this.konva.layer.add(...this.transformer.getNodes()); this.state = state; this.maskOpacity = this.manager.stateApi.getMaskOpacity(); @@ -123,12 +114,6 @@ export class CanvasInpaintMask { } }; - // updateOpacity = (arg?: { opacity: number }) => { - // this.log.trace('Updating opacity'); - // const opacity = get(arg, 'opacity', this.state.opacity); - // this.konva.objectGroup.opacity(opacity); - // }; - updateVisibility = (arg?: { isEnabled: boolean }) => { this.log.trace('Updating visibility'); const isEnabled = get(arg, 'isEnabled', this.state.isEnabled); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index d32834053d0..194c87984cd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -2,13 +2,10 @@ import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; -import { konvaNodeToBlob, previewBlob } from 'features/controlLayers/konva/util'; import type { CanvasLayerState, CanvasV2State, GetLoggingContext } from 'features/controlLayers/store/types'; -import { imageDTOToImageObject } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { get } from 'lodash-es'; import type { Logger } from 'roarr'; -import { uploadImage } from 'services/api/endpoints/images'; export class CanvasLayer { static TYPE = 'layer' as const; @@ -25,7 +22,6 @@ export class CanvasLayer { konva: { layer: Konva.Layer; - objectGroup: Konva.Group; }; transformer: CanvasTransformer; renderer: CanvasObjectRenderer; @@ -47,15 +43,11 @@ export class CanvasLayer { listening: false, imageSmoothingEnabled: false, }), - objectGroup: new Konva.Group({ name: CanvasLayer.KONVA_OBJECT_GROUP_NAME, listening: false }), }; this.transformer = new CanvasTransformer(this); this.renderer = new CanvasObjectRenderer(this); - this.konva.layer.add(this.konva.objectGroup); - this.konva.layer.add(...this.transformer.getNodes()); - this.state = state; } @@ -121,26 +113,7 @@ export class CanvasLayer { updateOpacity = (arg?: { opacity: number }) => { this.log.trace('Updating opacity'); const opacity = get(arg, 'opacity', this.state.opacity); - this.konva.objectGroup.opacity(opacity); - }; - - rasterize = async () => { - this.log.debug('Rasterizing layer'); - - const objectGroupClone = this.konva.objectGroup.clone(); - const interactionRectClone = this.transformer.konva.proxyRect.clone(); - const rect = interactionRectClone.getClientRect(); - const blob = await konvaNodeToBlob(objectGroupClone, rect); - if (this.manager._isDebugging) { - previewBlob(blob, 'Rasterized layer'); - } - const imageDTO = await uploadImage(blob, `${this.id}_rasterized.png`, 'other', true); - const imageObject = imageDTOToImageObject(imageDTO); - await this.renderer.renderObject(imageObject, true); - this.manager.stateApi.rasterizeEntity( - { id: this.id, imageObject, position: { x: Math.round(rect.x), y: Math.round(rect.y) } }, - this.type - ); + this.renderer.konva.objectGroup.opacity(opacity); }; repr = () => { @@ -167,15 +140,15 @@ export class CanvasLayer { rotation: this.transformer.konva.proxyRect.rotation(), }, objectGroupAttrs: { - x: this.konva.objectGroup.x(), - y: this.konva.objectGroup.y(), - scaleX: this.konva.objectGroup.scaleX(), - scaleY: this.konva.objectGroup.scaleY(), - width: this.konva.objectGroup.width(), - height: this.konva.objectGroup.height(), - rotation: this.konva.objectGroup.rotation(), - offsetX: this.konva.objectGroup.offsetX(), - offsetY: this.konva.objectGroup.offsetY(), + x: this.renderer.konva.objectGroup.x(), + y: this.renderer.konva.objectGroup.y(), + scaleX: this.renderer.konva.objectGroup.scaleX(), + scaleY: this.renderer.konva.objectGroup.scaleY(), + width: this.renderer.konva.objectGroup.width(), + height: this.renderer.konva.objectGroup.height(), + rotation: this.renderer.konva.objectGroup.rotation(), + offsetX: this.renderer.konva.objectGroup.offsetX(), + offsetY: this.renderer.konva.objectGroup.offsetY(), }, }; this.log.trace(info, msg); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 571eac133bb..e7e044ce3d3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -122,7 +122,7 @@ export class CanvasManager { log: Logger; workerLog: Logger; - isTransforming: PubSub; + transformingEntity: PubSub; _store: Store; _prevState: CanvasV2State; @@ -208,7 +208,7 @@ export class CanvasManager { this.log.error('Worker message error'); }; - this.isTransforming = new PubSub(false); + this.transformingEntity = new PubSub(null); this.toolState = new PubSub(this.stateApi.getToolState()); this.currentFill = new PubSub(this.getCurrentFill()); this.selectedEntityIdentifier = new PubSub( @@ -377,11 +377,24 @@ export class CanvasManager { }; getTransformingLayer() { - return Array.from(this.layers.values()).find((layer) => layer.transformer.isTransforming); + const transformingEntity = this.transformingEntity.getValue(); + if (!transformingEntity) { + return null; + } + + const { id, type } = transformingEntity; + + if (type === 'layer') { + return this.layers.get(id) ?? null; + } else if (type === 'inpaint_mask') { + return this.inpaintMask; + } + + return null; } getIsTransforming() { - return Boolean(this.getTransformingLayer()); + return Boolean(this.transformingEntity.getValue()); } startTransform() { @@ -395,7 +408,7 @@ export class CanvasManager { 'No selected layer' ); layer.adapter.transformer.startTransform(); - this.isTransforming.publish(true); + this.transformingEntity.publish({ id: layer.state.id, type: layer.state.type }); } async applyTransform() { @@ -403,7 +416,7 @@ export class CanvasManager { if (layer) { await layer.transformer.applyTransform(); } - this.isTransforming.publish(false); + this.transformingEntity.publish(null); } cancelTransform() { @@ -411,7 +424,7 @@ export class CanvasManager { if (layer) { layer.transformer.stopTransform(); } - this.isTransforming.publish(false); + this.transformingEntity.publish(null); } render = async () => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index c62d2a3d68c..6103ae7f6b6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -8,16 +8,18 @@ import type { CanvasInpaintMask } from 'features/controlLayers/konva/CanvasInpai import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; -import { getPrefixedId } from 'features/controlLayers/konva/util'; -import type { - CanvasBrushLineState, - CanvasEraserLineState, - CanvasImageState, - CanvasRectState, - RgbColor, +import { getPrefixedId, konvaNodeToBlob, previewBlob } from 'features/controlLayers/konva/util'; +import { + type CanvasBrushLineState, + type CanvasEraserLineState, + type CanvasImageState, + type CanvasRectState, + imageDTOToImageObject, + type RgbColor, } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; +import { uploadImage } from 'services/api/endpoints/images'; import { assert } from 'tsafe'; /** @@ -34,6 +36,7 @@ type AnyObjectState = CanvasBrushLineState | CanvasEraserLineState | CanvasImage */ export class CanvasObjectRenderer { static TYPE = 'object_renderer'; + static KONVA_OBJECT_GROUP_NAME = 'object-group'; static KONVA_COMPOSITING_RECT_NAME = 'compositing-rect'; id: string; @@ -63,6 +66,10 @@ export class CanvasObjectRenderer { * A object containing singleton Konva nodes. */ konva: { + /** + * A Konva Group that holds all the object renderers. + */ + objectGroup: Konva.Group; /** * The compositing rect is used to draw the inpaint mask as a single shape with a given opacity. * @@ -74,6 +81,8 @@ export class CanvasObjectRenderer { * of 'source-in'. The shapes effectively become a mask for the "compositing rect". * * This node is only added when the parent of the renderer is an inpaint mask or region, which require this behavior. + * + * The compositing rect is not added to the object group. */ compositingRect: Konva.Rect | null; }; @@ -87,16 +96,19 @@ export class CanvasObjectRenderer { this.log.trace('Creating object renderer'); this.konva = { + objectGroup: new Konva.Group({ name: CanvasObjectRenderer.KONVA_OBJECT_GROUP_NAME, listening: false }), compositingRect: null, }; + this.parent.konva.layer.add(this.konva.objectGroup); + if (this.parent.type === 'inpaint_mask') { this.konva.compositingRect = new Konva.Rect({ name: CanvasObjectRenderer.KONVA_COMPOSITING_RECT_NAME, listening: false, globalCompositeOperation: 'source-in', }); - this.parent.konva.objectGroup.add(this.konva.compositingRect); + this.parent.konva.layer.add(this.konva.compositingRect); } this.subscriptions.add( @@ -184,7 +196,7 @@ export class CanvasObjectRenderer { if (!renderer) { renderer = new CanvasBrushLineRenderer(objectState, this); this.renderers.set(renderer.id, renderer); - this.parent.konva.objectGroup.add(renderer.konva.group); + this.konva.objectGroup.add(renderer.konva.group); } didRender = renderer.update(objectState, force || isFirstRender); @@ -194,7 +206,7 @@ export class CanvasObjectRenderer { if (!renderer) { renderer = new CanvasEraserLineRenderer(objectState, this); this.renderers.set(renderer.id, renderer); - this.parent.konva.objectGroup.add(renderer.konva.group); + this.konva.objectGroup.add(renderer.konva.group); } didRender = renderer.update(objectState, force || isFirstRender); @@ -204,7 +216,7 @@ export class CanvasObjectRenderer { if (!renderer) { renderer = new CanvasRectRenderer(objectState, this); this.renderers.set(renderer.id, renderer); - this.parent.konva.objectGroup.add(renderer.konva.group); + this.konva.objectGroup.add(renderer.konva.group); } didRender = renderer.update(objectState, force || isFirstRender); @@ -214,7 +226,7 @@ export class CanvasObjectRenderer { if (!renderer) { renderer = new CanvasImageRenderer(objectState, this); this.renderers.set(renderer.id, renderer); - this.parent.konva.objectGroup.add(renderer.konva.group); + this.konva.objectGroup.add(renderer.konva.group); } didRender = await renderer.update(objectState, force || isFirstRender); } @@ -311,6 +323,25 @@ export class CanvasObjectRenderer { return this.renderers.size > 0 || this.buffer !== null; }; + rasterize = async () => { + this.log.debug('Rasterizing entity'); + + const objectGroupClone = this.konva.objectGroup.clone(); + const interactionRectClone = this.parent.transformer.konva.proxyRect.clone(); + const rect = interactionRectClone.getClientRect(); + const blob = await konvaNodeToBlob(objectGroupClone, rect); + if (this.manager._isDebugging) { + previewBlob(blob, 'Rasterized layer'); + } + const imageDTO = await uploadImage(blob, `${this.id}_rasterized.png`, 'other', true); + const imageObject = imageDTOToImageObject(imageDTO); + await this.renderObject(imageObject, true); + this.manager.stateApi.rasterizeEntity( + { id: this.id, imageObject, position: { x: Math.round(rect.x), y: Math.round(rect.y) } }, + this.parent.type + ); + }; + /** * Destroys this renderer and all of its object renderers. */ diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index 0ed2359aadc..f8e64360517 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -23,6 +23,7 @@ import { imImageCacheChanged, imRectAdded, imTranslated, + inpaintMaskRasterized, layerBrushLineAdded, layerEraserLineAdded, layerImageCacheChanged, @@ -119,6 +120,8 @@ export class CanvasStateApi { log.trace({ arg, entityType }, 'Rasterizing entity'); if (entityType === 'layer') { this._store.dispatch(layerRasterized(arg)); + } else if (entityType === 'inpaint_mask') { + this._store.dispatch(inpaintMaskRasterized(arg)); } else { assert(false, 'Rasterizing not supported for this entity type'); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts index 1dd26767669..a80a5a02bf7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts @@ -149,7 +149,7 @@ export class CanvasTool { } else if (!isDrawableEntity) { // Non-drawable layers don't have tools stage.container().style.cursor = 'not-allowed'; - } else if (tool === 'move' || this.manager.isTransforming.getValue()) { + } else if (tool === 'move' || Boolean(this.manager.transformingEntity.getValue())) { // Move tool gets a pointer stage.container().style.cursor = 'default'; } else if (tool === 'rect') { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index ad99a5d7ce8..a96b3ce12d1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -251,7 +251,7 @@ export class CanvasTransformer { // This is called when a transform anchor is dragged. By this time, the transform constraints in the above // callbacks have been enforced, and the transformer has updated its nodes' attributes. We need to pass the // updated attributes to the object group, propagating the transformation on down. - this.parent.konva.objectGroup.setAttrs({ + this.parent.renderer.konva.objectGroup.setAttrs({ x: this.konva.proxyRect.x(), y: this.konva.proxyRect.y(), scaleX: this.konva.proxyRect.scaleX(), @@ -293,7 +293,7 @@ export class CanvasTransformer { scaleX: snappedScaleX, scaleY: snappedScaleY, }); - this.parent.konva.objectGroup.setAttrs({ + this.parent.renderer.konva.objectGroup.setAttrs({ x: snappedX, y: snappedY, scaleX: snappedScaleX, @@ -337,7 +337,7 @@ export class CanvasTransformer { // The object group is translated by the difference between the interaction rect's new and old positions (which is // stored as this.pixelRect) - this.parent.konva.objectGroup.setAttrs({ + this.parent.renderer.konva.objectGroup.setAttrs({ x: this.konva.proxyRect.x(), y: this.konva.proxyRect.y(), }); @@ -391,6 +391,10 @@ export class CanvasTransformer { this.syncInteractionState(); }) ); + + this.parent.konva.layer.add(this.konva.bboxOutline); + this.parent.konva.layer.add(this.konva.proxyRect); + this.parent.konva.layer.add(this.konva.transformer); } /** @@ -499,7 +503,7 @@ export class CanvasTransformer { */ applyTransform = async () => { this.log.debug('Applying transform'); - await this.parent.rasterize(); + await this.parent.renderer.rasterize(); this.requestRectCalculation(); this.stopTransform(); }; @@ -534,7 +538,7 @@ export class CanvasTransformer { scaleY: 1, rotation: 0, }; - this.parent.konva.objectGroup.setAttrs(attrs); + this.parent.renderer.konva.objectGroup.setAttrs(attrs); this.konva.bboxOutline.setAttrs(attrs); this.konva.proxyRect.setAttrs(attrs); }; @@ -547,7 +551,7 @@ export class CanvasTransformer { this.log.trace('Updating position'); const position = get(arg, 'position', this.parent.state.position); - this.parent.konva.objectGroup.setAttrs({ + this.parent.renderer.konva.objectGroup.setAttrs({ x: position.x + this.pixelRect.x, y: position.y + this.pixelRect.y, offsetX: this.pixelRect.x, @@ -603,7 +607,7 @@ export class CanvasTransformer { this.syncInteractionState(); this.update(this.parent.state.position, this.pixelRect); - this.parent.konva.objectGroup.setAttrs({ + this.parent.renderer.konva.objectGroup.setAttrs({ x: this.parent.state.position.x + this.pixelRect.x, y: this.parent.state.position.y + this.pixelRect.y, offsetX: this.pixelRect.x, @@ -625,7 +629,7 @@ export class CanvasTransformer { return; } - const rect = this.parent.konva.objectGroup.getClientRect({ skipTransform: true }); + const rect = this.parent.renderer.konva.objectGroup.getClientRect({ skipTransform: true }); if (!this.parent.renderer.needsPixelBbox()) { this.nodeRect = { ...rect }; @@ -638,7 +642,7 @@ export class CanvasTransformer { // We have eraser strokes - we must calculate the bbox using pixel data - const clone = this.parent.konva.objectGroup.clone(); + const clone = this.parent.renderer.konva.objectGroup.clone(); const canvas = clone.toCanvas(); const ctx = canvas.getContext('2d'); if (!ctx) { @@ -709,12 +713,6 @@ export class CanvasTransformer { this.konva.bboxOutline.visible(false); }; - /** - * Gets the nodes that make up the transformer, in the order they should be added to the layer. - * @returns The nodes that make up the transformer. - */ - getNodes = () => [this.konva.bboxOutline, this.konva.proxyRect, this.konva.transformer]; - /** * Gets a JSON-serializable object that describes the transformer. */ diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index c217ed06ac1..e5c5395e4a9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -345,6 +345,7 @@ export const { imBrushLineAdded, imEraserLineAdded, imRectAdded, + inpaintMaskRasterized, // Staging sessionStarted, sessionStartedStaging, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts index 8a94e523d86..a18041ce7a1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts @@ -6,6 +6,7 @@ import type { CanvasRectState, CanvasV2State, Coordinate, + EntityRasterizedArg, ScaleChangedArg, } from 'features/controlLayers/store/types'; import { imageDTOToImageWithDims } from 'features/controlLayers/store/types'; @@ -84,4 +85,10 @@ export const inpaintMaskReducers = { state.inpaintMask.bboxNeedsUpdate = true; state.layers.imageCache = null; }, + inpaintMaskRasterized: (state, action: PayloadAction) => { + const { imageObject, position } = action.payload; + state.inpaintMask.objects = [imageObject]; + state.inpaintMask.position = position; + state.inpaintMask.imageCache = null; + }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 54e046c5e0c..f87a93bd089 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -683,7 +683,7 @@ const zCanvasInpaintMaskState = z.object({ position: zCoordinate, bbox: zRect.nullable(), bboxNeedsUpdate: z.boolean(), - objects: z.array(zMaskObject), + objects: z.array(zCanvasObjectState), fill: zRgbColor, imageCache: zImageWithDims.nullable(), }); From d2c524e7fdeece6d291645071ce2dfc02fd70490 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 6 Aug 2024 13:37:41 +1000 Subject: [PATCH 305/678] fix(ui): no objects rendered until vis toggled --- .../src/features/controlLayers/konva/CanvasInpaintMask.ts | 2 +- .../web/src/features/controlLayers/konva/CanvasLayer.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts index bbc80b0f8cd..bcc1ebc14df 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts @@ -117,6 +117,6 @@ export class CanvasInpaintMask { updateVisibility = (arg?: { isEnabled: boolean }) => { this.log.trace('Updating visibility'); const isEnabled = get(arg, 'isEnabled', this.state.isEnabled); - this.konva.layer.visible(isEnabled && this.renderer.hasObjects()); + this.konva.layer.visible(isEnabled); }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 194c87984cd..11caef1bb05 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -70,6 +70,9 @@ export class CanvasLayer { this.log.debug('Updating'); const { position, objects, opacity, isEnabled } = state; + if (this.isFirstRender || isEnabled !== this.state.isEnabled) { + this.updateVisibility({ isEnabled }); + } if (this.isFirstRender || objects !== this.state.objects) { await this.updateObjects({ objects }); } @@ -79,9 +82,6 @@ export class CanvasLayer { if (this.isFirstRender || opacity !== this.state.opacity) { this.updateOpacity({ opacity }); } - if (this.isFirstRender || isEnabled !== this.state.isEnabled) { - this.updateVisibility({ isEnabled }); - } // this.transformer.syncInteractionState(); if (this.isFirstRender) { @@ -95,7 +95,7 @@ export class CanvasLayer { updateVisibility = (arg?: { isEnabled: boolean }) => { this.log.trace('Updating visibility'); const isEnabled = get(arg, 'isEnabled', this.state.isEnabled); - this.konva.layer.visible(isEnabled && this.renderer.hasObjects()); + this.konva.layer.visible(isEnabled); }; updateObjects = async (arg?: { objects: CanvasLayerState['objects'] }) => { From 718bed57588f9f3bec2edb94e52ad01fac3abf55 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 6 Aug 2024 13:43:32 +1000 Subject: [PATCH 306/678] fix(ui): brush preview fill for inpaint/region --- .../controlLayers/konva/CanvasManager.ts | 46 ++++++++++++------- .../controlLayers/konva/CanvasTool.ts | 4 +- .../src/features/controlLayers/store/types.ts | 1 + 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index e7e044ce3d3..b28197c70b2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -23,18 +23,19 @@ import { } from 'features/controlLayers/konva/util'; import type { Extents, ExtentsResult, GetBboxTask, WorkerLogMessage } from 'features/controlLayers/konva/worker'; import { $lastProgressEvent, $shouldShowStagedImage } from 'features/controlLayers/store/canvasV2Slice'; -import type { - CanvasControlAdapterState, - CanvasEntityIdentifier, - CanvasEntityState, - CanvasInpaintMaskState, - CanvasLayerState, - CanvasRegionalGuidanceState, - CanvasV2State, - Coordinate, - GenerationMode, - GetLoggingContext, - RgbaColor, +import { + type CanvasControlAdapterState, + type CanvasEntityIdentifier, + type CanvasEntityState, + type CanvasInpaintMaskState, + type CanvasLayerState, + type CanvasRegionalGuidanceState, + type CanvasV2State, + type Coordinate, + type GenerationMode, + type GetLoggingContext, + RGBA_WHITE, + type RgbaColor, } from 'features/controlLayers/store/types'; import type Konva from 'konva'; import { atom } from 'nanostores'; @@ -366,11 +367,22 @@ export class CanvasManager { let currentFill: RgbaColor = state.tool.fill; const selectedEntity = this.getSelectedEntity(); if (selectedEntity) { - // These two entity types use a compositing rect for opacity. Their alpha is always 1. - if (selectedEntity.state.type === 'regional_guidance') { - currentFill = { ...selectedEntity.state.fill, a: 1 }; - } else if (selectedEntity.state.type === 'inpaint_mask') { - currentFill = { ...state.inpaintMask.fill, a: 1 }; + // These two entity types use a compositing rect for opacity. Their fill is always white. + if (selectedEntity.state.type === 'regional_guidance' || selectedEntity.state.type === 'inpaint_mask') { + currentFill = RGBA_WHITE; + } + } + return currentFill; + }; + + getBrushPreviewFill = () => { + const state = this.stateApi.getState(); + let currentFill: RgbaColor = state.tool.fill; + const selectedEntity = this.getSelectedEntity(); + if (selectedEntity) { + // The brush should use the mask opacity for these entity types + if (selectedEntity.state.type === 'regional_guidance' || selectedEntity.state.type === 'inpaint_mask') { + currentFill = { ...selectedEntity.state.fill, a: this.stateApi.getSettings().maskOpacity }; } } return currentFill; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts index a80a5a02bf7..7a20d3579e5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts @@ -127,7 +127,6 @@ export class CanvasTool { const stage = this.manager.stage; const renderedEntityCount: number = 1; // TODO(psyche): this.manager should be renderable entity count const toolState = this.manager.stateApi.getToolState(); - const currentFill = this.manager.getCurrentFill(); const selectedEntity = this.manager.getSelectedEntity(); const cursorPos = this.manager.stateApi.$lastCursorPos.get(); const isDrawing = this.manager.stateApi.$isDrawing.get(); @@ -172,6 +171,7 @@ export class CanvasTool { // No need to render the brush preview if the cursor position or color is missing if (cursorPos && tool === 'brush') { + const brushPreviewFill = this.manager.getBrushPreviewFill(); const alignedCursorPos = alignCoordForTool(cursorPos, toolState.brush.width); const scale = stage.scaleX(); // Update the fill circle @@ -181,7 +181,7 @@ export class CanvasTool { x: alignedCursorPos.x, y: alignedCursorPos.y, radius, - fill: isDrawing ? '' : rgbaColorToString(currentFill), + fill: isDrawing ? '' : rgbaColorToString(brushPreviewFill), }); // Update the inner border of the brush preview diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index f87a93bd089..520f1086ae3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -503,6 +503,7 @@ const zRgbaColor = zRgbColor.extend({ }); export type RgbaColor = z.infer; export const RGBA_RED: RgbaColor = { r: 255, g: 0, b: 0, a: 1 }; +export const RGBA_WHITE: RgbaColor = { r: 255, g: 255, b: 255, a: 1 }; const zOpacity = z.number().gte(0).lte(1); From 2c2e6c5c2584361e50a1805d6d8f3df67fb0bd5e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 6 Aug 2024 13:45:47 +1000 Subject: [PATCH 307/678] fix(ui): render transformer over objects, fix issue w/ inpaint rect color --- .../web/src/features/controlLayers/konva/CanvasInpaintMask.ts | 2 +- .../web/src/features/controlLayers/konva/CanvasLayer.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts index bcc1ebc14df..fe6e43f1939 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts @@ -43,8 +43,8 @@ export class CanvasInpaintMask { }), }; - this.transformer = new CanvasTransformer(this); this.renderer = new CanvasObjectRenderer(this); + this.transformer = new CanvasTransformer(this); this.state = state; this.maskOpacity = this.manager.stateApi.getMaskOpacity(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 11caef1bb05..6e4da97d0d8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -45,8 +45,8 @@ export class CanvasLayer { }), }; - this.transformer = new CanvasTransformer(this); this.renderer = new CanvasObjectRenderer(this); + this.transformer = new CanvasTransformer(this); this.state = state; } From ae5543b6faa3db9877d726fe8f36a2ad7dc49ca7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 6 Aug 2024 14:15:45 +1000 Subject: [PATCH 308/678] feat(ui): esc cancels drawing buffer maybe this is not wanted? we'll see --- .../konva/CanvasObjectRenderer.ts | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index 6103ae7f6b6..17e411fa303 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -248,7 +248,7 @@ export class CanvasObjectRenderer { * @returns A promise that resolves to a boolean, indicating if the object was rendered. */ setBuffer = async (objectState: AnyObjectState): Promise => { - this.log.trace('Setting buffer object'); + this.log.trace('Setting buffer'); this.buffer = objectState; return await this.renderObject(this.buffer, true); @@ -258,7 +258,16 @@ export class CanvasObjectRenderer { * Clears the buffer object state. */ clearBuffer = () => { - this.log.trace('Clearing buffer object'); + this.log.trace('Clearing buffer'); + + if (this.buffer) { + const renderer = this.renderers.get(this.buffer.id); + if (renderer) { + this.log.trace('Destroying buffer object renderer'); + renderer.destroy(); + this.renderers.delete(renderer.id); + } + } this.buffer = null; }; @@ -268,22 +277,22 @@ export class CanvasObjectRenderer { */ commitBuffer = () => { if (!this.buffer) { - this.log.trace('No buffer object to commit'); + this.log.trace('No buffer to commit'); return; } - this.log.trace('Committing buffer object'); + this.log.trace('Committing buffer'); // We need to give the objects a fresh ID else they will be considered the same object when they are re-rendered as // a non-buffer object, and we won't trigger things like bbox calculation this.buffer.id = getPrefixedId(this.buffer.type); if (this.buffer.type === 'brush_line') { - this.manager.stateApi.addBrushLine({ id: this.parent.id, brushLine: this.buffer }, this.parent.type); + this.manager.stateApi.addBrushLine({ id: this.parent.id, brushLine: this.buffer }, this.parent.state.type); } else if (this.buffer.type === 'eraser_line') { - this.manager.stateApi.addEraserLine({ id: this.parent.id, eraserLine: this.buffer }, this.parent.type); + this.manager.stateApi.addEraserLine({ id: this.parent.id, eraserLine: this.buffer }, this.parent.state.type); } else if (this.buffer.type === 'rect') { - this.manager.stateApi.addRect({ id: this.parent.id, rect: this.buffer }, this.parent.type); + this.manager.stateApi.addRect({ id: this.parent.id, rect: this.buffer }, this.parent.state.type); } else { this.log.warn({ buffer: this.buffer }, 'Invalid buffer object type'); } From 008d8f491c8302789d0383d488897aabb63194fb Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 6 Aug 2024 15:56:30 +1000 Subject: [PATCH 309/678] feat(ui): region mask rendering --- .../components/HeadsUpDisplay.tsx | 4 +- .../{CanvasLayer.ts => CanvasLayerAdapter.ts} | 21 +-- .../controlLayers/konva/CanvasManager.ts | 123 ++++++++++-------- ...vasInpaintMask.ts => CanvasMaskAdapter.ts} | 36 +++-- .../konva/CanvasObjectRenderer.ts | 39 +++--- .../controlLayers/konva/CanvasStateApi.ts | 3 + .../controlLayers/konva/CanvasTransformer.ts | 41 +++--- .../features/controlLayers/konva/events.ts | 24 +++- .../controlLayers/store/canvasV2Slice.ts | 7 +- .../controlLayers/store/regionsReducers.ts | 11 ++ .../src/features/controlLayers/store/types.ts | 22 +++- 11 files changed, 200 insertions(+), 131 deletions(-) rename invokeai/frontend/web/src/features/controlLayers/konva/{CanvasLayer.ts => CanvasLayerAdapter.ts} (90%) rename invokeai/frontend/web/src/features/controlLayers/konva/{CanvasInpaintMask.ts => CanvasMaskAdapter.ts} (80%) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx index abf799c8934..5f3bcab13cd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx @@ -24,10 +24,10 @@ export const HeadsUpDisplay = memo(() => { return ( - + diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts similarity index 90% rename from invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts index 6e4da97d0d8..7eae3240bcb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts @@ -7,13 +7,9 @@ import Konva from 'konva'; import { get } from 'lodash-es'; import type { Logger } from 'roarr'; -export class CanvasLayer { - static TYPE = 'layer' as const; - static KONVA_LAYER_NAME = `${CanvasLayer.TYPE}_layer`; - static KONVA_OBJECT_GROUP_NAME = `${CanvasLayer.TYPE}_object-group`; - +export class CanvasLayerAdapter { id: string; - type = CanvasLayer.TYPE; + type: CanvasLayerState['type']; manager: CanvasManager; log: Logger; getLoggingContext: GetLoggingContext; @@ -29,8 +25,9 @@ export class CanvasLayer { isFirstRender: boolean = true; bboxNeedsUpdate: boolean = true; - constructor(state: CanvasLayerState, manager: CanvasManager) { + constructor(state: CanvasLayerAdapter['state'], manager: CanvasLayerAdapter['manager']) { this.id = state.id; + this.type = state.type; this.manager = manager; this.getLoggingContext = this.manager.buildGetLoggingContext(this); this.log = this.manager.buildLogger(this.getLoggingContext); @@ -39,7 +36,7 @@ export class CanvasLayer { this.konva = { layer: new Konva.Layer({ id: this.id, - name: CanvasLayer.KONVA_LAYER_NAME, + name: `${this.type}:layer`, listening: false, imageSmoothingEnabled: false, }), @@ -59,7 +56,11 @@ export class CanvasLayer { this.konva.layer.destroy(); }; - update = async (arg?: { state: CanvasLayerState; toolState: CanvasV2State['tool']; isSelected: boolean }) => { + update = async (arg?: { + state: CanvasLayerAdapter['state']; + toolState: CanvasV2State['tool']; + isSelected: boolean; + }) => { const state = get(arg, 'state', this.state); if (!this.isFirstRender && state === this.state) { @@ -119,7 +120,7 @@ export class CanvasLayer { repr = () => { return { id: this.id, - type: CanvasLayer.TYPE, + type: this.type, state: deepClone(this.state), bboxNeedsUpdate: this.bboxNeedsUpdate, transformer: this.transformer.repr(), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index b28197c70b2..6cac07952cc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -26,7 +26,6 @@ import { $lastProgressEvent, $shouldShowStagedImage } from 'features/controlLaye import { type CanvasControlAdapterState, type CanvasEntityIdentifier, - type CanvasEntityState, type CanvasInpaintMaskState, type CanvasLayerState, type CanvasRegionalGuidanceState, @@ -42,15 +41,14 @@ import { atom } from 'nanostores'; import type { Logger } from 'roarr'; import { getImageDTO as defaultGetImageDTO, uploadImage as defaultUploadImage } from 'services/api/endpoints/images'; import type { ImageCategory, ImageDTO } from 'services/api/types'; -import { assert } from 'tsafe'; import { CanvasBackground } from './CanvasBackground'; import { CanvasBbox } from './CanvasBbox'; import { CanvasControlAdapter } from './CanvasControlAdapter'; -import { CanvasInpaintMask } from './CanvasInpaintMask'; -import { CanvasLayer } from './CanvasLayer'; +import { CanvasLayerAdapter } from './CanvasLayerAdapter'; +import { CanvasMaskAdapter } from './CanvasMaskAdapter'; import { CanvasPreview } from './CanvasPreview'; -import { CanvasRegion } from './CanvasRegion'; +import type { CanvasRegion } from './CanvasRegion'; import { CanvasStagingArea } from './CanvasStagingArea'; import { CanvasStateApi } from './CanvasStateApi'; import { CanvasTool } from './CanvasTool'; @@ -86,20 +84,28 @@ type Util = { type EntityStateAndAdapter = | { + id: string; + type: CanvasLayerState['type']; state: CanvasLayerState; - adapter: CanvasLayer; + adapter: CanvasLayerAdapter; } | { + id: string; + type: CanvasInpaintMaskState['type']; state: CanvasInpaintMaskState; - adapter: CanvasInpaintMask; + adapter: CanvasMaskAdapter; } | { + id: string; + type: CanvasControlAdapterState['type']; state: CanvasControlAdapterState; adapter: CanvasControlAdapter; } | { + id: string; + type: CanvasRegionalGuidanceState['type']; state: CanvasRegionalGuidanceState; - adapter: CanvasRegion; + adapter: CanvasMaskAdapter; }; export const $canvasManager = atom(null); @@ -111,9 +117,9 @@ export class CanvasManager { stage: Konva.Stage; container: HTMLDivElement; controlAdapters: Map; - layers: Map; - regions: Map; - inpaintMask: CanvasInpaintMask; + layers: Map; + regions: Map; + inpaintMask: CanvasMaskAdapter; initialImage: CanvasInitialImage; util: Util; stateApi: CanvasStateApi; @@ -221,7 +227,7 @@ export class CanvasManager { (a, b) => a?.state === b?.state && a?.adapter === b?.adapter ); - this.inpaintMask = new CanvasInpaintMask(this.stateApi.getInpaintMaskState(), this); + this.inpaintMask = new CanvasMaskAdapter(this.stateApi.getInpaintMaskState(), this); this.stage.add(this.inpaintMask.konva.layer); } @@ -248,28 +254,6 @@ export class CanvasManager { await this.initialImage.render(this.stateApi.getInitialImageState()); } - async renderRegions() { - const { entities } = this.stateApi.getRegionsState(); - - // Destroy the konva nodes for nonexistent entities - for (const canvasRegion of this.regions.values()) { - if (!entities.find((rg) => rg.id === canvasRegion.id)) { - canvasRegion.destroy(); - this.regions.delete(canvasRegion.id); - } - } - - for (const entity of entities) { - let adapter = this.regions.get(entity.id); - if (!adapter) { - adapter = new CanvasRegion(entity, this); - this.regions.set(adapter.id, adapter); - this.stage.add(adapter.konva.layer); - } - await adapter.render(entity); - } - } - async renderProgressPreview() { await this.preview.progressPreview.render(this.stateApi.$lastProgressEvent.get()); } @@ -320,8 +304,10 @@ export class CanvasManager { this.stage.width(this.container.offsetWidth); this.stage.height(this.container.offsetHeight); this.stateApi.$stageAttrs.set({ - position: { x: this.stage.x(), y: this.stage.y() }, - dimensions: { width: this.stage.width(), height: this.stage.height() }, + x: this.stage.x(), + y: this.stage.y(), + width: this.stage.width(), + height: this.stage.height(), scale: this.stage.scaleX(), }); this.background.render(); @@ -330,8 +316,13 @@ export class CanvasManager { getEntity(identifier: CanvasEntityIdentifier): EntityStateAndAdapter | null { const state = this.stateApi.getState(); - let entityState: CanvasEntityState | null = null; - let entityAdapter: CanvasLayer | CanvasRegion | CanvasControlAdapter | CanvasInpaintMask | null = null; + let entityState: + | CanvasLayerState + | CanvasControlAdapterState + | CanvasRegionalGuidanceState + | CanvasInpaintMaskState + | null = null; + let entityAdapter: CanvasLayerAdapter | CanvasControlAdapter | CanvasRegion | CanvasMaskAdapter | null = null; if (identifier.type === 'layer') { entityState = state.layers.entities.find((i) => i.id === identifier.id) ?? null; @@ -348,7 +339,12 @@ export class CanvasManager { } if (entityState && entityAdapter && entityState.type === entityAdapter.type) { - return { state: entityState, adapter: entityAdapter } as EntityStateAndAdapter; + return { + id: entityState.id, + type: entityState.type, + state: entityState, + adapter: entityAdapter, + } as EntityStateAndAdapter; // TODO(psyche): make TS happy w/o this cast } return null; @@ -400,6 +396,8 @@ export class CanvasManager { return this.layers.get(id) ?? null; } else if (type === 'inpaint_mask') { return this.inpaintMask; + } else if (type === 'regional_guidance') { + return this.regions.get(id) ?? null; } return null; @@ -413,14 +411,14 @@ export class CanvasManager { if (this.getIsTransforming()) { return; } - const layer = this.getSelectedEntity(); + const entity = this.getSelectedEntity(); + if (!entity) { + this.log.warn('No entity selected to transform'); + return; + } // TODO(psyche): Support other entity types - assert( - layer && (layer.adapter instanceof CanvasLayer || layer.adapter instanceof CanvasInpaintMask), - 'No selected layer' - ); - layer.adapter.transformer.startTransform(); - this.transformingEntity.publish({ id: layer.state.id, type: layer.state.type }); + entity.adapter.transformer.startTransform(); + this.transformingEntity.publish({ id: entity.id, type: entity.type }); } async applyTransform() { @@ -460,7 +458,7 @@ export class CanvasManager { for (const entityState of state.layers.entities) { let adapter = this.layers.get(entityState.id); if (!adapter) { - adapter = new CanvasLayer(entityState, this); + adapter = new CanvasLayerAdapter(entityState, this); this.layers.set(adapter.id, adapter); this.stage.add(adapter.konva.layer); } @@ -491,7 +489,28 @@ export class CanvasManager { state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id ) { this.log.debug('Rendering regions'); - await this.renderRegions(); + + // Destroy the konva nodes for nonexistent entities + for (const canvasRegion of this.regions.values()) { + if (!state.regions.entities.find((rg) => rg.id === canvasRegion.id)) { + canvasRegion.destroy(); + this.regions.delete(canvasRegion.id); + } + } + + for (const entityState of state.regions.entities) { + let adapter = this.regions.get(entityState.id); + if (!adapter) { + adapter = new CanvasMaskAdapter(entityState, this); + this.regions.set(adapter.id, adapter); + this.stage.add(adapter.konva.layer); + } + await adapter.update({ + state: entityState, + toolState: state.tool, + isSelected: state.selectedEntityIdentifier?.id === entityState.id, + }); + } } if ( @@ -697,14 +716,14 @@ export class CanvasManager { | CanvasImageRenderer | CanvasTransformer | CanvasObjectRenderer - | CanvasLayer - | CanvasInpaintMask + | CanvasLayerAdapter + | CanvasMaskAdapter | CanvasStagingArea ): GetLoggingContext => { if ( - instance instanceof CanvasLayer || + instance instanceof CanvasLayerAdapter || instance instanceof CanvasStagingArea || - instance instanceof CanvasInpaintMask + instance instanceof CanvasMaskAdapter ) { return (extra?: JSONObject): JSONObject => { return { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts similarity index 80% rename from invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts index fe6e43f1939..9481fa2857b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts @@ -1,23 +1,24 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; -import type { CanvasInpaintMaskState, CanvasV2State, GetLoggingContext } from 'features/controlLayers/store/types'; +import type { + CanvasInpaintMaskState, + CanvasRegionalGuidanceState, + CanvasV2State, + GetLoggingContext, +} from 'features/controlLayers/store/types'; import Konva from 'konva'; import { get } from 'lodash-es'; import type { Logger } from 'roarr'; -export class CanvasInpaintMask { - static TYPE = 'inpaint_mask' as const; - static NAME_PREFIX = 'inpaint-mask'; - static KONVA_LAYER_NAME = `${CanvasInpaintMask.NAME_PREFIX}_layer`; - - id = CanvasInpaintMask.TYPE; - type = CanvasInpaintMask.TYPE; +export class CanvasMaskAdapter { + id: string; + type: CanvasInpaintMaskState['type'] | CanvasRegionalGuidanceState['type']; manager: CanvasManager; log: Logger; getLoggingContext: GetLoggingContext; - state: CanvasInpaintMaskState; + state: CanvasInpaintMaskState | CanvasRegionalGuidanceState; maskOpacity: number; transformer: CanvasTransformer; @@ -29,15 +30,18 @@ export class CanvasInpaintMask { layer: Konva.Layer; }; - constructor(state: CanvasInpaintMaskState, manager: CanvasManager) { + constructor(state: CanvasMaskAdapter['state'], manager: CanvasMaskAdapter['manager']) { + this.id = state.id; + this.type = state.type; this.manager = manager; this.getLoggingContext = this.manager.buildGetLoggingContext(this); this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.debug({ state }, 'Creating inpaint mask'); + this.log.debug({ state }, 'Creating mask'); this.konva = { layer: new Konva.Layer({ - name: CanvasInpaintMask.KONVA_LAYER_NAME, + id: this.id, + name: `${this.type}:layer`, listening: false, imageSmoothingEnabled: false, }), @@ -51,14 +55,18 @@ export class CanvasInpaintMask { } destroy = (): void => { - this.log.debug('Destroying inpaint mask'); + this.log.debug('Destroying mask'); // We need to call the destroy method on all children so they can do their own cleanup. this.transformer.destroy(); this.renderer.destroy(); this.konva.layer.destroy(); }; - update = async (arg?: { state: CanvasInpaintMaskState; toolState: CanvasV2State['tool']; isSelected: boolean }) => { + update = async (arg?: { + state: CanvasMaskAdapter['state']; + toolState: CanvasV2State['tool']; + isSelected: boolean; + }) => { const state = get(arg, 'state', this.state); const maskOpacity = this.manager.stateApi.getMaskOpacity(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index 17e411fa303..d56f2e76364 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -4,9 +4,9 @@ import { deepClone } from 'common/util/deepClone'; import { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine'; import { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine'; import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; -import type { CanvasInpaintMask } from 'features/controlLayers/konva/CanvasInpaintMask'; -import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; +import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; import { getPrefixedId, konvaNodeToBlob, previewBlob } from 'features/controlLayers/konva/util'; import { @@ -36,11 +36,11 @@ type AnyObjectState = CanvasBrushLineState | CanvasEraserLineState | CanvasImage */ export class CanvasObjectRenderer { static TYPE = 'object_renderer'; - static KONVA_OBJECT_GROUP_NAME = 'object-group'; - static KONVA_COMPOSITING_RECT_NAME = 'compositing-rect'; + static KONVA_OBJECT_GROUP_NAME = `${CanvasObjectRenderer.TYPE}:object_group`; + static KONVA_COMPOSITING_RECT_NAME = `${CanvasObjectRenderer.TYPE}:compositing_rect`; id: string; - parent: CanvasLayer | CanvasInpaintMask; + parent: CanvasLayerAdapter | CanvasMaskAdapter; manager: CanvasManager; log: Logger; getLoggingContext: (extra?: JSONObject) => JSONObject; @@ -87,7 +87,7 @@ export class CanvasObjectRenderer { compositingRect: Konva.Rect | null; }; - constructor(parent: CanvasLayer | CanvasInpaintMask) { + constructor(parent: CanvasLayerAdapter | CanvasMaskAdapter) { this.id = getPrefixedId(CanvasObjectRenderer.TYPE); this.parent = parent; this.manager = parent.manager; @@ -102,7 +102,7 @@ export class CanvasObjectRenderer { this.parent.konva.layer.add(this.konva.objectGroup); - if (this.parent.type === 'inpaint_mask') { + if (this.parent.type === 'inpaint_mask' || this.parent.type === 'regional_guidance') { this.konva.compositingRect = new Konva.Rect({ name: CanvasObjectRenderer.KONVA_COMPOSITING_RECT_NAME, listening: false, @@ -122,13 +122,13 @@ export class CanvasObjectRenderer { // The compositing rect must cover the whole stage at all times. When the stage is scaled, moved or resized, we // need to update the compositing rect to match the stage. this.subscriptions.add( - this.manager.stateApi.$stageAttrs.listen((attrs) => { + this.manager.stateApi.$stageAttrs.listen(({ x, y, width, height, scale }) => { if (this.konva.compositingRect) { this.konva.compositingRect.setAttrs({ - x: -attrs.position.x / attrs.scale, - y: -attrs.position.y / attrs.scale, - width: attrs.dimensions.width / attrs.scale, - height: attrs.dimensions.height / attrs.scale, + x: -x / scale, + y: -y / scale, + width: width / scale, + height: height / scale, }); } }) @@ -168,9 +168,14 @@ export class CanvasObjectRenderer { assert(this.konva.compositingRect, 'Missing compositing rect'); const rgbColor = rgbColorToString(fill); + const { x, y, width, height, scale } = this.manager.stateApi.$stageAttrs.get(); this.konva.compositingRect.setAttrs({ fill: rgbColor, opacity, + x: -x / scale, + y: -y / scale, + width: width / scale, + height: height / scale, }); }; @@ -288,11 +293,11 @@ export class CanvasObjectRenderer { this.buffer.id = getPrefixedId(this.buffer.type); if (this.buffer.type === 'brush_line') { - this.manager.stateApi.addBrushLine({ id: this.parent.id, brushLine: this.buffer }, this.parent.state.type); + this.manager.stateApi.addBrushLine({ id: this.parent.id, brushLine: this.buffer }, this.parent.type); } else if (this.buffer.type === 'eraser_line') { - this.manager.stateApi.addEraserLine({ id: this.parent.id, eraserLine: this.buffer }, this.parent.state.type); + this.manager.stateApi.addEraserLine({ id: this.parent.id, eraserLine: this.buffer }, this.parent.type); } else if (this.buffer.type === 'rect') { - this.manager.stateApi.addRect({ id: this.parent.id, rect: this.buffer }, this.parent.state.type); + this.manager.stateApi.addRect({ id: this.parent.id, rect: this.buffer }, this.parent.type); } else { this.log.warn({ buffer: this.buffer }, 'Invalid buffer object type'); } @@ -340,13 +345,13 @@ export class CanvasObjectRenderer { const rect = interactionRectClone.getClientRect(); const blob = await konvaNodeToBlob(objectGroupClone, rect); if (this.manager._isDebugging) { - previewBlob(blob, 'Rasterized layer'); + previewBlob(blob, 'Rasterized entity'); } const imageDTO = await uploadImage(blob, `${this.id}_rasterized.png`, 'other', true); const imageObject = imageDTOToImageObject(imageDTO); await this.renderObject(imageObject, true); this.manager.stateApi.rasterizeEntity( - { id: this.id, imageObject, position: { x: Math.round(rect.x), y: Math.round(rect.y) } }, + { id: this.parent.id, imageObject, position: { x: Math.round(rect.x), y: Math.round(rect.y) } }, this.parent.type ); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index f8e64360517..7f738d3fcfd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -31,6 +31,7 @@ import { layerRectAdded, layerReset, layerTranslated, + regionMaskRasterized, rgBrushLineAdded, rgEraserLineAdded, rgImageCacheChanged, @@ -122,6 +123,8 @@ export class CanvasStateApi { this._store.dispatch(layerRasterized(arg)); } else if (entityType === 'inpaint_mask') { this._store.dispatch(inpaintMaskRasterized(arg)); + } else if (entityType === 'regional_guidance') { + this._store.dispatch(regionMaskRasterized(arg)); } else { assert(false, 'Rasterizing not supported for this entity type'); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index a96b3ce12d1..cf6960052b2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -1,6 +1,6 @@ -import type { CanvasInpaintMask } from 'features/controlLayers/konva/CanvasInpaintMask'; -import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; +import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter'; import { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; import { getEmptyRect, getPrefixedId } from 'features/controlLayers/konva/util'; import type { Coordinate, GetLoggingContext, Rect } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -17,9 +17,10 @@ import type { Logger } from 'roarr'; */ export class CanvasTransformer { static TYPE = 'entity_transformer'; - static TRANSFORMER_NAME = `${CanvasTransformer.TYPE}:transformer`; - static PROXY_RECT_NAME = `${CanvasTransformer.TYPE}:proxy_rect`; - static BBOX_OUTLINE_NAME = `${CanvasTransformer.TYPE}:bbox_outline`; + static KONVA_TRANSFORMER_NAME = `${CanvasTransformer.TYPE}:transformer`; + static KONVA_PROXY_RECT_NAME = `${CanvasTransformer.TYPE}:proxy_rect`; + static KONVA_OUTLINE_RECT_NAME = `${CanvasTransformer.TYPE}:outline_rect`; + static STROKE_COLOR = 'hsl(200 76% 50% / 1)'; // invokeBlue.500 static ANCHOR_FILL_COLOR = CanvasTransformer.STROKE_COLOR; static ANCHOR_STROKE_COLOR = 'hsl(200 76% 77% / 1)'; // invokeBlue.200 @@ -32,7 +33,7 @@ export class CanvasTransformer { static ANCHOR_HIT_PADDING = 10; id: string; - parent: CanvasLayer | CanvasInpaintMask; + parent: CanvasLayerAdapter | CanvasMaskAdapter; manager: CanvasManager; log: Logger; getLoggingContext: GetLoggingContext; @@ -87,10 +88,10 @@ export class CanvasTransformer { konva: { transformer: Konva.Transformer; proxyRect: Konva.Rect; - bboxOutline: Konva.Rect; + outlineRect: Konva.Rect; }; - constructor(parent: CanvasLayer | CanvasInpaintMask) { + constructor(parent: CanvasLayerAdapter | CanvasMaskAdapter) { this.id = getPrefixedId(CanvasTransformer.TYPE); this.parent = parent; this.manager = parent.manager; @@ -99,16 +100,16 @@ export class CanvasTransformer { this.log = this.manager.buildLogger(this.getLoggingContext); this.konva = { - bboxOutline: new Konva.Rect({ + outlineRect: new Konva.Rect({ listening: false, draggable: false, - name: CanvasTransformer.BBOX_OUTLINE_NAME, + name: CanvasTransformer.KONVA_OUTLINE_RECT_NAME, stroke: CanvasTransformer.STROKE_COLOR, perfectDrawEnabled: false, strokeHitEnabled: false, }), transformer: new Konva.Transformer({ - name: CanvasTransformer.TRANSFORMER_NAME, + name: CanvasTransformer.KONVA_TRANSFORMER_NAME, // Visibility and listening are managed via activate() and deactivate() visible: false, listening: false, @@ -227,7 +228,7 @@ export class CanvasTransformer { }, }), proxyRect: new Konva.Rect({ - name: CanvasTransformer.PROXY_RECT_NAME, + name: CanvasTransformer.KONVA_PROXY_RECT_NAME, listening: false, draggable: true, }), @@ -330,7 +331,7 @@ export class CanvasTransformer { // The bbox should be updated to reflect the new position of the interaction rect, taking into account its padding // and border - this.konva.bboxOutline.setAttrs({ + this.konva.outlineRect.setAttrs({ x: this.konva.proxyRect.x() - this.manager.getScaledBboxPadding(), y: this.konva.proxyRect.y() - this.manager.getScaledBboxPadding(), }); @@ -392,7 +393,7 @@ export class CanvasTransformer { }) ); - this.parent.konva.layer.add(this.konva.bboxOutline); + this.parent.konva.layer.add(this.konva.outlineRect); this.parent.konva.layer.add(this.konva.proxyRect); this.parent.konva.layer.add(this.konva.transformer); } @@ -406,7 +407,7 @@ export class CanvasTransformer { const onePixel = this.manager.getScaledPixel(); const bboxPadding = this.manager.getScaledBboxPadding(); - this.konva.bboxOutline.setAttrs({ + this.konva.outlineRect.setAttrs({ x: position.x + bbox.x - bboxPadding, y: position.y + bbox.y - bboxPadding, width: bbox.width + bboxPadding * 2, @@ -473,7 +474,7 @@ export class CanvasTransformer { const onePixel = this.manager.getScaledPixel(); const bboxPadding = this.manager.getScaledBboxPadding(); - this.konva.bboxOutline.setAttrs({ + this.konva.outlineRect.setAttrs({ x: this.konva.proxyRect.x() - bboxPadding, y: this.konva.proxyRect.y() - bboxPadding, width: this.konva.proxyRect.width() * this.konva.proxyRect.scaleX() + bboxPadding * 2, @@ -539,7 +540,7 @@ export class CanvasTransformer { rotation: 0, }; this.parent.renderer.konva.objectGroup.setAttrs(attrs); - this.konva.bboxOutline.setAttrs(attrs); + this.konva.outlineRect.setAttrs(attrs); this.konva.proxyRect.setAttrs(attrs); }; @@ -706,11 +707,11 @@ export class CanvasTransformer { }; _showBboxOutline = () => { - this.konva.bboxOutline.visible(true); + this.konva.outlineRect.visible(true); }; _hideBboxOutline = () => { - this.konva.bboxOutline.visible(false); + this.konva.outlineRect.visible(false); }; /** @@ -735,7 +736,7 @@ export class CanvasTransformer { this.log.trace('Cleaning up listener'); cleanup(); } - this.konva.bboxOutline.destroy(); + this.konva.outlineRect.destroy(); this.konva.transformer.destroy(); this.konva.proxyRect.destroy(); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 75b8ac9b17c..486d3d422ca 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -487,8 +487,10 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { stage.scaleY(newScale); stage.position(newPos); $stageAttrs.set({ - position: newPos, - dimensions: { width: stage.width(), height: stage.height() }, + x: newPos.x, + y: newPos.y, + width: stage.width(), + height: stage.height(), scale: newScale, }); manager.background.render(); @@ -500,8 +502,10 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { //#region dragmove stage.on('dragmove', () => { $stageAttrs.set({ - position: { x: Math.floor(stage.x()), y: Math.floor(stage.y()) }, - dimensions: { width: stage.width(), height: stage.height() }, + x: Math.floor(stage.x()), + y: Math.floor(stage.y()), + width: stage.width(), + height: stage.height(), scale: stage.scaleX(), }); manager.background.render(); @@ -512,8 +516,10 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { stage.on('dragend', () => { // Stage position should always be an integer, else we get fractional pixels which are blurry $stageAttrs.set({ - position: { x: Math.floor(stage.x()), y: Math.floor(stage.y()) }, - dimensions: { width: stage.width(), height: stage.height() }, + x: Math.floor(stage.x()), + y: Math.floor(stage.y()), + width: stage.width(), + height: stage.height(), scale: stage.scaleX(), }); manager.preview.tool.render(); @@ -529,7 +535,11 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { } if (e.key === 'Escape') { // Cancel shape drawing on escape - $lastMouseDownPos.set(null); + const selectedEntity = getSelectedEntity(); + if (selectedEntity) { + selectedEntity.adapter.renderer.clearBuffer(); + $lastMouseDownPos.set(null); + } } else if (e.key === ' ') { // Select the view tool on space key down setToolBuffer(getToolState().selected); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index e5c5395e4a9..26143399368 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -289,6 +289,7 @@ export const { rgBrushLineAdded, rgEraserLineAdded, rgRectAdded, + regionMaskRasterized, // Compositing setInfillMethod, setInfillTileSize, @@ -371,8 +372,10 @@ const migrate = (state: any): any => { // Ephemeral state that does not need to be in redux export const $isPreviewVisible = atom(true); export const $stageAttrs = atom({ - position: { x: 0, y: 0 }, - dimensions: { width: 0, height: 0 }, + x: 0, + y: 0, + width: 0, + height: 0, scale: 0, }); export const $shouldShowStagedImage = atom(true); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index b847a24e86f..d4847310dbf 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -6,6 +6,7 @@ import type { CanvasRectState, CanvasV2State, CLIPVisionModelV2, + EntityRasterizedArg, IPMethodV2, PositionChangedArg, ScaleChangedArg, @@ -361,4 +362,14 @@ export const regionsReducers = { rg.bboxNeedsUpdate = true; state.layers.imageCache = null; }, + regionMaskRasterized: (state, action: PayloadAction) => { + const { id, imageObject, position } = action.payload; + const rg = selectRG(state, id); + if (!rg) { + return; + } + rg.objects = [imageObject]; + rg.position = position; + rg.imageCache = null; + }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 520f1086ae3..5fa10f9d85f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,7 +1,7 @@ import type { JSONObject } from 'common/types'; import type { CanvasControlAdapter } from 'features/controlLayers/konva/CanvasControlAdapter'; -import { CanvasInpaintMask } from 'features/controlLayers/konva/CanvasInpaintMask'; -import { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; +import { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter'; +import { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; import { CanvasRegion } from 'features/controlLayers/konva/CanvasRegion'; import { getObjectId } from 'features/controlLayers/konva/util'; import { zModelIdentifierField } from 'features/nodes/types/common'; @@ -658,7 +658,7 @@ export const zCanvasRegionalGuidanceState = z.object({ position: zCoordinate, bbox: zRect.nullable(), bboxNeedsUpdate: z.boolean(), - objects: z.array(zMaskObject), + objects: z.array(zCanvasObjectState), positivePrompt: zParameterPositivePrompt.nullable(), negativePrompt: zParameterNegativePrompt.nullable(), ipAdapters: z.array(zCanvasIPAdapterState), @@ -933,7 +933,13 @@ export type CanvasV2State = { }; }; -export type StageAttrs = { position: Coordinate; dimensions: Dimensions; scale: number }; +export type StageAttrs = { + x: Coordinate['x']; + y: Coordinate['y']; + width: Dimensions['width']; + height: Dimensions['height']; + scale: number; +}; export type PositionChangedArg = { id: string; position: Coordinate }; export type ScaleChangedArg = { id: string; scale: Coordinate; position: Coordinate }; export type BboxChangedArg = { id: string; bbox: Rect | null }; @@ -969,9 +975,11 @@ export function isDrawableEntity( } export function isDrawableEntityAdapter( - adapter: CanvasLayer | CanvasRegion | CanvasControlAdapter | CanvasInpaintMask -): adapter is CanvasLayer | CanvasRegion | CanvasInpaintMask { - return adapter instanceof CanvasLayer || adapter instanceof CanvasRegion || adapter instanceof CanvasInpaintMask; + adapter: CanvasLayerAdapter | CanvasRegion | CanvasControlAdapter | CanvasMaskAdapter +): adapter is CanvasLayerAdapter | CanvasRegion | CanvasMaskAdapter { + return ( + adapter instanceof CanvasLayerAdapter || adapter instanceof CanvasRegion || adapter instanceof CanvasMaskAdapter + ); } export function isDrawableEntityType( From 206d1b231a29140726bebf2d5b28fd29a53e9c68 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 6 Aug 2024 16:06:17 +1000 Subject: [PATCH 310/678] tidy(ui): remove unused state & actions --- .../controlLayers/konva/CanvasStateApi.ts | 12 +-- .../controlLayers/store/canvasV2Slice.ts | 12 +-- .../store/inpaintMaskReducers.ts | 34 +------- .../controlLayers/store/regionsReducers.ts | 82 ++++--------------- .../src/features/controlLayers/store/types.ts | 10 +-- 5 files changed, 30 insertions(+), 120 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index 7f738d3fcfd..8f9a664aa13 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -21,8 +21,8 @@ import { imBrushLineAdded, imEraserLineAdded, imImageCacheChanged, + imMoved, imRectAdded, - imTranslated, inpaintMaskRasterized, layerBrushLineAdded, layerEraserLineAdded, @@ -31,12 +31,12 @@ import { layerRectAdded, layerReset, layerTranslated, - regionMaskRasterized, rgBrushLineAdded, rgEraserLineAdded, rgImageCacheChanged, + rgMoved, + rgRasterized, rgRectAdded, - rgTranslated, toolBufferChanged, toolChanged, } from 'features/controlLayers/store/canvasV2Slice'; @@ -80,9 +80,9 @@ export class CanvasStateApi { if (entityType === 'layer') { this._store.dispatch(layerTranslated(arg)); } else if (entityType === 'regional_guidance') { - this._store.dispatch(rgTranslated(arg)); + this._store.dispatch(rgMoved(arg)); } else if (entityType === 'inpaint_mask') { - this._store.dispatch(imTranslated(arg)); + this._store.dispatch(imMoved(arg)); } else if (entityType === 'control_adapter') { this._store.dispatch(caTranslated(arg)); } @@ -124,7 +124,7 @@ export class CanvasStateApi { } else if (entityType === 'inpaint_mask') { this._store.dispatch(inpaintMaskRasterized(arg)); } else if (entityType === 'regional_guidance') { - this._store.dispatch(regionMaskRasterized(arg)); + this._store.dispatch(rgRasterized(arg)); } else { assert(false, 'Rasterizing not supported for this entity type'); } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 26143399368..6e8fd2e3fb1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -44,8 +44,6 @@ const initialState: CanvasV2State = { inpaintMask: { id: 'inpaint_mask', type: 'inpaint_mask', - bbox: null, - bboxNeedsUpdate: false, fill: RGBA_RED, imageCache: null, isEnabled: true, @@ -264,8 +262,7 @@ export const { rgRecalled, rgReset, rgIsEnabledToggled, - rgTranslated, - rgBboxChanged, + rgMoved, rgDeleted, rgAllDeleted, rgMovedForwardOne, @@ -285,11 +282,10 @@ export const { rgIPAdapterMethodChanged, rgIPAdapterModelChanged, rgIPAdapterCLIPVisionModelChanged, - rgScaled, rgBrushLineAdded, rgEraserLineAdded, rgRectAdded, - regionMaskRasterized, + rgRasterized, // Compositing setInfillMethod, setInfillTileSize, @@ -338,11 +334,9 @@ export const { imReset, imRecalled, imIsEnabledToggled, - imTranslated, - imBboxChanged, + imMoved, imFillChanged, imImageCacheChanged, - imScaled, imBrushLineAdded, imEraserLineAdded, imRectAdded, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts index a18041ce7a1..416c6bd341e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts @@ -7,10 +7,8 @@ import type { CanvasV2State, Coordinate, EntityRasterizedArg, - ScaleChangedArg, } from 'features/controlLayers/store/types'; import { imageDTOToImageWithDims } from 'features/controlLayers/store/types'; -import type { IRect } from 'konva/lib/types'; import type { ImageDTO } from 'services/api/types'; import type { RgbColor } from './types'; @@ -18,8 +16,6 @@ import type { RgbColor } from './types'; export const inpaintMaskReducers = { imReset: (state) => { state.inpaintMask.objects = []; - state.inpaintMask.bbox = null; - state.inpaintMask.bboxNeedsUpdate = false; state.inpaintMask.imageCache = null; }, imRecalled: (state, action: PayloadAction<{ data: CanvasInpaintMaskState }>) => { @@ -30,35 +26,10 @@ export const inpaintMaskReducers = { imIsEnabledToggled: (state) => { state.inpaintMask.isEnabled = !state.inpaintMask.isEnabled; }, - imTranslated: (state, action: PayloadAction<{ position: Coordinate }>) => { + imMoved: (state, action: PayloadAction<{ position: Coordinate }>) => { const { position } = action.payload; state.inpaintMask.position = position; }, - imScaled: (state, action: PayloadAction) => { - const { scale, position } = action.payload; - for (const obj of state.inpaintMask.objects) { - if (obj.type === 'brush_line') { - obj.points = obj.points.map((point) => point * scale); - obj.strokeWidth *= scale; - } else if (obj.type === 'eraser_line') { - obj.points = obj.points.map((point) => point * scale); - obj.strokeWidth *= scale; - } else if (obj.type === 'rect') { - obj.x *= scale; - obj.y *= scale; - obj.height *= scale; - obj.width *= scale; - } - } - state.inpaintMask.position = position; - state.inpaintMask.bboxNeedsUpdate = true; - state.inpaintMask.imageCache = null; - }, - imBboxChanged: (state, action: PayloadAction<{ bbox: IRect | null }>) => { - const { bbox } = action.payload; - state.inpaintMask.bbox = bbox; - state.inpaintMask.bboxNeedsUpdate = false; - }, imFillChanged: (state, action: PayloadAction<{ fill: RgbColor }>) => { const { fill } = action.payload; state.inpaintMask.fill = fill; @@ -70,19 +41,16 @@ export const inpaintMaskReducers = { imBrushLineAdded: (state, action: PayloadAction<{ brushLine: CanvasBrushLineState }>) => { const { brushLine } = action.payload; state.inpaintMask.objects.push(brushLine); - state.inpaintMask.bboxNeedsUpdate = true; state.layers.imageCache = null; }, imEraserLineAdded: (state, action: PayloadAction<{ eraserLine: CanvasEraserLineState }>) => { const { eraserLine } = action.payload; state.inpaintMask.objects.push(eraserLine); - state.inpaintMask.bboxNeedsUpdate = true; state.layers.imageCache = null; }, imRectAdded: (state, action: PayloadAction<{ rect: CanvasRectState }>) => { const { rect } = action.payload; state.inpaintMask.objects.push(rect); - state.inpaintMask.bboxNeedsUpdate = true; state.layers.imageCache = null; }, inpaintMaskRasterized: (state, action: PayloadAction) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index d4847310dbf..6d209967760 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -1,5 +1,6 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; +import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { CanvasBrushLineState, CanvasEraserLineState, @@ -9,16 +10,13 @@ import type { EntityRasterizedArg, IPMethodV2, PositionChangedArg, - ScaleChangedArg, } from 'features/controlLayers/store/types'; import { imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas'; -import type { IRect } from 'konva/lib/types'; import { isEqual } from 'lodash-es'; import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; -import { v4 as uuidv4 } from 'uuid'; import type { CanvasIPAdapterState, CanvasRegionalGuidanceState, RgbColor } from './types'; @@ -59,8 +57,6 @@ export const regionsReducers = { id, type: 'regional_guidance', isEnabled: true, - bbox: null, - bboxNeedsUpdate: false, objects: [], fill: getRGMaskFill(state), position: { x: 0, y: 0 }, @@ -73,7 +69,7 @@ export const regionsReducers = { state.regions.entities.push(rg); state.selectedEntityIdentifier = { type: 'regional_guidance', id }; }, - prepare: () => ({ payload: { id: uuidv4() } }), + prepare: () => ({ payload: { id: getPrefixedId('regional_guidance') } }), }, rgReset: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -82,8 +78,6 @@ export const regionsReducers = { return; } rg.objects = []; - rg.bbox = null; - rg.bboxNeedsUpdate = false; rg.imageCache = null; }, rgRecalled: (state, action: PayloadAction<{ data: CanvasRegionalGuidanceState }>) => { @@ -98,45 +92,13 @@ export const regionsReducers = { rg.isEnabled = !rg.isEnabled; } }, - rgTranslated: (state, action: PayloadAction) => { + rgMoved: (state, action: PayloadAction) => { const { id, position } = action.payload; const rg = selectRG(state, id); if (rg) { rg.position = position; } }, - rgScaled: (state, action: PayloadAction) => { - const { id, scale, position } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - for (const obj of rg.objects) { - if (obj.type === 'brush_line') { - obj.points = obj.points.map((point) => point * scale); - obj.strokeWidth *= scale; - } else if (obj.type === 'eraser_line') { - obj.points = obj.points.map((point) => point * scale); - obj.strokeWidth *= scale; - } else if (obj.type === 'rect') { - obj.x *= scale; - obj.y *= scale; - obj.height *= scale; - obj.width *= scale; - } - } - rg.position = position; - rg.bboxNeedsUpdate = true; - state.layers.imageCache = null; - }, - rgBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { - const { id, bbox } = action.payload; - const rg = selectRG(state, id); - if (rg) { - rg.bbox = bbox; - rg.bboxNeedsUpdate = false; - } - }, rgDeleted: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; state.regions.entities = state.regions.entities.filter((ca) => ca.id !== id); @@ -232,25 +194,20 @@ export const regionsReducers = { } rg.ipAdapters = rg.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId); }, - rgIPAdapterImageChanged: { - reducer: ( - state, - action: PayloadAction<{ id: string; ipAdapterId: string; imageDTO: ImageDTO | null; objectId: string }> - ) => { - const { id, ipAdapterId, imageDTO, objectId } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); - if (!ipa) { - return; - } - ipa.imageObject = imageDTO ? imageDTOToImageObject(id, objectId, imageDTO) : null; - }, - prepare: (payload: { id: string; ipAdapterId: string; imageDTO: ImageDTO | null }) => ({ - payload: { ...payload, objectId: uuidv4() }, - }), + rgIPAdapterImageChanged: ( + state, + action: PayloadAction<{ id: string; ipAdapterId: string; imageDTO: ImageDTO | null; objectId: string }> + ) => { + const { id, ipAdapterId, imageDTO } = action.payload; + const rg = selectRG(state, id); + if (!rg) { + return; + } + const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); + if (!ipa) { + return; + } + ipa.imageObject = imageDTO ? imageDTOToImageObject(imageDTO) : null; }, rgIPAdapterWeightChanged: (state, action: PayloadAction<{ id: string; ipAdapterId: string; weight: number }>) => { const { id, ipAdapterId, weight } = action.payload; @@ -337,7 +294,6 @@ export const regionsReducers = { } rg.objects.push(brushLine); - rg.bboxNeedsUpdate = true; state.layers.imageCache = null; }, rgEraserLineAdded: (state, action: PayloadAction<{ id: string; eraserLine: CanvasEraserLineState }>) => { @@ -348,7 +304,6 @@ export const regionsReducers = { } rg.objects.push(eraserLine); - rg.bboxNeedsUpdate = true; state.layers.imageCache = null; }, rgRectAdded: (state, action: PayloadAction<{ id: string; rect: CanvasRectState }>) => { @@ -359,10 +314,9 @@ export const regionsReducers = { } rg.objects.push(rect); - rg.bboxNeedsUpdate = true; state.layers.imageCache = null; }, - regionMaskRasterized: (state, action: PayloadAction) => { + rgRasterized: (state, action: PayloadAction) => { const { id, imageObject, position } = action.payload; const rg = selectRG(state, id); if (!rg) { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 5fa10f9d85f..993294832a2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -656,13 +656,11 @@ export const zCanvasRegionalGuidanceState = z.object({ type: z.literal('regional_guidance'), isEnabled: z.boolean(), position: zCoordinate, - bbox: zRect.nullable(), - bboxNeedsUpdate: z.boolean(), objects: z.array(zCanvasObjectState), + fill: zRgbColor, positivePrompt: zParameterPositivePrompt.nullable(), negativePrompt: zParameterNegativePrompt.nullable(), ipAdapters: z.array(zCanvasIPAdapterState), - fill: zRgbColor, autoNegative: zAutoNegative, imageCache: zImageWithDims.nullable(), }); @@ -682,10 +680,8 @@ const zCanvasInpaintMaskState = z.object({ type: z.literal('inpaint_mask'), isEnabled: z.boolean(), position: zCoordinate, - bbox: zRect.nullable(), - bboxNeedsUpdate: z.boolean(), - objects: z.array(zCanvasObjectState), fill: zRgbColor, + objects: z.array(zCanvasObjectState), imageCache: zImageWithDims.nullable(), }); export type CanvasInpaintMaskState = z.infer; @@ -705,8 +701,6 @@ const zCanvasControlAdapterStateBase = z.object({ type: z.literal('control_adapter'), isEnabled: z.boolean(), position: zCoordinate, - bbox: zRect.nullable(), - bboxNeedsUpdate: z.boolean(), opacity: zOpacity, filters: z.array(zFilter), weight: z.number().gte(-1).lte(2), From 00020f49fecb8c3f5ed63419d7908c15428c32c5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 6 Aug 2024 18:00:19 +1000 Subject: [PATCH 311/678] tidy(ui): remove unused code, initial image --- .../middleware/listenerMiddleware/index.ts | 2 - .../listeners/canvasSessionRequested.ts | 29 -- .../components/CanvasEntityList.tsx | 7 +- .../components/ControlLayersToolbar.tsx | 4 +- .../components/InitialImage/InitialImage.tsx | 25 -- .../InitialImage/InitialImageActionsMenu.tsx | 0 .../InitialImage/InitialImageHeader.tsx | 34 -- .../InitialImage/InitialImagePreview.tsx | 100 ------ .../InitialImage/InitialImageSettings.tsx | 13 - .../components/NewSessionButton.tsx | 15 - .../controlLayers/konva/CanvasInitialImage.ts | 65 ---- .../controlLayers/konva/CanvasManager.ts | 47 +-- .../controlLayers/konva/CanvasRegion.ts | 292 ------------------ .../controlLayers/konva/CanvasStateApi.ts | 3 - .../controlLayers/konva/entityBbox.ts | 250 --------------- .../controlLayers/store/canvasV2Slice.ts | 18 -- .../store/initialImageReducers.ts | 38 --- .../controlLayers/store/sessionReducers.ts | 4 - .../src/features/controlLayers/store/types.ts | 24 +- 19 files changed, 10 insertions(+), 960 deletions(-) delete mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSessionRequested.ts delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImage.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageActionsMenu.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageHeader.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImagePreview.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageSettings.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/NewSessionButton.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts delete mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts delete mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts delete mode 100644 invokeai/frontend/web/src/features/controlLayers/store/initialImageReducers.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts index 79b90776b8c..29df0bf5422 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -9,7 +9,6 @@ import { addBatchEnqueuedListener } from 'app/store/middleware/listenerMiddlewar import { addDeleteBoardAndImagesFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted'; import { addBoardIdSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/boardIdSelected'; import { addBulkDownloadListeners } from 'app/store/middleware/listenerMiddleware/listeners/bulkDownload'; -import { addCanvasSessionRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasSessionRequested'; import { addControlAdapterPreprocessor } from 'app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor'; import { addEnqueueRequestedLinear } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear'; import { addEnqueueRequestedNodes } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes'; @@ -90,7 +89,6 @@ addBatchEnqueuedListener(startAppListening); // addStagingAreaImageSavedListener(startAppListening); // addCommitStagingAreaImageListener(startAppListening); addStagingListeners(startAppListening); -addCanvasSessionRequestedListener(startAppListening); // Socket.IO addGeneratorProgressEventListener(startAppListening); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSessionRequested.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSessionRequested.ts deleted file mode 100644 index 0e1c92dcffc..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSessionRequested.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { - layerAdded, - layerImageAdded, - sessionRequested, - sessionStarted, -} from 'features/controlLayers/store/canvasV2Slice'; -import { getImageDTO } from 'services/api/endpoints/images'; -import { assert } from 'tsafe'; - -export const addCanvasSessionRequestedListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: sessionRequested, - effect: async (action, { getState, dispatch }) => { - const initialImageObject = getState().canvasV2.initialImage.imageObject; - if (initialImageObject) { - // We have an initial image that needs to be converted to a layer - dispatch(layerAdded()); - const newLayer = getState().canvasV2.layers.entities[0]; - assert(newLayer, 'Expected new layer to be created'); - const imageDTO = await getImageDTO(initialImageObject.image.name); - assert(imageDTO, 'Unable to fetch initial image DTO'); - dispatch(layerImageAdded({ id: newLayer.id, imageDTO })); - } - - dispatch(sessionStarted()); - }, - }); -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx index d9d0255d143..e76b6ab3ed2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx @@ -1,9 +1,7 @@ /* eslint-disable i18next/no-literal-string */ import { Flex } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import { CAEntityList } from 'features/controlLayers/components/ControlAdapter/CAEntityList'; -import { InitialImage } from 'features/controlLayers/components/InitialImage/InitialImage'; import { IM } from 'features/controlLayers/components/InpaintMask/IM'; import { IPAEntityList } from 'features/controlLayers/components/IPAdapter/IPAEntityList'; import { LayerEntityList } from 'features/controlLayers/components/Layer/LayerEntityList'; @@ -11,17 +9,14 @@ import { RGEntityList } from 'features/controlLayers/components/RegionalGuidance import { memo } from 'react'; export const CanvasEntityList = memo(() => { - const isCanvasSessionActive = useAppSelector((s) => s.canvasV2.session.isActive); - return ( - {isCanvasSessionActive && } + - {!isCanvasSessionActive && } ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index 05a7ae638f4..1b8d52585ee 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -7,7 +7,6 @@ import { BrushWidth } from 'features/controlLayers/components/BrushWidth'; import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover'; import { EraserWidth } from 'features/controlLayers/components/EraserWidth'; import { FillColorPicker } from 'features/controlLayers/components/FillColorPicker'; -import { NewSessionButton } from 'features/controlLayers/components/NewSessionButton'; import { ResetCanvasButton } from 'features/controlLayers/components/ResetCanvasButton'; import { ToolChooser } from 'features/controlLayers/components/ToolChooser'; import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup'; @@ -25,7 +24,7 @@ export const ControlLayersToolbar = memo(() => { return; } for (const l of canvasManager.layers.values()) { - l.calculateBbox(); + l.transformer.requestRectCalculation(); } }, [canvasManager]); const onChangeDebugging = useCallback( @@ -61,7 +60,6 @@ export const ControlLayersToolbar = memo(() => { - diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImage.tsx deleted file mode 100644 index 00fdc673c0f..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImage.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useDisclosure } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; -import { InitialImageHeader } from 'features/controlLayers/components/InitialImage/InitialImageHeader'; -import { InitialImageSettings } from 'features/controlLayers/components/InitialImage/InitialImageSettings'; -import { entitySelected } from 'features/controlLayers/store/canvasV2Slice'; -import { memo, useCallback } from 'react'; - -export const InitialImage = memo(() => { - const dispatch = useAppDispatch(); - const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === 'initial_image'); - const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); - const onSelect = useCallback(() => { - dispatch(entitySelected({ id: 'initial_image', type: 'initial_image' })); - }, [dispatch]); - - return ( - - - {isOpen && } - - ); -}); - -InitialImage.displayName = 'InitialImage'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageActionsMenu.tsx deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageHeader.tsx deleted file mode 100644 index 8af3e98f408..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageHeader.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Spacer } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; -import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; -import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; -import { iiIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -type Props = { - onToggleVisibility: () => void; -}; - -export const InitialImageHeader = memo(({ onToggleVisibility }: Props) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const isEnabled = useAppSelector((s) => s.canvasV2.initialImage.isEnabled); - const onToggleIsEnabled = useCallback(() => { - dispatch(iiIsEnabledToggled()); - }, [dispatch]); - const title = useMemo(() => { - return `${t('controlLayers.initialImage')}`; - }, [t]); - - return ( - - - - - - ); -}); - -InitialImageHeader.displayName = 'InitialImageHeader'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImagePreview.tsx deleted file mode 100644 index dca1a155fa7..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImagePreview.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { Flex, useShiftModifier } from '@invoke-ai/ui-library'; -import { skipToken } from '@reduxjs/toolkit/query'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIDndImage from 'common/components/IAIDndImage'; -import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; -import { bboxHeightChanged, bboxWidthChanged, iiReset } from 'features/controlLayers/store/canvasV2Slice'; -import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; -import type { ImageDraggableData, InitialImageDropData } from 'features/dnd/types'; -import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; -import { memo, useCallback, useEffect, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi'; -import { useGetImageDTOQuery } from 'services/api/endpoints/images'; - -export const InitialImagePreview = memo(() => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const initialImage = useAppSelector((s) => s.canvasV2.initialImage); - const isConnected = useAppSelector((s) => s.system.isConnected); - const optimalDimension = useAppSelector(selectOptimalDimension); - const shift = useShiftModifier(); - - const { currentData: imageDTO, isError: isErrorControlImage } = useGetImageDTOQuery( - initialImage.imageObject?.image.name ?? skipToken - ); - - const onReset = useCallback(() => { - dispatch(iiReset()); - }, [dispatch]); - - const onUseSize = useCallback(() => { - if (!imageDTO) { - return; - } - - const options = { updateAspectRatio: true, clamp: true }; - if (shift) { - const { width, height } = imageDTO; - dispatch(bboxWidthChanged({ width, ...options })); - dispatch(bboxHeightChanged({ height, ...options })); - } else { - const { width, height } = calculateNewSize(imageDTO.width / imageDTO.height, optimalDimension * optimalDimension); - dispatch(bboxWidthChanged({ width, ...options })); - dispatch(bboxHeightChanged({ height, ...options })); - } - }, [imageDTO, dispatch, optimalDimension, shift]); - - const draggableData = useMemo(() => { - if (imageDTO) { - return { - id: 'initial_image', - payloadType: 'IMAGE_DTO', - payload: { imageDTO }, - }; - } - }, [imageDTO]); - - const droppableData = useMemo( - () => ({ id: 'initial_image', actionType: 'SET_INITIAL_IMAGE' }), - [] - ); - - useEffect(() => { - if (isConnected && isErrorControlImage) { - onReset(); - } - }, [onReset, isConnected, isErrorControlImage]); - - return ( - - - - - {imageDTO && ( - - } - tooltip={t('controlnet.resetControlImage')} - /> - } - tooltip={ - shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions') - } - /> - - )} - - - ); -}); - -InitialImagePreview.displayName = 'InitialImagePreview'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageSettings.tsx deleted file mode 100644 index 9c9da2f5366..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageSettings.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; -import { InitialImagePreview } from 'features/controlLayers/components/InitialImage/InitialImagePreview'; -import { memo } from 'react'; - -export const InitialImageSettings = memo(() => { - return ( - - - - ); -}); - -InitialImageSettings.displayName = 'InitialImageSettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/NewSessionButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/NewSessionButton.tsx deleted file mode 100644 index a04ce3089b5..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/NewSessionButton.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Button } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { sessionRequested } from 'features/controlLayers/store/canvasV2Slice'; -import { memo, useCallback } from 'react'; - -export const NewSessionButton = memo(() => { - const dispatch = useAppDispatch(); - const onClick = useCallback(() => { - dispatch(sessionRequested()); - }, [dispatch]); - - return ; -}); - -NewSessionButton.displayName = 'NewSessionButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts deleted file mode 100644 index 52bb4a398a7..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; -import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { InitialImageEntity } from 'features/controlLayers/store/types'; -import Konva from 'konva'; - -export class CanvasInitialImage { - static NAME_PREFIX = 'initial-image'; - static LAYER_NAME = `${CanvasInitialImage.NAME_PREFIX}_layer`; - static GROUP_NAME = `${CanvasInitialImage.NAME_PREFIX}_group`; - static OBJECT_GROUP_NAME = `${CanvasInitialImage.NAME_PREFIX}_object-group`; - - private state: InitialImageEntity; - - id = 'initial_image'; - manager: CanvasManager; - - konva: { - layer: Konva.Layer; - group: Konva.Group; - objectGroup: Konva.Group; - }; - - image: CanvasImageRenderer | null; - - constructor(state: InitialImageEntity, manager: CanvasManager) { - this.manager = manager; - this.konva = { - layer: new Konva.Layer({ name: CanvasInitialImage.LAYER_NAME, imageSmoothingEnabled: true, listening: false }), - group: new Konva.Group({ name: CanvasInitialImage.GROUP_NAME, listening: false }), - objectGroup: new Konva.Group({ name: CanvasInitialImage.OBJECT_GROUP_NAME, listening: false }), - }; - this.konva.group.add(this.konva.objectGroup); - this.konva.layer.add(this.konva.group); - - this.image = null; - this.state = state; - } - - async render(state: InitialImageEntity) { - this.state = state; - - if (!this.state.imageObject) { - this.konva.layer.visible(false); - return; - } - - if (!this.image) { - this.image = new CanvasImageRenderer(this.state.imageObject); - this.konva.objectGroup.add(this.image.konva.group); - await this.image.update(this.state.imageObject, true); - } else if (!this.image.isLoading && !this.image.isError) { - await this.image.update(this.state.imageObject); - } - - if (this.state && this.state.isEnabled && !this.image?.isLoading && !this.image?.isError) { - this.konva.layer.visible(true); - } else { - this.konva.layer.visible(false); - } - } - - destroy(): void { - this.konva.layer.destroy(); - } -} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 6cac07952cc..d16f7997e7e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -6,7 +6,6 @@ import { PubSub } from 'common/util/PubSub/PubSub'; import type { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine'; import type { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine'; import type { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; -import { CanvasInitialImage } from 'features/controlLayers/konva/CanvasInitialImage'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { CanvasProgressPreview } from 'features/controlLayers/konva/CanvasProgressPreview'; import type { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; @@ -15,7 +14,6 @@ import { getCompositeLayerImage, getControlAdapterImage, getGenerationMode, - getInitialImage, getInpaintMaskImage, getPrefixedId, getRegionMaskImage, @@ -48,7 +46,6 @@ import { CanvasControlAdapter } from './CanvasControlAdapter'; import { CanvasLayerAdapter } from './CanvasLayerAdapter'; import { CanvasMaskAdapter } from './CanvasMaskAdapter'; import { CanvasPreview } from './CanvasPreview'; -import type { CanvasRegion } from './CanvasRegion'; import { CanvasStagingArea } from './CanvasStagingArea'; import { CanvasStateApi } from './CanvasStateApi'; import { CanvasTool } from './CanvasTool'; @@ -120,7 +117,6 @@ export class CanvasManager { layers: Map; regions: Map; inpaintMask: CanvasMaskAdapter; - initialImage: CanvasInitialImage; util: Util; stateApi: CanvasStateApi; preview: CanvasPreview; @@ -188,9 +184,6 @@ export class CanvasManager { this.regions = new Map(); this.controlAdapters = new Map(); - this.initialImage = new CanvasInitialImage(this.stateApi.getInitialImageState(), this); - this.stage.add(this.initialImage.konva.layer); - this._worker.onmessage = (event: MessageEvent) => { const { type, data } = event.data; if (type === 'log') { @@ -250,10 +243,6 @@ export class CanvasManager { this._worker.postMessage(task, [data.buffer]); } - async renderInitialImage() { - await this.initialImage.render(this.stateApi.getInitialImageState()); - } - async renderProgressPreview() { await this.preview.progressPreview.render(this.stateApi.$lastProgressEvent.get()); } @@ -286,7 +275,6 @@ export class CanvasManager { const regions = getRegionsState().entities; let zIndex = 0; this.background.konva.layer.zIndex(++zIndex); - this.initialImage.konva.layer.zIndex(++zIndex); for (const layer of layers) { this.layers.get(layer.id)?.konva.layer.zIndex(++zIndex); } @@ -322,7 +310,7 @@ export class CanvasManager { | CanvasRegionalGuidanceState | CanvasInpaintMaskState | null = null; - let entityAdapter: CanvasLayerAdapter | CanvasControlAdapter | CanvasRegion | CanvasMaskAdapter | null = null; + let entityAdapter: CanvasLayerAdapter | CanvasControlAdapter | CanvasMaskAdapter | null = null; if (identifier.type === 'layer') { entityState = state.layers.entities.find((i) => i.id === identifier.id) ?? null; @@ -470,17 +458,6 @@ export class CanvasManager { } } - if ( - this._isFirstRender || - state.initialImage !== this._prevState.initialImage || - state.bbox.rect !== this._prevState.bbox.rect || - state.tool.selected !== this._prevState.tool.selected || - state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id - ) { - this.log.debug('Rendering initial image'); - await this.renderInitialImage(); - } - if ( this._isFirstRender || state.regions.entities !== this._prevState.regions.entities || @@ -546,8 +523,7 @@ export class CanvasManager { if ( this._isFirstRender || state.bbox !== this._prevState.bbox || - state.tool.selected !== this._prevState.tool.selected || - state.session.isActive !== this._prevState.session.isActive + state.tool.selected !== this._prevState.tool.selected ) { this.log.debug('Rendering generation bbox'); await this.preview.bbox.render(); @@ -656,18 +632,7 @@ export class CanvasManager { } getGenerationMode(): GenerationMode { - const session = this.stateApi.getSession(); - if (session.isActive) { - return getGenerationMode({ manager: this }); - } - - const initialImageState = this.stateApi.getInitialImageState(); - - if (initialImageState.imageObject && initialImageState.isEnabled) { - return 'img2img'; - } - - return 'txt2img'; + return getGenerationMode({ manager: this }); } getControlAdapterImage(arg: Omit[0], 'manager'>) { @@ -683,11 +648,7 @@ export class CanvasManager { } getInitialImage(arg: Omit[0], 'manager'>) { - if (this.stateApi.getSession().isActive) { - return getCompositeLayerImage({ ...arg, manager: this }); - } else { - return getInitialImage({ ...arg, manager: this }); - } + return getCompositeLayerImage({ ...arg, manager: this }); } getLoggingContext() { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts deleted file mode 100644 index 74a0cdd6de2..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts +++ /dev/null @@ -1,292 +0,0 @@ -import { rgbColorToString } from 'common/util/colorCodeTransformers'; -import { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine'; -import { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine'; -import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; -import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox'; -import { mapId } from 'features/controlLayers/konva/util'; -import type { - CanvasBrushLineState, - CanvasEraserLineState, - CanvasRectState, - CanvasRegionalGuidanceState, -} from 'features/controlLayers/store/types'; -import { isDrawingTool, RGBA_RED } from 'features/controlLayers/store/types'; -import Konva from 'konva'; -import { assert } from 'tsafe'; - -export class CanvasRegion { - static NAME_PREFIX = 'region'; - static LAYER_NAME = `${CanvasRegion.NAME_PREFIX}_layer`; - static TRANSFORMER_NAME = `${CanvasRegion.NAME_PREFIX}_transformer`; - static GROUP_NAME = `${CanvasRegion.NAME_PREFIX}_group`; - static OBJECT_GROUP_NAME = `${CanvasRegion.NAME_PREFIX}_object-group`; - static COMPOSITING_RECT_NAME = `${CanvasRegion.NAME_PREFIX}_compositing-rect`; - static TYPE = 'regional_guidance' as const; - - private drawingBuffer: CanvasBrushLineState | CanvasEraserLineState | CanvasRectState | null; - private state: CanvasRegionalGuidanceState; - - id: string; - type = CanvasRegion.TYPE; - manager: CanvasManager; - - konva: { - layer: Konva.Layer; - group: Konva.Group; - objectGroup: Konva.Group; - compositingRect: Konva.Rect; - transformer: Konva.Transformer; - }; - - objects: Map; - - constructor(state: CanvasRegionalGuidanceState, manager: CanvasManager) { - this.id = state.id; - this.manager = manager; - - this.konva = { - layer: new Konva.Layer({ name: CanvasRegion.LAYER_NAME, listening: false }), - group: new Konva.Group({ name: CanvasRegion.GROUP_NAME, listening: false }), - objectGroup: new Konva.Group({ name: CanvasRegion.OBJECT_GROUP_NAME, listening: false }), - transformer: new Konva.Transformer({ - name: CanvasRegion.TRANSFORMER_NAME, - shouldOverdrawWholeArea: true, - draggable: true, - dragDistance: 0, - enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], - rotateEnabled: false, - flipEnabled: false, - }), - compositingRect: new Konva.Rect({ name: CanvasRegion.COMPOSITING_RECT_NAME, listening: false }), - }; - this.konva.group.add(this.konva.objectGroup); - this.konva.layer.add(this.konva.group); - this.konva.transformer.on('transformend', () => { - this.manager.stateApi.onScaleChanged( - { - id: this.id, - scale: this.konva.group.scaleX(), - position: { x: this.konva.group.x(), y: this.konva.group.y() }, - }, - 'regional_guidance' - ); - }); - this.konva.transformer.on('dragend', () => { - this.manager.stateApi.setEntityPosition( - { id: this.id, position: { x: this.konva.group.x(), y: this.konva.group.y() } }, - 'regional_guidance' - ); - }); - this.konva.layer.add(this.konva.transformer); - this.konva.group.add(this.konva.compositingRect); - this.objects = new Map(); - this.drawingBuffer = null; - this.state = state; - } - - destroy(): void { - this.konva.layer.destroy(); - } - - getDrawingBuffer() { - return this.drawingBuffer; - } - - async setDrawingBuffer(obj: CanvasBrushLineState | CanvasEraserLineState | CanvasRectState | null) { - this.drawingBuffer = obj; - if (this.drawingBuffer) { - if (this.drawingBuffer.type === 'brush_line') { - this.drawingBuffer.color = RGBA_RED; - } else if (this.drawingBuffer.type === 'rect') { - this.drawingBuffer.color = RGBA_RED; - } - - await this.renderObject(this.drawingBuffer, true); - this.updateGroup(true); - } - } - - finalizeDrawingBuffer() { - if (!this.drawingBuffer) { - return; - } - if (this.drawingBuffer.type === 'brush_line') { - this.manager.stateApi.addBrushLine({ id: this.id, brushLine: this.drawingBuffer }, 'regional_guidance'); - } else if (this.drawingBuffer.type === 'eraser_line') { - this.manager.stateApi.addEraserLine({ id: this.id, eraserLine: this.drawingBuffer }, 'regional_guidance'); - } else if (this.drawingBuffer.type === 'rect') { - this.manager.stateApi.addRect({ id: this.id, rect: this.drawingBuffer }, 'regional_guidance'); - } - this.setDrawingBuffer(null); - } - - async render(state: CanvasRegionalGuidanceState) { - this.state = state; - - // Update the layer's position and listening state - this.konva.group.setAttrs({ - x: state.position.x, - y: state.position.y, - scaleX: 1, - scaleY: 1, - }); - - let didDraw = false; - - const objectIds = state.objects.map(mapId); - // Destroy any objects that are no longer in state - for (const object of this.objects.values()) { - if (!objectIds.includes(object.id)) { - this.objects.delete(object.id); - object.destroy(); - didDraw = true; - } - } - - for (const obj of state.objects) { - if (await this.renderObject(obj)) { - didDraw = true; - } - } - - if (this.drawingBuffer) { - if (await this.renderObject(this.drawingBuffer)) { - didDraw = true; - } - } - - this.updateGroup(didDraw); - } - - private async renderObject(obj: CanvasRegionalGuidanceState['objects'][number], force = false): Promise { - if (obj.type === 'brush_line') { - let brushLine = this.objects.get(obj.id); - assert(brushLine instanceof CanvasBrushLineRenderer || brushLine === undefined); - - if (!brushLine) { - brushLine = new CanvasBrushLineRenderer(obj); - this.objects.set(brushLine.id, brushLine); - this.konva.objectGroup.add(brushLine.konva.group); - return true; - } else { - if (brushLine.update(obj, force)) { - return true; - } - } - } else if (obj.type === 'eraser_line') { - let eraserLine = this.objects.get(obj.id); - assert(eraserLine instanceof CanvasEraserLineRenderer || eraserLine === undefined); - - if (!eraserLine) { - eraserLine = new CanvasEraserLineRenderer(obj); - this.objects.set(eraserLine.id, eraserLine); - this.konva.objectGroup.add(eraserLine.konva.group); - return true; - } else { - if (eraserLine.update(obj, force)) { - return true; - } - } - } else if (obj.type === 'rect') { - let rect = this.objects.get(obj.id); - assert(rect instanceof CanvasRectRenderer || rect === undefined); - - if (!rect) { - rect = new CanvasRectRenderer(obj); - this.objects.set(rect.id, rect); - this.konva.objectGroup.add(rect.konva.group); - return true; - } else { - if (rect.update(obj, force)) { - return true; - } - } - } - - return false; - } - - updateGroup(didDraw: boolean) { - this.konva.layer.visible(this.state.isEnabled); - - // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work - this.konva.group.opacity(1); - - if (didDraw) { - // Convert the color to a string, stripping the alpha - the object group will handle opacity. - const rgbColor = rgbColorToString(this.state.fill); - const maskOpacity = this.manager.stateApi.getMaskOpacity(); - this.konva.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 - ...getNodeBboxFast(this.konva.objectGroup), - fill: rgbColor, - opacity: maskOpacity, - // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) - globalCompositeOperation: 'source-in', - // This rect must always be on top of all other shapes - zIndex: this.objects.size + 1, - }); - } - - const isSelected = this.manager.stateApi.getIsSelected(this.id); - const selectedTool = this.manager.stateApi.getToolState().selected; - - if (this.objects.size === 0) { - // If the layer is totally empty, reset the cache and bail out. - this.konva.layer.listening(false); - this.konva.transformer.nodes([]); - if (this.konva.group.isCached()) { - this.konva.group.clearCache(); - } - return; - } - - if (isSelected && selectedTool === 'move') { - // When the layer is selected and being moved, we should always cache it. - // We should update the cache if we drew to the layer. - if (!this.konva.group.isCached() || didDraw) { - this.konva.group.cache(); - } - // Activate the transformer - this.konva.layer.listening(true); - this.konva.transformer.nodes([this.konva.group]); - this.konva.transformer.forceUpdate(); - return; - } - - if (isSelected && selectedTool !== 'move') { - // If the layer is selected but not using the move tool, we don't want the layer to be listening. - this.konva.layer.listening(false); - // The transformer also does not need to be active. - this.konva.transformer.nodes([]); - if (isDrawingTool(selectedTool)) { - // We are using a drawing tool (brush, eraser, rect). These tools change the layer's rendered appearance, so we - // should never be cached. - if (this.konva.group.isCached()) { - this.konva.group.clearCache(); - } - } else { - // We are using a non-drawing tool (move, view, bbox), so we should cache the layer. - // We should update the cache if we drew to the layer. - if (!this.konva.group.isCached() || didDraw) { - this.konva.group.cache(); - } - } - return; - } - - if (!isSelected) { - // Unselected layers should not be listening - this.konva.layer.listening(false); - // The transformer also does not need to be active. - this.konva.transformer.nodes([]); - // Update the layer's cache if it's not already cached or we drew to it. - if (!this.konva.group.isCached() || didDraw) { - this.konva.group.cache(); - } - - return; - } - } -} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index 8f9a664aa13..6d6b2368559 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -188,9 +188,6 @@ export class CanvasStateApi { getInpaintMaskState = () => { return this.getState().inpaintMask; }; - getInitialImageState = () => { - return this.getState().initialImage; - }; getMaskOpacity = () => { return this.getState().settings.maskOpacity; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts deleted file mode 100644 index 8e8763460bc..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/entityBbox.ts +++ /dev/null @@ -1,250 +0,0 @@ -import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; -import { getLayerBboxId } from 'features/controlLayers/konva/naming'; -import { imageDataToDataURL } from 'features/controlLayers/konva/util'; -import type { - BboxChangedArg, - CanvasControlAdapterState, - CanvasEntityState, - CanvasLayerState, - CanvasRegionalGuidanceState, -} from 'features/controlLayers/store/types'; -import Konva from 'konva'; -import type { IRect } from 'konva/lib/types'; -import { assert } from 'tsafe'; - -/** - * Creates a bounding box rect for a layer. - * @param entity The layer state for the layer to create the bounding box for - * @param konvaLayer The konva layer to attach the bounding box to - */ -export const createBboxRect = (entity: CanvasEntityState, konvaLayer: Konva.Layer): Konva.Rect => { - const rect = new Konva.Rect({ - id: getLayerBboxId(entity.id), - name: 'bbox', - strokeWidth: 1, - visible: false, - }); - konvaLayer.add(rect); - return rect; -}; - -/** - * Logic to create and render bounding boxes for layers. - * Some utils are included for calculating bounding boxes. - */ - -type Extents = { - minX: number; - minY: number; - maxX: number; - maxY: number; -}; - -const GET_CLIENT_RECT_CONFIG = { skipTransform: true }; - -/** - * Get the bounding box of an image. - * @param imageData The ImageData object to get the bounding box of. - * @returns The minimum and maximum x and y values of the image's bounding box, or null if the image has no pixels. - */ -export const getImageDataBbox = (imageData: ImageData): Extents | null => { - const { data, width, height } = imageData; - let minX = width; - let minY = height; - let maxX = -1; - let maxY = -1; - let alpha = 0; - let isEmpty = true; - - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - alpha = data[(y * width + x) * 4 + 3] ?? 0; - if (alpha > 0) { - isEmpty = false; - if (x < minX) { - minX = x; - } - if (x > maxX) { - maxX = x; - } - if (y < minY) { - minY = y; - } - if (y > maxY) { - maxY = y; - } - } - } - } - - return isEmpty ? null : { minX, minY, maxX: maxX + 1, maxY: maxY + 1 }; -}; - -/** - * Clones a regional guidance konva layer onto an offscreen stage/canvas. This allows the pixel data for a given layer - * to be captured, manipulated or analyzed without interference from other layers. - * @param layer The konva layer to clone. - * @param filterChildren A callback to filter out unwanted children - * @returns The cloned stage and layer. - */ -const getIsolatedLayerClone = ( - layer: Konva.Layer, - filterChildren: (node: Konva.Node) => boolean -): { stageClone: Konva.Stage; layerClone: Konva.Layer } => { - const stage = layer.getStage(); - - // Construct an offscreen canvas with the same dimensions as the layer's stage. - const offscreenStageContainer = document.createElement('div'); - const stageClone = new Konva.Stage({ - container: offscreenStageContainer, - x: stage.x(), - y: stage.y(), - width: stage.width(), - height: stage.height(), - }); - - // Clone the layer and filter out unwanted children. - const layerClone = layer.clone(); - stageClone.add(layerClone); - - for (const child of layerClone.getChildren()) { - if (filterChildren(child) && child.hasChildren()) { - // We need to cache the group to ensure it composites out eraser strokes correctly - child.opacity(1); - child.cache(); - } else { - // Filter out unwanted children. - child.destroy(); - } - } - - return { stageClone, layerClone }; -}; - -/** - * Get the bounding box of a regional prompt konva layer. This function has special handling for regional prompt layers. - * @param layer The konva layer to get the bounding box of. - * @param preview Whether to open a new tab displaying the rendered layer, which is used to calculate the bbox. - */ -const getLayerBboxPixels = ( - layer: Konva.Layer, - filterChildren: (node: Konva.Node) => boolean, - preview: boolean = false -): IRect | null => { - // To calculate the layer's bounding box, we must first export it to a pixel array, then do some math. - // - // Though it is relatively fast, we can't use Konva's `getClientRect`. It programmatically determines the rect - // by calculating the extents of individual shapes from their "vector" shape data. - // - // This doesn't work when some shapes are drawn with composite operations that "erase" pixels, like eraser lines. - // These shapes' extents are still calculated as if they were solid, leading to a bounding box that is too large. - const { stageClone, layerClone } = getIsolatedLayerClone(layer, filterChildren); - - // Get a worst-case rect using the relatively fast `getClientRect`. - const layerRect = layerClone.getClientRect(); - if (layerRect.width === 0 || layerRect.height === 0) { - return null; - } - // Capture the image data with the above rect. - const layerImageData = stageClone - .toCanvas(layerRect) - .getContext('2d') - ?.getImageData(0, 0, layerRect.width, layerRect.height); - assert(layerImageData, "Unable to get layer's image data"); - - if (preview) { - openBase64ImageInTab([{ base64: imageDataToDataURL(layerImageData), caption: layer.id() }]); - } - - // Calculate the layer's bounding box. - const layerBbox = getImageDataBbox(layerImageData); - - if (!layerBbox) { - return null; - } - - // Correct the bounding box to be relative to the layer's position. - const correctedLayerBbox = { - x: layerBbox.minX - Math.floor(stageClone.x()) + layerRect.x - Math.floor(layer.x()), - y: layerBbox.minY - Math.floor(stageClone.y()) + layerRect.y - Math.floor(layer.y()), - width: layerBbox.maxX - layerBbox.minX, - height: layerBbox.maxY - layerBbox.minY, - }; - - return correctedLayerBbox; -}; - -/** - * Get the bounding box of a konva node. This function is faster than `getLayerBboxPixels` but less accurate. It - * should only be used when there are no eraser strokes or shapes in the node. - * @param node The konva node to get the bounding box of. - * @returns The bounding box of the node. - */ -export const getNodeBboxFast = (node: Konva.Node): IRect => { - const bbox = node.getClientRect(GET_CLIENT_RECT_CONFIG); - return bbox; -}; - -// TODO(psyche): fix this -const filterRGChildren = (node: Konva.Node): boolean => true; -const filterLayerChildren = (node: Konva.Node): boolean => true; -const filterCAChildren = (node: Konva.Node): boolean => true; - -/** - * Calculates the bbox of each regional guidance layer. Only calculates if the mask has changed. - * @param stage The konva stage - * @param entityStates An array of layers to calculate bboxes for - * @param onBboxChanged Callback for when the bounding box changes - */ -export const updateBboxes = ( - stage: Konva.Stage, - layers: CanvasLayerState[], - controlAdapters: CanvasControlAdapterState[], - regions: CanvasRegionalGuidanceState[], - onBboxChanged: (arg: BboxChangedArg, entityType: CanvasEntityState['type']) => void -): void => { - for (const entityState of [...layers, ...controlAdapters, ...regions]) { - const konvaLayer = stage.findOne(`#${entityState.id}`); - assert(konvaLayer, `Layer ${entityState.id} not found in stage`); - // We only need to recalculate the bbox if the layer has changed - if (entityState.bboxNeedsUpdate) { - const bboxRect = konvaLayer.findOne('.bbox') ?? createBboxRect(entityState, konvaLayer); - - // Hide the bbox while we calculate the new bbox, else the bbox will be included in the calculation - const visible = bboxRect.visible(); - bboxRect.visible(false); - - if (entityState.type === 'layer') { - if (entityState.objects.length === 0) { - // No objects - no bbox to calculate - onBboxChanged({ id: entityState.id, bbox: null }, 'layer'); - } else { - onBboxChanged({ id: entityState.id, bbox: getLayerBboxPixels(konvaLayer, filterLayerChildren) }, 'layer'); - } - } else if (entityState.type === 'control_adapter') { - if (!entityState.imageObject && !entityState.processedImageObject) { - // No objects - no bbox to calculate - onBboxChanged({ id: entityState.id, bbox: null }, 'control_adapter'); - } else { - onBboxChanged( - { id: entityState.id, bbox: getLayerBboxPixels(konvaLayer, filterCAChildren) }, - 'control_adapter' - ); - } - } else if (entityState.type === 'regional_guidance') { - if (entityState.objects.length === 0) { - // No objects - no bbox to calculate - onBboxChanged({ id: entityState.id, bbox: null }, 'regional_guidance'); - } else { - onBboxChanged( - { id: entityState.id, bbox: getLayerBboxPixels(konvaLayer, filterRGChildren) }, - 'regional_guidance' - ); - } - } - - // Restore the visibility of the bbox - bboxRect.visible(visible); - } - } -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 6e8fd2e3fb1..8252f4627fc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -5,7 +5,6 @@ import { deepClone } from 'common/util/deepClone'; import { bboxReducers } from 'features/controlLayers/store/bboxReducers'; import { compositingReducers } from 'features/controlLayers/store/compositingReducers'; import { controlAdaptersReducers } from 'features/controlLayers/store/controlAdaptersReducers'; -import { initialImageReducers } from 'features/controlLayers/store/initialImageReducers'; import { inpaintMaskReducers } from 'features/controlLayers/store/inpaintMaskReducers'; import { ipAdaptersReducers } from 'features/controlLayers/store/ipAdaptersReducers'; import { layersReducers } from 'features/controlLayers/store/layersReducers'; @@ -33,14 +32,6 @@ const initialState: CanvasV2State = { ipAdapters: { entities: [] }, regions: { entities: [] }, loras: [], - initialImage: { - id: 'initial_image', - type: 'initial_image', - bbox: null, - bboxNeedsUpdate: false, - isEnabled: true, - imageObject: null, - }, inpaintMask: { id: 'inpaint_mask', type: 'inpaint_mask', @@ -125,7 +116,6 @@ const initialState: CanvasV2State = { refinerStart: 0.8, }, session: { - isActive: false, isStaging: false, stagedImages: [], selectedStagedImageIndex: 0, @@ -148,7 +138,6 @@ export const canvasV2Slice = createSlice({ ...bboxReducers, ...inpaintMaskReducers, ...sessionReducers, - ...initialImageReducers, entitySelected: (state, action: PayloadAction) => { state.selectedEntityIdentifier = action.payload; }, @@ -175,7 +164,6 @@ export const canvasV2Slice = createSlice({ state.session = deepClone(initialState.session); state.tool = deepClone(initialState.tool); state.inpaintMask = deepClone(initialState.inpaintMask); - state.initialImage = deepClone(initialState.initialImage); }, }, }); @@ -342,18 +330,12 @@ export const { imRectAdded, inpaintMaskRasterized, // Staging - sessionStarted, sessionStartedStaging, sessionImageStaged, sessionStagedImageDiscarded, sessionStagingAreaReset, sessionNextStagedImageSelected, sessionPrevStagedImageSelected, - // Initial image - iiRecalled, - iiIsEnabledToggled, - iiReset, - iiImageChanged, } = canvasV2Slice.actions; export const selectCanvasV2Slice = (state: RootState) => state.canvasV2; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/initialImageReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/initialImageReducers.ts deleted file mode 100644 index f50edeefaa2..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/store/initialImageReducers.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import { isEqual } from 'lodash-es'; -import type { ImageDTO } from 'services/api/types'; - -import type { CanvasV2State, InitialImageEntity } from './types'; -import { imageDTOToImageObject } from './types'; - -export const initialImageReducers = { - iiRecalled: (state, action: PayloadAction<{ data: InitialImageEntity }>) => { - const { data } = action.payload; - state.initialImage = data; - state.selectedEntityIdentifier = { type: 'initial_image', id: 'initial_image' }; - }, - iiIsEnabledToggled: (state) => { - if (!state.initialImage) { - return; - } - state.initialImage.isEnabled = !state.initialImage.isEnabled; - }, - iiReset: (state) => { - state.initialImage.imageObject = null; - }, - iiImageChanged: (state, action: PayloadAction<{ imageDTO: ImageDTO }>) => { - const { imageDTO } = action.payload; - if (!state.initialImage) { - return; - } - const newImageObject = imageDTOToImageObject(imageDTO); - if (isEqual(newImageObject, state.initialImage.imageObject)) { - return; - } - state.initialImage.bbox = null; - state.initialImage.bboxNeedsUpdate = true; - state.initialImage.isEnabled = true; - state.initialImage.imageObject = newImageObject; - state.selectedEntityIdentifier = { type: 'initial_image', id: 'initial_image' }; - }, -} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts index 03236aeaa22..7b21cb12c63 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts @@ -2,10 +2,6 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import type { CanvasV2State, StagingAreaImage } from 'features/controlLayers/store/types'; export const sessionReducers = { - sessionStarted: (state) => { - state.session.isActive = true; - state.selectedEntityIdentifier = { id: 'inpaint_mask', type: 'inpaint_mask' }; - }, sessionStartedStaging: (state) => { state.session.isStaging = true; state.session.selectedStagedImageIndex = 0; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 993294832a2..e1a32fd22ef 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -2,7 +2,6 @@ import type { JSONObject } from 'common/types'; import type { CanvasControlAdapter } from 'features/controlLayers/konva/CanvasControlAdapter'; import { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter'; import { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; -import { CanvasRegion } from 'features/controlLayers/konva/CanvasRegion'; import { getObjectId } from 'features/controlLayers/konva/util'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { AspectRatioState } from 'features/parameters/components/DocumentSize/types'; @@ -686,16 +685,6 @@ const zCanvasInpaintMaskState = z.object({ }); export type CanvasInpaintMaskState = z.infer; -const zInitialImageEntity = z.object({ - id: z.literal('initial_image'), - type: z.literal('initial_image'), - isEnabled: z.boolean(), - bbox: zRect.nullable(), - bboxNeedsUpdate: z.boolean(), - imageObject: zCanvasImageState.nullable(), -}); -export type InitialImageEntity = z.infer; - const zCanvasControlAdapterStateBase = z.object({ id: zId, type: z.literal('control_adapter'), @@ -818,8 +807,7 @@ export type CanvasEntityState = | CanvasControlAdapterState | CanvasRegionalGuidanceState | CanvasInpaintMaskState - | CanvasIPAdapterState - | InitialImageEntity; + | CanvasIPAdapterState; export type CanvasEntityIdentifier = Pick; export type LoRA = { @@ -847,7 +835,6 @@ export type CanvasV2State = { ipAdapters: { entities: CanvasIPAdapterState[] }; regions: { entities: CanvasRegionalGuidanceState[] }; loras: LoRA[]; - initialImage: InitialImageEntity; tool: { selected: Tool; selectedBuffer: Tool | null; @@ -920,7 +907,6 @@ export type CanvasV2State = { refinerStart: number; }; session: { - isActive: boolean; isStaging: boolean; stagedImages: StagingAreaImage[]; selectedStagedImageIndex: number; @@ -969,11 +955,9 @@ export function isDrawableEntity( } export function isDrawableEntityAdapter( - adapter: CanvasLayerAdapter | CanvasRegion | CanvasControlAdapter | CanvasMaskAdapter -): adapter is CanvasLayerAdapter | CanvasRegion | CanvasMaskAdapter { - return ( - adapter instanceof CanvasLayerAdapter || adapter instanceof CanvasRegion || adapter instanceof CanvasMaskAdapter - ); + adapter: CanvasLayerAdapter | CanvasControlAdapter | CanvasMaskAdapter +): adapter is CanvasLayerAdapter | CanvasMaskAdapter { + return adapter instanceof CanvasLayerAdapter || adapter instanceof CanvasMaskAdapter; } export function isDrawableEntityType( From d35120feb9697857f3493e0d0952bea5f2e29aeb Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 6 Aug 2024 18:36:28 +1000 Subject: [PATCH 312/678] perf(ui): disable stroke, perfect draw on compositing rect --- .../controlLayers/konva/CanvasObjectRenderer.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index d56f2e76364..2e4c2b81946 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -8,7 +8,11 @@ import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLaye import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; -import { getPrefixedId, konvaNodeToBlob, previewBlob } from 'features/controlLayers/konva/util'; +import { + getPrefixedId, + konvaNodeToBlob, + previewBlob, +} from 'features/controlLayers/konva/util'; import { type CanvasBrushLineState, type CanvasEraserLineState, @@ -105,8 +109,10 @@ export class CanvasObjectRenderer { if (this.parent.type === 'inpaint_mask' || this.parent.type === 'regional_guidance') { this.konva.compositingRect = new Konva.Rect({ name: CanvasObjectRenderer.KONVA_COMPOSITING_RECT_NAME, - listening: false, globalCompositeOperation: 'source-in', + listening: false, + strokeEnabled: false, + perfectDrawEnabled: false, }); this.parent.konva.layer.add(this.konva.compositingRect); } From adf293f6c99856d446f87ba6fcca979153484007 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 6 Aug 2024 18:41:40 +1000 Subject: [PATCH 313/678] feat(ui): up line tension to 0.3 --- .../web/src/features/controlLayers/konva/CanvasBrushLine.ts | 2 +- .../web/src/features/controlLayers/konva/CanvasEraserLine.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts index 1b53344cca1..1117bb2c4b8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts @@ -46,7 +46,7 @@ export class CanvasBrushLineRenderer { listening: false, shadowForStrokeEnabled: false, strokeWidth, - tension: 0, + tension: 0.3, lineCap: 'round', lineJoin: 'round', globalCompositeOperation: 'source-over', diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts index 26540ae9740..24b9b1de6c8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts @@ -45,7 +45,7 @@ export class CanvasEraserLineRenderer { listening: false, shadowForStrokeEnabled: false, strokeWidth, - tension: 0, + tension: 0.3, lineCap: 'round', lineJoin: 'round', globalCompositeOperation: 'destination-out', From 83657e0a687079f5a6e668cb3153b094acfc86b2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 6 Aug 2024 18:57:31 +1000 Subject: [PATCH 314/678] perf(ui): do not add duplicate points to lines --- .../features/controlLayers/konva/events.ts | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 486d3d422ca..97975c9e404 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -332,14 +332,18 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (toolState.selected === 'brush') { const drawingBuffer = selectedEntity.adapter.renderer.buffer; if (drawingBuffer) { - if (drawingBuffer?.type === 'brush_line') { - const nextPoint = getNextPoint(pos, toolState, getLastPointOfLine(drawingBuffer.points)); - if (nextPoint) { + if (drawingBuffer.type === 'brush_line') { + const lastPoint = getLastPointOfLine(drawingBuffer.points); + const nextPoint = getNextPoint(pos, toolState, lastPoint); + if (lastPoint && nextPoint) { const normalizedPoint = offsetCoord(nextPoint, selectedEntity.state.position); const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); - drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); - await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); - $lastAddedPoint.set(alignedPoint); + // Do not add duplicate points + if (lastPoint.x !== alignedPoint.x || lastPoint.y !== alignedPoint.y) { + drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); + await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); + $lastAddedPoint.set(alignedPoint); + } } } else { await selectedEntity.adapter.renderer.clearBuffer(); @@ -366,13 +370,17 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { const drawingBuffer = selectedEntity.adapter.renderer.buffer; if (drawingBuffer) { if (drawingBuffer.type === 'eraser_line') { - const nextPoint = getNextPoint(pos, toolState, getLastPointOfLine(drawingBuffer.points)); - if (nextPoint) { + const lastPoint = getLastPointOfLine(drawingBuffer.points); + const nextPoint = getNextPoint(pos, toolState, lastPoint); + if (lastPoint && nextPoint) { const normalizedPoint = offsetCoord(nextPoint, selectedEntity.state.position); const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); - drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); - await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); - $lastAddedPoint.set(alignedPoint); + // Do not add duplicate points + if (lastPoint.x !== alignedPoint.x || lastPoint.y !== alignedPoint.y) { + drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); + await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); + $lastAddedPoint.set(alignedPoint); + } } } else { await selectedEntity.adapter.renderer.clearBuffer(); From 97886bf62eed147e44e047347ac41c879bd9a88e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 6 Aug 2024 21:27:00 +1000 Subject: [PATCH 315/678] tidy(ui): massive cleanup - create a context for entity identifiers, massively simplifying UI for each entity int he list - consolidate common redux actions - remove now-unused code --- .../listeners/imageDeletionListeners.ts | 4 +- .../listeners/modelSelected.ts | 9 +- .../components/ControlAdapter/CA.tsx | 22 +- .../ControlAdapter/CAActionsMenu.tsx | 80 +----- .../ControlAdapter/CAEntityHeader.tsx | 30 +- .../ControlAdapter/CAOpacityAndFilter.tsx | 8 +- .../components/IPAdapter/IPA.tsx | 2 +- .../components/InpaintMask/IM.tsx | 24 +- .../components/InpaintMask/IMActionsMenu.tsx | 19 +- .../components/InpaintMask/IMHeader.tsx | 19 +- .../controlLayers/components/Layer/Layer.tsx | 24 +- .../components/Layer/LayerActionsMenu.tsx | 80 +----- .../components/Layer/LayerHeader.tsx | 34 +-- .../components/Layer/LayerOpacity.tsx | 14 +- .../components/Layer/LayerSettings.tsx | 8 +- .../components/RegionalGuidance/RG.tsx | 25 +- .../RegionalGuidance/RGActionsMenu.tsx | 73 +---- .../components/RegionalGuidance/RGHeader.tsx | 31 +- .../RGMaskFillColorPicker.tsx | 14 +- .../RegionalGuidance/RGSettings.tsx | 8 +- .../RegionalGuidance/RGSettingsPopover.tsx | 14 +- .../controlLayers/components/ToolChooser.tsx | 35 +-- .../common/CanvasEntityActionMenuItems.tsx | 127 +++++++++ .../common/CanvasEntityContainer.tsx | 31 +- .../common/CanvasEntityDeleteButton.tsx | 16 +- .../common/CanvasEntityEnabledToggle.tsx | 21 +- .../components/common/CanvasEntityTitle.tsx | 30 +- .../contexts/EntityIdentifierContext.ts | 11 + .../hooks/useCanvasDeleteLayerHotkey.ts | 19 +- .../hooks/useCanvasResetLayerHotkey.ts | 15 +- .../controlLayers/hooks/useEntityIsEnabled.ts | 22 ++ .../hooks/useEntitySelectionColor.ts | 27 ++ .../hooks/useIsEntitySelected.ts | 12 + .../controlLayers/konva/CanvasBbox.ts | 7 - .../controlLayers/konva/CanvasLayerAdapter.ts | 14 +- .../controlLayers/konva/CanvasMaskAdapter.ts | 8 + .../konva/CanvasObjectRenderer.ts | 27 +- .../controlLayers/konva/CanvasStateApi.ts | 123 +++----- .../controlLayers/konva/CanvasTransformer.ts | 4 +- .../controlLayers/store/canvasV2Slice.ts | 268 +++++++++++++++--- .../store/controlAdaptersReducers.ts | 94 +----- .../store/inpaintMaskReducers.ts | 42 +-- .../controlLayers/store/layersReducers.ts | 135 +-------- .../controlLayers/store/regionsReducers.ts | 111 +------- .../src/features/controlLayers/store/types.ts | 17 +- 45 files changed, 724 insertions(+), 1034 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/contexts/EntityIdentifierContext.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsEnabled.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/useEntitySelectionColor.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/useIsEntitySelected.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts index 4c9a045d60d..41944fc405c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts @@ -4,8 +4,8 @@ import type { AppDispatch, RootState } from 'app/store/store'; import { caImageChanged, caProcessedImageChanged, + entityDeleted, ipaImageChanged, - layerDeleted, } from 'features/controlLayers/store/canvasV2Slice'; import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions'; import { isModalOpenChanged } from 'features/deleteImageModal/store/slice'; @@ -66,7 +66,7 @@ const deleteLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: Im } } if (shouldDelete) { - dispatch(layerDeleted({ id })); + dispatch(entityDeleted({ entityIdentifier: { id, type: 'layer' } })); } }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts index aa0e9a3d61e..e07dcdc8440 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts @@ -1,6 +1,11 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { caIsEnabledToggled, loraDeleted, modelChanged, vaeSelected } from 'features/controlLayers/store/canvasV2Slice'; +import { + entityIsEnabledToggled, + loraDeleted, + modelChanged, + vaeSelected, +} from 'features/controlLayers/store/canvasV2Slice'; import { modelSelected } from 'features/parameters/store/actions'; import { zParameterModel } from 'features/parameters/types/parameterSchemas'; import { toast } from 'features/toast/toast'; @@ -49,7 +54,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = if (ca.model?.base !== newBaseModel) { modelsCleared += 1; if (ca.isEnabled) { - dispatch(caIsEnabledToggled({ id: ca.id })); + dispatch(entityIsEnabledToggled({ entityIdentifier: { id: ca.id, type: 'control_adapter' } })); } } }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx index 9bb6dff7919..8240b3664a5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx @@ -1,28 +1,26 @@ import { useDisclosure } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CAHeader } from 'features/controlLayers/components/ControlAdapter/CAEntityHeader'; import { CASettings } from 'features/controlLayers/components/ControlAdapter/CASettings'; -import { entitySelected } from 'features/controlLayers/store/canvasV2Slice'; -import { memo, useCallback } from 'react'; +import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { memo, useMemo } from 'react'; type Props = { id: string; }; export const CA = memo(({ id }: Props) => { - const dispatch = useAppDispatch(); - const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id); + const entityIdentifier = useMemo(() => ({ id, type: 'control_adapter' }), [id]); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); - const onSelect = useCallback(() => { - dispatch(entitySelected({ id, type: 'control_adapter' })); - }, [dispatch, id]); return ( - - - {isOpen && } - + + + + {isOpen && } + + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx index 1c532dda0f4..6285d7be9c1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx @@ -1,84 +1,14 @@ -import { Menu, MenuItem, MenuList } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { Menu, MenuList } from '@invoke-ai/ui-library'; +import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/common/CanvasEntityActionMenuItems'; import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; -import { - caDeleted, - caMovedBackwardOne, - caMovedForwardOne, - caMovedToBack, - caMovedToFront, - selectCanvasV2Slice, -} from 'features/controlLayers/store/canvasV2Slice'; -import { selectCAOrThrow } from 'features/controlLayers/store/controlAdaptersReducers'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { - PiArrowDownBold, - PiArrowLineDownBold, - PiArrowLineUpBold, - PiArrowUpBold, - PiTrashSimpleBold, -} from 'react-icons/pi'; - -type Props = { - id: string; -}; - -export const CAActionsMenu = memo(({ id }: Props) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const selectValidActions = useMemo( - () => - createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { - const ca = selectCAOrThrow(canvasV2, id); - const caIndex = canvasV2.controlAdapters.entities.indexOf(ca); - const caCount = canvasV2.controlAdapters.entities.length; - return { - canMoveForward: caIndex < caCount - 1, - canMoveBackward: caIndex > 0, - canMoveToFront: caIndex < caCount - 1, - canMoveToBack: caIndex > 0, - }; - }), - [id] - ); - const validActions = useAppSelector(selectValidActions); - const onDelete = useCallback(() => { - dispatch(caDeleted({ id })); - }, [dispatch, id]); - const moveForwardOne = useCallback(() => { - dispatch(caMovedForwardOne({ id })); - }, [dispatch, id]); - const moveToFront = useCallback(() => { - dispatch(caMovedToFront({ id })); - }, [dispatch, id]); - const moveBackwardOne = useCallback(() => { - dispatch(caMovedBackwardOne({ id })); - }, [dispatch, id]); - const moveToBack = useCallback(() => { - dispatch(caMovedToBack({ id })); - }, [dispatch, id]); +import { memo } from 'react'; +export const CAActionsMenu = memo(() => { return ( - }> - {t('controlLayers.moveToFront')} - - }> - {t('controlLayers.moveForward')} - - }> - {t('controlLayers.moveBackward')} - - }> - {t('controlLayers.moveToBack')} - - } color="error.300"> - {t('common.delete')} - + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityHeader.tsx index 2eef6f5cb67..b95945f4fe2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityHeader.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityHeader.tsx @@ -1,41 +1,25 @@ import { Spacer } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; import { CAActionsMenu } from 'features/controlLayers/components/ControlAdapter/CAActionsMenu'; import { CAOpacityAndFilter } from 'features/controlLayers/components/ControlAdapter/CAOpacityAndFilter'; -import { caDeleted, caIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice'; -import { selectCAOrThrow } from 'features/controlLayers/store/controlAdaptersReducers'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; +import { memo } from 'react'; type Props = { - id: string; - isSelected: boolean; onToggleVisibility: () => void; }; -export const CAHeader = memo(({ id, isSelected, onToggleVisibility }: Props) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const isEnabled = useAppSelector((s) => selectCAOrThrow(s.canvasV2, id).isEnabled); - const onToggleIsEnabled = useCallback(() => { - dispatch(caIsEnabledToggled({ id })); - }, [dispatch, id]); - const onDelete = useCallback(() => { - dispatch(caDeleted({ id })); - }, [dispatch, id]); - +export const CAHeader = memo(({ onToggleVisibility }: Props) => { return ( - - + + - - - + + + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAOpacityAndFilter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAOpacityAndFilter.tsx index 25117459f65..b440aad03ee 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAOpacityAndFilter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAOpacityAndFilter.tsx @@ -14,6 +14,7 @@ import { } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { caFilterChanged, caOpacityChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectCAOrThrow } from 'features/controlLayers/store/controlAdaptersReducers'; import type { ChangeEvent } from 'react'; @@ -21,14 +22,11 @@ import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiDropHalfFill } from 'react-icons/pi'; -type Props = { - id: string; -}; - const marks = [0, 25, 50, 75, 100]; const formatPct = (v: number | string) => `${v} %`; -export const CAOpacityAndFilter = memo(({ id }: Props) => { +export const CAOpacityAndFilter = memo(() => { + const { id } = useEntityIdentifierContext(); const { t } = useTranslation(); const dispatch = useAppDispatch(); const opacity = useAppSelector((s) => Math.round(selectCAOrThrow(s.canvasV2, id).opacity * 100)); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx index 8281cadc32c..ab1cdc987e1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx @@ -15,7 +15,7 @@ export const IPA = memo(({ id }: Props) => { const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); const onSelect = useCallback(() => { - dispatch(entitySelected({ id, type: 'ip_adapter' })); + dispatch(entitySelected({ entityIdentifier: { id, type: 'ip_adapter' } })); }, [dispatch, id]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx index 55a332270d8..87344190748 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx @@ -1,25 +1,21 @@ import { useDisclosure } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { IMHeader } from 'features/controlLayers/components/InpaintMask/IMHeader'; import { IMSettings } from 'features/controlLayers/components/InpaintMask/IMSettings'; -import { entitySelected } from 'features/controlLayers/store/canvasV2Slice'; -import { memo, useCallback } from 'react'; +import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { memo, useMemo } from 'react'; export const IM = memo(() => { - const dispatch = useAppDispatch(); - const selectedBorderColor = useAppSelector((s) => rgbColorToString(s.canvasV2.inpaintMask.fill)); - const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === 'inpaint_mask'); + const entityIdentifier = useMemo(() => ({ id: 'inpaint_mask', type: 'inpaint_mask' }), []); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: false }); - const onSelect = useCallback(() => { - dispatch(entitySelected({ id: 'inpaint_mask', type: 'inpaint_mask' })); - }, [dispatch]); return ( - - - {isOpen && } - + + + + {isOpen && } + + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMActionsMenu.tsx index 14462abc738..a7cfc75eb2a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMActionsMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMActionsMenu.tsx @@ -1,25 +1,14 @@ -import { Menu, MenuItem, MenuList } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; +import { Menu, MenuList } from '@invoke-ai/ui-library'; +import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/common/CanvasEntityActionMenuItems'; import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; -import { imReset } from 'features/controlLayers/store/canvasV2Slice'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiArrowCounterClockwiseBold } from 'react-icons/pi'; +import { memo } from 'react'; export const IMActionsMenu = memo(() => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const onReset = useCallback(() => { - dispatch(imReset()); - }, [dispatch]); - return ( - }> - {t('accessibility.reset')} - + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMHeader.tsx index 888c5b1eb36..283752b65ad 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMHeader.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMHeader.tsx @@ -1,32 +1,21 @@ import { Spacer } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; import { IMActionsMenu } from 'features/controlLayers/components/InpaintMask/IMActionsMenu'; -import { imIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; +import { memo } from 'react'; import { IMMaskFillColorPicker } from './IMMaskFillColorPicker'; type Props = { - isSelected: boolean; onToggleVisibility: () => void; }; -export const IMHeader = memo(({ isSelected, onToggleVisibility }: Props) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const isEnabled = useAppSelector((s) => s.canvasV2.inpaintMask.isEnabled); - const onToggleIsEnabled = useCallback(() => { - dispatch(imIsEnabledToggled()); - }, [dispatch]); - +export const IMHeader = memo(({ onToggleVisibility }: Props) => { return ( - - + + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx index 1e2b6fdf1d0..49d2a4dba3c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx @@ -1,35 +1,33 @@ import { useDisclosure } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIDroppable from 'common/components/IAIDroppable'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { LayerHeader } from 'features/controlLayers/components/Layer/LayerHeader'; import { LayerSettings } from 'features/controlLayers/components/Layer/LayerSettings'; -import { entitySelected } from 'features/controlLayers/store/canvasV2Slice'; +import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import type { LayerImageDropData } from 'features/dnd/types'; -import { memo, useCallback, useMemo } from 'react'; +import { memo, useMemo } from 'react'; type Props = { id: string; }; export const Layer = memo(({ id }: Props) => { - const dispatch = useAppDispatch(); - const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id); + const entityIdentifier = useMemo(() => ({ id, type: 'layer' }), [id]); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: false }); - const onSelect = useCallback(() => { - dispatch(entitySelected({ id, type: 'layer' })); - }, [dispatch, id]); const droppableData = useMemo( () => ({ id, actionType: 'ADD_LAYER_IMAGE', context: { id } }), [id] ); return ( - - - {isOpen && } - - + + + + {isOpen && } + + + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx index 7ab753012fc..53a30acc374 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx @@ -1,84 +1,14 @@ -import { Menu, MenuItem, MenuList } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { Menu, MenuList } from '@invoke-ai/ui-library'; +import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/common/CanvasEntityActionMenuItems'; import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; -import { - layerDeleted, - layerMovedBackwardOne, - layerMovedForwardOne, - layerMovedToBack, - layerMovedToFront, - selectCanvasV2Slice, -} from 'features/controlLayers/store/canvasV2Slice'; -import { selectLayerOrThrow } from 'features/controlLayers/store/layersReducers'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { - PiArrowDownBold, - PiArrowLineDownBold, - PiArrowLineUpBold, - PiArrowUpBold, - PiTrashSimpleBold, -} from 'react-icons/pi'; - -type Props = { - id: string; -}; - -export const LayerActionsMenu = memo(({ id }: Props) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const selectValidActions = useMemo( - () => - createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { - const layer = selectLayerOrThrow(canvasV2, id); - const layerIndex = canvasV2.layers.entities.indexOf(layer); - const layerCount = canvasV2.layers.entities.length; - return { - canMoveForward: layerIndex < layerCount - 1, - canMoveBackward: layerIndex > 0, - canMoveToFront: layerIndex < layerCount - 1, - canMoveToBack: layerIndex > 0, - }; - }), - [id] - ); - const validActions = useAppSelector(selectValidActions); - const onDelete = useCallback(() => { - dispatch(layerDeleted({ id })); - }, [dispatch, id]); - const moveForwardOne = useCallback(() => { - dispatch(layerMovedForwardOne({ id })); - }, [dispatch, id]); - const moveToFront = useCallback(() => { - dispatch(layerMovedToFront({ id })); - }, [dispatch, id]); - const moveBackwardOne = useCallback(() => { - dispatch(layerMovedBackwardOne({ id })); - }, [dispatch, id]); - const moveToBack = useCallback(() => { - dispatch(layerMovedToBack({ id })); - }, [dispatch, id]); +import { memo } from 'react'; +export const LayerActionsMenu = memo(() => { return ( - }> - {t('controlLayers.moveToFront')} - - }> - {t('controlLayers.moveForward')} - - }> - {t('controlLayers.moveBackward')} - - }> - {t('controlLayers.moveToBack')} - - } color="error.300"> - {t('common.delete')} - + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx index b53ca7f2753..5d8c17f957c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx @@ -1,46 +1,26 @@ import { Spacer } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; import { LayerActionsMenu } from 'features/controlLayers/components/Layer/LayerActionsMenu'; -import { layerDeleted, layerIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice'; -import { selectLayerOrThrow } from 'features/controlLayers/store/layersReducers'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; +import { memo } from 'react'; import { LayerOpacity } from './LayerOpacity'; type Props = { - id: string; - isSelected: boolean; onToggleVisibility: () => void; }; -export const LayerHeader = memo(({ id, isSelected, onToggleVisibility }: Props) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const isEnabled = useAppSelector((s) => selectLayerOrThrow(s.canvasV2, id).isEnabled); - const objectCount = useAppSelector((s) => selectLayerOrThrow(s.canvasV2, id).objects.length); - const onToggleIsEnabled = useCallback(() => { - dispatch(layerIsEnabledToggled({ id })); - }, [dispatch, id]); - const onDelete = useCallback(() => { - dispatch(layerDeleted({ id })); - }, [dispatch, id]); - const title = useMemo(() => { - return `${t('controlLayers.layer')} (${t('controlLayers.objects', { count: objectCount })})`; - }, [objectCount, t]); - +export const LayerHeader = memo(({ onToggleVisibility }: Props) => { return ( - - + + - - - + + + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerOpacity.tsx index da1582310ef..0d42659966c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerOpacity.tsx @@ -13,28 +13,26 @@ import { } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { layerOpacityChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectLayerOrThrow } from 'features/controlLayers/store/layersReducers'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiDropHalfFill } from 'react-icons/pi'; -type Props = { - id: string; -}; - const marks = [0, 25, 50, 75, 100]; const formatPct = (v: number | string) => `${v} %`; -export const LayerOpacity = memo(({ id }: Props) => { +export const LayerOpacity = memo(() => { + const entityIdentifier = useEntityIdentifierContext(); const { t } = useTranslation(); const dispatch = useAppDispatch(); - const opacity = useAppSelector((s) => Math.round(selectLayerOrThrow(s.canvasV2, id).opacity * 100)); + const opacity = useAppSelector((s) => Math.round(selectLayerOrThrow(s.canvasV2, entityIdentifier.id).opacity * 100)); const onChangeOpacity = useCallback( (v: number) => { - dispatch(layerOpacityChanged({ id, opacity: v / 100 })); + dispatch(layerOpacityChanged({ id: entityIdentifier.id, opacity: v / 100 })); }, - [dispatch, id] + [dispatch, entityIdentifier.id] ); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerSettings.tsx index 8af3e818141..111e30d96f5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerSettings.tsx @@ -1,11 +1,9 @@ import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { memo } from 'react'; -type Props = { - id: string; -}; - -export const LayerSettings = memo(({ id }: Props) => { +export const LayerSettings = memo(() => { + const entityIdentifier = useEntityIdentifierContext(); return PLACEHOLDER; }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx index fe923169cb6..1da28a8fdc0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx @@ -1,30 +1,25 @@ import { useDisclosure } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { RGHeader } from 'features/controlLayers/components/RegionalGuidance/RGHeader'; import { RGSettings } from 'features/controlLayers/components/RegionalGuidance/RGSettings'; -import { entitySelected } from 'features/controlLayers/store/canvasV2Slice'; -import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; -import { memo, useCallback } from 'react'; +import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { memo, useMemo } from 'react'; type Props = { id: string; }; export const RG = memo(({ id }: Props) => { - const dispatch = useAppDispatch(); - const selectedBorderColor = useAppSelector((s) => rgbColorToString(selectRGOrThrow(s.canvasV2, id).fill)); - const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id); + const entityIdentifier = useMemo(() => ({ id, type: 'regional_guidance' }), [id]); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); - const onSelect = useCallback(() => { - dispatch(entitySelected({ id, type: 'regional_guidance' })); - }, [dispatch, id]); return ( - - - {isOpen && } - + + + + {isOpen && } + + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx index 784253b5d2a..0f59f275df2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx @@ -1,37 +1,22 @@ import { Menu, MenuDivider, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/common/CanvasEntityActionMenuItems'; import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { useAddIPAdapterToRGLayer } from 'features/controlLayers/hooks/addLayerHooks'; import { - rgDeleted, - rgMovedBackwardOne, - rgMovedForwardOne, - rgMovedToBack, - rgMovedToFront, rgNegativePromptChanged, rgPositivePromptChanged, - rgReset, selectCanvasV2Slice, } from 'features/controlLayers/store/canvasV2Slice'; import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { - PiArrowCounterClockwiseBold, - PiArrowDownBold, - PiArrowLineDownBold, - PiArrowLineUpBold, - PiArrowUpBold, - PiPlusBold, - PiTrashSimpleBold, -} from 'react-icons/pi'; - -type Props = { - id: string; -}; +import { PiPlusBold } from 'react-icons/pi'; -export const RGActionsMenu = memo(({ id }: Props) => { +export const RGActionsMenu = memo(() => { + const { id } = useEntityIdentifierContext(); const { t } = useTranslation(); const dispatch = useAppDispatch(); const [onAddIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToRGLayer(id); @@ -39,13 +24,7 @@ export const RGActionsMenu = memo(({ id }: Props) => { () => createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { const rg = selectRGOrThrow(canvasV2, id); - const rgIndex = canvasV2.regions.entities.indexOf(rg); - const rgCount = canvasV2.regions.entities.length; return { - isMoveForwardOneDisabled: rgIndex < rgCount - 1, - isMoveBackardOneDisabled: rgIndex > 0, - isMoveToFrontDisabled: rgIndex < rgCount - 1, - isMoveToBackDisabled: rgIndex > 0, isAddPositivePromptDisabled: rg.positivePrompt === null, isAddNegativePromptDisabled: rg.negativePrompt === null, }; @@ -53,29 +32,11 @@ export const RGActionsMenu = memo(({ id }: Props) => { [id] ); const actions = useAppSelector(selectActionsValidity); - const onDelete = useCallback(() => { - dispatch(rgDeleted({ id })); - }, [dispatch, id]); - const onReset = useCallback(() => { - dispatch(rgReset({ id })); - }, [dispatch, id]); - const onMoveForwardOne = useCallback(() => { - dispatch(rgMovedForwardOne({ id })); - }, [dispatch, id]); - const onMoveToFront = useCallback(() => { - dispatch(rgMovedToFront({ id })); - }, [dispatch, id]); - const onMoveBackwardOne = useCallback(() => { - dispatch(rgMovedBackwardOne({ id })); - }, [dispatch, id]); - const onMoveToBack = useCallback(() => { - dispatch(rgMovedToBack({ id })); - }, [dispatch, id]); const onAddPositivePrompt = useCallback(() => { - dispatch(rgPositivePromptChanged({ id, prompt: '' })); + dispatch(rgPositivePromptChanged({ id: id, prompt: '' })); }, [dispatch, id]); const onAddNegativePrompt = useCallback(() => { - dispatch(rgNegativePromptChanged({ id, prompt: '' })); + dispatch(rgNegativePromptChanged({ id: id, prompt: '' })); }, [dispatch, id]); return ( @@ -92,25 +53,7 @@ export const RGActionsMenu = memo(({ id }: Props) => { {t('controlLayers.addIPAdapter')} - }> - {t('controlLayers.moveToFront')} - - }> - {t('controlLayers.moveForward')} - - }> - {t('controlLayers.moveBackward')} - - }> - {t('controlLayers.moveToBack')} - - - }> - {t('accessibility.reset')} - - } color="error.300"> - {t('common.delete')} - +
); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGHeader.tsx index 866913a08cc..151e378ebe4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGHeader.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGHeader.tsx @@ -1,50 +1,41 @@ import { Badge, Spacer } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; import { RGActionsMenu } from 'features/controlLayers/components/RegionalGuidance/RGActionsMenu'; -import { rgDeleted, rgIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; -import { memo, useCallback } from 'react'; +import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { RGMaskFillColorPicker } from './RGMaskFillColorPicker'; import { RGSettingsPopover } from './RGSettingsPopover'; type Props = { - id: string; - isSelected: boolean; onToggleVisibility: () => void; }; -export const RGHeader = memo(({ id, isSelected, onToggleVisibility }: Props) => { +export const RGHeader = memo(({ onToggleVisibility }: Props) => { + const { id } = useEntityIdentifierContext(); const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const isEnabled = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).isEnabled); const autoNegative = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).autoNegative); - const onToggleIsEnabled = useCallback(() => { - dispatch(rgIsEnabledToggled({ id })); - }, [dispatch, id]); - const onDelete = useCallback(() => { - dispatch(rgDeleted({ id })); - }, [dispatch, id]); return ( - - + + {autoNegative === 'invert' && ( {t('controlLayers.autoNegative')} )} - - - - + + + + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx index 471febea280..2f1489cfb09 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx @@ -3,25 +3,23 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import RgbColorPicker from 'common/components/RgbColorPicker'; import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { stopPropagation } from 'common/util/stopPropagation'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { rgFillChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import { memo, useCallback } from 'react'; import type { RgbColor } from 'react-colorful'; import { useTranslation } from 'react-i18next'; -type Props = { - id: string; -}; - -export const RGMaskFillColorPicker = memo(({ id }: Props) => { +export const RGMaskFillColorPicker = memo(() => { + const entityIdentifier = useEntityIdentifierContext(); const { t } = useTranslation(); const dispatch = useAppDispatch(); - const fill = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).fill); + const fill = useAppSelector((s) => selectRGOrThrow(s.canvasV2, entityIdentifier.id).fill); const onChange = useCallback( (fill: RgbColor) => { - dispatch(rgFillChanged({ id, fill })); + dispatch(rgFillChanged({ id: entityIdentifier.id, fill })); }, - [dispatch, id] + [dispatch, entityIdentifier.id] ); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettings.tsx index 7ba5ec84a38..c13081d6442 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettings.tsx @@ -1,6 +1,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { AddPromptButtons } from 'features/controlLayers/components/AddPromptButtons'; import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import { memo } from 'react'; @@ -8,11 +9,8 @@ import { RGIPAdapters } from './RGIPAdapters'; import { RGNegativePrompt } from './RGNegativePrompt'; import { RGPositivePrompt } from './RGPositivePrompt'; -type Props = { - id: string; -}; - -export const RGSettings = memo(({ id }: Props) => { +export const RGSettings = memo(() => { + const { id } = useEntityIdentifierContext(); const hasPositivePrompt = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).positivePrompt !== null); const hasNegativePrompt = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).negativePrompt !== null); const hasIPAdapters = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).ipAdapters.length > 0); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettingsPopover.tsx index cf7c1574602..b82469fafc2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettingsPopover.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettingsPopover.tsx @@ -12,6 +12,7 @@ import { } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { rgAutoNegativeChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import type { ChangeEvent } from 'react'; @@ -19,19 +20,16 @@ import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiGearSixBold } from 'react-icons/pi'; -type Props = { - id: string; -}; - -export const RGSettingsPopover = memo(({ id }: Props) => { +export const RGSettingsPopover = memo(() => { + const entityIdentifier = useEntityIdentifierContext(); const { t } = useTranslation(); const dispatch = useAppDispatch(); - const autoNegative = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).autoNegative); + const autoNegative = useAppSelector((s) => selectRGOrThrow(s.canvasV2, entityIdentifier.id).autoNegative); const onChange = useCallback( (e: ChangeEvent) => { - dispatch(rgAutoNegativeChanged({ id, autoNegative: e.target.checked ? 'invert' : 'off' })); + dispatch(rgAutoNegativeChanged({ id: entityIdentifier.id, autoNegative: e.target.checked ? 'invert' : 'off' })); }, - [dispatch, id] + [dispatch, entityIdentifier.id] ); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx index 6d440391271..860bf3c601c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx @@ -13,32 +13,19 @@ import { useCanvasResetLayerHotkey } from 'features/controlLayers/hooks/useCanva export const ToolChooser: React.FC = () => { useCanvasResetLayerHotkey(); useCanvasDeleteLayerHotkey(); - const isCanvasSessionActive = useAppSelector((s) => s.canvasV2.session.isActive); const isTransforming = useAppSelector((s) => s.canvasV2.tool.isTransforming); - if (isCanvasSessionActive) { - return ( - <> - - - - - - - - - - - ); - } - return ( - - - - - - - + <> + + + + + + + + + + ); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx new file mode 100644 index 00000000000..cb0094fd56d --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx @@ -0,0 +1,127 @@ +import { MenuItem } from '@invoke-ai/ui-library'; +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { + entityArrangedBackwardOne, + entityArrangedForwardOne, + entityArrangedToBack, + entityArrangedToFront, + entityDeleted, + entityReset, + selectCanvasV2Slice, +} from 'features/controlLayers/store/canvasV2Slice'; +import type { CanvasEntityIdentifier, CanvasV2State } from 'features/controlLayers/store/types'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + PiArrowCounterClockwiseBold, + PiArrowDownBold, + PiArrowLineDownBold, + PiArrowLineUpBold, + PiArrowUpBold, + PiTrashSimpleBold, +} from 'react-icons/pi'; + +const getIndexAndCount = ( + canvasV2: CanvasV2State, + { id, type }: CanvasEntityIdentifier +): { index: number; count: number } => { + if (type === 'layer') { + return { + index: canvasV2.layers.entities.findIndex((entity) => entity.id === id), + count: canvasV2.layers.entities.length, + }; + } else if (type === 'control_adapter') { + return { + index: canvasV2.controlAdapters.entities.findIndex((entity) => entity.id === id), + count: canvasV2.controlAdapters.entities.length, + }; + } else if (type === 'regional_guidance') { + return { + index: canvasV2.regions.entities.findIndex((entity) => entity.id === id), + count: canvasV2.regions.entities.length, + }; + } else { + return { + index: -1, + count: 0, + }; + } +}; + +export const CanvasEntityActionMenuItems = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const entityIdentifier = useEntityIdentifierContext(); + const selectValidActions = useMemo( + () => + createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { + const { index, count } = getIndexAndCount(canvasV2, entityIdentifier); + return { + isArrangeable: + entityIdentifier.type === 'layer' || + entityIdentifier.type === 'control_adapter' || + entityIdentifier.type === 'regional_guidance', + isDeleteable: entityIdentifier.type !== 'inpaint_mask', + canMoveForwardOne: index < count - 1, + canMoveBackwardOne: index > 0, + canMoveToFront: index < count - 1, + canMoveToBack: index > 0, + }; + }), + [entityIdentifier] + ); + + const validActions = useAppSelector(selectValidActions); + + const deleteEntity = useCallback(() => { + dispatch(entityDeleted({ entityIdentifier })); + }, [dispatch, entityIdentifier]); + const resetEntity = useCallback(() => { + dispatch(entityReset({ entityIdentifier })); + }, [dispatch, entityIdentifier]); + const moveForwardOne = useCallback(() => { + dispatch(entityArrangedForwardOne({ entityIdentifier })); + }, [dispatch, entityIdentifier]); + const moveToFront = useCallback(() => { + dispatch(entityArrangedToFront({ entityIdentifier })); + }, [dispatch, entityIdentifier]); + const moveBackwardOne = useCallback(() => { + dispatch(entityArrangedBackwardOne({ entityIdentifier })); + }, [dispatch, entityIdentifier]); + const moveToBack = useCallback(() => { + dispatch(entityArrangedToBack({ entityIdentifier })); + }, [dispatch, entityIdentifier]); + + return ( + <> + {validActions.isArrangeable && ( + <> + }> + {t('controlLayers.moveToFront')} + + }> + {t('controlLayers.moveForward')} + + }> + {t('controlLayers.moveBackward')} + + }> + {t('controlLayers.moveToBack')} + + + )} + }> + {t('accessibility.reset')} + + {validActions.isDeleteable && ( + } color="error.300"> + {t('common.delete')} + + )} + + ); +}); + +CanvasEntityActionMenuItems.displayName = 'CanvasEntityActionMenuItems'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx index 9093086fbdc..550ba0d020d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx @@ -1,22 +1,23 @@ -import type { ChakraProps } from '@invoke-ai/ui-library'; import { Flex } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useEntitySelectionColor } from 'features/controlLayers/hooks/useEntitySelectionColor'; +import { useIsEntitySelected } from 'features/controlLayers/hooks/useIsEntitySelected'; +import { entitySelected } from 'features/controlLayers/store/canvasV2Slice'; import type { PropsWithChildren } from 'react'; import { memo, useCallback } from 'react'; -type Props = PropsWithChildren<{ - isSelected: boolean; - onSelect: () => void; - selectedBorderColor?: ChakraProps['bg']; -}>; - -export const CanvasEntityContainer = memo((props: Props) => { - const { isSelected, onSelect, selectedBorderColor = 'base.400', children } = props; - const _onSelect = useCallback(() => { +export const CanvasEntityContainer = memo((props: PropsWithChildren) => { + const dispatch = useAppDispatch(); + const entityIdentifier = useEntityIdentifierContext(); + const isSelected = useIsEntitySelected(entityIdentifier); + const selectionColor = useEntitySelectionColor(entityIdentifier); + const onClick = useCallback(() => { if (isSelected) { return; } - onSelect(); - }, [isSelected, onSelect]); + dispatch(entitySelected({ entityIdentifier })); + }, [dispatch, entityIdentifier, isSelected]); return ( { flexDir="column" w="full" bg={isSelected ? 'base.800' : 'base.850'} - onClick={_onSelect} + onClick={onClick} borderInlineStartWidth={5} - borderColor={isSelected ? selectedBorderColor : 'base.800'} + borderColor={isSelected ? selectionColor : 'base.800'} borderRadius="base" > - {children} + {props.children} ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityDeleteButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityDeleteButton.tsx index 1cbb0fa29a8..c51410413bb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityDeleteButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityDeleteButton.tsx @@ -1,13 +1,19 @@ import { IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; -import { memo } from 'react'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { entityDeleted } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiTrashSimpleBold } from 'react-icons/pi'; -type Props = { onDelete: () => void }; - -export const CanvasEntityDeleteButton = memo(({ onDelete }: Props) => { +export const CanvasEntityDeleteButton = memo(() => { const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const entityIdentifier = useEntityIdentifierContext(); + const onClick = useCallback(() => { + dispatch(entityDeleted({ entityIdentifier })); + }, [dispatch, entityIdentifier]); return ( { aria-label={t('common.delete')} tooltip={t('common.delete')} icon={} - onClick={onDelete} + onClick={onClick} onDoubleClick={stopPropagation} // double click expands the layer /> ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx index eaa41fcfe9e..e33a5ce9c32 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx @@ -1,16 +1,21 @@ import { IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; -import { memo } from 'react'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useEntityIsEnabled } from 'features/controlLayers/hooks/useEntityIsEnabled'; +import { entityIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiCheckBold } from 'react-icons/pi'; -type Props = { - isEnabled: boolean; - onToggle: () => void; -}; - -export const CanvasEntityEnabledToggle = memo(({ isEnabled, onToggle }: Props) => { +export const CanvasEntityEnabledToggle = memo(() => { const { t } = useTranslation(); + const entityIdentifier = useEntityIdentifierContext(); + const isEnabled = useEntityIsEnabled(entityIdentifier); + const dispatch = useAppDispatch(); + const onClick = useCallback(() => { + dispatch(entityIsEnabledToggled({ entityIdentifier })); + }, [dispatch, entityIdentifier]); return ( : undefined} - onClick={onToggle} + onClick={onClick} colorScheme="base" onDoubleClick={stopPropagation} // double click expands the layer /> diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitle.tsx index 1fc3d6e9f03..cbb48fa2469 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitle.tsx @@ -1,12 +1,30 @@ import { Text } from '@invoke-ai/ui-library'; -import { memo } from 'react'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useIsEntitySelected } from 'features/controlLayers/hooks/useIsEntitySelected'; +import { memo, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { assert } from 'tsafe'; -type Props = { - title: string; - isSelected: boolean; -}; +export const CanvasEntityTitle = memo(() => { + const { t } = useTranslation(); + const entityIdentifier = useEntityIdentifierContext(); + const isSelected = useIsEntitySelected(entityIdentifier); + const title = useMemo(() => { + if (entityIdentifier.type === 'inpaint_mask') { + return t('controlLayers.inpaintMask'); + } else if (entityIdentifier.type === 'control_adapter') { + return t('controlLayers.globalControlAdapter'); + } else if (entityIdentifier.type === 'layer') { + return t('controlLayers.layer'); + } else if (entityIdentifier.type === 'ip_adapter') { + return t('controlLayers.ipAdapter'); + } else if (entityIdentifier.type === 'regional_guidance') { + return t('controlLayers.regionalGuidance'); + } else { + assert(false, 'Unexpected entity type'); + } + }, [entityIdentifier.type, t]); -export const CanvasEntityTitle = memo(({ title, isSelected }: Props) => { return ( {title} diff --git a/invokeai/frontend/web/src/features/controlLayers/contexts/EntityIdentifierContext.ts b/invokeai/frontend/web/src/features/controlLayers/contexts/EntityIdentifierContext.ts new file mode 100644 index 00000000000..04489e8e9d9 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/contexts/EntityIdentifierContext.ts @@ -0,0 +1,11 @@ +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { createContext, useContext } from 'react'; +import { assert } from 'tsafe'; + +export const EntityIdentifierContext = createContext(null); + +export const useEntityIdentifierContext = (): CanvasEntityIdentifier => { + const entityIdentifier = useContext(EntityIdentifierContext); + assert(entityIdentifier, 'useEntityIdentifier must be used within a EntityIdentifierProvider'); + return entityIdentifier; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts index 1e2fb57901e..ca5607c97cb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts @@ -2,10 +2,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; import { - caDeleted, - ipaDeleted, - layerDeleted, - rgDeleted, + entityDeleted, selectCanvasV2Slice, } from 'features/controlLayers/store/canvasV2Slice'; import { useCallback, useMemo } from 'react'; @@ -26,19 +23,7 @@ export function useCanvasDeleteLayerHotkey() { if (selectedEntityIdentifier === null) { return; } - const { type, id } = selectedEntityIdentifier; - if (type === 'layer') { - dispatch(layerDeleted({ id })); - } - if (type === 'regional_guidance') { - dispatch(rgDeleted({ id })); - } - if (type === 'control_adapter') { - dispatch(caDeleted({ id })); - } - if (type === 'ip_adapter') { - dispatch(ipaDeleted({ id })); - } + dispatch(entityDeleted({ entityIdentifier: selectedEntityIdentifier })); }, [dispatch, selectedEntityIdentifier]); const isDeleteEnabled = useMemo( diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts index 2d1c3c74f0c..1a8301f2046 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts @@ -2,9 +2,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; import { - imReset, - layerReset, - rgReset, + entityReset, selectCanvasV2Slice, } from 'features/controlLayers/store/canvasV2Slice'; import { useCallback, useMemo } from 'react'; @@ -25,16 +23,7 @@ export function useCanvasResetLayerHotkey() { if (selectedEntityIdentifier === null) { return; } - const { type, id } = selectedEntityIdentifier; - if (type === 'layer') { - dispatch(layerReset({ id })); - } - if (type === 'regional_guidance') { - dispatch(rgReset({ id })); - } - if (type === 'inpaint_mask') { - dispatch(imReset()); - } + dispatch(entityReset({ entityIdentifier: selectedEntityIdentifier })); }, [dispatch, selectedEntityIdentifier]); const isResetEnabled = useMemo( diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsEnabled.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsEnabled.ts new file mode 100644 index 00000000000..e37b402ea6a --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsEnabled.ts @@ -0,0 +1,22 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { useAppSelector } from 'app/store/storeHooks'; +import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/canvasV2Slice'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { useMemo } from 'react'; + +export const useEntityIsEnabled = (entityIdentifier: CanvasEntityIdentifier) => { + const selectIsEnabled = useMemo( + () => + createSelector(selectCanvasV2Slice, (canvasV2) => { + const entity = selectEntity(canvasV2, entityIdentifier); + if (!entity) { + return false; + } else { + return entity.isEnabled; + } + }), + [entityIdentifier] + ); + const isEnabled = useAppSelector(selectIsEnabled); + return isEnabled; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntitySelectionColor.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntitySelectionColor.ts new file mode 100644 index 00000000000..48f56985898 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntitySelectionColor.ts @@ -0,0 +1,27 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { useAppSelector } from 'app/store/storeHooks'; +import { rgbColorToString } from 'common/util/colorCodeTransformers'; +import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/canvasV2Slice'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { useMemo } from 'react'; + +export const useEntitySelectionColor = (entityIdentifier: CanvasEntityIdentifier) => { + const selectSelectionColor = useMemo( + () => + createSelector(selectCanvasV2Slice, (canvasV2) => { + const entity = selectEntity(canvasV2, entityIdentifier); + if (!entity) { + return 'base.400'; + } else if (entity.type === 'inpaint_mask') { + return rgbColorToString(entity.fill); + } else if (entity.type === 'regional_guidance') { + return rgbColorToString(entity.fill); + } else { + return 'base.400'; + } + }), + [entityIdentifier] + ); + const selectionColor = useAppSelector(selectSelectionColor); + return selectionColor; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useIsEntitySelected.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useIsEntitySelected.ts new file mode 100644 index 00000000000..8d5c3ca86f7 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useIsEntitySelected.ts @@ -0,0 +1,12 @@ +import { useAppSelector } from 'app/store/storeHooks'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { useMemo } from 'react'; + +export const useIsEntitySelected = (entityIdentifier: CanvasEntityIdentifier) => { + const selectedEntityIdentifier = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier); + const isSelected = useMemo(() => { + return selectedEntityIdentifier?.id === entityIdentifier.id; + }, [selectedEntityIdentifier, entityIdentifier.id]); + + return isSelected; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts index c9331df1908..7e72ef0ca7b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts @@ -218,16 +218,9 @@ export class CanvasBbox { } render() { - const session = this.manager.stateApi.getSession(); const bbox = this.manager.stateApi.getBbox(); const toolState = this.manager.stateApi.getToolState(); - if (!session.isActive) { - this.konva.group.listening(false); - this.konva.group.visible(false); - return; - } - this.konva.group.visible(true); this.konva.group.listening(toolState.selected === 'bbox'); this.konva.rect.setAttrs({ diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts index 7eae3240bcb..511ab0e8945 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts @@ -2,7 +2,12 @@ import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; -import type { CanvasLayerState, CanvasV2State, GetLoggingContext } from 'features/controlLayers/store/types'; +import type { + CanvasEntityIdentifier, + CanvasLayerState, + CanvasV2State, + GetLoggingContext, +} from 'features/controlLayers/store/types'; import Konva from 'konva'; import { get } from 'lodash-es'; import type { Logger } from 'roarr'; @@ -48,6 +53,13 @@ export class CanvasLayerAdapter { this.state = state; } + /** + * Get this entity's entity identifier + */ + getEntityIdentifier = (): CanvasEntityIdentifier => { + return { id: this.id, type: this.type }; + }; + destroy = (): void => { this.log.debug('Destroying layer'); // We need to call the destroy method on all children so they can do their own cleanup. diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts index 9481fa2857b..13318f240d9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts @@ -2,6 +2,7 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; import type { + CanvasEntityIdentifier, CanvasInpaintMaskState, CanvasRegionalGuidanceState, CanvasV2State, @@ -54,6 +55,13 @@ export class CanvasMaskAdapter { this.maskOpacity = this.manager.stateApi.getMaskOpacity(); } + /** + * Get this entity's entity identifier + */ + getEntityIdentifier = (): CanvasEntityIdentifier => { + return { id: this.id, type: this.type }; + }; + destroy = (): void => { this.log.debug('Destroying mask'); // We need to call the destroy method on all children so they can do their own cleanup. diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index 2e4c2b81946..aa82ba25676 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -8,11 +8,7 @@ import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLaye import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; -import { - getPrefixedId, - konvaNodeToBlob, - previewBlob, -} from 'features/controlLayers/konva/util'; +import { getPrefixedId, konvaNodeToBlob, previewBlob } from 'features/controlLayers/konva/util'; import { type CanvasBrushLineState, type CanvasEraserLineState, @@ -299,11 +295,17 @@ export class CanvasObjectRenderer { this.buffer.id = getPrefixedId(this.buffer.type); if (this.buffer.type === 'brush_line') { - this.manager.stateApi.addBrushLine({ id: this.parent.id, brushLine: this.buffer }, this.parent.type); + this.manager.stateApi.addBrushLine({ + entityIdentifier: this.parent.getEntityIdentifier(), + brushLine: this.buffer, + }); } else if (this.buffer.type === 'eraser_line') { - this.manager.stateApi.addEraserLine({ id: this.parent.id, eraserLine: this.buffer }, this.parent.type); + this.manager.stateApi.addEraserLine({ + entityIdentifier: this.parent.getEntityIdentifier(), + eraserLine: this.buffer, + }); } else if (this.buffer.type === 'rect') { - this.manager.stateApi.addRect({ id: this.parent.id, rect: this.buffer }, this.parent.type); + this.manager.stateApi.addRect({ entityIdentifier: this.parent.getEntityIdentifier(), rect: this.buffer }); } else { this.log.warn({ buffer: this.buffer }, 'Invalid buffer object type'); } @@ -356,10 +358,11 @@ export class CanvasObjectRenderer { const imageDTO = await uploadImage(blob, `${this.id}_rasterized.png`, 'other', true); const imageObject = imageDTOToImageObject(imageDTO); await this.renderObject(imageObject, true); - this.manager.stateApi.rasterizeEntity( - { id: this.parent.id, imageObject, position: { x: Math.round(rect.x), y: Math.round(rect.y) } }, - this.parent.type - ); + this.manager.stateApi.rasterizeEntity({ + entityIdentifier: this.parent.getEntityIdentifier(), + imageObject, + position: { x: Math.round(rect.x), y: Math.round(rect.y) }, + }); }; /** diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index 6d6b2368559..844b7a1b135 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -15,44 +15,31 @@ import { $stageAttrs, bboxChanged, brushWidthChanged, - caTranslated, + entityBrushLineAdded, + entityEraserLineAdded, + entityMoved, + entityRasterized, + entityRectAdded, + entityReset, entitySelected, eraserWidthChanged, - imBrushLineAdded, - imEraserLineAdded, imImageCacheChanged, - imMoved, - imRectAdded, - inpaintMaskRasterized, - layerBrushLineAdded, - layerEraserLineAdded, layerImageCacheChanged, - layerRasterized, - layerRectAdded, - layerReset, - layerTranslated, - rgBrushLineAdded, - rgEraserLineAdded, rgImageCacheChanged, - rgMoved, - rgRasterized, - rgRectAdded, toolBufferChanged, toolChanged, } from 'features/controlLayers/store/canvasV2Slice'; import type { - CanvasBrushLineState, - CanvasEntityIdentifier, - CanvasEntityState, - CanvasEraserLineState, - CanvasRectState, - EntityRasterizedArg, - PositionChangedArg, + EntityBrushLineAddedPayload, + EntityEraserLineAddedPayload, + EntityIdentifierPayload, + EntityMovedPayload, + EntityRasterizedPayload, + EntityRectAddedPayload, Rect, Tool, } from 'features/controlLayers/store/types'; import type { ImageDTO } from 'services/api/types'; -import { assert } from 'tsafe'; const log = logger('canvas'); @@ -69,67 +56,31 @@ export class CanvasStateApi { getState = () => { return this._store.getState().canvasV2; }; - resetEntity = (arg: { id: string }, entityType: CanvasEntityState['type']) => { - log.trace({ arg, entityType }, 'Resetting entity'); - if (entityType === 'layer') { - this._store.dispatch(layerReset(arg)); - } - }; - setEntityPosition = (arg: PositionChangedArg, entityType: CanvasEntityState['type']) => { - log.trace({ arg, entityType }, 'Setting entity position'); - if (entityType === 'layer') { - this._store.dispatch(layerTranslated(arg)); - } else if (entityType === 'regional_guidance') { - this._store.dispatch(rgMoved(arg)); - } else if (entityType === 'inpaint_mask') { - this._store.dispatch(imMoved(arg)); - } else if (entityType === 'control_adapter') { - this._store.dispatch(caTranslated(arg)); - } - }; - addBrushLine = (arg: { id: string; brushLine: CanvasBrushLineState }, entityType: CanvasEntityState['type']) => { - log.trace({ arg, entityType }, 'Adding brush line'); - if (entityType === 'layer') { - this._store.dispatch(layerBrushLineAdded(arg)); - } else if (entityType === 'regional_guidance') { - this._store.dispatch(rgBrushLineAdded(arg)); - } else if (entityType === 'inpaint_mask') { - this._store.dispatch(imBrushLineAdded(arg)); - } - }; - addEraserLine = (arg: { id: string; eraserLine: CanvasEraserLineState }, entityType: CanvasEntityState['type']) => { - log.trace({ arg, entityType }, 'Adding eraser line'); - if (entityType === 'layer') { - this._store.dispatch(layerEraserLineAdded(arg)); - } else if (entityType === 'regional_guidance') { - this._store.dispatch(rgEraserLineAdded(arg)); - } else if (entityType === 'inpaint_mask') { - this._store.dispatch(imEraserLineAdded(arg)); - } - }; - addRect = (arg: { id: string; rect: CanvasRectState }, entityType: CanvasEntityState['type']) => { - log.trace({ arg, entityType }, 'Adding rect'); - if (entityType === 'layer') { - this._store.dispatch(layerRectAdded(arg)); - } else if (entityType === 'regional_guidance') { - this._store.dispatch(rgRectAdded(arg)); - } else if (entityType === 'inpaint_mask') { - this._store.dispatch(imRectAdded(arg)); - } - }; - rasterizeEntity = (arg: EntityRasterizedArg, entityType: CanvasEntityState['type']) => { - log.trace({ arg, entityType }, 'Rasterizing entity'); - if (entityType === 'layer') { - this._store.dispatch(layerRasterized(arg)); - } else if (entityType === 'inpaint_mask') { - this._store.dispatch(inpaintMaskRasterized(arg)); - } else if (entityType === 'regional_guidance') { - this._store.dispatch(rgRasterized(arg)); - } else { - assert(false, 'Rasterizing not supported for this entity type'); - } - }; - setSelectedEntity = (arg: CanvasEntityIdentifier) => { + resetEntity = (arg: EntityIdentifierPayload) => { + log.trace(arg, 'Resetting entity'); + this._store.dispatch(entityReset(arg)); + }; + setEntityPosition = (arg: EntityMovedPayload) => { + log.trace(arg, 'Setting entity position'); + this._store.dispatch(entityMoved(arg)); + }; + addBrushLine = (arg: EntityBrushLineAddedPayload) => { + log.trace(arg, 'Adding brush line'); + this._store.dispatch(entityBrushLineAdded(arg)); + }; + addEraserLine = (arg: EntityEraserLineAddedPayload) => { + log.trace(arg, 'Adding eraser line'); + this._store.dispatch(entityEraserLineAdded(arg)); + }; + addRect = (arg: EntityRectAddedPayload) => { + log.trace(arg, 'Adding rect'); + this._store.dispatch(entityRectAdded(arg)); + }; + rasterizeEntity = (arg: EntityRasterizedPayload) => { + log.trace(arg, 'Rasterizing entity'); + this._store.dispatch(entityRasterized(arg)); + }; + setSelectedEntity = (arg: EntityIdentifierPayload) => { log.trace({ arg }, 'Setting selected entity'); this._store.dispatch(entitySelected(arg)); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index cf6960052b2..8ee2ba6ffbe 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -356,7 +356,7 @@ export class CanvasTransformer { }; this.log.trace({ position }, 'Position changed'); - this.manager.stateApi.setEntityPosition({ id: this.parent.id, position }, this.parent.type); + this.manager.stateApi.setEntityPosition({ entityIdentifier: this.parent.getEntityIdentifier(), position }); }); this.subscriptions.add( @@ -600,7 +600,7 @@ export class CanvasTransformer { // We shouldn't reset on the first render - the bbox will be calculated on the next render if (!this.parent.renderer.hasObjects()) { // The layer is fully transparent but has objects - reset it - this.manager.stateApi.resetEntity({ id: this.parent.id }, this.parent.type); + this.manager.stateApi.resetEntity({ entityIdentifier: this.parent.getEntityIdentifier() }); } this.syncInteractionState(); return; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 8252f4627fc..4159d9f7daa 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -1,6 +1,7 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createAction, createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; +import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { deepClone } from 'common/util/deepClone'; import { bboxReducers } from 'features/controlLayers/store/bboxReducers'; import { compositingReducers } from 'features/controlLayers/store/compositingReducers'; @@ -20,8 +21,20 @@ import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; import { pick } from 'lodash-es'; import { atom } from 'nanostores'; import type { InvocationDenoiseProgressEvent } from 'services/events/types'; +import { assert } from 'tsafe'; -import type { CanvasEntityIdentifier, CanvasV2State, Coordinate, StageAttrs } from './types'; +import type { + CanvasEntityIdentifier, + CanvasV2State, + Coordinate, + EntityBrushLineAddedPayload, + EntityEraserLineAddedPayload, + EntityIdentifierPayload, + EntityMovedPayload, + EntityRasterizedPayload, + EntityRectAddedPayload, + StageAttrs, +} from './types'; import { RGBA_RED } from './types'; const initialState: CanvasV2State = { @@ -122,6 +135,23 @@ const initialState: CanvasV2State = { }, }; +export function selectEntity(state: CanvasV2State, { id, type }: CanvasEntityIdentifier) { + switch (type) { + case 'layer': + return state.layers.entities.find((layer) => layer.id === id); + case 'control_adapter': + return state.controlAdapters.entities.find((ca) => ca.id === id); + case 'inpaint_mask': + return state.inpaintMask; + case 'regional_guidance': + return state.regions.entities.find((rg) => rg.id === id); + case 'ip_adapter': + return state.ipAdapters.entities.find((ip) => ip.id === id); + default: + return; + } +} + export const canvasV2Slice = createSlice({ name: 'canvasV2', initialState, @@ -138,8 +168,184 @@ export const canvasV2Slice = createSlice({ ...bboxReducers, ...inpaintMaskReducers, ...sessionReducers, - entitySelected: (state, action: PayloadAction) => { - state.selectedEntityIdentifier = action.payload; + entitySelected: (state, action: PayloadAction) => { + const { entityIdentifier } = action.payload; + state.selectedEntityIdentifier = entityIdentifier; + }, + entityReset: (state, action: PayloadAction) => { + const { entityIdentifier } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } else if (entity.type === 'layer') { + entity.isEnabled = true; + entity.objects = []; + entity.position = { x: 0, y: 0 }; + state.layers.imageCache = null; + } else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { + entity.isEnabled = true; + entity.objects = []; + entity.position = { x: 0, y: 0 }; + entity.imageCache = null; + } else { + assert(false, 'Not implemented'); + } + }, + entityIsEnabledToggled: (state, action: PayloadAction) => { + const { entityIdentifier } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } + entity.isEnabled = !entity.isEnabled; + }, + entityMoved: (state, action: PayloadAction) => { + const { entityIdentifier, position } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } else if (entity.type === 'layer') { + entity.position = position; + state.layers.imageCache = null; + } else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { + entity.position = position; + entity.imageCache = null; + } else { + assert(false, 'Not implemented'); + } + }, + entityRasterized: (state, action: PayloadAction) => { + const { entityIdentifier, imageObject, position } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } else if (entity.type === 'layer') { + entity.objects = [imageObject]; + entity.position = position; + state.layers.imageCache = null; + } else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { + entity.objects = [imageObject]; + entity.position = position; + entity.imageCache = null; + } else { + assert(false, 'Not implemented'); + } + }, + entityBrushLineAdded: (state, action: PayloadAction) => { + const { entityIdentifier, brushLine } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } else if (entity.type === 'layer') { + entity.objects.push(brushLine); + state.layers.imageCache = null; + } else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { + entity.objects.push(brushLine); + entity.imageCache = null; + } else { + assert(false, 'Not implemented'); + } + }, + entityEraserLineAdded: (state, action: PayloadAction) => { + const { entityIdentifier, eraserLine } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } else if (entity.type === 'layer') { + entity.objects.push(eraserLine); + state.layers.imageCache = null; + } else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { + entity.objects.push(eraserLine); + entity.imageCache = null; + } else { + assert(false, 'Not implemented'); + } + }, + entityRectAdded: (state, action: PayloadAction) => { + const { entityIdentifier, rect } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } else if (entity.type === 'layer') { + entity.objects.push(rect); + state.layers.imageCache = null; + } else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { + entity.objects.push(rect); + entity.imageCache = null; + } else { + assert(false, 'Not implemented'); + } + }, + entityDeleted: (state, action: PayloadAction) => { + const { entityIdentifier } = action.payload; + if (entityIdentifier.type === 'layer') { + state.layers.entities = state.layers.entities.filter((layer) => layer.id !== entityIdentifier.id); + state.layers.imageCache = null; + } else if (entityIdentifier.type === 'regional_guidance') { + state.regions.entities = state.regions.entities.filter((rg) => rg.id !== entityIdentifier.id); + } else { + assert(false, 'Not implemented'); + } + }, + entityArrangedForwardOne: (state, action: PayloadAction) => { + const { entityIdentifier } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } + if (entity.type === 'layer') { + moveOneToEnd(state.layers.entities, entity); + state.layers.imageCache = null; + } else if (entity.type === 'regional_guidance') { + moveOneToEnd(state.regions.entities, entity); + } else if (entity.type === 'control_adapter') { + moveOneToEnd(state.controlAdapters.entities, entity); + } + }, + entityArrangedToFront: (state, action: PayloadAction) => { + const { entityIdentifier } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } + if (entity.type === 'layer') { + moveToEnd(state.layers.entities, entity); + state.layers.imageCache = null; + } else if (entity.type === 'regional_guidance') { + moveToEnd(state.regions.entities, entity); + } else if (entity.type === 'control_adapter') { + moveToEnd(state.controlAdapters.entities, entity); + } + }, + entityArrangedBackwardOne: (state, action: PayloadAction) => { + const { entityIdentifier } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } + if (entity.type === 'layer') { + moveOneToStart(state.layers.entities, entity); + state.layers.imageCache = null; + } else if (entity.type === 'regional_guidance') { + moveOneToStart(state.regions.entities, entity); + } else if (entity.type === 'control_adapter') { + moveOneToStart(state.controlAdapters.entities, entity); + } + }, + entityArrangedToBack: (state, action: PayloadAction) => { + const { entityIdentifier } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } + if (entity.type === 'layer') { + moveToStart(state.layers.entities, entity); + state.layers.imageCache = null; + } else if (entity.type === 'regional_guidance') { + moveToStart(state.regions.entities, entity); + } else if (entity.type === 'control_adapter') { + moveToStart(state.controlAdapters.entities, entity); + } }, allEntitiesDeleted: (state) => { state.regions.entities = []; @@ -176,10 +382,23 @@ export const { toolChanged, toolBufferChanged, maskOpacityChanged, - entitySelected, allEntitiesDeleted, clipToBboxChanged, canvasReset, + // All entities + entitySelected, + entityReset, + entityIsEnabledToggled, + entityMoved, + entityRasterized, + entityBrushLineAdded, + entityEraserLineAdded, + entityRectAdded, + entityDeleted, + entityArrangedForwardOne, + entityArrangedToFront, + entityArrangedBackwardOne, + entityArrangedToBack, // bbox bboxChanged, bboxScaledSizeChanged, @@ -193,23 +412,10 @@ export const { // layers layerAdded, layerRecalled, - layerDeleted, - layerReset, - layerMovedForwardOne, - layerMovedToFront, - layerMovedBackwardOne, - layerMovedToBack, - layerIsEnabledToggled, layerOpacityChanged, - layerTranslated, - layerBboxChanged, layerImageAdded, layerAllDeleted, layerImageCacheChanged, - layerBrushLineAdded, - layerEraserLineAdded, - layerRectAdded, - layerRasterized, // IP Adapters ipaAdded, ipaRecalled, @@ -224,16 +430,8 @@ export const { ipaBeginEndStepPctChanged, // Control Adapters caAdded, - caBboxChanged, - caDeleted, caAllDeleted, - caIsEnabledToggled, - caMovedBackwardOne, - caMovedForwardOne, - caMovedToBack, - caMovedToFront, caOpacityChanged, - caTranslated, caRecalled, caImageChanged, caProcessedImageChanged, @@ -244,19 +442,10 @@ export const { caProcessorPendingBatchIdChanged, caWeightChanged, caBeginEndStepPctChanged, - caScaled, // Regions rgAdded, rgRecalled, - rgReset, - rgIsEnabledToggled, - rgMoved, - rgDeleted, rgAllDeleted, - rgMovedForwardOne, - rgMovedToFront, - rgMovedBackwardOne, - rgMovedToBack, rgPositivePromptChanged, rgNegativePromptChanged, rgFillChanged, @@ -270,10 +459,6 @@ export const { rgIPAdapterMethodChanged, rgIPAdapterModelChanged, rgIPAdapterCLIPVisionModelChanged, - rgBrushLineAdded, - rgEraserLineAdded, - rgRectAdded, - rgRasterized, // Compositing setInfillMethod, setInfillTileSize, @@ -319,16 +504,9 @@ export const { loraIsEnabledChanged, loraAllDeleted, // Inpaint mask - imReset, imRecalled, - imIsEnabledToggled, - imMoved, imFillChanged, imImageCacheChanged, - imBrushLineAdded, - imEraserLineAdded, - imRectAdded, - inpaintMaskRasterized, // Staging sessionStartedStaging, sessionImageStaged, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts index aa77102263f..ac819399dae 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts @@ -1,24 +1,20 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { zModelIdentifierField } from 'features/nodes/types/common'; -import type { IRect } from 'konva/lib/types'; import { isEqual } from 'lodash-es'; import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; import type { - CanvasV2State, CanvasControlAdapterState, + CanvasControlNetState, + CanvasT2IAdapterState, + CanvasV2State, ControlModeV2, ControlNetConfig, - CanvasControlNetState, Filter, - PositionChangedArg, ProcessorConfig, - ScaleChangedArg, T2IAdapterConfig, - CanvasT2IAdapterState, } from './types'; import { buildControlAdapterProcessorV2, imageDTOToImageObject } from './types'; @@ -56,58 +52,6 @@ export const controlAdaptersReducers = { state.controlAdapters.entities.push(data); state.selectedEntityIdentifier = { type: 'control_adapter', id: data.id }; }, - caIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - ca.isEnabled = !ca.isEnabled; - }, - caTranslated: (state, action: PayloadAction) => { - const { id, position } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - ca.position = position; - }, - caScaled: (state, action: PayloadAction) => { - const { id, scale, position } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - if (ca.imageObject) { - ca.imageObject.x *= scale; - ca.imageObject.y *= scale; - ca.imageObject.height *= scale; - ca.imageObject.width *= scale; - } - - if (ca.processedImageObject) { - ca.processedImageObject.x *= scale; - ca.processedImageObject.y *= scale; - ca.processedImageObject.height *= scale; - ca.processedImageObject.width *= scale; - } - ca.position = position; - ca.bboxNeedsUpdate = true; - state.layers.imageCache = null; - }, - caBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { - const { id, bbox } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - ca.bbox = bbox; - ca.bboxNeedsUpdate = false; - }, - caDeleted: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - state.controlAdapters.entities = state.controlAdapters.entities.filter((ca) => ca.id !== id); - }, caAllDeleted: (state) => { state.controlAdapters.entities = []; }, @@ -119,38 +63,6 @@ export const controlAdaptersReducers = { } ca.opacity = opacity; }, - caMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - moveOneToEnd(state.controlAdapters.entities, ca); - }, - caMovedToFront: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - moveToEnd(state.controlAdapters.entities, ca); - }, - caMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - moveOneToStart(state.controlAdapters.entities, ca); - }, - caMovedToBack: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - moveToStart(state.controlAdapters.entities, ca); - }, caImageChanged: { reducer: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null; objectId: string }>) => { const { id, imageDTO, objectId } = action.payload; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts index 416c6bd341e..ae7e52805d1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts @@ -1,35 +1,16 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import type { - CanvasBrushLineState, - CanvasEraserLineState, - CanvasInpaintMaskState, - CanvasRectState, - CanvasV2State, - Coordinate, - EntityRasterizedArg, -} from 'features/controlLayers/store/types'; +import type { CanvasInpaintMaskState, CanvasV2State } from 'features/controlLayers/store/types'; import { imageDTOToImageWithDims } from 'features/controlLayers/store/types'; import type { ImageDTO } from 'services/api/types'; import type { RgbColor } from './types'; export const inpaintMaskReducers = { - imReset: (state) => { - state.inpaintMask.objects = []; - state.inpaintMask.imageCache = null; - }, imRecalled: (state, action: PayloadAction<{ data: CanvasInpaintMaskState }>) => { const { data } = action.payload; state.inpaintMask = data; state.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id }; }, - imIsEnabledToggled: (state) => { - state.inpaintMask.isEnabled = !state.inpaintMask.isEnabled; - }, - imMoved: (state, action: PayloadAction<{ position: Coordinate }>) => { - const { position } = action.payload; - state.inpaintMask.position = position; - }, imFillChanged: (state, action: PayloadAction<{ fill: RgbColor }>) => { const { fill } = action.payload; state.inpaintMask.fill = fill; @@ -38,25 +19,4 @@ export const inpaintMaskReducers = { const { imageDTO } = action.payload; state.inpaintMask.imageCache = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; }, - imBrushLineAdded: (state, action: PayloadAction<{ brushLine: CanvasBrushLineState }>) => { - const { brushLine } = action.payload; - state.inpaintMask.objects.push(brushLine); - state.layers.imageCache = null; - }, - imEraserLineAdded: (state, action: PayloadAction<{ eraserLine: CanvasEraserLineState }>) => { - const { eraserLine } = action.payload; - state.inpaintMask.objects.push(eraserLine); - state.layers.imageCache = null; - }, - imRectAdded: (state, action: PayloadAction<{ rect: CanvasRectState }>) => { - const { rect } = action.payload; - state.inpaintMask.objects.push(rect); - state.layers.imageCache = null; - }, - inpaintMaskRasterized: (state, action: PayloadAction) => { - const { imageObject, position } = action.payload; - state.inpaintMask.objects = [imageObject]; - state.inpaintMask.position = position; - state.inpaintMask.imageCache = null; - }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index 4a00aa821fa..78be3cf6f13 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -1,21 +1,10 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import type { IRect } from 'konva/lib/types'; import { merge } from 'lodash-es'; import type { ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; -import type { - CanvasBrushLineState, - CanvasEraserLineState, - CanvasLayerState, - CanvasRectState, - CanvasV2State, - EntityRasterizedArg, - ImageObjectAddedArg, - PositionChangedArg, -} from './types'; +import type { CanvasLayerState, CanvasV2State, ImageObjectAddedArg } from './types'; import { imageDTOToImageObject, imageDTOToImageWithDims } from './types'; export const selectLayer = (state: CanvasV2State, id: string) => state.layers.entities.find((layer) => layer.id === id); @@ -52,52 +41,6 @@ export const layersReducers = { state.selectedEntityIdentifier = { type: 'layer', id: data.id }; state.layers.imageCache = null; }, - layerIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - layer.isEnabled = !layer.isEnabled; - state.layers.imageCache = null; - }, - layerTranslated: (state, action: PayloadAction) => { - const { id, position } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - layer.position = position; - state.layers.imageCache = null; - }, - layerBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { - const { id, bbox } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - if (bbox === null) { - // TODO(psyche): Clear objects when bbox is cleared - right now this doesn't work bc bbox calculation for layers - // doesn't work - always returns null - // layer.objects = []; - } - }, - layerReset: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - layer.isEnabled = true; - layer.objects = []; - state.layers.imageCache = null; - layer.position = { x: 0, y: 0 }; - }, - layerDeleted: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - state.layers.entities = state.layers.entities.filter((l) => l.id !== id); - state.layers.imageCache = null; - }, layerAllDeleted: (state) => { state.layers.entities = []; state.layers.imageCache = null; @@ -111,72 +54,6 @@ export const layersReducers = { layer.opacity = opacity; state.layers.imageCache = null; }, - layerMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - moveOneToEnd(state.layers.entities, layer); - state.layers.imageCache = null; - }, - layerMovedToFront: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - moveToEnd(state.layers.entities, layer); - state.layers.imageCache = null; - }, - layerMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - moveOneToStart(state.layers.entities, layer); - state.layers.imageCache = null; - }, - layerMovedToBack: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - moveToStart(state.layers.entities, layer); - state.layers.imageCache = null; - }, - layerBrushLineAdded: (state, action: PayloadAction<{ id: string; brushLine: CanvasBrushLineState }>) => { - const { id, brushLine } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - - layer.objects.push(brushLine); - state.layers.imageCache = null; - }, - layerEraserLineAdded: (state, action: PayloadAction<{ id: string; eraserLine: CanvasEraserLineState }>) => { - const { id, eraserLine } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - - layer.objects.push(eraserLine); - state.layers.imageCache = null; - }, - layerRectAdded: (state, action: PayloadAction<{ id: string; rect: CanvasRectState }>) => { - const { id, rect } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - - layer.objects.push(rect); - state.layers.imageCache = null; - }, layerImageAdded: ( state, action: PayloadAction @@ -198,14 +75,4 @@ export const layersReducers = { const { imageDTO } = action.payload; state.layers.imageCache = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; }, - layerRasterized: (state, action: PayloadAction) => { - const { id, imageObject, position } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - layer.objects = [imageObject]; - layer.position = position; - state.layers.imageCache = null; - }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index 6d209967760..2b3a96be8d0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -1,16 +1,6 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import type { - CanvasBrushLineState, - CanvasEraserLineState, - CanvasRectState, - CanvasV2State, - CLIPVisionModelV2, - EntityRasterizedArg, - IPMethodV2, - PositionChangedArg, -} from 'features/controlLayers/store/types'; +import type { CanvasV2State, CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types'; import { imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas'; @@ -71,73 +61,14 @@ export const regionsReducers = { }, prepare: () => ({ payload: { id: getPrefixedId('regional_guidance') } }), }, - rgReset: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - rg.objects = []; - rg.imageCache = null; - }, rgRecalled: (state, action: PayloadAction<{ data: CanvasRegionalGuidanceState }>) => { const { data } = action.payload; state.regions.entities.push(data); state.selectedEntityIdentifier = { type: 'regional_guidance', id: data.id }; }, - rgIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const rg = selectRG(state, id); - if (rg) { - rg.isEnabled = !rg.isEnabled; - } - }, - rgMoved: (state, action: PayloadAction) => { - const { id, position } = action.payload; - const rg = selectRG(state, id); - if (rg) { - rg.position = position; - } - }, - rgDeleted: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - state.regions.entities = state.regions.entities.filter((ca) => ca.id !== id); - }, rgAllDeleted: (state) => { state.regions.entities = []; }, - rgMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - moveOneToEnd(state.regions.entities, rg); - }, - rgMovedToFront: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - moveToEnd(state.regions.entities, rg); - }, - rgMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - moveOneToStart(state.regions.entities, rg); - }, - rgMovedToBack: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - moveToStart(state.regions.entities, rg); - }, rgPositivePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => { const { id, prompt } = action.payload; const rg = selectRG(state, id); @@ -286,44 +217,4 @@ export const regionsReducers = { } ipa.clipVisionModel = clipVisionModel; }, - rgBrushLineAdded: (state, action: PayloadAction<{ id: string; brushLine: CanvasBrushLineState }>) => { - const { id, brushLine } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - - rg.objects.push(brushLine); - state.layers.imageCache = null; - }, - rgEraserLineAdded: (state, action: PayloadAction<{ id: string; eraserLine: CanvasEraserLineState }>) => { - const { id, eraserLine } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - - rg.objects.push(eraserLine); - state.layers.imageCache = null; - }, - rgRectAdded: (state, action: PayloadAction<{ id: string; rect: CanvasRectState }>) => { - const { id, rect } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - - rg.objects.push(rect); - state.layers.imageCache = null; - }, - rgRasterized: (state, action: PayloadAction) => { - const { id, imageObject, position } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - rg.objects = [imageObject]; - rg.position = position; - rg.imageCache = null; - }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index e1a32fd22ef..0d67d5d41f7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -924,11 +924,20 @@ export type PositionChangedArg = { id: string; position: Coordinate }; export type ScaleChangedArg = { id: string; scale: Coordinate; position: Coordinate }; export type BboxChangedArg = { id: string; bbox: Rect | null }; -export type BrushLineAddedArg = { id: string; brushLine: CanvasBrushLineState }; -export type EraserLineAddedArg = { id: string; eraserLine: CanvasEraserLineState }; -export type RectAddedArg = { id: string; rect: CanvasRectState }; +export type EntityIdentifierPayload = { entityIdentifier: CanvasEntityIdentifier }; +export type EntityMovedPayload = { entityIdentifier: CanvasEntityIdentifier; position: Coordinate }; +export type EntityBrushLineAddedPayload = { entityIdentifier: CanvasEntityIdentifier; brushLine: CanvasBrushLineState }; +export type EntityEraserLineAddedPayload = { + entityIdentifier: CanvasEntityIdentifier; + eraserLine: CanvasEraserLineState; +}; +export type EntityRectAddedPayload = { entityIdentifier: CanvasEntityIdentifier; rect: CanvasRectState }; +export type EntityRasterizedPayload = { + entityIdentifier: CanvasEntityIdentifier; + imageObject: CanvasImageState; + position: Coordinate; +}; export type ImageObjectAddedArg = { id: string; imageDTO: ImageDTO; position?: Coordinate }; -export type EntityRasterizedArg = { id: string; imageObject: CanvasImageState; position: Coordinate }; //#region Type guards export const isLine = (obj: CanvasObjectState): obj is CanvasBrushLineState | CanvasEraserLineState => { From d4fb80772e6a035b3cdd98fe2c1ad16075d02381 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Aug 2024 08:06:22 +1000 Subject: [PATCH 316/678] tidy(ui): move transformer statics into class --- .../controlLayers/konva/CanvasManager.ts | 15 +------- .../controlLayers/konva/CanvasTransformer.ts | 37 +++++++++++-------- 2 files changed, 23 insertions(+), 29 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index d16f7997e7e..f3619ba53f5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -108,9 +108,6 @@ type EntityStateAndAdapter = export const $canvasManager = atom(null); export class CanvasManager { - static BBOX_PADDING_PX = 5; - static BBOX_DEBOUNCE_MS = 300; - stage: Konva.Stage; container: HTMLDivElement; controlAdapters: Map; @@ -619,16 +616,8 @@ export class CanvasManager { return this.stage.position(); } - getScaledPixel(): number { - return 1 / this.getStageScale(); - } - - getScaledBboxPadding(): number { - return CanvasManager.BBOX_PADDING_PX / this.getStageScale(); - } - - getTransformerPadding(): number { - return CanvasManager.BBOX_PADDING_PX; + getScaledPixels(pixels: number): number { + return pixels / this.getStageScale(); } getGenerationMode(): GenerationMode { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index 8ee2ba6ffbe..d513161c284 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -1,5 +1,5 @@ import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter'; -import { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; import { getEmptyRect, getPrefixedId } from 'features/controlLayers/konva/util'; import type { Coordinate, GetLoggingContext, Rect } from 'features/controlLayers/store/types'; @@ -21,16 +21,21 @@ export class CanvasTransformer { static KONVA_PROXY_RECT_NAME = `${CanvasTransformer.TYPE}:proxy_rect`; static KONVA_OUTLINE_RECT_NAME = `${CanvasTransformer.TYPE}:outline_rect`; - static STROKE_COLOR = 'hsl(200 76% 50% / 1)'; // invokeBlue.500 - static ANCHOR_FILL_COLOR = CanvasTransformer.STROKE_COLOR; + static RECT_CALC_DEBOUNCE_MS = 300; + static OUTLINE_PADDING = 5; + static OUTLINE_COLOR = 'hsl(200 76% 50% / 1)'; // invokeBlue.500 + + static ANCHOR_FILL_COLOR = CanvasTransformer.OUTLINE_COLOR; static ANCHOR_STROKE_COLOR = 'hsl(200 76% 77% / 1)'; // invokeBlue.200 + static ANCHOR_CORNER_RADIUS_RATIO = 0.5; + static ANCHOR_STROKE_WIDTH = 2; + static ANCHOR_HIT_PADDING = 10; + static RESIZE_ANCHOR_SIZE = 8; + static ROTATE_ANCHOR_FILL_COLOR = 'hsl(200 76% 95% / 1)'; // invokeBlue.50 static ROTATE_ANCHOR_STROKE_COLOR = 'hsl(200 76% 40% / 1)'; // invokeBlue.700 static ROTATE_ANCHOR_SIZE = 12; - static ANCHOR_CORNER_RADIUS_RATIO = 0.5; - static ANCHOR_STROKE_WIDTH = 2; - static ANCHOR_HIT_PADDING = 10; id: string; parent: CanvasLayerAdapter | CanvasMaskAdapter; @@ -104,7 +109,7 @@ export class CanvasTransformer { listening: false, draggable: false, name: CanvasTransformer.KONVA_OUTLINE_RECT_NAME, - stroke: CanvasTransformer.STROKE_COLOR, + stroke: CanvasTransformer.OUTLINE_COLOR, perfectDrawEnabled: false, strokeHitEnabled: false, }), @@ -120,9 +125,9 @@ export class CanvasTransformer { // Transforming will retain aspect ratio only when shift is held keepRatio: false, // The padding is the distance between the transformer bbox and the nodes - padding: this.manager.getTransformerPadding(), + padding: CanvasTransformer.OUTLINE_PADDING, // This is `invokeBlue.400` - stroke: CanvasTransformer.STROKE_COLOR, + stroke: CanvasTransformer.OUTLINE_COLOR, anchorFill: CanvasTransformer.ANCHOR_FILL_COLOR, anchorStroke: CanvasTransformer.ANCHOR_STROKE_COLOR, anchorStrokeWidth: CanvasTransformer.ANCHOR_STROKE_WIDTH, @@ -332,8 +337,8 @@ export class CanvasTransformer { // The bbox should be updated to reflect the new position of the interaction rect, taking into account its padding // and border this.konva.outlineRect.setAttrs({ - x: this.konva.proxyRect.x() - this.manager.getScaledBboxPadding(), - y: this.konva.proxyRect.y() - this.manager.getScaledBboxPadding(), + x: this.konva.proxyRect.x() - this.manager.getScaledPixels(CanvasTransformer.OUTLINE_PADDING), + y: this.konva.proxyRect.y() - this.manager.getScaledPixels(CanvasTransformer.OUTLINE_PADDING), }); // The object group is translated by the difference between the interaction rect's new and old positions (which is @@ -404,8 +409,8 @@ export class CanvasTransformer { * @param bbox The bounding box of the parent entity */ update = (position: Coordinate, bbox: Rect) => { - const onePixel = this.manager.getScaledPixel(); - const bboxPadding = this.manager.getScaledBboxPadding(); + const onePixel = this.manager.getScaledPixels(1); + const bboxPadding = this.manager.getScaledPixels(CanvasTransformer.OUTLINE_PADDING); this.konva.outlineRect.setAttrs({ x: position.x + bbox.x - bboxPadding, @@ -471,8 +476,8 @@ export class CanvasTransformer { * Updates the transformer's scale. This is called when the stage is scaled. */ syncScale = () => { - const onePixel = this.manager.getScaledPixel(); - const bboxPadding = this.manager.getScaledBboxPadding(); + const onePixel = this.manager.getScaledPixels(1); + const bboxPadding = this.manager.getScaledPixels(CanvasTransformer.OUTLINE_PADDING); this.konva.outlineRect.setAttrs({ x: this.konva.proxyRect.x() - bboxPadding, @@ -672,7 +677,7 @@ export class CanvasTransformer { clone.destroy(); } ); - }, CanvasManager.BBOX_DEBOUNCE_MS); + }, CanvasTransformer.RECT_CALC_DEBOUNCE_MS); requestRectCalculation = () => { this.isPendingRectCalculation = true; From 5de7efc1dc56aeb34297ed144e684499bde3733f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Aug 2024 08:16:15 +1000 Subject: [PATCH 317/678] tidy(ui): "useIsEntitySelected" -> "useEntityIsSelected" --- .../controlLayers/components/common/CanvasEntityContainer.tsx | 4 ++-- .../controlLayers/components/common/CanvasEntityTitle.tsx | 4 ++-- .../hooks/{useIsEntitySelected.ts => useEntityIsSelected.ts} | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename invokeai/frontend/web/src/features/controlLayers/hooks/{useIsEntitySelected.ts => useEntityIsSelected.ts} (87%) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx index 550ba0d020d..5cc84f8a327 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx @@ -2,7 +2,7 @@ import { Flex } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { useEntitySelectionColor } from 'features/controlLayers/hooks/useEntitySelectionColor'; -import { useIsEntitySelected } from 'features/controlLayers/hooks/useIsEntitySelected'; +import { useEntityIsSelected } from 'features/controlLayers/hooks/useEntityIsSelected'; import { entitySelected } from 'features/controlLayers/store/canvasV2Slice'; import type { PropsWithChildren } from 'react'; import { memo, useCallback } from 'react'; @@ -10,7 +10,7 @@ import { memo, useCallback } from 'react'; export const CanvasEntityContainer = memo((props: PropsWithChildren) => { const dispatch = useAppDispatch(); const entityIdentifier = useEntityIdentifierContext(); - const isSelected = useIsEntitySelected(entityIdentifier); + const isSelected = useEntityIsSelected(entityIdentifier); const selectionColor = useEntitySelectionColor(entityIdentifier); const onClick = useCallback(() => { if (isSelected) { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitle.tsx index cbb48fa2469..3853f7249c7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitle.tsx @@ -1,6 +1,6 @@ import { Text } from '@invoke-ai/ui-library'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { useIsEntitySelected } from 'features/controlLayers/hooks/useIsEntitySelected'; +import { useEntityIsSelected } from 'features/controlLayers/hooks/useEntityIsSelected'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { assert } from 'tsafe'; @@ -8,7 +8,7 @@ import { assert } from 'tsafe'; export const CanvasEntityTitle = memo(() => { const { t } = useTranslation(); const entityIdentifier = useEntityIdentifierContext(); - const isSelected = useIsEntitySelected(entityIdentifier); + const isSelected = useEntityIsSelected(entityIdentifier); const title = useMemo(() => { if (entityIdentifier.type === 'inpaint_mask') { return t('controlLayers.inpaintMask'); diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useIsEntitySelected.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsSelected.ts similarity index 87% rename from invokeai/frontend/web/src/features/controlLayers/hooks/useIsEntitySelected.ts rename to invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsSelected.ts index 8d5c3ca86f7..80fe4a61b47 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useIsEntitySelected.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsSelected.ts @@ -2,7 +2,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; -export const useIsEntitySelected = (entityIdentifier: CanvasEntityIdentifier) => { +export const useEntityIsSelected = (entityIdentifier: CanvasEntityIdentifier) => { const selectedEntityIdentifier = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier); const isSelected = useMemo(() => { return selectedEntityIdentifier?.id === entityIdentifier.id; From 87521b07cebb1a33b9d8e0d1e1b5fc1c435f4c8e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Aug 2024 08:23:51 +1000 Subject: [PATCH 318/678] feat(ui): restore object count to layer titles --- .../components/common/CanvasEntityTitle.tsx | 22 ++---------- .../hooks/useEntityObjectCount.ts | 28 +++++++++++++++ .../controlLayers/hooks/useEntityTitle.ts | 36 +++++++++++++++++++ 3 files changed, 67 insertions(+), 19 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/useEntityObjectCount.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitle.tsx index 3853f7249c7..79e0677cdc2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitle.tsx @@ -1,29 +1,13 @@ import { Text } from '@invoke-ai/ui-library'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { useEntityIsSelected } from 'features/controlLayers/hooks/useEntityIsSelected'; -import { memo, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { assert } from 'tsafe'; +import { useEntityTitle } from 'features/controlLayers/hooks/useEntityTitle'; +import { memo } from 'react'; export const CanvasEntityTitle = memo(() => { - const { t } = useTranslation(); const entityIdentifier = useEntityIdentifierContext(); const isSelected = useEntityIsSelected(entityIdentifier); - const title = useMemo(() => { - if (entityIdentifier.type === 'inpaint_mask') { - return t('controlLayers.inpaintMask'); - } else if (entityIdentifier.type === 'control_adapter') { - return t('controlLayers.globalControlAdapter'); - } else if (entityIdentifier.type === 'layer') { - return t('controlLayers.layer'); - } else if (entityIdentifier.type === 'ip_adapter') { - return t('controlLayers.ipAdapter'); - } else if (entityIdentifier.type === 'regional_guidance') { - return t('controlLayers.regionalGuidance'); - } else { - assert(false, 'Unexpected entity type'); - } - }, [entityIdentifier.type, t]); + const title = useEntityTitle(entityIdentifier); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityObjectCount.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityObjectCount.ts new file mode 100644 index 00000000000..4a9f383b4c5 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityObjectCount.ts @@ -0,0 +1,28 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { useAppSelector } from 'app/store/storeHooks'; +import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/canvasV2Slice'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { useMemo } from 'react'; + +export const useEntityObjectCount = (entityIdentifier: CanvasEntityIdentifier) => { + const selectObjectCount = useMemo( + () => + createSelector(selectCanvasV2Slice, (canvasV2) => { + const entity = selectEntity(canvasV2, entityIdentifier); + if (!entity) { + return 0; + } else if (entity.type === 'layer') { + return entity.objects.length; + } else if (entity.type === 'inpaint_mask') { + return entity.objects.length; + } else if (entity.type === 'regional_guidance') { + return entity.objects.length; + } else { + return 0; + } + }), + [entityIdentifier] + ); + const objectCount = useAppSelector(selectObjectCount); + return objectCount; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts new file mode 100644 index 00000000000..82b9a457d6f --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts @@ -0,0 +1,36 @@ +import { useEntityObjectCount } from 'features/controlLayers/hooks/useEntityObjectCount'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { assert } from 'tsafe'; + +export const useEntityTitle = (entityIdentifier: CanvasEntityIdentifier) => { + const { t } = useTranslation(); + + const objectCount = useEntityObjectCount(entityIdentifier); + + const title = useMemo(() => { + const parts: string[] = []; + if (entityIdentifier.type === 'inpaint_mask') { + parts.push(t('controlLayers.inpaintMask')); + } else if (entityIdentifier.type === 'control_adapter') { + parts.push(t('controlLayers.globalControlAdapter')); + } else if (entityIdentifier.type === 'layer') { + parts.push(t('controlLayers.layer')); + } else if (entityIdentifier.type === 'ip_adapter') { + parts.push(t('controlLayers.ipAdapter')); + } else if (entityIdentifier.type === 'regional_guidance') { + parts.push(t('controlLayers.regionalGuidance')); + } else { + assert(false, 'Unexpected entity type'); + } + + if (objectCount > 0) { + parts.push(`(${objectCount})`); + } + + return parts.join(' '); + }, [entityIdentifier.type, objectCount, t]); + + return title; +}; From 08c2089b3de7043bf5bf9fab1acf723641473869 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Aug 2024 09:34:18 +1000 Subject: [PATCH 319/678] feat(ui): no padding on transformer outlines --- .../web/src/features/controlLayers/konva/CanvasTransformer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index d513161c284..ddc0caffd2f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -22,7 +22,7 @@ export class CanvasTransformer { static KONVA_OUTLINE_RECT_NAME = `${CanvasTransformer.TYPE}:outline_rect`; static RECT_CALC_DEBOUNCE_MS = 300; - static OUTLINE_PADDING = 5; + static OUTLINE_PADDING = 0; static OUTLINE_COLOR = 'hsl(200 76% 50% / 1)'; // invokeBlue.500 static ANCHOR_FILL_COLOR = CanvasTransformer.OUTLINE_COLOR; From 2a7cffed2aa9f247de6583d87b35dd3a7f2f5580 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Aug 2024 15:32:53 +1000 Subject: [PATCH 320/678] feat(ui): txt2img, img2img, inpaint & outpaint working --- .../listeners/enqueueRequestedLinear.ts | 5 +- .../web/src/common/util/arrayBuffer.ts | 16 --- .../controlLayers/konva/CanvasLayerAdapter.ts | 4 +- .../controlLayers/konva/CanvasManager.ts | 130 +++++++++++++++--- .../controlLayers/konva/CanvasMaskAdapter.ts | 11 ++ .../konva/CanvasObjectRenderer.ts | 37 ++++- .../controlLayers/konva/CanvasTransformer.ts | 4 + .../src/features/controlLayers/konva/util.ts | 50 ++++--- .../nodes/util/graph/generation/addInpaint.ts | 7 +- .../util/graph/generation/addOutpaint.ts | 7 +- .../nodes/util/graph/generation/addRegions.ts | 4 +- 11 files changed, 206 insertions(+), 69 deletions(-) delete mode 100644 invokeai/frontend/web/src/common/util/arrayBuffer.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index f802319a068..d9cfae607a4 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -21,7 +21,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) assert(manager, 'No model found in state'); let didStartStaging = false; - if (!state.canvasV2.session.isStaging && state.canvasV2.session.isActive) { + if (!state.canvasV2.session.isStaging) { dispatch(sessionStartedStaging()); didStartStaging = true; } @@ -49,7 +49,8 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) ); req.reset(); await req.unwrap(); - } catch { + } catch (error) { + console.log('Error in enqueueRequestedLinear', error); if (didStartStaging && getState().canvasV2.session.isStaging) { dispatch(sessionStagingAreaReset()); } diff --git a/invokeai/frontend/web/src/common/util/arrayBuffer.ts b/invokeai/frontend/web/src/common/util/arrayBuffer.ts deleted file mode 100644 index f7ac9db03f8..00000000000 --- a/invokeai/frontend/web/src/common/util/arrayBuffer.ts +++ /dev/null @@ -1,16 +0,0 @@ -export const getImageDataTransparency = (imageData: ImageData) => { - let isFullyTransparent = true; - let isPartiallyTransparent = false; - const len = imageData.data.length; - for (let i = 3; i < len; i += 4) { - if (imageData.data[i] !== 0) { - isFullyTransparent = false; - } else { - isPartiallyTransparent = true; - } - if (!isFullyTransparent && isPartiallyTransparent) { - return { isFullyTransparent, isPartiallyTransparent }; - } - } - return { isFullyTransparent, isPartiallyTransparent }; -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts index 511ab0e8945..2328b5beb26 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts @@ -28,7 +28,6 @@ export class CanvasLayerAdapter { renderer: CanvasObjectRenderer; isFirstRender: boolean = true; - bboxNeedsUpdate: boolean = true; constructor(state: CanvasLayerAdapter['state'], manager: CanvasLayerAdapter['manager']) { this.id = state.id; @@ -40,6 +39,8 @@ export class CanvasLayerAdapter { this.konva = { layer: new Konva.Layer({ + // We need the ID on the layer to help with building the composite initial image + // See `getCompositeLayerStageClone()` id: this.id, name: `${this.type}:layer`, listening: false, @@ -134,7 +135,6 @@ export class CanvasLayerAdapter { id: this.id, type: this.type, state: deepClone(this.state), - bboxNeedsUpdate: this.bboxNeedsUpdate, transformer: this.transformer.repr(), renderer: this.renderer.repr(), }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index f3619ba53f5..09371a74880 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -13,32 +13,41 @@ import type { CanvasTransformer } from 'features/controlLayers/konva/CanvasTrans import { getCompositeLayerImage, getControlAdapterImage, - getGenerationMode, + getImageDataTransparency, getInpaintMaskImage, getPrefixedId, getRegionMaskImage, + konvaNodeToBlob, + konvaNodeToImageData, nanoid, } from 'features/controlLayers/konva/util'; import type { Extents, ExtentsResult, GetBboxTask, WorkerLogMessage } from 'features/controlLayers/konva/worker'; import { $lastProgressEvent, $shouldShowStagedImage } from 'features/controlLayers/store/canvasV2Slice'; -import { - type CanvasControlAdapterState, - type CanvasEntityIdentifier, - type CanvasInpaintMaskState, - type CanvasLayerState, - type CanvasRegionalGuidanceState, - type CanvasV2State, - type Coordinate, - type GenerationMode, - type GetLoggingContext, - RGBA_WHITE, - type RgbaColor, +import type { + CanvasControlAdapterState, + CanvasEntityIdentifier, + CanvasInpaintMaskState, + CanvasLayerState, + CanvasRegionalGuidanceState, + CanvasV2State, + Coordinate, + GenerationMode, + GetLoggingContext, + Rect, + RgbaColor, } from 'features/controlLayers/store/types'; +import { RGBA_RED } from 'features/controlLayers/store/types'; +import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers'; import type Konva from 'konva'; import { atom } from 'nanostores'; import type { Logger } from 'roarr'; -import { getImageDTO as defaultGetImageDTO, uploadImage as defaultUploadImage } from 'services/api/endpoints/images'; +import { + getImageDTO as defaultGetImageDTO, + getImageDTO, + uploadImage as defaultUploadImage, +} from 'services/api/endpoints/images'; import type { ImageCategory, ImageDTO } from 'services/api/types'; +import { assert } from 'tsafe'; import { CanvasBackground } from './CanvasBackground'; import { CanvasBbox } from './CanvasBbox'; @@ -350,7 +359,8 @@ export class CanvasManager { if (selectedEntity) { // These two entity types use a compositing rect for opacity. Their fill is always white. if (selectedEntity.state.type === 'regional_guidance' || selectedEntity.state.type === 'inpaint_mask') { - currentFill = RGBA_WHITE; + currentFill = RGBA_RED; + // currentFill = RGBA_WHITE; } } return currentFill; @@ -620,8 +630,96 @@ export class CanvasManager { return pixels / this.getStageScale(); } + getCompositeLayerStageClone = (): Konva.Stage => { + const layersState = this.stateApi.getLayersState(); + const stageClone = this.stage.clone(); + + stageClone.scaleX(1); + stageClone.scaleY(1); + stageClone.x(0); + stageClone.y(0); + + const validLayers = layersState.entities.filter(isValidLayer); + // getLayers() returns the internal `children` array of the stage directly - calling destroy on a layer will + // mutate that array. We need to clone the array to avoid mutating the original. + for (const konvaLayer of stageClone.getLayers().slice()) { + if (!validLayers.find((l) => l.id === konvaLayer.id())) { + konvaLayer.destroy(); + } + } + + return stageClone; + }; + + getCompositeLayerBlob = (rect?: Rect): Promise => { + return konvaNodeToBlob(this.getCompositeLayerStageClone(), rect); + }; + + getCompositeLayerImageData = (rect?: Rect): ImageData => { + return konvaNodeToImageData(this.getCompositeLayerStageClone(), rect); + }; + + getCompositeLayerImageDTO = async (rect?: Rect): Promise => { + const blob = await this.getCompositeLayerBlob(rect); + const imageDTO = await this.util.uploadImage(blob, 'composite-layer.png', 'general', true); + this.stateApi.setLayerImageCache(imageDTO); + return imageDTO; + }; + + getInpaintMaskBlob = (rect?: Rect): Promise => { + return this.inpaintMask.renderer.getBlob({ rect }); + }; + + getInpaintMaskImageData = (rect?: Rect): ImageData => { + return this.inpaintMask.renderer.getImageData({ rect }); + }; + + getInpaintMaskImageDTO = async (rect?: Rect): Promise => { + const blob = await this.inpaintMask.renderer.getBlob({ rect }); + const imageDTO = await this.util.uploadImage(blob, 'inpaint-mask.png', 'mask', true); + this.stateApi.setInpaintMaskImageCache(imageDTO); + return imageDTO; + }; + + getRegionMaskImageDTO = async (id: string, rect?: Rect): Promise => { + const region = this.getEntity({ id, type: 'regional_guidance' }); + assert(region?.type === 'regional_guidance'); + if (region.state.imageCache) { + const imageDTO = await getImageDTO(region.state.imageCache.name); + if (imageDTO) { + return imageDTO; + } + } + return region.adapter.renderer.getImageDTO({ + rect, + category: 'other', + is_intermediate: true, + onUploaded: (imageDTO) => { + this.stateApi.setRegionMaskImageCache(region.state.id, imageDTO); + }, + }); + }; + getGenerationMode(): GenerationMode { - return getGenerationMode({ manager: this }); + const { rect } = this.stateApi.getBbox(); + const inpaintMaskImageData = this.getInpaintMaskImageData(rect); + const inpaintMaskTransparency = getImageDataTransparency(inpaintMaskImageData); + const compositeLayerImageData = this.getCompositeLayerImageData(rect); + const compositeLayerTransparency = getImageDataTransparency(compositeLayerImageData); + if (compositeLayerTransparency === 'FULLY_TRANSPARENT') { + // When the initial image is fully transparent, we are always doing txt2img + return 'txt2img'; + } else if (compositeLayerTransparency === 'PARTIALLY_TRANSPARENT') { + // When the initial image is partially transparent, we are always outpainting + return 'outpaint'; + } else if (inpaintMaskTransparency === 'FULLY_TRANSPARENT') { + // compositeLayerTransparency === 'OPAQUE' + // When the inpaint mask is fully transparent, we are doing img2img + return 'img2img'; + } else { + // Else at least some of the inpaint mask is opaque, so we are inpainting + return 'inpaint'; + } } getControlAdapterImage(arg: Omit[0], 'manager'>) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts index 13318f240d9..eae7ff8ccf4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts @@ -1,3 +1,4 @@ +import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; @@ -41,6 +42,8 @@ export class CanvasMaskAdapter { this.konva = { layer: new Konva.Layer({ + // We need the ID on the layer to help with building the composite initial image + // See `getCompositeLayerStageClone()` id: this.id, name: `${this.type}:layer`, listening: false, @@ -135,4 +138,12 @@ export class CanvasMaskAdapter { const isEnabled = get(arg, 'isEnabled', this.state.isEnabled); this.konva.layer.visible(isEnabled); }; + + repr = () => { + return { + id: this.id, + type: this.type, + state: deepClone(this.state), + }; + }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index aa82ba25676..66aebccb196 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -8,18 +8,20 @@ import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLaye import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; -import { getPrefixedId, konvaNodeToBlob, previewBlob } from 'features/controlLayers/konva/util'; +import { getPrefixedId, konvaNodeToBlob, konvaNodeToImageData, previewBlob } from 'features/controlLayers/konva/util'; import { type CanvasBrushLineState, type CanvasEraserLineState, type CanvasImageState, type CanvasRectState, imageDTOToImageObject, + type Rect, type RgbColor, } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; import { uploadImage } from 'services/api/endpoints/images'; +import type { ImageCategory, ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; /** @@ -348,10 +350,8 @@ export class CanvasObjectRenderer { rasterize = async () => { this.log.debug('Rasterizing entity'); - const objectGroupClone = this.konva.objectGroup.clone(); - const interactionRectClone = this.parent.transformer.konva.proxyRect.clone(); - const rect = interactionRectClone.getClientRect(); - const blob = await konvaNodeToBlob(objectGroupClone, rect); + const rect = this.parent.transformer.getRelativeRect(); + const blob = await this.getBlob({ rect }); if (this.manager._isDebugging) { previewBlob(blob, 'Rasterized entity'); } @@ -365,6 +365,33 @@ export class CanvasObjectRenderer { }); }; + getBlob = ({ rect }: { rect?: Rect }): Promise => { + return konvaNodeToBlob(this.konva.objectGroup.clone(), rect); + }; + + getImageData = ({ rect }: { rect?: Rect }): ImageData => { + return konvaNodeToImageData(this.konva.objectGroup.clone(), rect); + }; + + getImageDTO = async ({ + rect, + category, + is_intermediate, + onUploaded, + }: { + rect?: Rect; + category: ImageCategory; + is_intermediate: boolean; + onUploaded?: (imageDTO: ImageDTO) => void; + }): Promise => { + const blob = await this.getBlob({ rect }); + const imageDTO = await uploadImage(blob, `${this.id}.png`, category, is_intermediate); + if (onUploaded) { + onUploaded(imageDTO); + } + return imageDTO; + }; + /** * Destroys this renderer and all of its object renderers. */ diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index ddc0caffd2f..7bf3aea965e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -685,6 +685,10 @@ export class CanvasTransformer { this.calculateRect(); }; + getRelativeRect = (): Rect => { + return this.konva.proxyRect.getClientRect({ relativeTo: this.parent.konva.layer }); + }; + _enableTransform = () => { this.isTransformEnabled = true; this.konva.transformer.visible(true); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index ff663244553..5c4f1eddfbd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -1,4 +1,3 @@ -import { getImageDataTransparency } from 'common/util/arrayBuffer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasObjectState, @@ -329,7 +328,7 @@ export const previewBlob = async (blob: Blob, label?: string) => { export function getInpaintMaskLayerClone(arg: { manager: CanvasManager }): Konva.Layer { const { manager } = arg; const layerClone = manager.inpaintMask.konva.layer.clone(); - const objectGroupClone = manager.inpaintMask.konva.group.clone(); + const objectGroupClone = manager.inpaintMask.renderer.konva.objectGroup.clone(); layerClone.destroyChildren(); layerClone.add(objectGroupClone); @@ -347,7 +346,7 @@ export function getRegionMaskLayerClone(arg: { manager: CanvasManager; id: strin assert(canvasRegion, `Canvas region with id ${id} not found`); const layerClone = canvasRegion.konva.layer.clone(); - const objectGroupClone = canvasRegion.konva.group.clone(); + const objectGroupClone = canvasRegion.renderer.konva.objectGroup.clone(); layerClone.destroyChildren(); layerClone.add(objectGroupClone); @@ -407,27 +406,42 @@ export function getCompositeLayerStageClone(arg: { manager: CanvasManager }): Ko const validLayers = layersState.entities.filter(isValidLayer); console.log(validLayers); - // Konva bug (?) - when iterating over the array returned from `stage.getLayers()`, if you destroy a layer, the array - // is mutated in-place and the next iteration will skip the next layer. To avoid this, we first collect the layers - // to delete in a separate array and then destroy them. - // TODO(psyche): Maybe report this? - const toDelete: Konva.Layer[] = []; - - for (const konvaLayer of stageClone.getLayers()) { - const layer = validLayers.find((l) => l.id === konvaLayer.id()); - if (!layer) { - console.log('deleting', konvaLayer); - toDelete.push(konvaLayer); + // getLayers() returns the internal `children` array of the stage directly - calling destroy on a layer will + // mutate that array. We need to clone the array to avoid mutating the original. + for (const konvaLayer of stageClone.getLayers().slice()) { + if (!validLayers.find((l) => l.id === konvaLayer.id())) { + console.log('destroying', konvaLayer.id()); + konvaLayer.destroy(); } } - for (const konvaLayer of toDelete) { - konvaLayer.destroy(); - } - return stageClone; } +export type Transparency = 'FULLY_TRANSPARENT' | 'PARTIALLY_TRANSPARENT' | 'OPAQUE'; +export function getImageDataTransparency(imageData: ImageData): Transparency { + let isFullyTransparent = true; + let isPartiallyTransparent = false; + const len = imageData.data.length; + for (let i = 3; i < len; i += 4) { + if (imageData.data[i] !== 0) { + isFullyTransparent = false; + } else { + isPartiallyTransparent = true; + } + if (!isFullyTransparent && isPartiallyTransparent) { + return 'PARTIALLY_TRANSPARENT'; + } + } + if (isFullyTransparent) { + return 'FULLY_TRANSPARENT'; + } + if (isPartiallyTransparent) { + return 'PARTIALLY_TRANSPARENT'; + } + return 'OPAQUE'; +} + export function getGenerationMode(arg: { manager: CanvasManager }): GenerationMode { const { manager } = arg; const { x, y, width, height } = manager.stateApi.getBbox().rect; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts index 24c10a6ea3c..75cf71dcdfb 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts @@ -2,7 +2,7 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasV2State, Dimensions } from 'features/controlLayers/store/types'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import type { ParameterPrecision } from 'features/parameters/types/parameterSchemas'; -import { isEqual, pick } from 'lodash-es'; +import { isEqual } from 'lodash-es'; import type { Invocation } from 'services/api/types'; export const addInpaint = async ( @@ -21,9 +21,8 @@ export const addInpaint = async ( ): Promise> => { denoise.denoising_start = denoising_start; - const cropBbox = pick(bbox.rect, ['x', 'y', 'width', 'height']); - const initialImage = await manager.getInitialImage({ bbox: cropBbox }); - const maskImage = await manager.getInpaintMaskImage({ bbox: cropBbox }); + const initialImage = await manager.getCompositeLayerImageDTO(bbox.rect); + const maskImage = await manager.getInpaintMaskImageDTO(bbox.rect); if (!isEqual(scaledSize, originalSize)) { // Scale before processing requires some resizing diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index 6a7c5ec6f88..fcf5b77393e 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts @@ -3,7 +3,7 @@ import type { CanvasV2State, Dimensions } from 'features/controlLayers/store/typ import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { getInfill } from 'features/nodes/util/graph/graphBuilderUtils'; import type { ParameterPrecision } from 'features/parameters/types/parameterSchemas'; -import { isEqual, pick } from 'lodash-es'; +import { isEqual } from 'lodash-es'; import type { Invocation } from 'services/api/types'; export const addOutpaint = async ( @@ -22,9 +22,8 @@ export const addOutpaint = async ( ): Promise> => { denoise.denoising_start = denoising_start; - const cropBbox = pick(bbox.rect, ['x', 'y', 'width', 'height']); - const initialImage = await manager.getInitialImage({ bbox: cropBbox }); - const maskImage = await manager.getInpaintMaskImage({ bbox: cropBbox }); + const initialImage = await manager.getCompositeLayerImageDTO(bbox.rect); + const maskImage = await manager.getInpaintMaskImageDTO(bbox.rect); const infill = getInfill(g, compositing); if (!isEqual(scaledSize, originalSize)) { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts index 4fd328dc96a..992f07ed6ba 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts @@ -1,6 +1,6 @@ import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { CanvasIPAdapterState, Rect, CanvasRegionalGuidanceState } from 'features/controlLayers/store/types'; +import type { CanvasIPAdapterState, CanvasRegionalGuidanceState, Rect } from 'features/controlLayers/store/types'; import { PROMPT_REGION_INVERT_TENSOR_MASK_PREFIX, PROMPT_REGION_MASK_TO_TENSOR_PREFIX, @@ -44,7 +44,7 @@ export const addRegions = async ( for (const region of validRegions) { // Upload the mask image, or get the cached image if it exists - const { image_name } = await manager.getRegionMaskImage({ id: region.id, bbox }); + const { image_name } = await manager.getRegionMaskImageDTO(region.id, bbox); // The main mask-to-tensor node const maskToTensor = g.addNode({ From 3ac88acf11bfbd58972aa5e9b00ce0d81bd6b46e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Aug 2024 17:11:06 +1000 Subject: [PATCH 321/678] chore(ui): add `async-mutex` dep --- invokeai/frontend/web/package.json | 1 + invokeai/frontend/web/pnpm-lock.yaml | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 79a4ce114b7..a69eedad017 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -63,6 +63,7 @@ "@nanostores/react": "^0.7.3", "@reduxjs/toolkit": "2.2.3", "@roarr/browser-log-writer": "^1.3.0", + "async-mutex": "^0.5.0", "chakra-react-select": "^4.9.1", "compare-versions": "^6.1.1", "dateformat": "^5.0.3", diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index a4778ac733d..0efc407fcc3 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -38,6 +38,9 @@ dependencies: '@roarr/browser-log-writer': specifier: ^1.3.0 version: 1.3.0 + async-mutex: + specifier: ^0.5.0 + version: 0.5.0 chakra-react-select: specifier: ^4.9.1 version: 4.9.1(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.13.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) @@ -5894,6 +5897,12 @@ packages: tslib: 2.6.3 dev: true + /async-mutex@0.5.0: + resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} + dependencies: + tslib: 2.6.3 + dev: false + /attr-accept@2.2.2: resolution: {integrity: sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==} engines: {node: '>=4'} From d6acd96dec0f1e092c463293d617738b5dd73393 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Aug 2024 17:20:18 +1000 Subject: [PATCH 322/678] feat(ui): clean up state, add mutex for image loading, add thumbnail loading --- .../listeners/imageDeletionListeners.ts | 6 +- .../ControlAdapter/CAImagePreview.tsx | 4 +- .../components/IPAdapter/IPAImagePreview.tsx | 2 +- .../konva/CanvasControlAdapter.ts | 2 +- .../controlLayers/konva/CanvasImage.ts | 79 ++++++++++++------- .../controlLayers/konva/CanvasManager.ts | 2 +- .../controlLayers/konva/CanvasRect.ts | 14 +--- .../controlLayers/konva/CanvasStagingArea.ts | 2 +- .../features/controlLayers/konva/events.ts | 13 ++- .../src/features/controlLayers/store/types.ts | 52 +++++------- .../deleteImageModal/store/selectors.ts | 6 +- .../src/features/metadata/util/recallers.ts | 2 +- .../graph/generation/addControlAdapters.ts | 4 +- .../util/graph/generation/addIPAdapters.ts | 2 +- .../nodes/util/graph/generation/addRegions.ts | 2 +- 15 files changed, 96 insertions(+), 96 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts index 41944fc405c..4d57cd6b02b 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts @@ -41,7 +41,7 @@ const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: Im const deleteControlAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { state.canvasV2.controlAdapters.entities.forEach(({ id, imageObject, processedImageObject }) => { - if (imageObject?.image.name === imageDTO.image_name || processedImageObject?.image.name === imageDTO.image_name) { + if (imageObject?.image.image_name === imageDTO.image_name || processedImageObject?.image.image_name === imageDTO.image_name) { dispatch(caImageChanged({ id, imageDTO: null })); dispatch(caProcessedImageChanged({ id, imageDTO: null })); } @@ -50,7 +50,7 @@ const deleteControlAdapterImages = (state: RootState, dispatch: AppDispatch, ima const deleteIPAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { state.canvasV2.ipAdapters.entities.forEach(({ id, imageObject }) => { - if (imageObject?.image.name === imageDTO.image_name) { + if (imageObject?.image.image_name === imageDTO.image_name) { dispatch(ipaImageChanged({ id, imageDTO: null })); } }); @@ -60,7 +60,7 @@ const deleteLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: Im state.canvasV2.layers.entities.forEach(({ id, objects }) => { let shouldDelete = false; for (const obj of objects) { - if (obj.type === 'image' && obj.image.name === imageDTO.image_name) { + if (obj.type === 'image' && obj.image.image_name === imageDTO.image_name) { shouldDelete = true; break; } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx index c8a998b5738..60aa39ee150 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx @@ -47,10 +47,10 @@ export const CAImagePreview = memo( const [isMouseOverImage, setIsMouseOverImage] = useState(false); const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery( - controlAdapter.imageObject?.image.name ?? skipToken + controlAdapter.imageObject?.image.image_name ?? skipToken ); const { currentData: processedControlImage, isError: isErrorProcessedControlImage } = useGetImageDTOQuery( - controlAdapter.processedImageObject?.image.name ?? skipToken + controlAdapter.processedImageObject?.image.image_name ?? skipToken ); const [changeIsIntermediate] = useChangeImageIsIntermediateMutation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx index f0b7aa4362f..4dc44c9ae0c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx @@ -29,7 +29,7 @@ export const IPAImagePreview = memo(({ image, onChangeImage, ipAdapterId, droppa const optimalDimension = useAppSelector(selectOptimalDimension); const shift = useShiftModifier(); - const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(image?.name ?? skipToken); + const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(image?.image_name ?? skipToken); const handleResetControlImage = useCallback(() => { onChangeImage(null); }, [onChangeImage]); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts index a03e1eb9e13..2b67c62f8c1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts @@ -72,7 +72,7 @@ export class CanvasControlAdapter extends CanvasEntity { this.image = new CanvasImageRenderer(imageObject, this); this.updateGroup(true); this.konva.objectGroup.add(this.image.konva.group); - await this.image.updateImageSource(imageObject.image.name); + await this.image.updateImageSource(imageObject.image.image_name); } else if (!this.image.isLoading && !this.image.isError) { if (await this.image.update(imageObject)) { didDraw = true; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts index 1e574217cc5..cd10e482b74 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -1,3 +1,4 @@ +import { Mutex } from 'async-mutex'; import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; @@ -29,12 +30,15 @@ export class CanvasImageRenderer { placeholder: { group: Konva.Group; rect: Konva.Rect; text: Konva.Text }; image: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately }; - imageName: string | null; - isLoading: boolean; - isError: boolean; + thumbnailElement: HTMLImageElement | null = null; + imageElement: HTMLImageElement | null = null; + isLoading: boolean = false; + isError: boolean = false; + mutex = new Mutex(); constructor(state: CanvasImageState, parent: CanvasObjectRenderer) { - const { id, width, height, x, y } = state; + const { id, image } = state; + const { width, height } = image; this.id = id; this.parent = parent; this.manager = parent.manager; @@ -44,7 +48,7 @@ export class CanvasImageRenderer { this.log.trace({ state }, 'Creating image'); this.konva = { - group: new Konva.Group({ name: CanvasImageRenderer.GROUP_NAME, listening: false, x, y }), + group: new Konva.Group({ name: CanvasImageRenderer.GROUP_NAME, listening: false }), placeholder: { group: new Konva.Group({ name: CanvasImageRenderer.PLACEHOLDER_GROUP_NAME, listening: false }), rect: new Konva.Rect({ @@ -73,10 +77,6 @@ export class CanvasImageRenderer { this.konva.placeholder.group.add(this.konva.placeholder.rect); this.konva.placeholder.group.add(this.konva.placeholder.text); this.konva.group.add(this.konva.placeholder.group); - - this.imageName = null; - this.isLoading = false; - this.isError = false; this.state = state; } @@ -94,22 +94,50 @@ export class CanvasImageRenderer { const imageDTO = await getImageDTO(imageName); if (imageDTO === null) { - this.log.error({ imageName }, 'Image not found'); + this.onFailedToLoadImage(); return; } - const imageEl = await loadImage(imageDTO.image_url); + loadImage(imageDTO.thumbnail_url) + .then((thumbnailElement) => { + this.thumbnailElement = thumbnailElement; + this.mutex.runExclusive(this.updateImageElement); + }) + .catch(this.onFailedToLoadImage); + loadImage(imageDTO.image_url) + .then((imageElement) => { + this.imageElement = imageElement; + this.mutex.runExclusive(this.updateImageElement); + }) + .catch(this.onFailedToLoadImage); + } catch { + this.onFailedToLoadImage(); + } + }; - if (this.konva.image) { + onFailedToLoadImage = () => { + this.log({ image: this.state.image }, 'Failed to load image'); + this.konva.image?.visible(false); + this.isLoading = false; + this.isError = true; + this.konva.placeholder.text.text(t('common.imageFailedToLoad', 'Image Failed to Load')); + this.konva.placeholder.group.visible(true); + }; + + updateImageElement = () => { + const element = this.imageElement ?? this.thumbnailElement; + + if (element) { + if (this.konva.image && this.konva.image.image() !== element) { this.konva.image.setAttrs({ - image: imageEl, + image: element, }); } else { this.konva.image = new Konva.Image({ name: CanvasImageRenderer.IMAGE_NAME, listening: false, - image: imageEl, - width: this.state.width, - height: this.state.height, + image: element, + width: this.state.image.width, + height: this.state.image.height, }); this.konva.group.add(this.konva.image); } @@ -122,18 +150,9 @@ export class CanvasImageRenderer { this.konva.image.filters([]); } - this.imageName = imageName; this.isLoading = false; this.isError = false; this.konva.placeholder.group.visible(false); - } catch { - this.log({ imageName }, 'Failed to load image'); - this.konva.image?.visible(false); - this.imageName = null; - this.isLoading = false; - this.isError = true; - this.konva.placeholder.text.text(t('common.imageFailedToLoad', 'Image Failed to Load')); - this.konva.placeholder.group.visible(true); } }; @@ -141,11 +160,12 @@ export class CanvasImageRenderer { if (force || this.state !== state) { this.log.trace({ state }, 'Updating image'); - const { width, height, x, y, image, filters } = state; - if (force || (this.state.image.name !== image.name && !this.isLoading)) { - await this.updateImageSource(image.name); + const { image, filters } = state; + const { width, height, image_name } = image; + if (force || (this.state.image.image_name !== image_name && !this.isLoading)) { + await this.updateImageSource(image_name); } - this.konva.image?.setAttrs({ x, y, width, height }); + this.konva.image?.setAttrs({ width, height }); if (filters.length > 0) { this.konva.image?.cache(); this.konva.image?.filters(filters.map((f) => FILTER_MAP[f])); @@ -177,7 +197,6 @@ export class CanvasImageRenderer { id: this.id, type: CanvasImageRenderer.TYPE, parent: this.parent.id, - imageName: this.imageName, isLoading: this.isLoading, isError: this.isError, state: deepClone(this.state), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 09371a74880..11a52d22e34 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -685,7 +685,7 @@ export class CanvasManager { const region = this.getEntity({ id, type: 'regional_guidance' }); assert(region?.type === 'regional_guidance'); if (region.state.imageCache) { - const imageDTO = await getImageDTO(region.state.imageCache.name); + const imageDTO = await getImageDTO(region.state.imageCache); if (imageDTO) { return imageDTO; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts index 8748379da48..6231e8b23f1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts @@ -25,7 +25,7 @@ export class CanvasRectRenderer { isFirstRender: boolean = false; constructor(state: CanvasRectState, parent: CanvasObjectRenderer) { - const { id, x, y, width, height, color } = state; + const { id, rect, color } = state; this.id = id; this.parent = parent; this.manager = parent.manager; @@ -37,10 +37,7 @@ export class CanvasRectRenderer { group: new Konva.Group({ name: CanvasRectRenderer.GROUP_NAME, listening: false }), rect: new Konva.Rect({ name: CanvasRectRenderer.RECT_NAME, - x, - y, - width, - height, + ...rect, listening: false, fill: rgbaColorToString(color), }), @@ -54,12 +51,9 @@ export class CanvasRectRenderer { this.isFirstRender = false; this.log.trace({ state }, 'Updating rect'); - const { x, y, width, height, color } = state; + const { rect, color } = state; this.konva.rect.setAttrs({ - x, - y, - width, - height, + ...rect, fill: rgbaColorToString(color), }); this.state = state; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts index 47b679ed870..50872886ef7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts @@ -53,7 +53,7 @@ export class CanvasStagingArea { height, filters: [], image: { - name: image_name, + image_name: image_name, width, height, }, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 97975c9e404..2ce607860e6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -261,10 +261,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntity.adapter.renderer.setBuffer({ id: getObjectId('rect', true), type: 'rect', - x: Math.round(normalizedPoint.x), - y: Math.round(normalizedPoint.y), - width: 0, - height: 0, + rect: { x: Math.round(normalizedPoint.x), y: Math.round(normalizedPoint.y), width: 0, height: 0 }, color: getCurrentFill(), }); } @@ -407,8 +404,8 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (drawingBuffer) { if (drawingBuffer.type === 'rect') { const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); - drawingBuffer.width = Math.round(normalizedPoint.x - drawingBuffer.x); - drawingBuffer.height = Math.round(normalizedPoint.y - drawingBuffer.y); + drawingBuffer.rect.width = Math.round(normalizedPoint.x - drawingBuffer.rect.x); + drawingBuffer.rect.height = Math.round(normalizedPoint.y - drawingBuffer.rect.y); await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); } else { await selectedEntity.adapter.renderer.clearBuffer(); @@ -447,8 +444,8 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); await selectedEntity.adapter.renderer.commitBuffer(); } else if (toolState.selected === 'rect' && drawingBuffer?.type === 'rect') { - drawingBuffer.width = Math.round(normalizedPoint.x - drawingBuffer.x); - drawingBuffer.height = Math.round(normalizedPoint.y - drawingBuffer.y); + drawingBuffer.rect.width = Math.round(normalizedPoint.x - drawingBuffer.rect.x); + drawingBuffer.rect.height = Math.round(normalizedPoint.y - drawingBuffer.rect.y); await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); await selectedEntity.adapter.renderer.commitBuffer(); } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 0d67d5d41f7..45262c8831f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -43,7 +43,7 @@ import { z } from 'zod'; export const zId = z.string().min(1); export const zImageWithDims = z.object({ - name: z.string(), + image_name: z.string(), width: z.number().int().positive(), height: z.number().int().positive(), }); @@ -248,7 +248,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { buildNode: (image, config) => ({ ...config, type: 'canny_image_processor', - image: { image_name: image.name }, + image: { image_name: image.image_name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -265,7 +265,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { buildNode: (image, config) => ({ ...config, type: 'color_map_image_processor', - image: { image_name: image.name }, + image: { image_name: image.image_name }, }), }, content_shuffle_image_processor: { @@ -281,7 +281,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.name }, + image: { image_name: image.image_name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -297,7 +297,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.name }, + image: { image_name: image.image_name }, resolution: minDim(image), }), }, @@ -312,7 +312,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.name }, + image: { image_name: image.image_name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -327,7 +327,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.name }, + image: { image_name: image.image_name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -343,7 +343,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.name }, + image: { image_name: image.image_name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -360,7 +360,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.name }, + image: { image_name: image.image_name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -377,7 +377,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.name }, + image: { image_name: image.image_name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -394,7 +394,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.name }, + image: { image_name: image.image_name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -409,7 +409,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.name }, + image: { image_name: image.image_name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -427,7 +427,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.name }, + image: { image_name: image.image_name }, image_resolution: minDim(image), }), }, @@ -443,7 +443,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.name }, + image: { image_name: image.image_name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -458,7 +458,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.name }, + image: { image_name: image.image_name }, }), }, }; @@ -548,10 +548,7 @@ export type CanvasEraserLineState = z.infer; const zCanvasRectState = z.object({ id: zId, type: z.literal('rect'), - x: z.number(), - y: z.number(), - width: z.number().min(1), - height: z.number().min(1), + rect: zRect, color: zRgbaColor, }); export type CanvasRectState = z.infer; @@ -563,10 +560,6 @@ const zCanvasImageState = z.object({ id: zId, type: z.literal('image'), image: zImageWithDims, - x: z.number(), - y: z.number(), - width: z.number().min(1), - height: z.number().min(1), filters: z.array(zFilter), }); export type CanvasImageState = z.infer; @@ -589,6 +582,7 @@ export const zCanvasLayerState = z.object({ position: zCoordinate, opacity: zOpacity, objects: z.array(zCanvasObjectState), + imageCache: z.string().min(1).nullable(), }); export type CanvasLayerState = z.infer; @@ -661,7 +655,7 @@ export const zCanvasRegionalGuidanceState = z.object({ negativePrompt: zParameterNegativePrompt.nullable(), ipAdapters: z.array(zCanvasIPAdapterState), autoNegative: zAutoNegative, - imageCache: zImageWithDims.nullable(), + imageCache: z.string().min(1).nullable(), }); export type CanvasRegionalGuidanceState = z.infer; @@ -681,7 +675,7 @@ const zCanvasInpaintMaskState = z.object({ position: zCoordinate, fill: zRgbColor, objects: z.array(zCanvasObjectState), - imageCache: zImageWithDims.nullable(), + imageCache: z.string().min(1).nullable(), }); export type CanvasInpaintMaskState = z.infer; @@ -773,7 +767,7 @@ export const buildControlAdapterProcessorV2 = ( }; export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO): ImageWithDims => ({ - name: image_name, + image_name, width, height, }); @@ -783,13 +777,9 @@ export const imageDTOToImageObject = (imageDTO: ImageDTO, overrides?: Partial { const isLayerImage = canvasV2.layers.entities.some((layer) => - layer.objects.some((obj) => obj.type === 'image' && obj.image.name === image_name) + layer.objects.some((obj) => obj.type === 'image' && obj.image.image_name === image_name) ); const isNodesImage = nodes.nodes @@ -22,10 +22,10 @@ export const getImageUsage = (nodes: NodesState, canvasV2: CanvasV2State, image_ ); const isControlAdapterImage = canvasV2.controlAdapters.entities.some( - (ca) => ca.imageObject?.image.name === image_name || ca.processedImageObject?.image.name === image_name + (ca) => ca.imageObject?.image.image_name === image_name || ca.processedImageObject?.image.image_name === image_name ); - const isIPAdapterImage = canvasV2.ipAdapters.entities.some((ipa) => ipa.imageObject?.image.name === image_name); + const isIPAdapterImage = canvasV2.ipAdapters.entities.some((ipa) => ipa.imageObject?.image.image_name === image_name); const imageUsage: ImageUsage = { isLayerImage, diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index fbf5fb172c2..5ea7fb0ad0e 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -334,7 +334,7 @@ const recallLayer: MetadataRecallFunc = async (layer) => { const invalidObjects: string[] = []; for (const obj of clone.objects) { if (obj.type === 'image') { - const imageDTO = await getImageDTO(obj.image.name); + const imageDTO = await getImageDTO(obj.image.image_name); if (!imageDTO) { invalidObjects.push(obj.id); } diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts index dfc7eb0aa10..881f56f762a 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts @@ -129,12 +129,12 @@ const buildControlImage = ( if (processedImage && processorConfig) { // We've processed the image in the app - use it for the control image. return { - image_name: processedImage.name, + image_name: processedImage.image_name, }; } else if (image) { // No processor selected, and we have an image - the user provided a processed image, use it for the control image. return { - image_name: image.name, + image_name: image.image_name, }; } assert(false, 'Attempted to add unprocessed control image'); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts index 2bfd97786da..569b57310d8 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts @@ -49,7 +49,7 @@ const addIPAdapter = (ipa: CanvasIPAdapterState, g: Graph, denoise: Invocation<' begin_step_percent: beginEndStepPct[0], end_step_percent: beginEndStepPct[1], image: { - image_name: imageObject.image.name, + image_name: imageObject.image.image_name, }, }); g.addEdge(ipAdapter, 'ip_adapter', ipAdapterCollect, 'item'); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts index 992f07ed6ba..5e65a3a6693 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts @@ -191,7 +191,7 @@ export const addRegions = async ( begin_step_percent: beginEndStepPct[0], end_step_percent: beginEndStepPct[1], image: { - image_name: imageObject.image.name, + image_name: imageObject.image.image_name, }, }); From 0a34bebd9c362ffa2df95ba1e421db7b0d5766a1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Aug 2024 17:32:57 +1000 Subject: [PATCH 323/678] tidy(ui): clean up unused logic --- .../controlLayers/konva/CanvasManager.ts | 20 -- .../src/features/controlLayers/konva/util.ts | 295 +----------------- 2 files changed, 12 insertions(+), 303 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 11a52d22e34..c0c578eeef0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -11,12 +11,8 @@ import { CanvasProgressPreview } from 'features/controlLayers/konva/CanvasProgre import type { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; import type { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; import { - getCompositeLayerImage, - getControlAdapterImage, getImageDataTransparency, - getInpaintMaskImage, getPrefixedId, - getRegionMaskImage, konvaNodeToBlob, konvaNodeToImageData, nanoid, @@ -722,22 +718,6 @@ export class CanvasManager { } } - getControlAdapterImage(arg: Omit[0], 'manager'>) { - return getControlAdapterImage({ ...arg, manager: this }); - } - - getRegionMaskImage(arg: Omit[0], 'manager'>) { - return getRegionMaskImage({ ...arg, manager: this }); - } - - getInpaintMaskImage(arg: Omit[0], 'manager'>) { - return getInpaintMaskImage({ ...arg, manager: this }); - } - - getInitialImage(arg: Omit[0], 'manager'>) { - return getCompositeLayerImage({ ...arg, manager: this }); - } - getLoggingContext() { return { // timestamp: new Date().toISOString(), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index 5c4f1eddfbd..f5c25c580e9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -1,17 +1,8 @@ -import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { - CanvasObjectState, - Coordinate, - GenerationMode, - Rect, - RgbaColor, -} from 'features/controlLayers/store/types'; -import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers'; +import type { CanvasObjectState, Coordinate, Rect, RgbaColor } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; import type { Vector2d } from 'konva/lib/types'; import { customAlphabet } from 'nanoid'; -import type { ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; /** @@ -312,7 +303,7 @@ export const getPixelUnderCursor = (stage: Konva.Stage): RgbaColor | null => { return { r, g, b, a }; }; -export const previewBlob = async (blob: Blob, label?: string) => { +export const previewBlob = (blob: Blob, label?: string) => { const url = URL.createObjectURL(blob); const w = window.open(''); if (!w) { @@ -325,99 +316,6 @@ export const previewBlob = async (blob: Blob, label?: string) => { w.document.write(``); }; -export function getInpaintMaskLayerClone(arg: { manager: CanvasManager }): Konva.Layer { - const { manager } = arg; - const layerClone = manager.inpaintMask.konva.layer.clone(); - const objectGroupClone = manager.inpaintMask.renderer.konva.objectGroup.clone(); - - layerClone.destroyChildren(); - layerClone.add(objectGroupClone); - - objectGroupClone.opacity(1); - objectGroupClone.cache(); - - return layerClone; -} - -export function getRegionMaskLayerClone(arg: { manager: CanvasManager; id: string }): Konva.Layer { - const { id, manager } = arg; - - const canvasRegion = manager.regions.get(id); - assert(canvasRegion, `Canvas region with id ${id} not found`); - - const layerClone = canvasRegion.konva.layer.clone(); - const objectGroupClone = canvasRegion.renderer.konva.objectGroup.clone(); - - layerClone.destroyChildren(); - layerClone.add(objectGroupClone); - - objectGroupClone.opacity(1); - objectGroupClone.cache(); - - return layerClone; -} - -export function getControlAdapterLayerClone(arg: { manager: CanvasManager; id: string }): Konva.Layer { - const { id, manager } = arg; - - const controlAdapter = manager.controlAdapters.get(id); - assert(controlAdapter, `Canvas region with id ${id} not found`); - - const controlAdapterClone = controlAdapter.konva.layer.clone(); - const objectGroupClone = controlAdapter.konva.group.clone(); - - controlAdapterClone.destroyChildren(); - controlAdapterClone.add(objectGroupClone); - - objectGroupClone.opacity(1); - objectGroupClone.cache(); - - return controlAdapterClone; -} - -export function getInitialImageLayerClone(arg: { manager: CanvasManager }): Konva.Layer { - const { manager } = arg; - - const initialImage = manager.initialImage; - - const initialImageClone = initialImage.konva.layer.clone(); - const objectGroupClone = initialImage.konva.group.clone(); - - initialImageClone.destroyChildren(); - initialImageClone.add(objectGroupClone); - - objectGroupClone.opacity(1); - objectGroupClone.cache(); - - return initialImageClone; -} - -export function getCompositeLayerStageClone(arg: { manager: CanvasManager }): Konva.Stage { - const { manager } = arg; - - const layersState = manager.stateApi.getLayersState(); - - const stageClone = manager.stage.clone(); - - stageClone.scaleX(1); - stageClone.scaleY(1); - stageClone.x(0); - stageClone.y(0); - - const validLayers = layersState.entities.filter(isValidLayer); - console.log(validLayers); - // getLayers() returns the internal `children` array of the stage directly - calling destroy on a layer will - // mutate that array. We need to clone the array to avoid mutating the original. - for (const konvaLayer of stageClone.getLayers().slice()) { - if (!validLayers.find((l) => l.id === konvaLayer.id())) { - console.log('destroying', konvaLayer.id()); - konvaLayer.destroy(); - } - } - - return stageClone; -} - export type Transparency = 'FULLY_TRANSPARENT' | 'PARTIALLY_TRANSPARENT' | 'OPAQUE'; export function getImageDataTransparency(imageData: ImageData): Transparency { let isFullyTransparent = true; @@ -442,186 +340,17 @@ export function getImageDataTransparency(imageData: ImageData): Transparency { return 'OPAQUE'; } -export function getGenerationMode(arg: { manager: CanvasManager }): GenerationMode { - const { manager } = arg; - const { x, y, width, height } = manager.stateApi.getBbox().rect; - const inpaintMaskLayer = getInpaintMaskLayerClone(arg); - const inpaintMaskImageData = konvaNodeToImageData(inpaintMaskLayer, { x, y, width, height }); - const inpaintMaskTransparency = getImageDataTransparency(inpaintMaskImageData); - const compositeLayer = getCompositeLayerStageClone(arg); - const compositeLayerImageData = konvaNodeToImageData(compositeLayer, { x, y, width, height }); - imageDataToBlob(compositeLayerImageData).then((blob) => { - previewBlob(blob, 'composite layer'); - }); - const compositeLayerTransparency = getImageDataTransparency(compositeLayerImageData); - if (compositeLayerTransparency.isPartiallyTransparent) { - if (compositeLayerTransparency.isFullyTransparent) { - return 'txt2img'; - } - return 'outpaint'; - } else { - if (!inpaintMaskTransparency.isFullyTransparent) { - return 'inpaint'; - } - return 'img2img'; - } -} - -export async function getRegionMaskImage(arg: { - manager: CanvasManager; - id: string; - bbox?: Rect; - preview?: boolean; -}): Promise { - const { manager, id, bbox, preview = false } = arg; - const region = manager.stateApi.getRegionsState().entities.find((entity) => entity.id === id); - assert(region, `Region entity state with id ${id} not found`); - - // if (region.imageCache) { - // const imageDTO = await this.util.getImageDTO(region.imageCache.name); - // if (imageDTO) { - // return imageDTO; - // } - // } - - const layerClone = getRegionMaskLayerClone({ id, manager }); - const blob = await konvaNodeToBlob(layerClone, bbox); - - if (preview) { - previewBlob(blob, `region ${region.id} mask`); - } - - layerClone.destroy(); - - const imageDTO = await manager.util.uploadImage(blob, `${region.id}_mask.png`, 'mask', true); - manager.stateApi.setRegionMaskImageCache(region.id, imageDTO); - return imageDTO; -} - -export async function getControlAdapterImage(arg: { - manager: CanvasManager; - id: string; - bbox?: Rect; - preview?: boolean; -}): Promise { - const { manager, id, bbox, preview = false } = arg; - const ca = manager.stateApi.getControlAdaptersState().entities.find((entity) => entity.id === id); - assert(ca, `Control adapter entity state with id ${id} not found`); - - // if (region.imageCache) { - // const imageDTO = await this.util.getImageDTO(region.imageCache.name); - // if (imageDTO) { - // return imageDTO; - // } - // } - - const layerClone = getControlAdapterLayerClone({ id, manager }); - const blob = await konvaNodeToBlob(layerClone, bbox); - - if (preview) { - previewBlob(blob, `region ${ca.id} mask`); - } - - layerClone.destroy(); - - const imageDTO = await manager.util.uploadImage(blob, `${ca.id}_control_image.png`, 'control', true); - // manager.stateApi.onRegionMaskImageCached(ca.id, imageDTO); - return imageDTO; -} - -export async function getInitialImage(arg: { - manager: CanvasManager; - bbox?: Rect; - preview?: boolean; -}): Promise { - const { manager, bbox, preview = false } = arg; - - // if (region.imageCache) { - // const imageDTO = await this.util.getImageDTO(region.imageCache.name); - // if (imageDTO) { - // return imageDTO; - // } - // } - - const layerClone = getInitialImageLayerClone({ manager }); - const blob = await konvaNodeToBlob(layerClone, bbox); - - if (preview) { - previewBlob(blob, 'initial image'); - } - - layerClone.destroy(); - - const imageDTO = await manager.util.uploadImage(blob, 'initial_image.png', 'other', true); - // manager.stateApi.onRegionMaskImageCached(ca.id, imageDTO); - return imageDTO; -} - -export async function getInpaintMaskImage(arg: { - manager: CanvasManager; - bbox?: Rect; - preview?: boolean; -}): Promise { - const { manager, bbox, preview = false } = arg; - // const inpaintMask = this.stateApi.getInpaintMaskState(); - - // if (inpaintMask.imageCache) { - // const imageDTO = await this.util.getImageDTO(inpaintMask.imageCache.name); - // if (imageDTO) { - // return imageDTO; - // } - // } - - const layerClone = getInpaintMaskLayerClone({ manager }); - const blob = await konvaNodeToBlob(layerClone, bbox); - - if (preview) { - previewBlob(blob, 'inpaint mask'); - } - - layerClone.destroy(); - - const imageDTO = await manager.util.uploadImage(blob, 'inpaint_mask.png', 'mask', true); - manager.stateApi.setInpaintMaskImageCache(imageDTO); - return imageDTO; -} - -export async function getCompositeLayerImage(arg: { - manager: CanvasManager; - bbox?: Rect; - preview?: boolean; -}): Promise { - const { manager, bbox, preview = false } = arg; - // const { imageCache } = this.stateApi.getLayersState(); - - // if (imageCache) { - // const imageDTO = await this.util.getImageDTO(imageCache.name); - // if (imageDTO) { - // return imageDTO; - // } - // } - - const stageClone = getCompositeLayerStageClone({ manager }); - - const blob = await konvaNodeToBlob(stageClone, bbox); - - if (preview) { - previewBlob(blob, 'image source'); - } - - stageClone.destroy(); - - const imageDTO = await manager.util.uploadImage(blob, 'base_layer.png', 'general', true); - manager.stateApi.setLayerImageCache(imageDTO); - return imageDTO; -} - -export function loadImage(src: string, imageEl?: HTMLImageElement): Promise { +/** + * Loads an image from a URL and returns a promise that resolves with the loaded image element. + * @param src The image source URL + * @returns A promise that resolves with the loaded image element + */ +export function loadImage(src: string): Promise { return new Promise((resolve, reject) => { - const _imageEl = imageEl ?? new Image(); - _imageEl.onload = () => resolve(_imageEl); - _imageEl.onerror = (error) => reject(error); - _imageEl.src = src; + const imageElement = new Image(); + imageElement.onload = () => resolve(imageElement); + imageElement.onerror = (error) => reject(error); + imageElement.src = src; }); } From ebef4feddbb0891af083020b7626f46add3452fe Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Aug 2024 17:38:58 +1000 Subject: [PATCH 324/678] fix(ui): ip adapter list item --- .../components/IPAdapter/IPA.tsx | 34 +++++++++++-------- .../components/IPAdapter/IPAHeader.tsx | 26 +++----------- .../components/IPAdapter/IPASettings.tsx | 8 ++--- 3 files changed, 28 insertions(+), 40 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx index ab1cdc987e1..af3d3aa6c33 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx @@ -1,28 +1,34 @@ -import { useDisclosure } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { Spacer, useDisclosure } from '@invoke-ai/ui-library'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; -import { IPAHeader } from 'features/controlLayers/components/IPAdapter/IPAHeader'; +import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; +import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; +import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; +import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; import { IPASettings } from 'features/controlLayers/components/IPAdapter/IPASettings'; -import { entitySelected } from 'features/controlLayers/store/canvasV2Slice'; -import { memo, useCallback } from 'react'; +import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { memo, useMemo } from 'react'; type Props = { id: string; }; export const IPA = memo(({ id }: Props) => { - const dispatch = useAppDispatch(); - const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id); + const entityIdentifier = useMemo(() => ({ id, type: 'ip_adapter' }), [id]); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); - const onSelect = useCallback(() => { - dispatch(entitySelected({ entityIdentifier: { id, type: 'ip_adapter' } })); - }, [dispatch, id]); return ( - - - {isOpen && } - + + + + + + + + + {isOpen && } + + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeader.tsx index 81e63929e8e..d220bcbc0c5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeader.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeader.tsx @@ -1,37 +1,21 @@ import { Spacer } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; -import { ipaDeleted, ipaIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice'; -import { selectIPAOrThrow } from 'features/controlLayers/store/ipAdaptersReducers'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; +import { memo } from 'react'; type Props = { - id: string; - isSelected: boolean; onToggleVisibility: () => void; }; -export const IPAHeader = memo(({ id, isSelected, onToggleVisibility }: Props) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const isEnabled = useAppSelector((s) => selectIPAOrThrow(s.canvasV2, id).isEnabled); - const onToggleIsEnabled = useCallback(() => { - dispatch(ipaIsEnabledToggled({ id })); - }, [dispatch, id]); - const onDelete = useCallback(() => { - dispatch(ipaDeleted({ id })); - }, [dispatch, id]); - +export const IPAHeader = memo(({ onToggleVisibility }: Props) => { return ( - - + + - + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx index b051c7b9b53..5121bc27dcb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx @@ -4,6 +4,7 @@ import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginE import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; import { Weight } from 'features/controlLayers/components/common/Weight'; import { IPAMethod } from 'features/controlLayers/components/IPAdapter/IPAMethod'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { ipaBeginEndStepPctChanged, ipaCLIPVisionModelChanged, @@ -21,12 +22,9 @@ import type { ImageDTO, IPAdapterModelConfig, IPALayerImagePostUploadAction } fr import { IPAImagePreview } from './IPAImagePreview'; import { IPAModelCombobox } from './IPAModelCombobox'; -type Props = { - id: string; -}; - -export const IPASettings = memo(({ id }: Props) => { +export const IPASettings = memo(() => { const dispatch = useAppDispatch(); + const { id } = useEntityIdentifierContext(); const ipAdapter = useAppSelector((s) => selectIPAOrThrow(s.canvasV2, id)); const onChangeBeginEndStepPct = useCallback( From 85a33ff6aafc70af02b28ea1349cbac9b15cef6c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Aug 2024 17:44:11 +1000 Subject: [PATCH 325/678] feat(ui): simplify canvas list item headers --- .../components/ControlAdapter/CA.tsx | 20 +++++++-- .../ControlAdapter/CAEntityHeader.tsx | 27 ------------ .../components/ControlAdapter/CASettings.tsx | 8 ++-- .../components/IPAdapter/IPA.tsx | 2 +- .../components/IPAdapter/IPAHeader.tsx | 23 ---------- .../components/InpaintMask/IM.tsx | 17 ++++++-- .../components/InpaintMask/IMHeader.tsx | 26 ----------- .../controlLayers/components/Layer/Layer.tsx | 19 ++++++-- .../components/Layer/LayerHeader.tsx | 28 ------------ .../components/RegionalGuidance/RG.tsx | 23 ++++++++-- .../components/RegionalGuidance/RGBadges.tsx | 24 +++++++++++ .../components/RegionalGuidance/RGHeader.tsx | 43 ------------------- .../components/common/CanvasEntityHeader.tsx | 8 ++-- 13 files changed, 97 insertions(+), 171 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityHeader.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeader.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMHeader.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGBadges.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGHeader.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx index 8240b3664a5..1ee01e66c64 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx @@ -1,6 +1,11 @@ -import { useDisclosure } from '@invoke-ai/ui-library'; +import { Spacer, useDisclosure } from '@invoke-ai/ui-library'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; -import { CAHeader } from 'features/controlLayers/components/ControlAdapter/CAEntityHeader'; +import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; +import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; +import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; +import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; +import { CAActionsMenu } from 'features/controlLayers/components/ControlAdapter/CAActionsMenu'; +import { CAOpacityAndFilter } from 'features/controlLayers/components/ControlAdapter/CAOpacityAndFilter'; import { CASettings } from 'features/controlLayers/components/ControlAdapter/CASettings'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; @@ -17,8 +22,15 @@ export const CA = memo(({ id }: Props) => { return ( - - {isOpen && } + + + + + + + + + {isOpen && } ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityHeader.tsx deleted file mode 100644 index b95945f4fe2..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityHeader.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Spacer } from '@invoke-ai/ui-library'; -import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; -import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; -import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; -import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; -import { CAActionsMenu } from 'features/controlLayers/components/ControlAdapter/CAActionsMenu'; -import { CAOpacityAndFilter } from 'features/controlLayers/components/ControlAdapter/CAOpacityAndFilter'; -import { memo } from 'react'; - -type Props = { - onToggleVisibility: () => void; -}; - -export const CAHeader = memo(({ onToggleVisibility }: Props) => { - return ( - - - - - - - - - ); -}); - -CAHeader.displayName = 'CAEntityHeader'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx index 70cde437e7d..ca01e973641 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx @@ -8,6 +8,7 @@ import { CAImagePreview } from 'features/controlLayers/components/ControlAdapter import { CAModelCombobox } from 'features/controlLayers/components/ControlAdapter/CAModelCombobox'; import { CAProcessorConfig } from 'features/controlLayers/components/ControlAdapter/CAProcessorConfig'; import { CAProcessorTypeSelect } from 'features/controlLayers/components/ControlAdapter/CAProcessorTypeSelect'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { caBeginEndStepPctChanged, caControlModeChanged, @@ -31,11 +32,8 @@ import type { T2IAdapterModelConfig, } from 'services/api/types'; -type Props = { - id: string; -}; - -export const CASettings = memo(({ id }: Props) => { +export const CASettings = memo(() => { + const { id } = useEntityIdentifierContext(); const dispatch = useAppDispatch(); const { t } = useTranslation(); const [isExpanded, toggleIsExpanded] = useToggle(false); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx index af3d3aa6c33..a4befcdbdd9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx @@ -20,7 +20,7 @@ export const IPA = memo(({ id }: Props) => { return ( - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeader.tsx deleted file mode 100644 index d220bcbc0c5..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeader.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Spacer } from '@invoke-ai/ui-library'; -import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; -import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; -import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; -import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; -import { memo } from 'react'; - -type Props = { - onToggleVisibility: () => void; -}; - -export const IPAHeader = memo(({ onToggleVisibility }: Props) => { - return ( - - - - - - - ); -}); - -IPAHeader.displayName = 'IPAHeader'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx index 87344190748..0193cfa3c90 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx @@ -1,18 +1,29 @@ -import { useDisclosure } from '@invoke-ai/ui-library'; +import { Spacer, useDisclosure } from '@invoke-ai/ui-library'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; -import { IMHeader } from 'features/controlLayers/components/InpaintMask/IMHeader'; +import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; +import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; +import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; +import { IMActionsMenu } from 'features/controlLayers/components/InpaintMask/IMActionsMenu'; import { IMSettings } from 'features/controlLayers/components/InpaintMask/IMSettings'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; +import { IMMaskFillColorPicker } from './IMMaskFillColorPicker'; + export const IM = memo(() => { const entityIdentifier = useMemo(() => ({ id: 'inpaint_mask', type: 'inpaint_mask' }), []); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: false }); return ( - + + + + + + + {isOpen && } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMHeader.tsx deleted file mode 100644 index 283752b65ad..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMHeader.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Spacer } from '@invoke-ai/ui-library'; -import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; -import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; -import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; -import { IMActionsMenu } from 'features/controlLayers/components/InpaintMask/IMActionsMenu'; -import { memo } from 'react'; - -import { IMMaskFillColorPicker } from './IMMaskFillColorPicker'; - -type Props = { - onToggleVisibility: () => void; -}; - -export const IMHeader = memo(({ onToggleVisibility }: Props) => { - return ( - - - - - - - - ); -}); - -IMHeader.displayName = 'IMHeader'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx index 49d2a4dba3c..ffc6ce9b744 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx @@ -1,13 +1,19 @@ -import { useDisclosure } from '@invoke-ai/ui-library'; +import { Spacer, useDisclosure } from '@invoke-ai/ui-library'; import IAIDroppable from 'common/components/IAIDroppable'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; -import { LayerHeader } from 'features/controlLayers/components/Layer/LayerHeader'; +import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; +import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; +import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; +import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; +import { LayerActionsMenu } from 'features/controlLayers/components/Layer/LayerActionsMenu'; import { LayerSettings } from 'features/controlLayers/components/Layer/LayerSettings'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import type { LayerImageDropData } from 'features/dnd/types'; import { memo, useMemo } from 'react'; +import { LayerOpacity } from './LayerOpacity'; + type Props = { id: string; }; @@ -23,7 +29,14 @@ export const Layer = memo(({ id }: Props) => { return ( - + + + + + + + + {isOpen && } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx deleted file mode 100644 index 5d8c17f957c..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Spacer } from '@invoke-ai/ui-library'; -import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; -import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; -import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; -import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; -import { LayerActionsMenu } from 'features/controlLayers/components/Layer/LayerActionsMenu'; -import { memo } from 'react'; - -import { LayerOpacity } from './LayerOpacity'; - -type Props = { - onToggleVisibility: () => void; -}; - -export const LayerHeader = memo(({ onToggleVisibility }: Props) => { - return ( - - - - - - - - - ); -}); - -LayerHeader.displayName = 'LayerHeader'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx index 1da28a8fdc0..e197ac08370 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx @@ -1,11 +1,19 @@ -import { useDisclosure } from '@invoke-ai/ui-library'; +import { Spacer, useDisclosure } from '@invoke-ai/ui-library'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; -import { RGHeader } from 'features/controlLayers/components/RegionalGuidance/RGHeader'; +import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; +import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; +import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; +import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; +import { RGActionsMenu } from 'features/controlLayers/components/RegionalGuidance/RGActionsMenu'; +import { RGBadges } from 'features/controlLayers/components/RegionalGuidance/RGBadges'; import { RGSettings } from 'features/controlLayers/components/RegionalGuidance/RGSettings'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; +import { RGMaskFillColorPicker } from './RGMaskFillColorPicker'; +import { RGSettingsPopover } from './RGSettingsPopover'; + type Props = { id: string; }; @@ -16,7 +24,16 @@ export const RG = memo(({ id }: Props) => { return ( - + + + + + + + + + + {isOpen && } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGBadges.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGBadges.tsx new file mode 100644 index 00000000000..82eea7b0a07 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGBadges.tsx @@ -0,0 +1,24 @@ +import { Badge } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const RGBadges = memo(() => { + const { id } = useEntityIdentifierContext(); + const { t } = useTranslation(); + const autoNegative = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).autoNegative); + + return ( + <> + {autoNegative === 'invert' && ( + + {t('controlLayers.autoNegative')} + + )} + + ); +}); + +RGBadges.displayName = 'RGBadges'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGHeader.tsx deleted file mode 100644 index 151e378ebe4..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGHeader.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Badge, Spacer } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; -import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; -import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; -import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; -import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; -import { RGActionsMenu } from 'features/controlLayers/components/RegionalGuidance/RGActionsMenu'; -import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { RGMaskFillColorPicker } from './RGMaskFillColorPicker'; -import { RGSettingsPopover } from './RGSettingsPopover'; - -type Props = { - onToggleVisibility: () => void; -}; - -export const RGHeader = memo(({ onToggleVisibility }: Props) => { - const { id } = useEntityIdentifierContext(); - const { t } = useTranslation(); - const autoNegative = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).autoNegative); - - return ( - - - - - {autoNegative === 'invert' && ( - - {t('controlLayers.autoNegative')} - - )} - - - - - - ); -}); - -RGHeader.displayName = 'RGHeader'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx index 31977b61afb..e8aca8057ed 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx @@ -1,12 +1,10 @@ +import type { FlexProps } from '@invoke-ai/ui-library'; import { Flex } from '@invoke-ai/ui-library'; -import type { PropsWithChildren } from 'react'; import { memo } from 'react'; -type Props = PropsWithChildren<{ onToggle: () => void }>; - -export const CanvasEntityHeader = memo(({ children, onToggle }: Props) => { +export const CanvasEntityHeader = memo(({ children, ...rest }: FlexProps) => { return ( - + {children} ); From 36f7d0957a0ca87a3ebe400fefa5add66a553a6d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Aug 2024 17:49:02 +1000 Subject: [PATCH 326/678] tidy(ui): clearer component names for control adapters --- .../components/CanvasEntityList.tsx | 4 ++-- .../{CA.tsx => ControlAdapter.tsx} | 16 ++++++------- ...Menu.tsx => ControlAdapterActionsMenu.tsx} | 4 ++-- ...sx => ControlAdapterControlModeSelect.tsx} | 4 ++-- ...iew.tsx => ControlAdapterImagePreview.tsx} | 4 ++-- ...AEntityList.tsx => ControlAdapterList.tsx} | 8 +++---- ...elCombobox.tsx => ControlAdapterModel.tsx} | 4 ++-- ...tsx => ControlAdapterOpacityAndFilter.tsx} | 4 ++-- ....tsx => ControlAdapterProcessorConfig.tsx} | 4 ++-- ... => ControlAdapterProcessorTypeSelect.tsx} | 4 ++-- ...ettings.tsx => ControlAdapterSettings.tsx} | 24 +++++++++---------- 11 files changed, 40 insertions(+), 40 deletions(-) rename invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/{CA.tsx => ControlAdapter.tsx} (70%) rename invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/{CAActionsMenu.tsx => ControlAdapterActionsMenu.tsx} (78%) rename invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/{CAControlModeSelect.tsx => ControlAdapterControlModeSelect.tsx} (90%) rename invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/{CAImagePreview.tsx => ControlAdapterImagePreview.tsx} (98%) rename invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/{CAEntityList.tsx => ControlAdapterList.tsx} (82%) rename invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/{CAModelCombobox.tsx => ControlAdapterModel.tsx} (93%) rename invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/{CAOpacityAndFilter.tsx => ControlAdapterOpacityAndFilter.tsx} (95%) rename invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/{CAProcessorConfig.tsx => ControlAdapterProcessorConfig.tsx} (95%) rename invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/{CAProcessorTypeSelect.tsx => ControlAdapterProcessorTypeSelect.tsx} (93%) rename invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/{CASettings.tsx => ControlAdapterSettings.tsx} (80%) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx index e76b6ab3ed2..236ffdb1617 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx @@ -1,7 +1,7 @@ /* eslint-disable i18next/no-literal-string */ import { Flex } from '@invoke-ai/ui-library'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; -import { CAEntityList } from 'features/controlLayers/components/ControlAdapter/CAEntityList'; +import { ControlAdapterList } from 'features/controlLayers/components/ControlAdapter/ControlAdapterList'; import { IM } from 'features/controlLayers/components/InpaintMask/IM'; import { IPAEntityList } from 'features/controlLayers/components/IPAdapter/IPAEntityList'; import { LayerEntityList } from 'features/controlLayers/components/Layer/LayerEntityList'; @@ -14,7 +14,7 @@ export const CanvasEntityList = memo(() => { - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapter.tsx similarity index 70% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapter.tsx index 1ee01e66c64..280fd742880 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapter.tsx @@ -4,9 +4,9 @@ import { CanvasEntityDeleteButton } from 'features/controlLayers/components/comm import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; -import { CAActionsMenu } from 'features/controlLayers/components/ControlAdapter/CAActionsMenu'; -import { CAOpacityAndFilter } from 'features/controlLayers/components/ControlAdapter/CAOpacityAndFilter'; -import { CASettings } from 'features/controlLayers/components/ControlAdapter/CASettings'; +import { ControlAdapterActionsMenu } from 'features/controlLayers/components/ControlAdapter/ControlAdapterActionsMenu'; +import { ControlAdapterOpacityAndFilter } from 'features/controlLayers/components/ControlAdapter/ControlAdapterOpacityAndFilter'; +import { ControlAdapterSettings } from 'features/controlLayers/components/ControlAdapter/ControlAdapterSettings'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; @@ -15,7 +15,7 @@ type Props = { id: string; }; -export const CA = memo(({ id }: Props) => { +export const ControlAdapter = memo(({ id }: Props) => { const entityIdentifier = useMemo(() => ({ id, type: 'control_adapter' }), [id]); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); @@ -26,14 +26,14 @@ export const CA = memo(({ id }: Props) => { - - + + - {isOpen && } + {isOpen && } ); }); -CA.displayName = 'CA'; +ControlAdapter.displayName = 'ControlAdapter'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterActionsMenu.tsx similarity index 78% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterActionsMenu.tsx index 6285d7be9c1..a9b5815a95d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterActionsMenu.tsx @@ -3,7 +3,7 @@ import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/c import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; import { memo } from 'react'; -export const CAActionsMenu = memo(() => { +export const ControlAdapterActionsMenu = memo(() => { return ( @@ -14,4 +14,4 @@ export const CAActionsMenu = memo(() => { ); }); -CAActionsMenu.displayName = 'CAActionsMenu'; +ControlAdapterActionsMenu.displayName = 'ControlAdapterActionsMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAControlModeSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterControlModeSelect.tsx similarity index 90% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAControlModeSelect.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterControlModeSelect.tsx index 9c8ecb896f1..e3f03b6d48f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAControlModeSelect.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterControlModeSelect.tsx @@ -12,7 +12,7 @@ type Props = { onChange: (controlMode: ControlModeV2) => void; }; -export const CAControlModeSelect = memo(({ controlMode, onChange }: Props) => { +export const ControlAdapterControlModeSelect = memo(({ controlMode, onChange }: Props) => { const { t } = useTranslation(); const CONTROL_MODE_DATA = useMemo( () => [ @@ -57,4 +57,4 @@ export const CAControlModeSelect = memo(({ controlMode, onChange }: Props) => { ); }); -CAControlModeSelect.displayName = 'CAControlModeSelect'; +ControlAdapterControlModeSelect.displayName = 'ControlAdapterControlModeSelect'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterImagePreview.tsx similarity index 98% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterImagePreview.tsx index 60aa39ee150..8faab377805 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterImagePreview.tsx @@ -28,7 +28,7 @@ type Props = { onErrorLoadingProcessedImage: () => void; }; -export const CAImagePreview = memo( +export const ControlAdapterImagePreview = memo( ({ controlAdapter, onChangeImage, @@ -226,4 +226,4 @@ export const CAImagePreview = memo( } ); -CAImagePreview.displayName = 'CAImagePreview'; +ControlAdapterImagePreview.displayName = 'ControlAdapterImagePreview'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterList.tsx similarity index 82% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityList.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterList.tsx index bc6d5bbe559..694dade9a80 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterList.tsx @@ -1,7 +1,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityGroupTitle } from 'features/controlLayers/components/common/CanvasEntityGroupTitle'; -import { CA } from 'features/controlLayers/components/ControlAdapter/CA'; +import { ControlAdapter } from 'features/controlLayers/components/ControlAdapter/ControlAdapter'; import { mapId } from 'features/controlLayers/konva/util'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { memo } from 'react'; @@ -11,7 +11,7 @@ const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) = return canvasV2.controlAdapters.entities.map(mapId).reverse(); }); -export const CAEntityList = memo(() => { +export const ControlAdapterList = memo(() => { const { t } = useTranslation(); const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'control_adapter')); const caIds = useAppSelector(selectEntityIds); @@ -28,11 +28,11 @@ export const CAEntityList = memo(() => { isSelected={isSelected} /> {caIds.map((id) => ( - + ))} ); } }); -CAEntityList.displayName = 'CAEntityList'; +ControlAdapterList.displayName = 'ControlAdapterList'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAModelCombobox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterModel.tsx similarity index 93% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAModelCombobox.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterModel.tsx index 703caf64c1e..13e4b17e907 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAModelCombobox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterModel.tsx @@ -11,7 +11,7 @@ type Props = { onChange: (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => void; }; -export const CAModelCombobox = memo(({ modelKey, onChange: onChangeModel }: Props) => { +export const ControlAdapterModel = memo(({ modelKey, onChange: onChangeModel }: Props) => { const { t } = useTranslation(); const currentBaseModel = useAppSelector((s) => s.canvasV2.params.model?.base); const [modelConfigs, { isLoading }] = useControlNetAndT2IAdapterModels(); @@ -60,4 +60,4 @@ export const CAModelCombobox = memo(({ modelKey, onChange: onChangeModel }: Prop ); }); -CAModelCombobox.displayName = 'CAModelCombobox'; +ControlAdapterModel.displayName = 'ControlAdapterModel'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAOpacityAndFilter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterOpacityAndFilter.tsx similarity index 95% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAOpacityAndFilter.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterOpacityAndFilter.tsx index b440aad03ee..ba4f85a1e10 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAOpacityAndFilter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterOpacityAndFilter.tsx @@ -25,7 +25,7 @@ import { PiDropHalfFill } from 'react-icons/pi'; const marks = [0, 25, 50, 75, 100]; const formatPct = (v: number | string) => `${v} %`; -export const CAOpacityAndFilter = memo(() => { +export const ControlAdapterOpacityAndFilter = memo(() => { const { id } = useEntityIdentifierContext(); const { t } = useTranslation(); const dispatch = useAppDispatch(); @@ -96,4 +96,4 @@ export const CAOpacityAndFilter = memo(() => { ); }); -CAOpacityAndFilter.displayName = 'CAOpacityAndFilter'; +ControlAdapterOpacityAndFilter.displayName = 'ControlAdapterOpacityAndFilter'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAProcessorConfig.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterProcessorConfig.tsx similarity index 95% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAProcessorConfig.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterProcessorConfig.tsx index a668c90a81f..bd7d96d5024 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAProcessorConfig.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterProcessorConfig.tsx @@ -17,7 +17,7 @@ type Props = { onChange: (config: ProcessorConfig | null) => void; }; -export const CAProcessorConfig = memo(({ config, onChange }: Props) => { +export const ControlAdapterProcessorConfig = memo(({ config, onChange }: Props) => { if (!config) { return null; } @@ -81,4 +81,4 @@ export const CAProcessorConfig = memo(({ config, onChange }: Props) => { } }); -CAProcessorConfig.displayName = 'CAProcessorConfig'; +ControlAdapterProcessorConfig.displayName = 'ControlAdapterProcessorConfig'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAProcessorTypeSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterProcessorTypeSelect.tsx similarity index 93% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAProcessorTypeSelect.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterProcessorTypeSelect.tsx index 70e9113c552..c936ff8a09a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAProcessorTypeSelect.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterProcessorTypeSelect.tsx @@ -22,7 +22,7 @@ const selectDisabledProcessors = createMemoizedSelector( (config) => config.sd.disabledControlNetProcessors ); -export const CAProcessorTypeSelect = memo(({ config, onChange }: Props) => { +export const ControlAdapterProcessorTypeSelect = memo(({ config, onChange }: Props) => { const { t } = useTranslation(); const disabledProcessors = useAppSelector(selectDisabledProcessors); const options = useMemo(() => { @@ -67,4 +67,4 @@ export const CAProcessorTypeSelect = memo(({ config, onChange }: Props) => { ); }); -CAProcessorTypeSelect.displayName = 'CAProcessorTypeSelect'; +ControlAdapterProcessorTypeSelect.displayName = 'ControlAdapterProcessorTypeSelect'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterSettings.tsx similarity index 80% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterSettings.tsx index ca01e973641..96a505cf7e0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterSettings.tsx @@ -3,11 +3,11 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct'; import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; import { Weight } from 'features/controlLayers/components/common/Weight'; -import { CAControlModeSelect } from 'features/controlLayers/components/ControlAdapter/CAControlModeSelect'; -import { CAImagePreview } from 'features/controlLayers/components/ControlAdapter/CAImagePreview'; -import { CAModelCombobox } from 'features/controlLayers/components/ControlAdapter/CAModelCombobox'; -import { CAProcessorConfig } from 'features/controlLayers/components/ControlAdapter/CAProcessorConfig'; -import { CAProcessorTypeSelect } from 'features/controlLayers/components/ControlAdapter/CAProcessorTypeSelect'; +import { ControlAdapterControlModeSelect } from 'features/controlLayers/components/ControlAdapter/ControlAdapterControlModeSelect'; +import { ControlAdapterImagePreview } from 'features/controlLayers/components/ControlAdapter/ControlAdapterImagePreview'; +import { ControlAdapterModel } from 'features/controlLayers/components/ControlAdapter/ControlAdapterModel'; +import { ControlAdapterProcessorConfig } from 'features/controlLayers/components/ControlAdapter/ControlAdapterProcessorConfig'; +import { ControlAdapterProcessorTypeSelect } from 'features/controlLayers/components/ControlAdapter/ControlAdapterProcessorTypeSelect'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { caBeginEndStepPctChanged, @@ -32,7 +32,7 @@ import type { T2IAdapterModelConfig, } from 'services/api/types'; -export const CASettings = memo(() => { +export const ControlAdapterSettings = memo(() => { const { id } = useEntityIdentifierContext(); const dispatch = useAppDispatch(); const { t } = useTranslation(); @@ -98,7 +98,7 @@ export const CASettings = memo(() => { - + { {controlAdapter.adapterType === 'controlnet' && ( - + )} - { <> - - + + )} @@ -151,4 +151,4 @@ export const CASettings = memo(() => { ); }); -CASettings.displayName = 'CASettings'; +ControlAdapterSettings.displayName = 'ControlAdapterSettings'; From 1a8d65d7a93bbbfa0c1bdbfc83398b4115285e89 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Aug 2024 17:50:27 +1000 Subject: [PATCH 327/678] tidy(ui): clearer component names for inpaint mask --- .../components/CanvasEntityList.tsx | 4 ++-- .../InpaintMask/{IM.tsx => InpaintMask.tsx} | 16 ++++++++-------- ...ctionsMenu.tsx => InpaintMaskActionsMenu.tsx} | 4 ++-- ...er.tsx => InpaintMaskMaskFillColorPicker.tsx} | 4 ++-- .../{IMSettings.tsx => InpaintMaskSettings.tsx} | 4 ++-- 5 files changed, 16 insertions(+), 16 deletions(-) rename invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/{IM.tsx => InpaintMask.tsx} (71%) rename invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/{IMActionsMenu.tsx => InpaintMaskActionsMenu.tsx} (80%) rename invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/{IMMaskFillColorPicker.tsx => InpaintMaskMaskFillColorPicker.tsx} (91%) rename invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/{IMSettings.tsx => InpaintMaskSettings.tsx} (66%) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx index 236ffdb1617..1d309d4e12f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx @@ -2,7 +2,7 @@ import { Flex } from '@invoke-ai/ui-library'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import { ControlAdapterList } from 'features/controlLayers/components/ControlAdapter/ControlAdapterList'; -import { IM } from 'features/controlLayers/components/InpaintMask/IM'; +import { InpaintMask } from 'features/controlLayers/components/InpaintMask/InpaintMask'; import { IPAEntityList } from 'features/controlLayers/components/IPAdapter/IPAEntityList'; import { LayerEntityList } from 'features/controlLayers/components/Layer/LayerEntityList'; import { RGEntityList } from 'features/controlLayers/components/RegionalGuidance/RGEntityList'; @@ -12,7 +12,7 @@ export const CanvasEntityList = memo(() => { return ( - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx similarity index 71% rename from invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx index 0193cfa3c90..be9ebbfd67a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx @@ -3,15 +3,15 @@ import { CanvasEntityContainer } from 'features/controlLayers/components/common/ import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; -import { IMActionsMenu } from 'features/controlLayers/components/InpaintMask/IMActionsMenu'; -import { IMSettings } from 'features/controlLayers/components/InpaintMask/IMSettings'; +import { InpaintMaskActionsMenu } from 'features/controlLayers/components/InpaintMask/InpaintMaskActionsMenu'; +import { InpaintMaskSettings } from 'features/controlLayers/components/InpaintMask/InpaintMaskSettings'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; -import { IMMaskFillColorPicker } from './IMMaskFillColorPicker'; +import { InpaintMaskMaskFillColorPicker } from './InpaintMaskMaskFillColorPicker'; -export const IM = memo(() => { +export const InpaintMask = memo(() => { const entityIdentifier = useMemo(() => ({ id: 'inpaint_mask', type: 'inpaint_mask' }), []); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: false }); return ( @@ -21,13 +21,13 @@ export const IM = memo(() => { - - + + - {isOpen && } + {isOpen && } ); }); -IM.displayName = 'IM'; +InpaintMask.displayName = 'InpaintMask'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskActionsMenu.tsx similarity index 80% rename from invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMActionsMenu.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskActionsMenu.tsx index a7cfc75eb2a..5ce40241955 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMActionsMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskActionsMenu.tsx @@ -3,7 +3,7 @@ import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/c import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; import { memo } from 'react'; -export const IMActionsMenu = memo(() => { +export const InpaintMaskActionsMenu = memo(() => { return ( @@ -14,4 +14,4 @@ export const IMActionsMenu = memo(() => { ); }); -IMActionsMenu.displayName = 'IMActionsMenu'; +InpaintMaskActionsMenu.displayName = 'InpaintMaskActionsMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx similarity index 91% rename from invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMMaskFillColorPicker.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx index 144012143db..891b38a4184 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMMaskFillColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx @@ -8,7 +8,7 @@ import { memo, useCallback } from 'react'; import type { RgbColor } from 'react-colorful'; import { useTranslation } from 'react-i18next'; -export const IMMaskFillColorPicker = memo(() => { +export const InpaintMaskMaskFillColorPicker = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const fill = useAppSelector((s) => s.canvasV2.inpaintMask.fill); @@ -43,4 +43,4 @@ export const IMMaskFillColorPicker = memo(() => { ); }); -IMMaskFillColorPicker.displayName = 'IMMaskFillColorPicker'; +InpaintMaskMaskFillColorPicker.displayName = 'InpaintMaskMaskFillColorPicker'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskSettings.tsx similarity index 66% rename from invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMSettings.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskSettings.tsx index 3c1b7296d9a..d7719694a8a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskSettings.tsx @@ -1,8 +1,8 @@ import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; import { memo } from 'react'; -export const IMSettings = memo(() => { +export const InpaintMaskSettings = memo(() => { return PLACEHOLDER; }); -IMSettings.displayName = 'IMSettings'; +InpaintMaskSettings.displayName = 'InpaintMaskSettings'; From aadba5579670b2ff54facb55a442303f2344abc0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Aug 2024 17:52:55 +1000 Subject: [PATCH 328/678] tidy(ui): clearer component names for ip adapter --- .../components/CanvasEntityList.tsx | 4 ++-- .../IPAdapter/{IPA.tsx => IPAdapter.tsx} | 8 ++++---- ...magePreview.tsx => IPAdapterImagePreview.tsx} | 4 ++-- .../{IPAEntityList.tsx => IPAdapterList.tsx} | 8 ++++---- .../{IPAMethod.tsx => IPAdapterMethod.tsx} | 4 ++-- .../{IPAModelCombobox.tsx => IPAdapterModel.tsx} | 4 ++-- .../{IPASettings.tsx => IPAdapterSettings.tsx} | 16 ++++++++-------- .../RegionalGuidance/RGIPAdapterSettings.tsx | 12 ++++++------ 8 files changed, 30 insertions(+), 30 deletions(-) rename invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/{IPA.tsx => IPAdapter.tsx} (86%) rename invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/{IPAImagePreview.tsx => IPAdapterImagePreview.tsx} (95%) rename invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/{IPAEntityList.tsx => IPAdapterList.tsx} (84%) rename invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/{IPAMethod.tsx => IPAdapterMethod.tsx} (92%) rename invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/{IPAModelCombobox.tsx => IPAdapterModel.tsx} (94%) rename invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/{IPASettings.tsx => IPAdapterSettings.tsx} (89%) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx index 1d309d4e12f..710decfd0f5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx @@ -3,7 +3,7 @@ import { Flex } from '@invoke-ai/ui-library'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import { ControlAdapterList } from 'features/controlLayers/components/ControlAdapter/ControlAdapterList'; import { InpaintMask } from 'features/controlLayers/components/InpaintMask/InpaintMask'; -import { IPAEntityList } from 'features/controlLayers/components/IPAdapter/IPAEntityList'; +import { IPAdapterList } from 'features/controlLayers/components/IPAdapter/IPAdapterList'; import { LayerEntityList } from 'features/controlLayers/components/Layer/LayerEntityList'; import { RGEntityList } from 'features/controlLayers/components/RegionalGuidance/RGEntityList'; import { memo } from 'react'; @@ -15,7 +15,7 @@ export const CanvasEntityList = memo(() => { - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapter.tsx similarity index 86% rename from invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapter.tsx index a4befcdbdd9..4b475361794 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapter.tsx @@ -4,7 +4,7 @@ import { CanvasEntityDeleteButton } from 'features/controlLayers/components/comm import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; -import { IPASettings } from 'features/controlLayers/components/IPAdapter/IPASettings'; +import { IPAdapterSettings } from 'features/controlLayers/components/IPAdapter/IPAdapterSettings'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; @@ -13,7 +13,7 @@ type Props = { id: string; }; -export const IPA = memo(({ id }: Props) => { +export const IPAdapter = memo(({ id }: Props) => { const entityIdentifier = useMemo(() => ({ id, type: 'ip_adapter' }), [id]); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); @@ -26,10 +26,10 @@ export const IPA = memo(({ id }: Props) => { - {isOpen && } + {isOpen && } ); }); -IPA.displayName = 'IPA'; +IPAdapter.displayName = 'IPAdapter'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterImagePreview.tsx similarity index 95% rename from invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterImagePreview.tsx index 4dc44c9ae0c..9e76aa1b917 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterImagePreview.tsx @@ -22,7 +22,7 @@ type Props = { postUploadAction: PostUploadAction; }; -export const IPAImagePreview = memo(({ image, onChangeImage, ipAdapterId, droppableData, postUploadAction }: Props) => { +export const IPAdapterImagePreview = memo(({ image, onChangeImage, ipAdapterId, droppableData, postUploadAction }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const isConnected = useAppSelector((s) => s.system.isConnected); @@ -97,4 +97,4 @@ export const IPAImagePreview = memo(({ image, onChangeImage, ipAdapterId, droppa ); }); -IPAImagePreview.displayName = 'IPAImagePreview'; +IPAdapterImagePreview.displayName = 'IPAdapterImagePreview'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterList.tsx similarity index 84% rename from invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAEntityList.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterList.tsx index 5e04bc61627..348f525ae5e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterList.tsx @@ -2,7 +2,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityGroupTitle } from 'features/controlLayers/components/common/CanvasEntityGroupTitle'; -import { IPA } from 'features/controlLayers/components/IPAdapter/IPA'; +import { IPAdapter } from 'features/controlLayers/components/IPAdapter/IPAdapter'; import { mapId } from 'features/controlLayers/konva/util'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { memo } from 'react'; @@ -12,7 +12,7 @@ const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) = return canvasV2.ipAdapters.entities.map(mapId).reverse(); }); -export const IPAEntityList = memo(() => { +export const IPAdapterList = memo(() => { const { t } = useTranslation(); const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'ip_adapter')); const ipaIds = useAppSelector(selectEntityIds); @@ -29,11 +29,11 @@ export const IPAEntityList = memo(() => { isSelected={isSelected} /> {ipaIds.map((id) => ( - + ))} ); } }); -IPAEntityList.displayName = 'IPAEntityList'; +IPAdapterList.displayName = 'IPAdapterList'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAMethod.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterMethod.tsx similarity index 92% rename from invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAMethod.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterMethod.tsx index 55c99fa6f7a..1e1165fbc70 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAMethod.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterMethod.tsx @@ -12,7 +12,7 @@ type Props = { onChange: (method: IPMethodV2) => void; }; -export const IPAMethod = memo(({ method, onChange }: Props) => { +export const IPAdapterMethod = memo(({ method, onChange }: Props) => { const { t } = useTranslation(); const options: { label: string; value: IPMethodV2 }[] = useMemo( () => [ @@ -41,4 +41,4 @@ export const IPAMethod = memo(({ method, onChange }: Props) => { ); }); -IPAMethod.displayName = 'IPAMethod'; +IPAdapterMethod.displayName = 'IPAdapterMethod'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAModelCombobox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterModel.tsx similarity index 94% rename from invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAModelCombobox.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterModel.tsx index 34e215dde7b..80e322d7f4f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAModelCombobox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterModel.tsx @@ -22,7 +22,7 @@ type Props = { onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModelV2) => void; }; -export const IPAModelCombobox = memo(({ modelKey, onChangeModel, clipVisionModel, onChangeCLIPVisionModel }: Props) => { +export const IPAdapterModel = memo(({ modelKey, onChangeModel, clipVisionModel, onChangeCLIPVisionModel }: Props) => { const { t } = useTranslation(); const currentBaseModel = useAppSelector((s) => s.canvasV2.params.model?.base); const [modelConfigs, { isLoading }] = useIPAdapterModels(); @@ -95,4 +95,4 @@ export const IPAModelCombobox = memo(({ modelKey, onChangeModel, clipVisionModel ); }); -IPAModelCombobox.displayName = 'IPAModelCombobox'; +IPAdapterModel.displayName = 'IPAdapterModel'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx similarity index 89% rename from invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx index 5121bc27dcb..a8d331bde94 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx @@ -3,7 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct'; import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; import { Weight } from 'features/controlLayers/components/common/Weight'; -import { IPAMethod } from 'features/controlLayers/components/IPAdapter/IPAMethod'; +import { IPAdapterMethod } from 'features/controlLayers/components/IPAdapter/IPAdapterMethod'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { ipaBeginEndStepPctChanged, @@ -19,10 +19,10 @@ import type { IPAImageDropData } from 'features/dnd/types'; import { memo, useCallback, useMemo } from 'react'; import type { ImageDTO, IPAdapterModelConfig, IPALayerImagePostUploadAction } from 'services/api/types'; -import { IPAImagePreview } from './IPAImagePreview'; -import { IPAModelCombobox } from './IPAModelCombobox'; +import { IPAdapterImagePreview } from './IPAdapterImagePreview'; +import { IPAdapterModel } from './IPAdapterModel'; -export const IPASettings = memo(() => { +export const IPAdapterSettings = memo(() => { const dispatch = useAppDispatch(); const { id } = useEntityIdentifierContext(); const ipAdapter = useAppSelector((s) => selectIPAOrThrow(s.canvasV2, id)); @@ -77,7 +77,7 @@ export const IPASettings = memo(() => { - { - + - { ); }); -IPASettings.displayName = 'IPASettings'; +IPAdapterSettings.displayName = 'IPAdapterSettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapterSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapterSettings.tsx index 0a79a4368a3..325fb48266c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapterSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapterSettings.tsx @@ -2,9 +2,9 @@ import { Box, Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct'; import { Weight } from 'features/controlLayers/components/common/Weight'; -import { IPAImagePreview } from 'features/controlLayers/components/IPAdapter/IPAImagePreview'; -import { IPAMethod } from 'features/controlLayers/components/IPAdapter/IPAMethod'; -import { IPAModelCombobox } from 'features/controlLayers/components/IPAdapter/IPAModelCombobox'; +import { IPAdapterImagePreview } from 'features/controlLayers/components/IPAdapter/IPAdapterImagePreview'; +import { IPAdapterMethod } from 'features/controlLayers/components/IPAdapter/IPAdapterMethod'; +import { IPAdapterModel } from 'features/controlLayers/components/IPAdapter/IPAdapterModel'; import { rgIPAdapterBeginEndStepPctChanged, rgIPAdapterCLIPVisionModelChanged, @@ -107,7 +107,7 @@ export const RGIPAdapterSettings = memo(({ id, ipAdapterId, ipAdapterNumber }: P - - + - Date: Wed, 7 Aug 2024 18:06:44 +1000 Subject: [PATCH 329/678] tidy(ui): clearer component names for regional guidance --- .../components/CanvasEntityList.tsx | 5 ++-- .../{RG.tsx => RegionalGuidance.tsx} | 24 +++++++++---------- ...nu.tsx => RegionalGuidanceActionsMenu.tsx} | 4 ++-- ...GBadges.tsx => RegionalGuidanceBadges.tsx} | 4 ++-- ...=> RegionalGuidanceDeletePromptButton.tsx} | 4 ++-- ...ist.tsx => RegionalGuidanceEntityList.tsx} | 8 +++---- ... => RegionalGuidanceIPAdapterSettings.tsx} | 4 ++-- ...ers.tsx => RegionalGuidanceIPAdapters.tsx} | 8 +++---- ...> RegionalGuidanceMaskFillColorPicker.tsx} | 4 ++-- ...tsx => RegionalGuidanceNegativePrompt.tsx} | 8 +++---- ...tsx => RegionalGuidancePositivePrompt.tsx} | 8 +++---- ...tings.tsx => RegionalGuidanceSettings.tsx} | 16 ++++++------- ...sx => RegionalGuidanceSettingsPopover.tsx} | 4 ++-- 13 files changed, 50 insertions(+), 51 deletions(-) rename invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/{RG.tsx => RegionalGuidance.tsx} (61%) rename invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/{RGActionsMenu.tsx => RegionalGuidanceActionsMenu.tsx} (95%) rename invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/{RGBadges.tsx => RegionalGuidanceBadges.tsx} (87%) rename invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/{RGDeletePromptButton.tsx => RegionalGuidanceDeletePromptButton.tsx} (75%) rename invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/{RGEntityList.tsx => RegionalGuidanceEntityList.tsx} (80%) rename invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/{RGIPAdapterSettings.tsx => RegionalGuidanceIPAdapterSettings.tsx} (96%) rename invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/{RGIPAdapters.tsx => RegionalGuidanceIPAdapters.tsx} (63%) rename invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/{RGMaskFillColorPicker.tsx => RegionalGuidanceMaskFillColorPicker.tsx} (92%) rename invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/{RGNegativePrompt.tsx => RegionalGuidanceNegativePrompt.tsx} (84%) rename invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/{RGPositivePrompt.tsx => RegionalGuidancePositivePrompt.tsx} (84%) rename invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/{RGSettings.tsx => RegionalGuidanceSettings.tsx} (63%) rename invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/{RGSettingsPopover.tsx => RegionalGuidanceSettingsPopover.tsx} (93%) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx index 710decfd0f5..9b3c9d3bc74 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx @@ -1,11 +1,10 @@ -/* eslint-disable i18next/no-literal-string */ import { Flex } from '@invoke-ai/ui-library'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import { ControlAdapterList } from 'features/controlLayers/components/ControlAdapter/ControlAdapterList'; import { InpaintMask } from 'features/controlLayers/components/InpaintMask/InpaintMask'; import { IPAdapterList } from 'features/controlLayers/components/IPAdapter/IPAdapterList'; import { LayerEntityList } from 'features/controlLayers/components/Layer/LayerEntityList'; -import { RGEntityList } from 'features/controlLayers/components/RegionalGuidance/RGEntityList'; +import { RegionalGuidanceEntityList } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList'; import { memo } from 'react'; export const CanvasEntityList = memo(() => { @@ -13,7 +12,7 @@ export const CanvasEntityList = memo(() => { - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx similarity index 61% rename from invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx index e197ac08370..68f0a9784b9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx @@ -4,21 +4,21 @@ import { CanvasEntityDeleteButton } from 'features/controlLayers/components/comm import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; -import { RGActionsMenu } from 'features/controlLayers/components/RegionalGuidance/RGActionsMenu'; -import { RGBadges } from 'features/controlLayers/components/RegionalGuidance/RGBadges'; -import { RGSettings } from 'features/controlLayers/components/RegionalGuidance/RGSettings'; +import { RegionalGuidanceActionsMenu } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceActionsMenu'; +import { RegionalGuidanceBadges } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges'; +import { RegionalGuidanceSettings } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; -import { RGMaskFillColorPicker } from './RGMaskFillColorPicker'; -import { RGSettingsPopover } from './RGSettingsPopover'; +import { RegionalGuidanceMaskFillColorPicker } from './RegionalGuidanceMaskFillColorPicker'; +import { RegionalGuidanceSettingsPopover } from './RegionalGuidanceSettingsPopover'; type Props = { id: string; }; -export const RG = memo(({ id }: Props) => { +export const RegionalGuidance = memo(({ id }: Props) => { const entityIdentifier = useMemo(() => ({ id, type: 'regional_guidance' }), [id]); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); return ( @@ -28,16 +28,16 @@ export const RG = memo(({ id }: Props) => { - - - - + + + + - {isOpen && } + {isOpen && } ); }); -RG.displayName = 'RG'; +RegionalGuidance.displayName = 'RegionalGuidance'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceActionsMenu.tsx similarity index 95% rename from invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceActionsMenu.tsx index 0f59f275df2..e8d9db88857 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceActionsMenu.tsx @@ -15,7 +15,7 @@ import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPlusBold } from 'react-icons/pi'; -export const RGActionsMenu = memo(() => { +export const RegionalGuidanceActionsMenu = memo(() => { const { id } = useEntityIdentifierContext(); const { t } = useTranslation(); const dispatch = useAppDispatch(); @@ -59,4 +59,4 @@ export const RGActionsMenu = memo(() => { ); }); -RGActionsMenu.displayName = 'RGActionsMenu'; +RegionalGuidanceActionsMenu.displayName = 'RegionalGuidanceActionsMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGBadges.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges.tsx similarity index 87% rename from invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGBadges.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges.tsx index 82eea7b0a07..541a2a25e33 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGBadges.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges.tsx @@ -5,7 +5,7 @@ import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -export const RGBadges = memo(() => { +export const RegionalGuidanceBadges = memo(() => { const { id } = useEntityIdentifierContext(); const { t } = useTranslation(); const autoNegative = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).autoNegative); @@ -21,4 +21,4 @@ export const RGBadges = memo(() => { ); }); -RGBadges.displayName = 'RGBadges'; +RegionalGuidanceBadges.displayName = 'RegionalGuidanceBadges'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGDeletePromptButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceDeletePromptButton.tsx similarity index 75% rename from invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGDeletePromptButton.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceDeletePromptButton.tsx index 4e994b43ec1..f44ede94d90 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGDeletePromptButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceDeletePromptButton.tsx @@ -7,7 +7,7 @@ type Props = { onDelete: () => void; }; -export const RGDeletePromptButton = memo(({ onDelete }: Props) => { +export const RegionalGuidanceDeletePromptButton = memo(({ onDelete }: Props) => { const { t } = useTranslation(); return ( @@ -21,4 +21,4 @@ export const RGDeletePromptButton = memo(({ onDelete }: Props) => { ); }); -RGDeletePromptButton.displayName = 'RGDeletePromptButton'; +RegionalGuidanceDeletePromptButton.displayName = 'RegionalGuidanceDeletePromptButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList.tsx similarity index 80% rename from invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGEntityList.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList.tsx index a00fea3e0a0..6a864d99cba 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList.tsx @@ -1,7 +1,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityGroupTitle } from 'features/controlLayers/components/common/CanvasEntityGroupTitle'; -import { RG } from 'features/controlLayers/components/RegionalGuidance/RG'; +import { RegionalGuidance } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidance'; import { mapId } from 'features/controlLayers/konva/util'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { memo } from 'react'; @@ -11,7 +11,7 @@ const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) = return canvasV2.regions.entities.map(mapId).reverse(); }); -export const RGEntityList = memo(() => { +export const RegionalGuidanceEntityList = memo(() => { const { t } = useTranslation(); const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'regional_guidance')); const rgIds = useAppSelector(selectEntityIds); @@ -28,11 +28,11 @@ export const RGEntityList = memo(() => { isSelected={isSelected} /> {rgIds.map((id) => ( - + ))} ); } }); -RGEntityList.displayName = 'RGEntityList'; +RegionalGuidanceEntityList.displayName = 'RegionalGuidanceEntityList'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapterSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx similarity index 96% rename from invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapterSettings.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx index 325fb48266c..ed8f4b58047 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapterSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx @@ -28,7 +28,7 @@ type Props = { ipAdapterNumber: number; }; -export const RGIPAdapterSettings = memo(({ id, ipAdapterId, ipAdapterNumber }: Props) => { +export const RegionalGuidanceIPAdapterSettings = memo(({ id, ipAdapterId, ipAdapterNumber }: Props) => { const dispatch = useAppDispatch(); const onDeleteIPAdapter = useCallback(() => { dispatch(rgIPAdapterDeleted({ id, ipAdapterId })); @@ -136,4 +136,4 @@ export const RGIPAdapterSettings = memo(({ id, ipAdapterId, ipAdapterNumber }: P ); }); -RGIPAdapterSettings.displayName = 'RGIPAdapterSettings'; +RegionalGuidanceIPAdapterSettings.displayName = 'RegionalGuidanceIPAdapterSettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapters.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapters.tsx similarity index 63% rename from invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapters.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapters.tsx index a6df88f1bc7..ef26749ff1c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapters.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapters.tsx @@ -1,6 +1,6 @@ import { Divider, Flex } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; -import { RGIPAdapterSettings } from 'features/controlLayers/components/RegionalGuidance/RGIPAdapterSettings'; +import { RegionalGuidanceIPAdapterSettings } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings'; import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import { memo } from 'react'; @@ -8,7 +8,7 @@ type Props = { id: string; }; -export const RGIPAdapters = memo(({ id }: Props) => { +export const RegionalGuidanceIPAdapters = memo(({ id }: Props) => { const ipAdapterIds = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).ipAdapters.map(({ id }) => id)); if (ipAdapterIds.length === 0) { @@ -24,11 +24,11 @@ export const RGIPAdapters = memo(({ id }: Props) => { )} - + ))} ); }); -RGIPAdapters.displayName = 'RGLayerIPAdapterList'; +RegionalGuidanceIPAdapters.displayName = 'RegionalGuidanceLayerIPAdapterList'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx similarity index 92% rename from invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx index 2f1489cfb09..da26f8940c6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx @@ -10,7 +10,7 @@ import { memo, useCallback } from 'react'; import type { RgbColor } from 'react-colorful'; import { useTranslation } from 'react-i18next'; -export const RGMaskFillColorPicker = memo(() => { +export const RegionalGuidanceMaskFillColorPicker = memo(() => { const entityIdentifier = useEntityIdentifierContext(); const { t } = useTranslation(); const dispatch = useAppDispatch(); @@ -46,4 +46,4 @@ export const RGMaskFillColorPicker = memo(() => { ); }); -RGMaskFillColorPicker.displayName = 'RGMaskFillColorPicker'; +RegionalGuidanceMaskFillColorPicker.displayName = 'RegionalGuidanceMaskFillColorPicker'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGNegativePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceNegativePrompt.tsx similarity index 84% rename from invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGNegativePrompt.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceNegativePrompt.tsx index db879be4f04..b35f2ece443 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGNegativePrompt.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceNegativePrompt.tsx @@ -1,6 +1,6 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { RGDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RGDeletePromptButton'; +import { RegionalGuidanceDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceDeletePromptButton'; import { rgNegativePromptChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; @@ -14,7 +14,7 @@ type Props = { id: string; }; -export const RGNegativePrompt = memo(({ id }: Props) => { +export const RegionalGuidanceNegativePrompt = memo(({ id }: Props) => { const prompt = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).negativePrompt ?? ''); const dispatch = useAppDispatch(); const textareaRef = useRef(null); @@ -50,7 +50,7 @@ export const RGNegativePrompt = memo(({ id }: Props) => { fontSize="sm" /> - + @@ -58,4 +58,4 @@ export const RGNegativePrompt = memo(({ id }: Props) => { ); }); -RGNegativePrompt.displayName = 'RGNegativePrompt'; +RegionalGuidanceNegativePrompt.displayName = 'RegionalGuidanceNegativePrompt'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGPositivePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidancePositivePrompt.tsx similarity index 84% rename from invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGPositivePrompt.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidancePositivePrompt.tsx index 49bca080da5..cd04eee31db 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGPositivePrompt.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidancePositivePrompt.tsx @@ -1,6 +1,6 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { RGDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RGDeletePromptButton'; +import { RegionalGuidanceDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceDeletePromptButton'; import { rgPositivePromptChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; @@ -14,7 +14,7 @@ type Props = { id: string; }; -export const RGPositivePrompt = memo(({ id }: Props) => { +export const RegionalGuidancePositivePrompt = memo(({ id }: Props) => { const prompt = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).positivePrompt ?? ''); const dispatch = useAppDispatch(); const textareaRef = useRef(null); @@ -50,7 +50,7 @@ export const RGPositivePrompt = memo(({ id }: Props) => { minH={28} /> - + @@ -58,4 +58,4 @@ export const RGPositivePrompt = memo(({ id }: Props) => { ); }); -RGPositivePrompt.displayName = 'RGPositivePrompt'; +RegionalGuidancePositivePrompt.displayName = 'RegionalGuidancePositivePrompt'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx similarity index 63% rename from invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettings.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx index c13081d6442..f7f96fd79ff 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx @@ -5,11 +5,11 @@ import { useEntityIdentifierContext } from 'features/controlLayers/contexts/Enti import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import { memo } from 'react'; -import { RGIPAdapters } from './RGIPAdapters'; -import { RGNegativePrompt } from './RGNegativePrompt'; -import { RGPositivePrompt } from './RGPositivePrompt'; +import { RegionalGuidanceIPAdapters } from './RegionalGuidanceIPAdapters'; +import { RegionalGuidanceNegativePrompt } from './RegionalGuidanceNegativePrompt'; +import { RegionalGuidancePositivePrompt } from './RegionalGuidancePositivePrompt'; -export const RGSettings = memo(() => { +export const RegionalGuidanceSettings = memo(() => { const { id } = useEntityIdentifierContext(); const hasPositivePrompt = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).positivePrompt !== null); const hasNegativePrompt = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).negativePrompt !== null); @@ -18,11 +18,11 @@ export const RGSettings = memo(() => { return ( {!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && } - {hasPositivePrompt && } - {hasNegativePrompt && } - {hasIPAdapters && } + {hasPositivePrompt && } + {hasNegativePrompt && } + {hasIPAdapters && } ); }); -RGSettings.displayName = 'RGSettings'; +RegionalGuidanceSettings.displayName = 'RegionalGuidanceSettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettingsPopover.tsx similarity index 93% rename from invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettingsPopover.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettingsPopover.tsx index b82469fafc2..0565026cfd1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettingsPopover.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettingsPopover.tsx @@ -20,7 +20,7 @@ import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiGearSixBold } from 'react-icons/pi'; -export const RGSettingsPopover = memo(() => { +export const RegionalGuidanceSettingsPopover = memo(() => { const entityIdentifier = useEntityIdentifierContext(); const { t } = useTranslation(); const dispatch = useAppDispatch(); @@ -60,4 +60,4 @@ export const RGSettingsPopover = memo(() => { ); }); -RGSettingsPopover.displayName = 'RGSettingsPopover'; +RegionalGuidanceSettingsPopover.displayName = 'RegionalGuidanceSettingsPopover'; From 657d59268c40801244e8c6cd8d7ec1f21c7fc92e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Aug 2024 18:17:52 +1000 Subject: [PATCH 330/678] fix(ui): properly destroy entities in manager cleanup --- .../src/features/controlLayers/konva/CanvasManager.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index c0c578eeef0..2d91a785773 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -605,6 +605,16 @@ export class CanvasManager { return () => { this.log.debug('Cleaning up konva renderer'); + this.inpaintMask.destroy(); + for (const region of this.regions.values()) { + region.destroy(); + } + for (const layer of this.layers.values()) { + layer.destroy(); + } + for (const controlAdapter of this.controlAdapters.values()) { + controlAdapter.destroy(); + } unsubscribeRenderer(); unsubscribeListeners(); unsubscribeShouldShowStagedImage(); From 318f2ee0039db72894e95d01a518200b9b1bb58d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Aug 2024 18:18:05 +1000 Subject: [PATCH 331/678] fix(ui): do not await commitBuffer --- .../features/controlLayers/konva/events.ts | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 2ce607860e6..ccf866fcc81 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -185,7 +185,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (e.evt.shiftKey && lastLinePoint) { // Create a straight line from the last line point if (selectedEntity.adapter.renderer.buffer) { - await selectedEntity.adapter.renderer.commitBuffer(); + selectedEntity.adapter.renderer.commitBuffer(); } await selectedEntity.adapter.renderer.setBuffer({ @@ -204,7 +204,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { }); } else { if (selectedEntity.adapter.renderer.buffer) { - await selectedEntity.adapter.renderer.commitBuffer(); + selectedEntity.adapter.renderer.commitBuffer(); } await selectedEntity.adapter.renderer.setBuffer({ id: getObjectId('brush_line', true), @@ -224,7 +224,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (e.evt.shiftKey && lastLinePoint) { // Create a straight line from the last line point if (selectedEntity.adapter.renderer.buffer) { - await selectedEntity.adapter.renderer.commitBuffer(); + selectedEntity.adapter.renderer.commitBuffer(); } await selectedEntity.adapter.renderer.setBuffer({ id: getObjectId('eraser_line', true), @@ -241,7 +241,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { }); } else { if (selectedEntity.adapter.renderer.buffer) { - await selectedEntity.adapter.renderer.commitBuffer(); + selectedEntity.adapter.renderer.commitBuffer(); } await selectedEntity.adapter.renderer.setBuffer({ id: getObjectId('eraser_line', true), @@ -256,7 +256,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (toolState.selected === 'rect') { if (selectedEntity.adapter.renderer.buffer) { - await selectedEntity.adapter.renderer.commitBuffer(); + selectedEntity.adapter.renderer.commitBuffer(); } await selectedEntity.adapter.renderer.setBuffer({ id: getObjectId('rect', true), @@ -281,7 +281,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (toolState.selected === 'brush') { const drawingBuffer = selectedEntity.adapter.renderer.buffer; if (drawingBuffer?.type === 'brush_line') { - await selectedEntity.adapter.renderer.commitBuffer(); + selectedEntity.adapter.renderer.commitBuffer(); } else { await selectedEntity.adapter.renderer.clearBuffer(); } @@ -290,7 +290,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (toolState.selected === 'eraser') { const drawingBuffer = selectedEntity.adapter.renderer.buffer; if (drawingBuffer?.type === 'eraser_line') { - await selectedEntity.adapter.renderer.commitBuffer(); + selectedEntity.adapter.renderer.commitBuffer(); } else { await selectedEntity.adapter.renderer.clearBuffer(); } @@ -299,7 +299,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (toolState.selected === 'rect') { const drawingBuffer = selectedEntity.adapter.renderer.buffer; if (drawingBuffer?.type === 'rect') { - await selectedEntity.adapter.renderer.commitBuffer(); + selectedEntity.adapter.renderer.commitBuffer(); } else { await selectedEntity.adapter.renderer.clearBuffer(); } @@ -347,7 +347,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { } } else { if (selectedEntity.adapter.renderer.buffer) { - await selectedEntity.adapter.renderer.commitBuffer(); + selectedEntity.adapter.renderer.commitBuffer(); } const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); @@ -384,7 +384,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { } } else { if (selectedEntity.adapter.renderer.buffer) { - await selectedEntity.adapter.renderer.commitBuffer(); + selectedEntity.adapter.renderer.commitBuffer(); } const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); @@ -437,17 +437,17 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); - await selectedEntity.adapter.renderer.commitBuffer(); + selectedEntity.adapter.renderer.commitBuffer(); } else if (toolState.selected === 'eraser' && drawingBuffer?.type === 'eraser_line') { const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); - await selectedEntity.adapter.renderer.commitBuffer(); + selectedEntity.adapter.renderer.commitBuffer(); } else if (toolState.selected === 'rect' && drawingBuffer?.type === 'rect') { drawingBuffer.rect.width = Math.round(normalizedPoint.x - drawingBuffer.rect.x); drawingBuffer.rect.height = Math.round(normalizedPoint.y - drawingBuffer.rect.y); await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); - await selectedEntity.adapter.renderer.commitBuffer(); + selectedEntity.adapter.renderer.commitBuffer(); } } From d2d298604c725bf5bff48f2310303d975d0b4a7f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Aug 2024 18:37:43 +1000 Subject: [PATCH 332/678] feat(ui): dnd image into layer --- .../listeners/imageDropped.ts | 10 ++--- .../components/CanvasDropArea.tsx | 19 +++++++++ .../components/ControlLayersEditor.tsx | 2 + .../controlLayers/components/Layer/Layer.tsx | 4 +- .../controlLayers/konva/CanvasManager.ts | 12 +++--- .../controlLayers/store/canvasV2Slice.ts | 2 +- .../controlLayers/store/layersReducers.ts | 42 ++++++++++--------- .../web/src/features/dnd/types/index.ts | 9 ++-- .../web/src/features/dnd/util/isValidDrop.ts | 2 +- .../ui/components/tabs/TextToImageTab.tsx | 8 +++- 10 files changed, 68 insertions(+), 42 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index 8368e42ec33..c551fa88b7d 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -5,9 +5,10 @@ import { parseify } from 'common/util/serialize'; import { caImageChanged, ipaImageChanged, - layerImageAdded, + layerAddedFromImage, rgIPAdapterImageChanged, } from 'features/controlLayers/store/canvasV2Slice'; +import { imageDTOToImageObject } from 'features/controlLayers/store/types'; import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import { isValidDrop } from 'features/dnd/util/isValidDrop'; import { @@ -28,7 +29,7 @@ export const dndDropped = createAction<{ export const addImageDroppedListener = (startAppListening: AppStartListening) => { startAppListening({ actionCreator: dndDropped, - effect: async (action, { dispatch, getState }) => { + effect: (action, { dispatch, getState }) => { const log = logger('dnd'); const { activeData, overData } = action.payload; if (!isValidDrop(overData, activeData)) { @@ -101,12 +102,11 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => * Image dropped on Raster layer */ if ( - overData.actionType === 'ADD_LAYER_IMAGE' && + overData.actionType === 'ADD_LAYER_FROM_IMAGE' && activeData.payloadType === 'IMAGE_DTO' && activeData.payload.imageDTO ) { - const { id } = overData.context; - dispatch(layerImageAdded({ id, imageDTO: activeData.payload.imageDTO })); + dispatch(layerAddedFromImage({ imageObject: imageDTOToImageObject(activeData.payload.imageDTO) })); return; } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx new file mode 100644 index 00000000000..b5a1c636a56 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx @@ -0,0 +1,19 @@ +import { Flex } from '@invoke-ai/ui-library'; +import IAIDroppable from 'common/components/IAIDroppable'; +import type { AddLayerFromImageDropData } from 'features/dnd/types'; +import { memo } from 'react'; + +const addLayerFromImageDropData: AddLayerFromImageDropData = { + id: 'add-layer-from-image-drop-data', + actionType: 'ADD_LAYER_FROM_IMAGE', +}; + +export const CanvasDropArea = memo(() => { + return ( + + + + ); +}); + +CanvasDropArea.displayName = 'CanvasDropArea'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx index a2a4bf9126e..71e35263b5e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx @@ -1,5 +1,6 @@ /* eslint-disable i18next/no-literal-string */ import { Flex } from '@invoke-ai/ui-library'; +import { CanvasDropArea } from 'features/controlLayers/components/CanvasDropArea'; import { ControlLayersToolbar } from 'features/controlLayers/components/ControlLayersToolbar'; import { StageComponent } from 'features/controlLayers/components/StageComponent'; import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar'; @@ -24,6 +25,7 @@ export const ControlLayersEditor = memo(() => { {/* */} + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx index ffc6ce9b744..d8f73cbaaa0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx @@ -9,7 +9,7 @@ import { LayerActionsMenu } from 'features/controlLayers/components/Layer/LayerA import { LayerSettings } from 'features/controlLayers/components/Layer/LayerSettings'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; -import type { LayerImageDropData } from 'features/dnd/types'; +import type { AddLayerFromImageDropData } from 'features/dnd/types'; import { memo, useMemo } from 'react'; import { LayerOpacity } from './LayerOpacity'; @@ -21,7 +21,7 @@ type Props = { export const Layer = memo(({ id }: Props) => { const entityIdentifier = useMemo(() => ({ id, type: 'layer' }), [id]); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: false }); - const droppableData = useMemo( + const droppableData = useMemo( () => ({ id, actionType: 'ADD_LAYER_IMAGE', context: { id } }), [id] ); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 2d91a785773..722c6ce4701 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -97,12 +97,12 @@ type EntityStateAndAdapter = state: CanvasInpaintMaskState; adapter: CanvasMaskAdapter; } - | { - id: string; - type: CanvasControlAdapterState['type']; - state: CanvasControlAdapterState; - adapter: CanvasControlAdapter; - } + // | { + // id: string; + // type: CanvasControlAdapterState['type']; + // state: CanvasControlAdapterState; + // adapter: CanvasControlAdapter; + // } | { id: string; type: CanvasRegionalGuidanceState['type']; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 4159d9f7daa..ca37dc9517f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -411,9 +411,9 @@ export const { bboxSizeOptimized, // layers layerAdded, + layerAddedFromImage, layerRecalled, layerOpacityChanged, - layerImageAdded, layerAllDeleted, layerImageCacheChanged, // IP Adapters diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index 78be3cf6f13..48e01cd1f3e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -4,8 +4,8 @@ import { merge } from 'lodash-es'; import type { ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; -import type { CanvasLayerState, CanvasV2State, ImageObjectAddedArg } from './types'; -import { imageDTOToImageObject, imageDTOToImageWithDims } from './types'; +import type { CanvasImageState, CanvasLayerState, CanvasV2State } from './types'; +import { imageDTOToImageWithDims } from './types'; export const selectLayer = (state: CanvasV2State, id: string) => state.layers.entities.find((layer) => layer.id === id); export const selectLayerOrThrow = (state: CanvasV2State, id: string) => { @@ -25,6 +25,7 @@ export const layersReducers = { objects: [], opacity: 1, position: { x: 0, y: 0 }, + imageCache: null, }; merge(layer, action.payload.overrides); state.layers.entities.push(layer); @@ -41,6 +42,26 @@ export const layersReducers = { state.selectedEntityIdentifier = { type: 'layer', id: data.id }; state.layers.imageCache = null; }, + layerAddedFromImage: { + reducer: (state, action: PayloadAction<{ id: string; imageObject: CanvasImageState }>) => { + const { id, imageObject } = action.payload; + const layer: CanvasLayerState = { + id, + type: 'layer', + isEnabled: true, + objects: [imageObject], + opacity: 1, + position: { x: 0, y: 0 }, + imageCache: null, + }; + state.layers.entities.push(layer); + state.selectedEntityIdentifier = { type: 'layer', id }; + state.layers.imageCache = null; + }, + prepare: (payload: { imageObject: CanvasImageState }) => ({ + payload: { ...payload, id: getPrefixedId('layer') }, + }), + }, layerAllDeleted: (state) => { state.layers.entities = []; state.layers.imageCache = null; @@ -54,23 +75,6 @@ export const layersReducers = { layer.opacity = opacity; state.layers.imageCache = null; }, - layerImageAdded: ( - state, - action: PayloadAction - ) => { - const { id, imageDTO, pos } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - const imageObject = imageDTOToImageObject(imageDTO); - if (pos) { - imageObject.x = pos.x; - imageObject.y = pos.y; - } - layer.objects.push(imageObject); - state.layers.imageCache = null; - }, layerImageCacheChanged: (state, action: PayloadAction<{ imageDTO: ImageDTO | null }>) => { const { imageDTO } = action.payload; state.layers.imageCache = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; diff --git a/invokeai/frontend/web/src/features/dnd/types/index.ts b/invokeai/frontend/web/src/features/dnd/types/index.ts index 19dcbf19c0b..43e2ca36f22 100644 --- a/invokeai/frontend/web/src/features/dnd/types/index.ts +++ b/invokeai/frontend/web/src/features/dnd/types/index.ts @@ -44,11 +44,8 @@ export type RGIPAdapterImageDropData = BaseDropData & { }; }; -export type LayerImageDropData = BaseDropData & { - actionType: 'ADD_LAYER_IMAGE'; - context: { - id: string; - }; +export type AddLayerFromImageDropData = BaseDropData & { + actionType: 'ADD_LAYER_FROM_IMAGE'; }; type UpscaleInitialImageDropData = BaseDropData & { @@ -94,7 +91,7 @@ export type TypesafeDroppableData = | RGIPAdapterImageDropData | SelectForCompareDropData | UpscaleInitialImageDropData - | LayerImageDropData; + | AddLayerFromImageDropData; type BaseDragData = { id: string; diff --git a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts index 128e5c5d501..0de8d48a94f 100644 --- a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts +++ b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts @@ -21,7 +21,7 @@ export const isValidDrop = (overData?: TypesafeDroppableData | null, activeData? return payloadType === 'IMAGE_DTO'; case 'SET_RG_IP_ADAPTER_IMAGE': return payloadType === 'IMAGE_DTO'; - case 'ADD_LAYER_IMAGE': + case 'ADD_LAYER_FROM_IMAGE': return payloadType === 'IMAGE_DTO'; case 'SET_UPSCALE_INITIAL_IMAGE': return payloadType === 'IMAGE_DTO'; diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx index 5be01950465..736c5559e03 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx @@ -10,8 +10,12 @@ const TextToImageTab = () => { return ( - {imageViewer.isOpen && } - + {imageViewer.isOpen && ( + <> + + + + )} ); }; From a3a933a797ad31b75c8001c9ed708a64a8535ed4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Aug 2024 18:48:21 +1000 Subject: [PATCH 333/678] fix(ui): do not await clearBuffer --- .../web/src/features/controlLayers/konva/events.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index ccf866fcc81..3fd6594ddf4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -270,7 +270,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { }); //#region mouseup - stage.on('mouseup', async () => { + stage.on('mouseup', () => { $isMouseDown.set(false); const pos = $lastCursorPos.get(); const selectedEntity = getSelectedEntity(); @@ -283,7 +283,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (drawingBuffer?.type === 'brush_line') { selectedEntity.adapter.renderer.commitBuffer(); } else { - await selectedEntity.adapter.renderer.clearBuffer(); + selectedEntity.adapter.renderer.clearBuffer(); } } @@ -292,7 +292,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (drawingBuffer?.type === 'eraser_line') { selectedEntity.adapter.renderer.commitBuffer(); } else { - await selectedEntity.adapter.renderer.clearBuffer(); + selectedEntity.adapter.renderer.clearBuffer(); } } @@ -301,7 +301,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (drawingBuffer?.type === 'rect') { selectedEntity.adapter.renderer.commitBuffer(); } else { - await selectedEntity.adapter.renderer.clearBuffer(); + selectedEntity.adapter.renderer.clearBuffer(); } } @@ -343,7 +343,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { } } } else { - await selectedEntity.adapter.renderer.clearBuffer(); + selectedEntity.adapter.renderer.clearBuffer(); } } else { if (selectedEntity.adapter.renderer.buffer) { @@ -380,7 +380,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { } } } else { - await selectedEntity.adapter.renderer.clearBuffer(); + selectedEntity.adapter.renderer.clearBuffer(); } } else { if (selectedEntity.adapter.renderer.buffer) { @@ -408,7 +408,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { drawingBuffer.rect.height = Math.round(normalizedPoint.y - drawingBuffer.rect.y); await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); } else { - await selectedEntity.adapter.renderer.clearBuffer(); + selectedEntity.adapter.renderer.clearBuffer(); } } } From 260ef8edd50d265225912ab1351e2ffc1316a117 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Aug 2024 22:50:35 +1000 Subject: [PATCH 334/678] feat(ui): add manual scale controls --- invokeai/frontend/web/public/locales/en.json | 2 + .../controlLayers/components/BrushWidth.tsx | 2 +- .../controlLayers/components/CanvasScale.tsx | 114 ++++++++++++++++++ .../components/ControlLayersToolbar.tsx | 2 + .../controlLayers/components/EraserWidth.tsx | 2 +- .../controlLayers/konva/CanvasManager.ts | 78 ++++++++++++ .../features/controlLayers/konva/events.ts | 27 +---- 7 files changed, 201 insertions(+), 26 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasScale.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 7cf98dac0ad..46fa92ae106 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1657,6 +1657,8 @@ "moveForward": "Move Forward", "moveBackward": "Move Backward", "brushSize": "Brush Size", + "width": "Width", + "zoom": "Zoom", "controlLayers": "Control Layers", "globalMaskOpacity": "Global Mask Opacity", "autoNegative": "Auto Negative", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/BrushWidth.tsx b/invokeai/frontend/web/src/features/controlLayers/components/BrushWidth.tsx index fdb7b33d84e..fa72e542596 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/BrushWidth.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/BrushWidth.tsx @@ -29,7 +29,7 @@ export const BrushWidth = memo(() => { ); return ( - {t('controlLayers.brushWidth')} + {t('controlLayers.width')} (isNaN(Number(v)) ? '' : `${round(Number(v), 2).toLocaleString()}%`); + +export const CanvasScale = memo(() => { + const { t } = useTranslation(); + const canvasManager = useStore($canvasManager); + const stageAttrs = useStore($stageAttrs); + const [localScale, setLocalScale] = useState(stageAttrs.scale * 100); + + const onChange = useCallback( + (scale: number) => { + if (!canvasManager) { + return; + } + canvasManager.setStageScale(scale / 100); + }, + [canvasManager] + ); + + const onReset = useCallback(() => { + if (!canvasManager) { + return; + } + + canvasManager.setStageScale(1); + }, [canvasManager]); + + const onBlur = useCallback(() => { + if (!canvasManager) { + return; + } + if (isNaN(Number(localScale))) { + return; + } + canvasManager.setStageScale(clamp(localScale / 100, MIN_CANVAS_SCALE, MAX_CANVAS_SCALE)); + }, [canvasManager, localScale]); + + const onChangeNumberInput = useCallback((valueAsString: string, valueAsNumber: number) => { + setLocalScale(valueAsNumber); + }, []); + + const onKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Enter') { + onBlur(); + } + }, + [onBlur] + ); + + useEffect(() => { + setLocalScale(stageAttrs.scale * 100); + }, [stageAttrs.scale]); + + return ( + + {t('controlLayers.zoom')} + + + + + + + + + + + + + + } variant="link" /> + + ); +}); + +CanvasScale.displayName = 'CanvasScale'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index 1b8d52585ee..e8d31eecf95 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -4,6 +4,7 @@ import { Flex, Switch } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; import { BrushWidth } from 'features/controlLayers/components/BrushWidth'; +import { CanvasScale } from 'features/controlLayers/components/CanvasScale'; import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover'; import { EraserWidth } from 'features/controlLayers/components/EraserWidth'; import { FillColorPicker } from 'features/controlLayers/components/FillColorPicker'; @@ -52,6 +53,7 @@ export const ControlLayersToolbar = memo(() => { {tool === 'brush' && } {tool === 'eraser' && } + debug diff --git a/invokeai/frontend/web/src/features/controlLayers/components/EraserWidth.tsx b/invokeai/frontend/web/src/features/controlLayers/components/EraserWidth.tsx index 0903763f2d2..deff803bb1c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/EraserWidth.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/EraserWidth.tsx @@ -29,7 +29,7 @@ export const EraserWidth = memo(() => { ); return ( - {t('controlLayers.eraserWidth')} + {t('controlLayers.width')} void) => { // We need the absolute cursor position - not the scaled position const cursorPos = stage.getPointerPosition(); if (cursorPos) { - // Stage's x and y scale are always the same - const stageScale = stage.scaleX(); // When wheeling on trackpad, e.evt.ctrlKey is true - in that case, let's reverse the direction const delta = e.evt.ctrlKey ? -e.evt.deltaY : e.evt.deltaY; - const mousePointTo = { - x: (cursorPos.x - stage.x()) / stageScale, - y: (cursorPos.y - stage.y()) / stageScale, - }; - const newScale = clamp(stageScale * CANVAS_SCALE_BY ** delta, MIN_CANVAS_SCALE, MAX_CANVAS_SCALE); - const newPos = { - x: cursorPos.x - mousePointTo.x * newScale, - y: cursorPos.y - mousePointTo.y * newScale, - }; - - stage.scaleX(newScale); - stage.scaleY(newScale); - stage.position(newPos); - $stageAttrs.set({ - x: newPos.x, - y: newPos.y, - width: stage.width(), - height: stage.height(), - scale: newScale, - }); - manager.background.render(); + const scale = manager.getStageScale() * CANVAS_SCALE_BY ** delta; + manager.setStageScale(scale, cursorPos); } } manager.preview.tool.render(); From 318086571d23d2f3a3a9ec6919762f21ff9d4bb0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Aug 2024 23:00:30 +1000 Subject: [PATCH 335/678] fix(ui): img2img --- .../nodes/util/graph/generation/addImageToImage.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts index 730077b304b..a1e991bc176 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts @@ -1,7 +1,7 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasV2State, Dimensions } from 'features/controlLayers/store/types'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; -import { isEqual, pick } from 'lodash-es'; +import { isEqual } from 'lodash-es'; import type { Invocation } from 'services/api/types'; export const addImageToImage = async ( @@ -17,15 +17,14 @@ export const addImageToImage = async ( ): Promise> => { denoise.denoising_start = denoising_start; - const cropBbox = pick(bbox.rect, ['x', 'y', 'width', 'height']); - const initialImage = await manager.getInitialImage({ bbox: cropBbox }); + const { image_name } = await manager.getCompositeLayerImageDTO(bbox.rect); if (!isEqual(scaledSize, originalSize)) { // Resize the initial image to the scaled size, denoise, then resize back to the original size const resizeImageToScaledSize = g.addNode({ id: 'initial_image_resize_in', type: 'img_resize', - image: { image_name: initialImage.image_name }, + image: { image_name }, ...scaledSize, }); const i2l = g.addNode({ id: 'i2l', type: 'i2l' }); @@ -44,7 +43,7 @@ export const addImageToImage = async ( return resizeImageToOriginalSize; } else { // No need to resize, just denoise - const i2l = g.addNode({ id: 'i2l', type: 'i2l', image: { image_name: initialImage.image_name } }); + const i2l = g.addNode({ id: 'i2l', type: 'i2l', image: { image_name } }); g.addEdge(vaeSource, 'vae', i2l, 'vae'); g.addEdge(i2l, 'latents', denoise, 'latents'); return l2i; From c22afa57253960feb95812613fca45ff5168ccd2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 8 Aug 2024 12:47:09 +1000 Subject: [PATCH 336/678] feat(ui): better scale changer component, reset view functionality --- invokeai/frontend/web/public/locales/en.json | 1 + .../components/CanvasResetViewButton.tsx | 49 +++++++ .../controlLayers/components/CanvasScale.tsx | 131 +++++++++++++----- .../components/ControlLayersToolbar.tsx | 2 + .../controlLayers/konva/CanvasBackground.ts | 17 +++ .../controlLayers/konva/CanvasManager.ts | 58 ++++++-- .../controlLayers/konva/CanvasPreview.ts | 5 + .../controlLayers/konva/CanvasTool.ts | 24 ++++ .../features/controlLayers/konva/events.ts | 8 -- .../src/features/controlLayers/konva/util.ts | 14 ++ 10 files changed, 252 insertions(+), 57 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasResetViewButton.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 46fa92ae106..fe8df2d409a 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1659,6 +1659,7 @@ "brushSize": "Brush Size", "width": "Width", "zoom": "Zoom", + "resetView": "Reset View", "controlLayers": "Control Layers", "globalMaskOpacity": "Global Mask Opacity", "autoNegative": "Auto Negative", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasResetViewButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasResetViewButton.tsx new file mode 100644 index 00000000000..5df90004fe2 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasResetViewButton.tsx @@ -0,0 +1,49 @@ +import { $shift, IconButton } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { $canvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { memo, useCallback } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; +import { PiArrowCounterClockwiseBold } from 'react-icons/pi'; + +export const CanvasResetViewButton = memo(() => { + const { t } = useTranslation(); + const canvasManager = useStore($canvasManager); + + const resetZoom = useCallback(() => { + if (!canvasManager) { + return; + } + canvasManager.setStageScale(1); + }, [canvasManager]); + + const resetView = useCallback(() => { + if (!canvasManager) { + return; + } + canvasManager.resetView(); + }, [canvasManager]); + + const onReset = useCallback(() => { + if ($shift.get()) { + resetZoom(); + } else { + resetView(); + } + }, [resetView, resetZoom]); + + useHotkeys('r', resetView); + useHotkeys('shift+r', resetZoom); + + return ( + } + variant="link" + /> + ); +}); + +CanvasResetViewButton.displayName = 'CanvasResetViewButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasScale.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasScale.tsx index cfb7a9ff29d..43456a658f4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasScale.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasScale.tsx @@ -1,4 +1,5 @@ import { + $shift, CompositeSlider, FormControl, FormLabel, @@ -6,6 +7,7 @@ import { NumberInput, NumberInputField, Popover, + PopoverAnchor, PopoverArrow, PopoverBody, PopoverContent, @@ -14,14 +16,60 @@ import { import { useStore } from '@nanostores/react'; import { $canvasManager } from 'features/controlLayers/konva/CanvasManager'; import { MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/controlLayers/konva/constants'; +import { snapToNearest } from 'features/controlLayers/konva/util'; import { $stageAttrs } from 'features/controlLayers/store/canvasV2Slice'; import { clamp, round } from 'lodash-es'; import type { KeyboardEvent } from 'react'; import { memo, useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { PiArrowCounterClockwiseBold } from 'react-icons/pi'; +import { PiCaretDownBold } from 'react-icons/pi'; -const formatPct = (v: number | string) => (isNaN(Number(v)) ? '' : `${round(Number(v), 2).toLocaleString()}%`); +function formatPct(v: number | string) { + if (isNaN(Number(v))) { + return ''; + } + + return `${round(Number(v), 2).toLocaleString()}%`; +} + +function mapSliderValueToScale(value: number) { + if (value <= 40) { + // 0 to 40 -> 10% to 100% + return 10 + (90 * value) / 40; + } else if (value <= 70) { + // 40 to 70 -> 100% to 500% + return 100 + (400 * (value - 40)) / 30; + } else { + // 70 to 100 -> 500% to 2000% + return 500 + (1500 * (value - 70)) / 30; + } +} + +function mapScaleToSliderValue(scale: number) { + if (scale <= 100) { + return ((scale - 10) * 40) / 90; + } else if (scale <= 500) { + return 40 + ((scale - 100) * 30) / 400; + } else { + return 70 + ((scale - 500) * 30) / 1500; + } +} + +function formatSliderValue(value: number) { + return String(mapSliderValueToScale(value)); +} + +const marks = [ + mapScaleToSliderValue(10), + mapScaleToSliderValue(50), + mapScaleToSliderValue(100), + mapScaleToSliderValue(500), + mapScaleToSliderValue(2000), +]; + +const sliderDefaultValue = mapScaleToSliderValue(100); + +const snapCandidates = marks.slice(1, marks.length - 1); export const CanvasScale = memo(() => { const { t } = useTranslation(); @@ -29,29 +77,29 @@ export const CanvasScale = memo(() => { const stageAttrs = useStore($stageAttrs); const [localScale, setLocalScale] = useState(stageAttrs.scale * 100); - const onChange = useCallback( + const onChangeSlider = useCallback( (scale: number) => { if (!canvasManager) { return; } - canvasManager.setStageScale(scale / 100); + let snappedScale = scale; + // Do not snap if shift key is held + if (!$shift.get()) { + snappedScale = snapToNearest(scale, snapCandidates, 2); + } + const mappedScale = mapSliderValueToScale(snappedScale); + canvasManager.setStageScale(mappedScale / 100); }, [canvasManager] ); - const onReset = useCallback(() => { - if (!canvasManager) { - return; - } - - canvasManager.setStageScale(1); - }, [canvasManager]); - const onBlur = useCallback(() => { if (!canvasManager) { return; } if (isNaN(Number(localScale))) { + canvasManager.setStageScale(1); + setLocalScale(100); return; } canvasManager.setStageScale(clamp(localScale / 100, MIN_CANVAS_SCALE, MAX_CANVAS_SCALE)); @@ -75,39 +123,54 @@ export const CanvasScale = memo(() => { }, [stageAttrs.scale]); return ( - - {t('controlLayers.zoom')} - - + + + {t('controlLayers.zoom')} + - + + + } + size="sm" + variant="link" + position="absolute" + insetInlineEnd={0} + h="full" + /> + - - - - - - - - - } variant="link" /> - + + + + + + + + + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index e8d31eecf95..db3b42aaf95 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -4,6 +4,7 @@ import { Flex, Switch } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; import { BrushWidth } from 'features/controlLayers/components/BrushWidth'; +import { CanvasResetViewButton } from 'features/controlLayers/components/CanvasResetViewButton'; import { CanvasScale } from 'features/controlLayers/components/CanvasScale'; import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover'; import { EraserWidth } from 'features/controlLayers/components/EraserWidth'; @@ -54,6 +55,7 @@ export const ControlLayersToolbar = memo(() => { {tool === 'eraser' && } + debug diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts index c75e8703b43..1143b5f1303 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts @@ -14,9 +14,19 @@ export class CanvasBackground { layer: Konva.Layer; }; + /** + * A set of subscriptions that should be cleaned up when the transformer is destroyed. + */ + subscriptions: Set<() => void> = new Set(); + constructor(manager: CanvasManager) { this.manager = manager; this.konva = { layer: new Konva.Layer({ name: CanvasBackground.LAYER_NAME, listening: false }) }; + this.subscriptions.add( + this.manager.stateApi.$stageAttrs.listen(() => { + this.render(); + }) + ); } render() { @@ -94,6 +104,13 @@ export class CanvasBackground { } } + destroy = () => { + for (const cleanup of this.subscriptions) { + cleanup(); + } + this.konva.layer.destroy(); + }; + /** * Gets the grid spacing. The value depends on the stage scale - at higher scales, the grid spacing is smaller. * @param scale The stage scale diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index cc779cdee67..6a3a5b6348a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -156,6 +156,19 @@ export class CanvasManager { this.container = container; this._store = store; this.stateApi = new CanvasStateApi(this._store, this); + + this.transformingEntity = new PubSub(null); + this.toolState = new PubSub(this.stateApi.getToolState()); + this.currentFill = new PubSub(this.getCurrentFill()); + this.selectedEntityIdentifier = new PubSub( + this.stateApi.getState().selectedEntityIdentifier, + (a, b) => a?.id === b?.id + ); + this.selectedEntity = new PubSub( + this.getSelectedEntity(), + (a, b) => a?.state === b?.state && a?.adapter === b?.adapter + ); + this._prevState = this.stateApi.getState(); this.log = logger('canvas').child((message) => { @@ -213,18 +226,6 @@ export class CanvasManager { this.log.error('Worker message error'); }; - this.transformingEntity = new PubSub(null); - this.toolState = new PubSub(this.stateApi.getToolState()); - this.currentFill = new PubSub(this.getCurrentFill()); - this.selectedEntityIdentifier = new PubSub( - this.stateApi.getState().selectedEntityIdentifier, - (a, b) => a?.id === b?.id - ); - this.selectedEntity = new PubSub( - this.getSelectedEntity(), - (a, b) => a?.state === b?.state && a?.adapter === b?.adapter - ); - this.inpaintMask = new CanvasMaskAdapter(this.stateApi.getInpaintMaskState(), this); this.stage.add(this.inpaintMask.konva.layer); } @@ -303,7 +304,34 @@ export class CanvasManager { height: this.stage.height(), scale: this.stage.scaleX(), }); - this.background.render(); + } + + resetView() { + const { width, height } = this.getStageSize(); + const { rect } = this.stateApi.getBbox(); + + const padding = 20; // Padding in absolute pixels + + const availableWidth = width - padding * 2; + const availableHeight = height - padding * 2; + + const scale = Math.min(availableWidth / rect.width, availableHeight / rect.height); + const x = -rect.x * scale + padding + (availableWidth - rect.width * scale) / 2; + const y = -rect.y * scale + padding + (availableHeight - rect.height * scale) / 2; + + this.stage.setAttrs({ + x, + y, + scaleX: scale, + scaleY: scale, + }); + + this.stateApi.$stageAttrs.set({ + ...this.stateApi.$stageAttrs.get(), + x, + y, + scale, + }); } getEntity(identifier: CanvasEntityIdentifier): EntityStateAndAdapter | null { @@ -618,6 +646,8 @@ export class CanvasManager { for (const controlAdapter of this.controlAdapters.values()) { controlAdapter.destroy(); } + this.background.destroy(); + this.preview.destroy(); unsubscribeRenderer(); unsubscribeListeners(); unsubscribeShouldShowStagedImage(); @@ -678,8 +708,6 @@ export class CanvasManager { height: this.stage.height(), scale: this.stage.scaleX(), }); - this.background.render(); - this.preview.tool.render(); } /** diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts index 882a01e7afb..ef0f6a579b8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts @@ -32,4 +32,9 @@ export class CanvasPreview { this.progressPreview = progressPreview; this.layer.add(this.progressPreview.konva.group); } + + destroy() { + this.tool.destroy(); + this.layer.destroy(); + } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts index 7a20d3579e5..11aeabac4dc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts @@ -42,6 +42,11 @@ export class CanvasTool { }; }; + /** + * A set of subscriptions that should be cleaned up when the transformer is destroyed. + */ + subscriptions: Set<() => void> = new Set(); + constructor(manager: CanvasManager) { this.manager = manager; this.konva = { @@ -102,8 +107,27 @@ export class CanvasTool { this.konva.eraser.group.add(this.konva.eraser.innerBorderCircle); this.konva.eraser.group.add(this.konva.eraser.outerBorderCircle); this.konva.group.add(this.konva.eraser.group); + + this.subscriptions.add( + this.manager.stateApi.$stageAttrs.listen(() => { + this.render(); + }) + ); + + this.subscriptions.add( + this.manager.toolState.subscribe(() => { + this.render(); + }) + ); } + destroy = () => { + for (const cleanup of this.subscriptions) { + cleanup(); + } + this.konva.group.destroy(); + }; + scaleTool = () => { const toolState = this.manager.stateApi.getToolState(); const scale = this.manager.stage.scaleX(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 054b433646b..55ebcbaab48 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -492,8 +492,6 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { height: stage.height(), scale: stage.scaleX(), }); - manager.background.render(); - manager.preview.tool.render(); }); //#region dragend @@ -531,13 +529,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { $spaceKey.set(true); $lastCursorPos.set(null); $lastMouseDownPos.set(null); - } else if (e.key === 'r') { - $lastCursorPos.set(null); - $lastMouseDownPos.set(null); - manager.background.render(); - // TODO(psyche): restore some kind of fit } - manager.preview.tool.render(); }; window.addEventListener('keydown', onKeyDown); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index f5c25c580e9..764d9759a9b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -374,3 +374,17 @@ export function getObjectId(type: CanvasObjectState['type'], isBuffer?: boolean) export const getEmptyRect = (): Rect => { return { x: 0, y: 0, width: 0, height: 0 }; }; +export function snapToNearest(value: number, candidateValues: number[], threshold: number): number { + let closest = value; + let minDiff = Number.MAX_VALUE; + + for (const candidate of candidateValues) { + const diff = Math.abs(value - candidate); + if (diff < minDiff && diff <= threshold) { + minDiff = diff; + closest = candidate; + } + } + + return closest; +} From fa3f109eb91a6efea3c77b8fad2ad5a015d05d91 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 8 Aug 2024 15:33:35 +1000 Subject: [PATCH 337/678] fix(ui): max scale 1 when reset view --- .../web/src/features/controlLayers/konva/CanvasManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 6a3a5b6348a..41d1423d86b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -315,7 +315,7 @@ export class CanvasManager { const availableWidth = width - padding * 2; const availableHeight = height - padding * 2; - const scale = Math.min(availableWidth / rect.width, availableHeight / rect.height); + const scale = Math.min(Math.min(availableWidth / rect.width, availableHeight / rect.height), 1); const x = -rect.x * scale + padding + (availableWidth - rect.width * scale) / 2; const y = -rect.y * scale + padding + (availableHeight - rect.height * scale) / 2; From aa73cbf459d2e50a2c3a459fdac70be91169096b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 8 Aug 2024 17:23:56 +1000 Subject: [PATCH 338/678] feat(nodes): temp disable canvas output crop --- invokeai/app/invocations/image.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index 340dc32f96e..fc446f860fe 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -1048,14 +1048,30 @@ def invoke(self, context: InvocationContext) -> CanvasV2MaskAndCropOutput: image = context.images.get_pil(self.image.image_name) mask = self._prepare_mask(context.images.get_pil(self.mask.image_name)) image.putalpha(mask) - bbox = image.getbbox() - image = image.crop(bbox) + # bbox = image.getbbox() + # image = image.crop(bbox) image_dto = context.images.save(image=image) return CanvasV2MaskAndCropOutput( image=ImageField(image_name=image_dto.image_name), - offset_x=bbox[0], - offset_y=bbox[1], + offset_x=0, + offset_y=0, width=image.width, height=image.height, ) + + # def invoke(self, context: InvocationContext) -> CanvasV2MaskAndCropOutput: + # image = context.images.get_pil(self.image.image_name) + # mask = self._prepare_mask(context.images.get_pil(self.mask.image_name)) + # image.putalpha(mask) + # bbox = image.getbbox() + # image = image.crop(bbox) + # image_dto = context.images.save(image=image) + + # return CanvasV2MaskAndCropOutput( + # image=ImageField(image_name=image_dto.image_name), + # offset_x=bbox[0], + # offset_y=bbox[1], + # width=image.width, + # height=image.height, + # ) From 3ce3056c4af80437c8ec97d3991f04ca29c0ac4e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 8 Aug 2024 17:24:16 +1000 Subject: [PATCH 339/678] fix(ui): staging area works --- .../listeners/imageDropped.ts | 11 ++- .../components/AddLayerButton.tsx | 2 +- .../controlLayers/konva/CanvasBbox.ts | 7 +- .../controlLayers/konva/CanvasImage.ts | 89 +++++++++-------- .../controlLayers/konva/CanvasManager.ts | 59 +++--------- .../controlLayers/konva/CanvasPreview.ts | 55 ++++++----- .../konva/CanvasProgressImage.ts | 96 +++++++++++++------ .../konva/CanvasProgressPreview.ts | 46 --------- .../controlLayers/konva/CanvasStagingArea.ts | 43 ++++++--- .../controlLayers/konva/CanvasTool.ts | 7 +- .../controlLayers/store/bboxReducers.ts | 32 ------- .../controlLayers/store/canvasV2Slice.ts | 1 - .../controlLayers/store/layersReducers.ts | 37 +++---- 13 files changed, 227 insertions(+), 258 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressPreview.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index c551fa88b7d..80c1f2bebda 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -5,9 +5,10 @@ import { parseify } from 'common/util/serialize'; import { caImageChanged, ipaImageChanged, - layerAddedFromImage, + layerAdded, rgIPAdapterImageChanged, } from 'features/controlLayers/store/canvasV2Slice'; +import type { CanvasLayerState } from 'features/controlLayers/store/types'; import { imageDTOToImageObject } from 'features/controlLayers/store/types'; import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import { isValidDrop } from 'features/dnd/util/isValidDrop'; @@ -106,7 +107,13 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => activeData.payloadType === 'IMAGE_DTO' && activeData.payload.imageDTO ) { - dispatch(layerAddedFromImage({ imageObject: imageDTOToImageObject(activeData.payload.imageDTO) })); + const imageObject = imageDTOToImageObject(activeData.payload.imageDTO); + const { x, y } = getState().canvasV2.bbox.rect; + const overrides: Partial = { + objects: [imageObject], + position: { x, y }, + }; + dispatch(layerAdded({ overrides, isSelected: true })); return; } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx index aee648be933..2c4ba2932b6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx @@ -15,7 +15,7 @@ export const AddLayerButton = memo(() => { dispatch(rgAdded()); }, [dispatch]); const addRasterLayer = useCallback(() => { - dispatch(layerAdded()); + dispatch(layerAdded({ isSelected: true })); }, [dispatch]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts index 7e72ef0ca7b..18defa079f4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts @@ -1,5 +1,6 @@ import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMultiple'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import type { CanvasPreview } from 'features/controlLayers/konva/CanvasPreview'; import type { Rect } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { atom } from 'nanostores'; @@ -23,6 +24,7 @@ export class CanvasBbox { static CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right']; static NO_ANCHORS: string[] = []; + parent: CanvasPreview; manager: CanvasManager; konva: { @@ -31,8 +33,9 @@ export class CanvasBbox { transformer: Konva.Transformer; }; - constructor(manager: CanvasManager) { - this.manager = manager; + constructor(parent: CanvasPreview) { + this.parent = parent; + this.manager = this.parent.manager; // Create a stash to hold onto the last aspect ratio of the bbox - this allows for locking the aspect ratio when // transforming the bbox. const bbox = this.manager.stateApi.getBbox(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts index cd10e482b74..c5c0e810d7c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -2,6 +2,7 @@ import { Mutex } from 'async-mutex'; import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; +import type { CanvasStagingArea } from 'features/controlLayers/konva/CanvasStagingArea'; import { FILTER_MAP } from 'features/controlLayers/konva/filters'; import { loadImage } from 'features/controlLayers/konva/util'; import type { CanvasImageState, GetLoggingContext } from 'features/controlLayers/store/types'; @@ -19,7 +20,7 @@ export class CanvasImageRenderer { static PLACEHOLDER_TEXT_NAME = `${CanvasImageRenderer.TYPE}_placeholder-text`; id: string; - parent: CanvasObjectRenderer; + parent: CanvasObjectRenderer | CanvasStagingArea; manager: CanvasManager; log: Logger; getLoggingContext: GetLoggingContext; @@ -36,7 +37,7 @@ export class CanvasImageRenderer { isError: boolean = false; mutex = new Mutex(); - constructor(state: CanvasImageState, parent: CanvasObjectRenderer) { + constructor(state: CanvasImageState, parent: CanvasObjectRenderer | CanvasStagingArea) { const { id, image } = state; const { width, height } = image; this.id = id; @@ -97,18 +98,16 @@ export class CanvasImageRenderer { this.onFailedToLoadImage(); return; } + // Load the thumbnail first, but let the image load in parallel loadImage(imageDTO.thumbnail_url) .then((thumbnailElement) => { this.thumbnailElement = thumbnailElement; - this.mutex.runExclusive(this.updateImageElement); - }) - .catch(this.onFailedToLoadImage); - loadImage(imageDTO.image_url) - .then((imageElement) => { - this.imageElement = imageElement; - this.mutex.runExclusive(this.updateImageElement); + this.updateImageElement(); }) .catch(this.onFailedToLoadImage); + + this.imageElement = await loadImage(imageDTO.image_url); + await this.updateImageElement(); } catch { this.onFailedToLoadImage(); } @@ -123,36 +122,50 @@ export class CanvasImageRenderer { this.konva.placeholder.group.visible(true); }; - updateImageElement = () => { - const element = this.imageElement ?? this.thumbnailElement; + updateImageElement = async () => { + const release = await this.mutex.acquire(); - if (element) { - if (this.konva.image && this.konva.image.image() !== element) { - this.konva.image.setAttrs({ - image: element, - }); - } else { - this.konva.image = new Konva.Image({ - name: CanvasImageRenderer.IMAGE_NAME, - listening: false, - image: element, - width: this.state.image.width, - height: this.state.image.height, - }); - this.konva.group.add(this.konva.image); - } - - if (this.state.filters.length > 0) { - this.konva.image.cache(); - this.konva.image.filters(this.state.filters.map((f) => FILTER_MAP[f])); - } else { - this.konva.image.clearCache(); - this.konva.image.filters([]); + try { + const element = this.imageElement ?? this.thumbnailElement; + const { width, height } = this.state.image; + + if (element) { + if (this.konva.image) { + this.log.trace('Updating Konva image attrs'); + this.konva.image.setAttrs({ + image: element, + width, + height, + }); + } else { + this.log.trace('Creating new Konva image'); + this.konva.image = new Konva.Image({ + name: CanvasImageRenderer.IMAGE_NAME, + listening: false, + image: element, + width, + height, + }); + this.konva.group.add(this.konva.image); + } + + if (this.state.filters.length > 0) { + this.konva.image.cache(); + this.konva.image.filters(this.state.filters.map((f) => FILTER_MAP[f])); + } else { + this.konva.image.clearCache(); + this.konva.image.filters([]); + } + + this.konva.placeholder.rect.setAttrs({ width, height }); + this.konva.placeholder.text.setAttrs({ width, height, fontSize: width / 16 }); + + this.isLoading = false; + this.isError = false; + this.konva.placeholder.group.visible(false); } - - this.isLoading = false; - this.isError = false; - this.konva.placeholder.group.visible(false); + } finally { + release(); } }; @@ -173,8 +186,6 @@ export class CanvasImageRenderer { this.konva.image?.clearCache(); this.konva.image?.filters([]); } - this.konva.placeholder.rect.setAttrs({ width, height }); - this.konva.placeholder.text.setAttrs({ width, height, fontSize: width / 16 }); this.state = state; return true; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 41d1423d86b..90d73eaa355 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -7,7 +7,6 @@ import type { CanvasBrushLineRenderer } from 'features/controlLayers/konva/Canva import type { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine'; import type { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; -import { CanvasProgressPreview } from 'features/controlLayers/konva/CanvasProgressPreview'; import type { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; import type { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; import { MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/controlLayers/konva/constants'; @@ -19,7 +18,6 @@ import { nanoid, } from 'features/controlLayers/konva/util'; import type { Extents, ExtentsResult, GetBboxTask, WorkerLogMessage } from 'features/controlLayers/konva/worker'; -import { $lastProgressEvent, $shouldShowStagedImage } from 'features/controlLayers/store/canvasV2Slice'; import type { CanvasControlAdapterState, CanvasEntityIdentifier, @@ -49,14 +47,12 @@ import type { ImageCategory, ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; import { CanvasBackground } from './CanvasBackground'; -import { CanvasBbox } from './CanvasBbox'; import { CanvasControlAdapter } from './CanvasControlAdapter'; import { CanvasLayerAdapter } from './CanvasLayerAdapter'; import { CanvasMaskAdapter } from './CanvasMaskAdapter'; import { CanvasPreview } from './CanvasPreview'; import { CanvasStagingArea } from './CanvasStagingArea'; import { CanvasStateApi } from './CanvasStateApi'; -import { CanvasTool } from './CanvasTool'; import { setStageEventHandlers } from './events'; // type Extents = { @@ -159,15 +155,6 @@ export class CanvasManager { this.transformingEntity = new PubSub(null); this.toolState = new PubSub(this.stateApi.getToolState()); - this.currentFill = new PubSub(this.getCurrentFill()); - this.selectedEntityIdentifier = new PubSub( - this.stateApi.getState().selectedEntityIdentifier, - (a, b) => a?.id === b?.id - ); - this.selectedEntity = new PubSub( - this.getSelectedEntity(), - (a, b) => a?.state === b?.state && a?.adapter === b?.adapter - ); this._prevState = this.stateApi.getState(); @@ -187,13 +174,8 @@ export class CanvasManager { uploadImage, }; - this.preview = new CanvasPreview( - new CanvasBbox(this), - new CanvasTool(this), - new CanvasStagingArea(this), - new CanvasProgressPreview(this) - ); - this.stage.add(this.preview.layer); + this.preview = new CanvasPreview(this); + this.stage.add(this.preview.getLayer()); this.background = new CanvasBackground(this); this.stage.add(this.background.konva.layer); @@ -226,6 +208,16 @@ export class CanvasManager { this.log.error('Worker message error'); }; + this.currentFill = new PubSub(this.getCurrentFill()); + this.selectedEntityIdentifier = new PubSub( + this.stateApi.getState().selectedEntityIdentifier, + (a, b) => a?.id === b?.id + ); + this.selectedEntity = new PubSub( + this.getSelectedEntity(), + (a, b) => a?.state === b?.state && a?.adapter === b?.adapter + ); + this.inpaintMask = new CanvasMaskAdapter(this.stateApi.getInpaintMaskState(), this); this.stage.add(this.inpaintMask.konva.layer); } @@ -249,10 +241,6 @@ export class CanvasManager { this._worker.postMessage(task, [data.buffer]); } - async renderProgressPreview() { - await this.preview.progressPreview.render(this.stateApi.$lastProgressEvent.get()); - } - async renderControlAdapters() { const { entities } = this.stateApi.getControlAdaptersState(); @@ -291,7 +279,7 @@ export class CanvasManager { this.regions.get(rg.id)?.konva.layer.zIndex(++zIndex); } this.inpaintMask.konva.layer.zIndex(++zIndex); - this.preview.layer.zIndex(++zIndex); + this.preview.getLayer().zIndex(++zIndex); } fitStageToContainer() { @@ -611,25 +599,6 @@ export class CanvasManager { const unsubscribeRenderer = this._store.subscribe(this.render); - // When we this flag, we need to render the staging area - const unsubscribeShouldShowStagedImage = $shouldShowStagedImage.subscribe( - async (shouldShowStagedImage, prevShouldShowStagedImage) => { - if (shouldShowStagedImage !== prevShouldShowStagedImage) { - this.log.debug('Rendering staging area'); - await this.preview.stagingArea.render(); - } - } - ); - - const unsubscribeLastProgressEvent = $lastProgressEvent.subscribe( - async (lastProgressEvent, prevLastProgressEvent) => { - if (lastProgressEvent !== prevLastProgressEvent) { - this.log.debug('Rendering progress image'); - await this.preview.progressPreview.render(lastProgressEvent); - } - } - ); - this.log.debug('First render of konva stage'); this.preview.tool.render(); this.render(); @@ -650,8 +619,6 @@ export class CanvasManager { this.preview.destroy(); unsubscribeRenderer(); unsubscribeListeners(); - unsubscribeShouldShowStagedImage(); - unsubscribeLastProgressEvent(); resizeObserver.disconnect(); }; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts index ef0f6a579b8..b5969299cc2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts @@ -1,40 +1,51 @@ -import type { CanvasProgressPreview } from 'features/controlLayers/konva/CanvasProgressPreview'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasProgressImage } from 'features/controlLayers/konva/CanvasProgressImage'; import Konva from 'konva'; -import type { CanvasBbox } from './CanvasBbox'; -import type { CanvasStagingArea } from './CanvasStagingArea'; -import type { CanvasTool } from './CanvasTool'; +import { CanvasBbox } from './CanvasBbox'; +import { CanvasStagingArea } from './CanvasStagingArea'; +import { CanvasTool } from './CanvasTool'; export class CanvasPreview { - layer: Konva.Layer; + manager: CanvasManager; + + konva: { + layer: Konva.Layer; + }; + tool: CanvasTool; bbox: CanvasBbox; stagingArea: CanvasStagingArea; - progressPreview: CanvasProgressPreview; + progressImage: CanvasProgressImage; - constructor( - bbox: CanvasBbox, - tool: CanvasTool, - stagingArea: CanvasStagingArea, - progressPreview: CanvasProgressPreview - ) { - this.layer = new Konva.Layer({ listening: true, imageSmoothingEnabled: false }); + constructor(manager: CanvasManager) { + this.manager = manager; + this.konva = { + layer: new Konva.Layer({ listening: true, imageSmoothingEnabled: false }), + }; - this.stagingArea = stagingArea; - this.layer.add(this.stagingArea.konva.group); + this.stagingArea = new CanvasStagingArea(this); + this.konva.layer.add(...this.stagingArea.getNodes()); - this.bbox = bbox; - this.layer.add(this.bbox.konva.group); + this.progressImage = new CanvasProgressImage(this); + this.konva.layer.add(...this.progressImage.getNodes()); - this.tool = tool; - this.layer.add(this.tool.konva.group); + this.bbox = new CanvasBbox(this); + this.konva.layer.add(this.bbox.konva.group); - this.progressPreview = progressPreview; - this.layer.add(this.progressPreview.konva.group); + this.tool = new CanvasTool(this); + this.konva.layer.add(this.tool.konva.group); } + getLayer = () => { + return this.konva.layer; + }; + destroy() { + // this.stagingArea.destroy(); // TODO(psyche): implement destroy + this.progressImage.destroy(); + // this.bbox.destroy(); // TODO(psyche): implement destroy this.tool.destroy(); - this.layer.destroy(); + this.konva.layer.destroy(); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts index edda6c26f59..c9ffe895d49 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts @@ -1,5 +1,9 @@ -import { loadImage } from 'features/controlLayers/konva/util'; +import { Mutex } from 'async-mutex'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import type { CanvasPreview } from 'features/controlLayers/konva/CanvasPreview'; +import { getPrefixedId, loadImage } from 'features/controlLayers/konva/util'; import Konva from 'konva'; +import type { InvocationDenoiseProgressEvent } from 'services/events/types'; export class CanvasProgressImage { static NAME_PREFIX = 'progress-image'; @@ -7,53 +11,86 @@ export class CanvasProgressImage { static IMAGE_NAME = `${CanvasProgressImage.NAME_PREFIX}_image`; id: string; - progressImageId: string | null; + parent: CanvasPreview; + manager: CanvasManager; + + /** + * A set of subscriptions that should be cleaned up when the transformer is destroyed. + */ + subscriptions: Set<() => void> = new Set(); + + progressImageId: string | null = null; konva: { group: Konva.Group; image: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately }; - isLoading: boolean; - isError: boolean; + isLoading: boolean = false; + isError: boolean = false; + imageElement: HTMLImageElement | null = null; + + lastProgressEvent: InvocationDenoiseProgressEvent | null = null; - constructor(arg: { id: string }) { - const { id } = arg; + mutex: Mutex = new Mutex(); + + constructor(parent: CanvasPreview) { + this.id = getPrefixedId(CanvasProgressImage.NAME_PREFIX); + this.parent = parent; + this.manager = parent.manager; this.konva = { group: new Konva.Group({ name: CanvasProgressImage.GROUP_NAME, listening: false }), image: null, }; - this.id = id; - this.progressImageId = null; - this.isLoading = false; - this.isError = false; + + this.manager.stateApi.$lastProgressEvent.listen((event) => { + this.lastProgressEvent = event; + this.render(); + }); } - async updateImageSource( - progressImageId: string, - dataURL: string, - x: number, - y: number, - width: number, - height: number - ) { - if (this.isLoading) { + getNodes = () => { + return [this.konva.group]; + }; + + render = async () => { + const release = await this.mutex.acquire(); + + if (!this.lastProgressEvent) { + this.konva.group.visible(false); + this.imageElement = null; + this.isLoading = false; + this.isError = false; + release(); + return; + } + + const { isStaging } = this.manager.stateApi.getSession(); + + if (!isStaging) { + release(); return; } + this.isLoading = true; + + const { x, y } = this.manager.stateApi.getBbox().rect; + const { dataURL, width, height } = this.lastProgressEvent.progress_image; try { - const imageEl = await loadImage(dataURL); + this.imageElement = await loadImage(dataURL); if (this.konva.image) { + console.log('UPDATING PROGRESS IMAGE') this.konva.image.setAttrs({ - image: imageEl, + image: this.imageElement, x, y, width, height, }); } else { + console.log('CREATING NEW PROGRESS IMAGE') this.konva.image = new Konva.Image({ name: CanvasProgressImage.IMAGE_NAME, listening: false, - image: imageEl, + image: this.imageElement, x, y, width, @@ -61,14 +98,19 @@ export class CanvasProgressImage { }); this.konva.group.add(this.konva.image); } - this.isLoading = false; - this.id = progressImageId; + this.konva.group.visible(true); } catch { this.isError = true; + } finally { + this.isLoading = false; + release(); } - } + }; - destroy() { + destroy = () => { + for (const unsubscribe of this.subscriptions) { + unsubscribe(); + } this.konva.group.destroy(); - } + }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressPreview.ts deleted file mode 100644 index 95c7910b527..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressPreview.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasProgressImage } from 'features/controlLayers/konva/CanvasProgressImage'; -import Konva from 'konva'; -import type { InvocationDenoiseProgressEvent } from 'services/events/types'; - -export class CanvasProgressPreview { - static NAME_PREFIX = 'progress-preview'; - static GROUP_NAME = `${CanvasProgressPreview.NAME_PREFIX}_group`; - - konva: { - group: Konva.Group; - progressImage: CanvasProgressImage; - }; - manager: CanvasManager; - - constructor(manager: CanvasManager) { - this.manager = manager; - this.konva = { - group: new Konva.Group({ name: CanvasProgressPreview.GROUP_NAME, listening: false }), - progressImage: new CanvasProgressImage({ id: 'progress-image' }), - }; - this.konva.group.add(this.konva.progressImage.konva.group); - } - - async render(lastProgressEvent: InvocationDenoiseProgressEvent | null) { - const bboxRect = this.manager.stateApi.getBbox().rect; - const session = this.manager.stateApi.getSession(); - - if (lastProgressEvent && session.isStaging) { - const { invocation, step, progress_image } = lastProgressEvent; - const { dataURL } = progress_image; - const { x, y, width, height } = bboxRect; - const progressImageId = `${invocation.id}_${step}`; - if ( - !this.konva.progressImage.isLoading && - !this.konva.progressImage.isError && - this.konva.progressImage.progressImageId !== progressImageId - ) { - await this.konva.progressImage.updateImageSource(progressImageId, dataURL, x, y, width, height); - this.konva.progressImage.konva.group.visible(true); - } - } else { - this.konva.progressImage.konva.group.visible(false); - } - } -} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts index 50872886ef7..be17df0404b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts @@ -1,5 +1,6 @@ import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import type { CanvasPreview } from 'features/controlLayers/konva/CanvasPreview'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { GetLoggingContext, StagingAreaImage } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -10,6 +11,7 @@ export class CanvasStagingArea { static GROUP_NAME = `${CanvasStagingArea.TYPE}_group`; id: string; + parent: CanvasPreview; manager: CanvasManager; log: Logger; getLoggingContext: GetLoggingContext; @@ -19,9 +21,15 @@ export class CanvasStagingArea { image: CanvasImageRenderer | null; selectedImage: StagingAreaImage | null; - constructor(manager: CanvasManager) { + /** + * A set of subscriptions that should be cleaned up when the transformer is destroyed. + */ + subscriptions: Set<() => void> = new Set(); + + constructor(parent: CanvasPreview) { this.id = getPrefixedId(CanvasStagingArea.TYPE); - this.manager = manager; + this.parent = parent; + this.manager = this.parent.manager; this.getLoggingContext = this.manager.buildGetLoggingContext(this); this.log = this.manager.buildLogger(this.getLoggingContext); this.log.debug('Creating staging area'); @@ -29,14 +37,17 @@ export class CanvasStagingArea { this.konva = { group: new Konva.Group({ name: CanvasStagingArea.GROUP_NAME, listening: false }) }; this.image = null; this.selectedImage = null; + + this.subscriptions.add(this.manager.stateApi.$shouldShowStagedImage.listen(this.render)); } render = async () => { const session = this.manager.stateApi.getSession(); - const bboxRect = this.manager.stateApi.getBbox().rect; + const { rect } = this.manager.stateApi.getBbox(); const shouldShowStagedImage = this.manager.stateApi.$shouldShowStagedImage.get(); this.selectedImage = session.stagedImages[session.selectedStagedImageIndex] ?? null; + this.konva.group.position({ x: rect.x, y: rect.y }); if (this.selectedImage) { const { imageDTO, offsetX, offsetY } = this.selectedImage; @@ -47,10 +58,6 @@ export class CanvasStagingArea { { id: 'staging-area-image', type: 'image', - x: 0, - y: 0, - width, - height, filters: [], image: { image_name: image_name, @@ -63,11 +70,7 @@ export class CanvasStagingArea { this.konva.group.add(this.image.konva.group); } - if (!this.image.isLoading && !this.image.isError && this.image.imageName !== imageDTO.image_name) { - this.image.konva.image?.width(imageDTO.width); - this.image.konva.image?.height(imageDTO.height); - this.image.konva.group.x(bboxRect.x + offsetX); - this.image.konva.group.y(bboxRect.y + offsetY); + if (!this.image.isLoading && !this.image.isError) { await this.image.updateImageSource(imageDTO.image_name); this.manager.stateApi.$lastProgressEvent.set(null); } @@ -77,6 +80,22 @@ export class CanvasStagingArea { } }; + getNodes = () => { + return [this.konva.group]; + }; + + destroy = () => { + if (this.image) { + this.image.destroy(); + } + for (const unsubscribe of this.subscriptions) { + unsubscribe(); + } + for (const node of this.getNodes()) { + node.destroy(); + } + }; + repr = () => { return { id: this.id, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts index 11aeabac4dc..1d2b7809060 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts @@ -1,5 +1,6 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import type { CanvasPreview } from 'features/controlLayers/konva/CanvasPreview'; import { BRUSH_BORDER_INNER_COLOR, BRUSH_BORDER_OUTER_COLOR, @@ -25,6 +26,7 @@ export class CanvasTool { static ERASER_INNER_BORDER_CIRCLE_NAME = `${CanvasTool.ERASER_NAME_PREFIX}_inner-border-circle`; static ERASER_OUTER_BORDER_CIRCLE_NAME = `${CanvasTool.ERASER_NAME_PREFIX}_outer-border-circle`; + parent: CanvasPreview; manager: CanvasManager; konva: { group: Konva.Group; @@ -47,8 +49,9 @@ export class CanvasTool { */ subscriptions: Set<() => void> = new Set(); - constructor(manager: CanvasManager) { - this.manager = manager; + constructor(parent: CanvasPreview) { + this.parent = parent; + this.manager = this.parent.manager; this.konva = { group: new Konva.Group({ name: CanvasTool.GROUP_NAME }), brush: { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts index a1c276f4ae5..81f9d2fd901 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts @@ -48,13 +48,6 @@ export const bboxReducers = { state.bbox.aspectRatio.id = 'Free'; state.bbox.aspectRatio.isLocked = false; } - - if (!state.session.isActive) { - if (state.initialImage.imageObject) { - state.initialImage.imageObject.width = state.bbox.rect.width; - state.initialImage.imageObject.height = state.bbox.rect.height; - } - } }, bboxHeightChanged: ( state, @@ -73,13 +66,6 @@ export const bboxReducers = { state.bbox.aspectRatio.id = 'Free'; state.bbox.aspectRatio.isLocked = false; } - - if (!state.session.isActive) { - if (state.initialImage.imageObject) { - state.initialImage.imageObject.width = state.bbox.rect.width; - state.initialImage.imageObject.height = state.bbox.rect.height; - } - } }, bboxAspectRatioLockToggled: (state) => { state.bbox.aspectRatio.isLocked = !state.bbox.aspectRatio.isLocked; @@ -99,12 +85,6 @@ export const bboxReducers = { state.bbox.rect.width = width; state.bbox.rect.height = height; } - if (!state.session.isActive) { - if (state.initialImage.imageObject) { - state.initialImage.imageObject.width = state.bbox.rect.width; - state.initialImage.imageObject.height = state.bbox.rect.height; - } - } }, bboxDimensionsSwapped: (state) => { state.bbox.aspectRatio.value = 1 / state.bbox.aspectRatio.value; @@ -122,12 +102,6 @@ export const bboxReducers = { state.bbox.rect.height = height; state.bbox.aspectRatio.id = ASPECT_RATIO_MAP[state.bbox.aspectRatio.id].inverseID; } - if (!state.session.isActive) { - if (state.initialImage.imageObject) { - state.initialImage.imageObject.width = state.bbox.rect.width; - state.initialImage.imageObject.height = state.bbox.rect.height; - } - } }, bboxSizeOptimized: (state) => { const optimalDimension = getOptimalDimension(state.params.model); @@ -140,11 +114,5 @@ export const bboxReducers = { state.bbox.rect.width = optimalDimension; state.bbox.rect.height = optimalDimension; } - if (!state.session.isActive) { - if (state.initialImage.imageObject) { - state.initialImage.imageObject.width = state.bbox.rect.width; - state.initialImage.imageObject.height = state.bbox.rect.height; - } - } }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index ca37dc9517f..7d5ac7a91a3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -411,7 +411,6 @@ export const { bboxSizeOptimized, // layers layerAdded, - layerAddedFromImage, layerRecalled, layerOpacityChanged, layerAllDeleted, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index 48e01cd1f3e..133e1213390 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -4,7 +4,7 @@ import { merge } from 'lodash-es'; import type { ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; -import type { CanvasImageState, CanvasLayerState, CanvasV2State } from './types'; +import type { CanvasLayerState, CanvasV2State } from './types'; import { imageDTOToImageWithDims } from './types'; export const selectLayer = (state: CanvasV2State, id: string) => state.layers.entities.find((layer) => layer.id === id); @@ -16,8 +16,11 @@ export const selectLayerOrThrow = (state: CanvasV2State, id: string) => { export const layersReducers = { layerAdded: { - reducer: (state, action: PayloadAction<{ id: string; overrides?: Partial }>) => { - const { id } = action.payload; + reducer: ( + state, + action: PayloadAction<{ id: string; overrides?: Partial; isSelected?: boolean }> + ) => { + const { id, overrides, isSelected } = action.payload; const layer: CanvasLayerState = { id, type: 'layer', @@ -27,12 +30,14 @@ export const layersReducers = { position: { x: 0, y: 0 }, imageCache: null, }; - merge(layer, action.payload.overrides); + merge(layer, overrides); state.layers.entities.push(layer); - state.selectedEntityIdentifier = { type: 'layer', id }; + if (isSelected) { + state.selectedEntityIdentifier = { type: 'layer', id }; + } state.layers.imageCache = null; }, - prepare: (payload: { overrides?: Partial }) => ({ + prepare: (payload: { overrides?: Partial; isSelected?: boolean }) => ({ payload: { ...payload, id: getPrefixedId('layer') }, }), }, @@ -42,26 +47,6 @@ export const layersReducers = { state.selectedEntityIdentifier = { type: 'layer', id: data.id }; state.layers.imageCache = null; }, - layerAddedFromImage: { - reducer: (state, action: PayloadAction<{ id: string; imageObject: CanvasImageState }>) => { - const { id, imageObject } = action.payload; - const layer: CanvasLayerState = { - id, - type: 'layer', - isEnabled: true, - objects: [imageObject], - opacity: 1, - position: { x: 0, y: 0 }, - imageCache: null, - }; - state.layers.entities.push(layer); - state.selectedEntityIdentifier = { type: 'layer', id }; - state.layers.imageCache = null; - }, - prepare: (payload: { imageObject: CanvasImageState }) => ({ - payload: { ...payload, id: getPrefixedId('layer') }, - }), - }, layerAllDeleted: (state) => { state.layers.entities = []; state.layers.imageCache = null; From 6b7ead44612a625d010fb7a72c5b3c0b8f7746de Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 8 Aug 2024 17:26:01 +1000 Subject: [PATCH 340/678] tidy(ui): remove unused code, comments --- .../listeners/socketio/socketInvocationComplete.ts | 6 +----- .../src/features/controlLayers/konva/CanvasProgressImage.ts | 2 -- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts index d8dcfb76b16..13057f9f506 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts @@ -2,7 +2,7 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { deepClone } from 'common/util/deepClone'; import { parseify } from 'common/util/serialize'; -import { $lastProgressEvent, sessionImageStaged } from 'features/controlLayers/store/canvasV2Slice'; +import { sessionImageStaged } from 'features/controlLayers/store/canvasV2Slice'; import { boardIdSelected, galleryViewChanged, imageSelected, offsetChanged } from 'features/gallery/store/gallerySlice'; import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; import { zNodeStatus } from 'features/nodes/types/invocation'; @@ -64,14 +64,10 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi const { offset_x, offset_y } = data.result; if (canvasV2.session.isStaging) { dispatch(sessionImageStaged({ stagingAreaImage: { imageDTO, offsetX: offset_x, offsetY: offset_y } })); - } else if (!canvasV2.session.isActive) { - $lastProgressEvent.set(null); } } else if (data.result.type === 'image_output') { if (canvasV2.session.isStaging) { dispatch(sessionImageStaged({ stagingAreaImage: { imageDTO, offsetX: 0, offsetY: 0 } })); - } else if (!canvasV2.session.isActive) { - $lastProgressEvent.set(null); } } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts index c9ffe895d49..7d5df77400d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts @@ -77,7 +77,6 @@ export class CanvasProgressImage { try { this.imageElement = await loadImage(dataURL); if (this.konva.image) { - console.log('UPDATING PROGRESS IMAGE') this.konva.image.setAttrs({ image: this.imageElement, x, @@ -86,7 +85,6 @@ export class CanvasProgressImage { height, }); } else { - console.log('CREATING NEW PROGRESS IMAGE') this.konva.image = new Konva.Image({ name: CanvasProgressImage.IMAGE_NAME, listening: false, From 9d9e845198c07bc352d566ec369ba343296a7164 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 8 Aug 2024 18:10:20 +1000 Subject: [PATCH 341/678] fix(ui): depth anything v2 --- .../frontend/web/src/features/controlLayers/store/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 45262c8831f..995d0e2d3bb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -91,7 +91,7 @@ const zContentShuffleProcessorConfig = z.object({ }); export type ContentShuffleProcessorConfig = z.infer; -const zDepthAnythingModelSize = z.enum(['large', 'base', 'small']); +const zDepthAnythingModelSize = z.enum(['large', 'base', 'small', 'small_v2']); export type DepthAnythingModelSize = z.infer; export const isDepthAnythingModelSize = (v: unknown): v is DepthAnythingModelSize => zDepthAnythingModelSize.safeParse(v).success; @@ -293,7 +293,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { buildDefaults: () => ({ id: 'depth_anything_image_processor', type: 'depth_anything_image_processor', - model_size: 'small', + model_size: 'small_v2', }), buildNode: (image, config) => ({ ...config, From 0eda34b41f45eaafaad7e0d8812dda78cf6dd9b9 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 8 Aug 2024 18:23:38 +1000 Subject: [PATCH 342/678] fix(ui): give up on thumbnail loading, causes flash during transformer --- .../features/controlLayers/konva/CanvasImage.ts | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts index c5c0e810d7c..915510fe2ac 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -31,7 +31,6 @@ export class CanvasImageRenderer { placeholder: { group: Konva.Group; rect: Konva.Rect; text: Konva.Text }; image: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately }; - thumbnailElement: HTMLImageElement | null = null; imageElement: HTMLImageElement | null = null; isLoading: boolean = false; isError: boolean = false; @@ -98,13 +97,6 @@ export class CanvasImageRenderer { this.onFailedToLoadImage(); return; } - // Load the thumbnail first, but let the image load in parallel - loadImage(imageDTO.thumbnail_url) - .then((thumbnailElement) => { - this.thumbnailElement = thumbnailElement; - this.updateImageElement(); - }) - .catch(this.onFailedToLoadImage); this.imageElement = await loadImage(imageDTO.image_url); await this.updateImageElement(); @@ -126,14 +118,13 @@ export class CanvasImageRenderer { const release = await this.mutex.acquire(); try { - const element = this.imageElement ?? this.thumbnailElement; - const { width, height } = this.state.image; + if (this.imageElement) { + const { width, height } = this.state.image; - if (element) { if (this.konva.image) { this.log.trace('Updating Konva image attrs'); this.konva.image.setAttrs({ - image: element, + image: this.imageElement, width, height, }); @@ -142,7 +133,7 @@ export class CanvasImageRenderer { this.konva.image = new Konva.Image({ name: CanvasImageRenderer.IMAGE_NAME, listening: false, - image: element, + image: this.imageElement, width, height, }); From 7339b3d8ccf3d6e84a686feb6a9d070a31448d37 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 8 Aug 2024 18:43:33 +1000 Subject: [PATCH 343/678] feat(ui): add trnalsation --- invokeai/frontend/web/public/locales/en.json | 1 + 1 file changed, 1 insertion(+) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index fe8df2d409a..03702112fda 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -80,6 +80,7 @@ "aboutDesc": "Using Invoke for work? Check out:", "aboutHeading": "Own Your Creative Power", "accept": "Accept", + "apply": "Apply", "add": "Add", "advanced": "Advanced", "ai": "ai", From 22712c5dac33ed110cbca9f58880de678bb04ace Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 8 Aug 2024 18:45:06 +1000 Subject: [PATCH 344/678] feat(ui): convert all my pubsubs to atoms its the same but better --- .../components/TransformToolButton.tsx | 2 +- .../controlLayers/konva/CanvasManager.ts | 104 +++++------------- .../konva/CanvasObjectRenderer.ts | 2 +- .../controlLayers/konva/CanvasTool.ts | 4 +- .../controlLayers/konva/CanvasTransformer.ts | 4 +- 5 files changed, 32 insertions(+), 84 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx index 2909174214e..f26575d6611 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx @@ -19,7 +19,7 @@ export const TransformToolButton = memo(() => { if (!canvasManager) { return; } - return canvasManager.transformingEntity.subscribe((newValue) => { + return canvasManager.$transformingEntity.listen((newValue) => { setIsTransforming(Boolean(newValue)); }); }, [canvasManager]); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 90d73eaa355..8f7a5e606de 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -2,7 +2,6 @@ import type { Store } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import type { JSONObject } from 'common/types'; -import { PubSub } from 'common/util/PubSub/PubSub'; import type { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine'; import type { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine'; import type { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; @@ -36,14 +35,11 @@ import { RGBA_RED } from 'features/controlLayers/store/types'; import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers'; import type Konva from 'konva'; import { clamp } from 'lodash-es'; +import type { WritableAtom } from 'nanostores'; import { atom } from 'nanostores'; import type { Logger } from 'roarr'; -import { - getImageDTO as defaultGetImageDTO, - getImageDTO, - uploadImage as defaultUploadImage, -} from 'services/api/endpoints/images'; -import type { ImageCategory, ImageDTO } from 'services/api/types'; +import { getImageDTO, uploadImage } from 'services/api/endpoints/images'; +import type { ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; import { CanvasBackground } from './CanvasBackground'; @@ -55,34 +51,6 @@ import { CanvasStagingArea } from './CanvasStagingArea'; import { CanvasStateApi } from './CanvasStateApi'; import { setStageEventHandlers } from './events'; -// type Extents = { -// minX: number; -// minY: number; -// maxX: number; -// maxY: number; -// }; -// type GetBboxTask = { -// id: string; -// type: 'get_bbox'; -// data: { imageData: ImageData }; -// }; - -// type GetBboxResult = { -// id: string; -// type: 'get_bbox'; -// data: { extents: Extents | null }; -// }; - -type Util = { - getImageDTO: (imageName: string) => Promise; - uploadImage: ( - blob: Blob, - fileName: string, - image_category: ImageCategory, - is_intermediate: boolean - ) => Promise; -}; - type EntityStateAndAdapter = | { id: string; @@ -118,7 +86,6 @@ export class CanvasManager { layers: Map; regions: Map; inpaintMask: CanvasMaskAdapter; - util: Util; stateApi: CanvasStateApi; preview: CanvasPreview; background: CanvasBackground; @@ -126,8 +93,6 @@ export class CanvasManager { log: Logger; workerLog: Logger; - transformingEntity: PubSub; - _store: Store; _prevState: CanvasV2State; _isFirstRender: boolean = true; @@ -136,26 +101,18 @@ export class CanvasManager { _worker: Worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module', name: 'worker' }); _tasks: Map void }> = new Map(); - toolState: PubSub; - currentFill: PubSub; - selectedEntity: PubSub; - selectedEntityIdentifier: PubSub; - - constructor( - stage: Konva.Stage, - container: HTMLDivElement, - store: Store, - getImageDTO: Util['getImageDTO'] = defaultGetImageDTO, - uploadImage: Util['uploadImage'] = defaultUploadImage - ) { + $transformingEntity: WritableAtom = atom(); + $toolState: WritableAtom = atom(); + $currentFill: WritableAtom = atom(); + $selectedEntity: WritableAtom = atom(); + $selectedEntityIdentifier: WritableAtom = atom(); + + constructor(stage: Konva.Stage, container: HTMLDivElement, store: Store) { this.stage = stage; this.container = container; this._store = store; this.stateApi = new CanvasStateApi(this._store, this); - this.transformingEntity = new PubSub(null); - this.toolState = new PubSub(this.stateApi.getToolState()); - this._prevState = this.stateApi.getState(); this.log = logger('canvas').child((message) => { @@ -169,11 +126,6 @@ export class CanvasManager { }); this.workerLog = logger('worker'); - this.util = { - getImageDTO, - uploadImage, - }; - this.preview = new CanvasPreview(this); this.stage.add(this.preview.getLayer()); @@ -208,15 +160,11 @@ export class CanvasManager { this.log.error('Worker message error'); }; - this.currentFill = new PubSub(this.getCurrentFill()); - this.selectedEntityIdentifier = new PubSub( - this.stateApi.getState().selectedEntityIdentifier, - (a, b) => a?.id === b?.id - ); - this.selectedEntity = new PubSub( - this.getSelectedEntity(), - (a, b) => a?.state === b?.state && a?.adapter === b?.adapter - ); + this.$transformingEntity.set(null); + this.$toolState.set(this.stateApi.getToolState()); + this.$selectedEntityIdentifier.set(this.stateApi.getState().selectedEntityIdentifier); + this.$currentFill.set(this.getCurrentFill()); + this.$selectedEntity.set(this.getSelectedEntity()); this.inpaintMask = new CanvasMaskAdapter(this.stateApi.getInpaintMaskState(), this); this.stage.add(this.inpaintMask.konva.layer); @@ -395,7 +343,7 @@ export class CanvasManager { }; getTransformingLayer() { - const transformingEntity = this.transformingEntity.getValue(); + const transformingEntity = this.$transformingEntity.get(); if (!transformingEntity) { return null; } @@ -414,7 +362,7 @@ export class CanvasManager { } getIsTransforming() { - return Boolean(this.transformingEntity.getValue()); + return Boolean(this.$transformingEntity.get()); } startTransform() { @@ -428,7 +376,7 @@ export class CanvasManager { } // TODO(psyche): Support other entity types entity.adapter.transformer.startTransform(); - this.transformingEntity.publish({ id: entity.id, type: entity.type }); + this.$transformingEntity.set({ id: entity.id, type: entity.type }); } async applyTransform() { @@ -436,7 +384,7 @@ export class CanvasManager { if (layer) { await layer.transformer.applyTransform(); } - this.transformingEntity.publish(null); + this.$transformingEntity.set(null); } cancelTransform() { @@ -444,7 +392,7 @@ export class CanvasManager { if (layer) { layer.transformer.stopTransform(); } - this.transformingEntity.publish(null); + this.$transformingEntity.set(null); } render = async () => { @@ -537,10 +485,10 @@ export class CanvasManager { await this.renderControlAdapters(); } - this.toolState.publish(state.tool); - this.selectedEntityIdentifier.publish(state.selectedEntityIdentifier); - this.selectedEntity.publish(this.getSelectedEntity()); - this.currentFill.publish(this.getCurrentFill()); + this.$toolState.set(state.tool); + this.$selectedEntityIdentifier.set(state.selectedEntityIdentifier); + this.$selectedEntity.set(this.getSelectedEntity()); + this.$currentFill.set(this.getCurrentFill()); if ( this._isFirstRender || @@ -740,7 +688,7 @@ export class CanvasManager { getCompositeLayerImageDTO = async (rect?: Rect): Promise => { const blob = await this.getCompositeLayerBlob(rect); - const imageDTO = await this.util.uploadImage(blob, 'composite-layer.png', 'general', true); + const imageDTO = await uploadImage(blob, 'composite-layer.png', 'general', true); this.stateApi.setLayerImageCache(imageDTO); return imageDTO; }; @@ -755,7 +703,7 @@ export class CanvasManager { getInpaintMaskImageDTO = async (rect?: Rect): Promise => { const blob = await this.inpaintMask.renderer.getBlob({ rect }); - const imageDTO = await this.util.uploadImage(blob, 'inpaint-mask.png', 'mask', true); + const imageDTO = await uploadImage(blob, 'inpaint-mask.png', 'mask', true); this.stateApi.setInpaintMaskImageCache(imageDTO); return imageDTO; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index 66aebccb196..c16d7c0f6c7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -116,7 +116,7 @@ export class CanvasObjectRenderer { } this.subscriptions.add( - this.manager.toolState.subscribe((newVal, oldVal) => { + this.manager.$toolState.listen((newVal, oldVal) => { if (newVal.selected !== oldVal.selected) { this.commitBuffer(); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts index 1d2b7809060..4c0efef39ee 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts @@ -118,7 +118,7 @@ export class CanvasTool { ); this.subscriptions.add( - this.manager.toolState.subscribe(() => { + this.manager.$toolState.listen(() => { this.render(); }) ); @@ -175,7 +175,7 @@ export class CanvasTool { } else if (!isDrawableEntity) { // Non-drawable layers don't have tools stage.container().style.cursor = 'not-allowed'; - } else if (tool === 'move' || Boolean(this.manager.transformingEntity.getValue())) { + } else if (tool === 'move' || Boolean(this.manager.$transformingEntity.get())) { // Move tool gets a pointer stage.container().style.cursor = 'default'; } else if (tool === 'rect') { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index 7bf3aea965e..c4ccf614a74 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -384,7 +384,7 @@ export class CanvasTransformer { // When the selected tool changes, we need to update the transformer's interaction state. this.subscriptions.add( - this.manager.toolState.subscribe((newVal, oldVal) => { + this.manager.$toolState.listen((newVal, oldVal) => { if (newVal.selected !== oldVal.selected) { this.syncInteractionState(); } @@ -393,7 +393,7 @@ export class CanvasTransformer { // When the selected entity changes, we need to update the transformer's interaction state. this.subscriptions.add( - this.manager.selectedEntityIdentifier.subscribe(() => { + this.manager.$selectedEntityIdentifier.listen(() => { this.syncInteractionState(); }) ); From e0573b721eee8607c438176064d512bf2afb873c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 8 Aug 2024 18:52:56 +1000 Subject: [PATCH 345/678] feat(ui): tidy up atoms --- .../components/TransformToolButton.tsx | 2 +- .../controlLayers/konva/CanvasManager.ts | 144 ++---------------- .../konva/CanvasObjectRenderer.ts | 2 +- .../controlLayers/konva/CanvasStateApi.ts | 110 +++++++++++++ .../controlLayers/konva/CanvasTool.ts | 8 +- .../controlLayers/konva/CanvasTransformer.ts | 4 +- .../features/controlLayers/konva/events.ts | 4 +- 7 files changed, 137 insertions(+), 137 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx index f26575d6611..bbabeb09e36 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx @@ -19,7 +19,7 @@ export const TransformToolButton = memo(() => { if (!canvasManager) { return; } - return canvasManager.$transformingEntity.listen((newValue) => { + return canvasManager.stateApi.$transformingEntity.listen((newValue) => { setIsTransforming(Boolean(newValue)); }); }, [canvasManager]); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 8f7a5e606de..08c7b6b6ec8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -18,24 +18,16 @@ import { } from 'features/controlLayers/konva/util'; import type { Extents, ExtentsResult, GetBboxTask, WorkerLogMessage } from 'features/controlLayers/konva/worker'; import type { - CanvasControlAdapterState, - CanvasEntityIdentifier, - CanvasInpaintMaskState, - CanvasLayerState, - CanvasRegionalGuidanceState, CanvasV2State, Coordinate, Dimensions, GenerationMode, GetLoggingContext, Rect, - RgbaColor, } from 'features/controlLayers/store/types'; -import { RGBA_RED } from 'features/controlLayers/store/types'; import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers'; import type Konva from 'konva'; import { clamp } from 'lodash-es'; -import type { WritableAtom } from 'nanostores'; import { atom } from 'nanostores'; import type { Logger } from 'roarr'; import { getImageDTO, uploadImage } from 'services/api/endpoints/images'; @@ -51,32 +43,6 @@ import { CanvasStagingArea } from './CanvasStagingArea'; import { CanvasStateApi } from './CanvasStateApi'; import { setStageEventHandlers } from './events'; -type EntityStateAndAdapter = - | { - id: string; - type: CanvasLayerState['type']; - state: CanvasLayerState; - adapter: CanvasLayerAdapter; - } - | { - id: string; - type: CanvasInpaintMaskState['type']; - state: CanvasInpaintMaskState; - adapter: CanvasMaskAdapter; - } - // | { - // id: string; - // type: CanvasControlAdapterState['type']; - // state: CanvasControlAdapterState; - // adapter: CanvasControlAdapter; - // } - | { - id: string; - type: CanvasRegionalGuidanceState['type']; - state: CanvasRegionalGuidanceState; - adapter: CanvasMaskAdapter; - }; - export const $canvasManager = atom(null); export class CanvasManager { @@ -101,12 +67,6 @@ export class CanvasManager { _worker: Worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module', name: 'worker' }); _tasks: Map void }> = new Map(); - $transformingEntity: WritableAtom = atom(); - $toolState: WritableAtom = atom(); - $currentFill: WritableAtom = atom(); - $selectedEntity: WritableAtom = atom(); - $selectedEntityIdentifier: WritableAtom = atom(); - constructor(stage: Konva.Stage, container: HTMLDivElement, store: Store) { this.stage = stage; this.container = container; @@ -160,11 +120,11 @@ export class CanvasManager { this.log.error('Worker message error'); }; - this.$transformingEntity.set(null); - this.$toolState.set(this.stateApi.getToolState()); - this.$selectedEntityIdentifier.set(this.stateApi.getState().selectedEntityIdentifier); - this.$currentFill.set(this.getCurrentFill()); - this.$selectedEntity.set(this.getSelectedEntity()); + this.stateApi.$transformingEntity.set(null); + this.stateApi.$toolState.set(this.stateApi.getToolState()); + this.stateApi.$selectedEntityIdentifier.set(this.stateApi.getState().selectedEntityIdentifier); + this.stateApi.$currentFill.set(this.stateApi.getCurrentFill()); + this.stateApi.$selectedEntity.set(this.stateApi.getSelectedEntity()); this.inpaintMask = new CanvasMaskAdapter(this.stateApi.getInpaintMaskState(), this); this.stage.add(this.inpaintMask.konva.layer); @@ -270,80 +230,8 @@ export class CanvasManager { }); } - getEntity(identifier: CanvasEntityIdentifier): EntityStateAndAdapter | null { - const state = this.stateApi.getState(); - - let entityState: - | CanvasLayerState - | CanvasControlAdapterState - | CanvasRegionalGuidanceState - | CanvasInpaintMaskState - | null = null; - let entityAdapter: CanvasLayerAdapter | CanvasControlAdapter | CanvasMaskAdapter | null = null; - - if (identifier.type === 'layer') { - entityState = state.layers.entities.find((i) => i.id === identifier.id) ?? null; - entityAdapter = this.layers.get(identifier.id) ?? null; - } else if (identifier.type === 'control_adapter') { - entityState = state.controlAdapters.entities.find((i) => i.id === identifier.id) ?? null; - entityAdapter = this.controlAdapters.get(identifier.id) ?? null; - } else if (identifier.type === 'regional_guidance') { - entityState = state.regions.entities.find((i) => i.id === identifier.id) ?? null; - entityAdapter = this.regions.get(identifier.id) ?? null; - } else if (identifier.type === 'inpaint_mask') { - entityState = state.inpaintMask; - entityAdapter = this.inpaintMask; - } - - if (entityState && entityAdapter && entityState.type === entityAdapter.type) { - return { - id: entityState.id, - type: entityState.type, - state: entityState, - adapter: entityAdapter, - } as EntityStateAndAdapter; // TODO(psyche): make TS happy w/o this cast - } - - return null; - } - - getSelectedEntity = () => { - const state = this.stateApi.getState(); - if (state.selectedEntityIdentifier) { - return this.getEntity(state.selectedEntityIdentifier); - } - return null; - }; - - getCurrentFill = () => { - const state = this.stateApi.getState(); - let currentFill: RgbaColor = state.tool.fill; - const selectedEntity = this.getSelectedEntity(); - if (selectedEntity) { - // These two entity types use a compositing rect for opacity. Their fill is always white. - if (selectedEntity.state.type === 'regional_guidance' || selectedEntity.state.type === 'inpaint_mask') { - currentFill = RGBA_RED; - // currentFill = RGBA_WHITE; - } - } - return currentFill; - }; - - getBrushPreviewFill = () => { - const state = this.stateApi.getState(); - let currentFill: RgbaColor = state.tool.fill; - const selectedEntity = this.getSelectedEntity(); - if (selectedEntity) { - // The brush should use the mask opacity for these entity types - if (selectedEntity.state.type === 'regional_guidance' || selectedEntity.state.type === 'inpaint_mask') { - currentFill = { ...selectedEntity.state.fill, a: this.stateApi.getSettings().maskOpacity }; - } - } - return currentFill; - }; - getTransformingLayer() { - const transformingEntity = this.$transformingEntity.get(); + const transformingEntity = this.stateApi.$transformingEntity.get(); if (!transformingEntity) { return null; } @@ -362,21 +250,21 @@ export class CanvasManager { } getIsTransforming() { - return Boolean(this.$transformingEntity.get()); + return Boolean(this.stateApi.$transformingEntity.get()); } startTransform() { if (this.getIsTransforming()) { return; } - const entity = this.getSelectedEntity(); + const entity = this.stateApi.getSelectedEntity(); if (!entity) { this.log.warn('No entity selected to transform'); return; } // TODO(psyche): Support other entity types entity.adapter.transformer.startTransform(); - this.$transformingEntity.set({ id: entity.id, type: entity.type }); + this.stateApi.$transformingEntity.set({ id: entity.id, type: entity.type }); } async applyTransform() { @@ -384,7 +272,7 @@ export class CanvasManager { if (layer) { await layer.transformer.applyTransform(); } - this.$transformingEntity.set(null); + this.stateApi.$transformingEntity.set(null); } cancelTransform() { @@ -392,7 +280,7 @@ export class CanvasManager { if (layer) { layer.transformer.stopTransform(); } - this.$transformingEntity.set(null); + this.stateApi.$transformingEntity.set(null); } render = async () => { @@ -485,10 +373,10 @@ export class CanvasManager { await this.renderControlAdapters(); } - this.$toolState.set(state.tool); - this.$selectedEntityIdentifier.set(state.selectedEntityIdentifier); - this.$selectedEntity.set(this.getSelectedEntity()); - this.$currentFill.set(this.getCurrentFill()); + this.stateApi.$toolState.set(state.tool); + this.stateApi.$selectedEntityIdentifier.set(state.selectedEntityIdentifier); + this.stateApi.$selectedEntity.set(this.stateApi.getSelectedEntity()); + this.stateApi.$currentFill.set(this.stateApi.getCurrentFill()); if ( this._isFirstRender || @@ -709,7 +597,7 @@ export class CanvasManager { }; getRegionMaskImageDTO = async (id: string, rect?: Rect): Promise => { - const region = this.getEntity({ id, type: 'regional_guidance' }); + const region = this.stateApi.getEntity({ id, type: 'regional_guidance' }); assert(region?.type === 'regional_guidance'); if (region.state.imageCache) { const imageDTO = await getImageDTO(region.state.imageCache); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index c16d7c0f6c7..55aef79d171 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -116,7 +116,7 @@ export class CanvasObjectRenderer { } this.subscriptions.add( - this.manager.$toolState.listen((newVal, oldVal) => { + this.manager.stateApi.$toolState.listen((newVal, oldVal) => { if (newVal.selected !== oldVal.selected) { this.commitBuffer(); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index 844b7a1b135..0027bdf589e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -2,7 +2,9 @@ import { $alt, $ctrl, $meta, $shift } from '@invoke-ai/ui-library'; import type { Store } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; +import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; import { $isDrawing, $isMouseDown, @@ -30,6 +32,11 @@ import { toolChanged, } from 'features/controlLayers/store/canvasV2Slice'; import type { + CanvasEntityIdentifier, + CanvasInpaintMaskState, + CanvasLayerState, + CanvasRegionalGuidanceState, + CanvasV2State, EntityBrushLineAddedPayload, EntityEraserLineAddedPayload, EntityIdentifierPayload, @@ -37,10 +44,40 @@ import type { EntityRasterizedPayload, EntityRectAddedPayload, Rect, + RgbaColor, Tool, } from 'features/controlLayers/store/types'; +import { RGBA_RED } from 'features/controlLayers/store/types'; +import type { WritableAtom } from 'nanostores'; +import { atom } from 'nanostores'; import type { ImageDTO } from 'services/api/types'; +type EntityStateAndAdapter = + | { + id: string; + type: CanvasLayerState['type']; + state: CanvasLayerState; + adapter: CanvasLayerAdapter; + } + | { + id: string; + type: CanvasInpaintMaskState['type']; + state: CanvasInpaintMaskState; + adapter: CanvasMaskAdapter; + } + // | { + // id: string; + // type: CanvasControlAdapterState['type']; + // state: CanvasControlAdapterState; + // adapter: CanvasControlAdapter; + // } + | { + id: string; + type: CanvasRegionalGuidanceState['type']; + state: CanvasRegionalGuidanceState; + adapter: CanvasMaskAdapter; + }; + const log = logger('canvas'); export class CanvasStateApi { @@ -152,6 +189,79 @@ export class CanvasStateApi { return this._store.getState().system.consoleLogLevel; }; + getEntity(identifier: CanvasEntityIdentifier): EntityStateAndAdapter | null { + const state = this.getState(); + + let entityState: EntityStateAndAdapter['state'] | null = null; + let entityAdapter: EntityStateAndAdapter['adapter'] | null = null; + + if (identifier.type === 'layer') { + entityState = state.layers.entities.find((i) => i.id === identifier.id) ?? null; + entityAdapter = this.manager.layers.get(identifier.id) ?? null; + } else if (identifier.type === 'control_adapter') { + entityState = state.controlAdapters.entities.find((i) => i.id === identifier.id) ?? null; + entityAdapter = this.manager.controlAdapters.get(identifier.id) ?? null; + } else if (identifier.type === 'regional_guidance') { + entityState = state.regions.entities.find((i) => i.id === identifier.id) ?? null; + entityAdapter = this.manager.regions.get(identifier.id) ?? null; + } else if (identifier.type === 'inpaint_mask') { + entityState = state.inpaintMask; + entityAdapter = this.manager.inpaintMask; + } + + if (entityState && entityAdapter && entityState.type === entityAdapter.type) { + return { + id: entityState.id, + type: entityState.type, + state: entityState, + adapter: entityAdapter, + } as EntityStateAndAdapter; // TODO(psyche): make TS happy w/o this cast + } + + return null; + } + + getSelectedEntity = () => { + const state = this.getState(); + if (state.selectedEntityIdentifier) { + return this.getEntity(state.selectedEntityIdentifier); + } + return null; + }; + + getCurrentFill = () => { + const state = this.getState(); + let currentFill: RgbaColor = state.tool.fill; + const selectedEntity = this.getSelectedEntity(); + if (selectedEntity) { + // These two entity types use a compositing rect for opacity. Their fill is always white. + if (selectedEntity.state.type === 'regional_guidance' || selectedEntity.state.type === 'inpaint_mask') { + currentFill = RGBA_RED; + // currentFill = RGBA_WHITE; + } + } + return currentFill; + }; + + getBrushPreviewFill = () => { + const state = this.getState(); + let currentFill: RgbaColor = state.tool.fill; + const selectedEntity = this.getSelectedEntity(); + if (selectedEntity) { + // The brush should use the mask opacity for these entity types + if (selectedEntity.state.type === 'regional_guidance' || selectedEntity.state.type === 'inpaint_mask') { + currentFill = { ...selectedEntity.state.fill, a: this.getSettings().maskOpacity }; + } + } + return currentFill; + }; + + $transformingEntity: WritableAtom = atom(); + $toolState: WritableAtom = atom(); + $currentFill: WritableAtom = atom(); + $selectedEntity: WritableAtom = atom(); + $selectedEntityIdentifier: WritableAtom = atom(); + // Read-write state, ephemeral interaction state $isDrawing = $isDrawing; $isMouseDown = $isMouseDown; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts index 4c0efef39ee..9eaa0bcee89 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts @@ -118,7 +118,7 @@ export class CanvasTool { ); this.subscriptions.add( - this.manager.$toolState.listen(() => { + this.manager.stateApi.$toolState.listen(() => { this.render(); }) ); @@ -154,7 +154,7 @@ export class CanvasTool { const stage = this.manager.stage; const renderedEntityCount: number = 1; // TODO(psyche): this.manager should be renderable entity count const toolState = this.manager.stateApi.getToolState(); - const selectedEntity = this.manager.getSelectedEntity(); + const selectedEntity = this.manager.stateApi.getSelectedEntity(); const cursorPos = this.manager.stateApi.$lastCursorPos.get(); const isDrawing = this.manager.stateApi.$isDrawing.get(); const isMouseDown = this.manager.stateApi.$isMouseDown.get(); @@ -175,7 +175,7 @@ export class CanvasTool { } else if (!isDrawableEntity) { // Non-drawable layers don't have tools stage.container().style.cursor = 'not-allowed'; - } else if (tool === 'move' || Boolean(this.manager.$transformingEntity.get())) { + } else if (tool === 'move' || Boolean(this.manager.stateApi.$transformingEntity.get())) { // Move tool gets a pointer stage.container().style.cursor = 'default'; } else if (tool === 'rect') { @@ -198,7 +198,7 @@ export class CanvasTool { // No need to render the brush preview if the cursor position or color is missing if (cursorPos && tool === 'brush') { - const brushPreviewFill = this.manager.getBrushPreviewFill(); + const brushPreviewFill = this.manager.stateApi.getBrushPreviewFill(); const alignedCursorPos = alignCoordForTool(cursorPos, toolState.brush.width); const scale = stage.scaleX(); // Update the fill circle diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index c4ccf614a74..ea4d8793ca7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -384,7 +384,7 @@ export class CanvasTransformer { // When the selected tool changes, we need to update the transformer's interaction state. this.subscriptions.add( - this.manager.$toolState.listen((newVal, oldVal) => { + this.manager.stateApi.$toolState.listen((newVal, oldVal) => { if (newVal.selected !== oldVal.selected) { this.syncInteractionState(); } @@ -393,7 +393,7 @@ export class CanvasTransformer { // When the selected entity changes, we need to update the transformer's interaction state. this.subscriptions.add( - this.manager.$selectedEntityIdentifier.listen(() => { + this.manager.stateApi.$selectedEntityIdentifier.listen(() => { this.syncInteractionState(); }) ); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 55ebcbaab48..5be9f5ae873 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -115,7 +115,7 @@ const getLastPointOfLastLineOfEntity = ( }; export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { - const { stage, stateApi, getCurrentFill, getSelectedEntity } = manager; + const { stage, stateApi } = manager; const { getToolState, setTool, @@ -130,6 +130,8 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { getSettings, setBrushWidth, setEraserWidth, + getCurrentFill, + getSelectedEntity, } = stateApi; function getIsPrimaryMouseDown(e: KonvaEventObject) { From b887cf46124c6cf8a7029ca7c8751c7a05e3c49d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 8 Aug 2024 19:43:50 +1000 Subject: [PATCH 346/678] fix(ui): scaled bbox preview --- .../controlLayers/konva/CanvasProgressImage.ts | 4 ++-- .../controlLayers/konva/CanvasStagingArea.ts | 12 ++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts index 7d5df77400d..64f16015b05 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts @@ -72,8 +72,8 @@ export class CanvasProgressImage { this.isLoading = true; - const { x, y } = this.manager.stateApi.getBbox().rect; - const { dataURL, width, height } = this.lastProgressEvent.progress_image; + const { x, y, width, height } = this.manager.stateApi.getBbox().rect; + const { dataURL } = this.lastProgressEvent.progress_image; try { this.imageElement = await loadImage(dataURL); if (this.konva.image) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts index be17df0404b..81c134419c5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts @@ -43,17 +43,21 @@ export class CanvasStagingArea { render = async () => { const session = this.manager.stateApi.getSession(); - const { rect } = this.manager.stateApi.getBbox(); + const { x, y, width, height } = this.manager.stateApi.getBbox().rect; const shouldShowStagedImage = this.manager.stateApi.$shouldShowStagedImage.get(); this.selectedImage = session.stagedImages[session.selectedStagedImageIndex] ?? null; - this.konva.group.position({ x: rect.x, y: rect.y }); + this.konva.group.position({ x, y }); if (this.selectedImage) { - const { imageDTO, offsetX, offsetY } = this.selectedImage; + const { + imageDTO, + // offsetX, // TODO(psyche): restore the crop in the node? + // offsetY // TODO(psyche): restore the crop in the node? + } = this.selectedImage; if (!this.image) { - const { image_name, width, height } = imageDTO; + const { image_name } = imageDTO; this.image = new CanvasImageRenderer( { id: 'staging-area-image', From 270c1304a8923c741174bd384feb1db35b923ee8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 12 Aug 2024 10:02:44 +1000 Subject: [PATCH 347/678] fix(ui): do not import button from chakra --- .../features/controlLayers/components/ControlLayersToolbar.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index db3b42aaf95..511c0b1368d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -1,6 +1,5 @@ /* eslint-disable i18next/no-literal-string */ -import { Button } from '@chakra-ui/react'; -import { Flex, Switch } from '@invoke-ai/ui-library'; +import { Button, Flex, Switch } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; import { BrushWidth } from 'features/controlLayers/components/BrushWidth'; From e213cfc2ba681031d97306565606f41e27bc81c0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 12 Aug 2024 15:27:10 +1000 Subject: [PATCH 348/678] feat(ui): always show marks on canvas scale slider --- .../web/src/features/controlLayers/components/CanvasScale.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasScale.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasScale.tsx index 43456a658f4..14a579552f4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasScale.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasScale.tsx @@ -167,6 +167,7 @@ export const CanvasScale = memo(() => { defaultValue={sliderDefaultValue} marks={marks} formatValue={formatSliderValue} + alwaysShowMarks /> From c697501285cda7c3fba4de501ab298e96a68d79e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 12 Aug 2024 17:33:56 +1000 Subject: [PATCH 349/678] feat(ui): better logging w/ path --- .../controlLayers/konva/CanvasBrushLine.ts | 19 +++-- .../controlLayers/konva/CanvasEraserLine.ts | 22 ++++-- .../controlLayers/konva/CanvasImage.ts | 28 ++++--- .../controlLayers/konva/CanvasLayerAdapter.ts | 22 +++--- .../controlLayers/konva/CanvasManager.ts | 76 ++++--------------- .../controlLayers/konva/CanvasMaskAdapter.ts | 10 ++- .../konva/CanvasObjectRenderer.ts | 37 +++++---- .../controlLayers/konva/CanvasRect.ts | 22 ++++-- .../controlLayers/konva/CanvasStagingArea.ts | 22 ++++-- .../controlLayers/konva/CanvasTransformer.ts | 16 ++-- 10 files changed, 141 insertions(+), 133 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts index 1117bb2c4b8..68d4e9ce2f6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts @@ -7,16 +7,19 @@ import type { CanvasBrushLineState } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; +const TYPE = 'brush_line'; + export class CanvasBrushLineRenderer { - static TYPE = 'brush_line'; - static GROUP_NAME = `${CanvasBrushLineRenderer.TYPE}_group`; - static LINE_NAME = `${CanvasBrushLineRenderer.TYPE}_line`; + static GROUP_NAME = `${TYPE}_group`; + static LINE_NAME = `${TYPE}_line`; + + readonly type = TYPE; id: string; + path: string[]; parent: CanvasObjectRenderer; manager: CanvasManager; log: Logger; - getLoggingContext: (extra?: JSONObject) => JSONObject; state: CanvasBrushLineState; konva: { @@ -29,8 +32,8 @@ export class CanvasBrushLineRenderer { this.id = id; this.parent = parent; this.manager = parent.manager; + this.path = this.parent.path.concat(this.id); - this.getLoggingContext = this.manager.buildGetLoggingContext(this); this.log = this.manager.buildLogger(this.getLoggingContext); this.log.trace({ state }, 'Creating brush line'); @@ -90,9 +93,13 @@ export class CanvasBrushLineRenderer { repr() { return { id: this.id, - type: CanvasBrushLineRenderer.TYPE, + type: this.type, parent: this.parent.id, state: deepClone(this.state), }; } + + getLoggingContext = (): JSONObject => { + return { ...this.parent.getLoggingContext(), path: this.path.join('.') }; + }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts index 24b9b1de6c8..953c94144a3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts @@ -1,22 +1,26 @@ +import type { JSONObject } from 'common/types'; import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; -import type { CanvasEraserLineState, GetLoggingContext } from 'features/controlLayers/store/types'; +import type { CanvasEraserLineState } from 'features/controlLayers/store/types'; import { RGBA_RED } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; +const TYPE = 'eraser_line'; + export class CanvasEraserLineRenderer { - static TYPE = 'eraser_line'; - static GROUP_NAME = `${CanvasEraserLineRenderer.TYPE}_group`; - static LINE_NAME = `${CanvasEraserLineRenderer.TYPE}_line`; + static GROUP_NAME = `${TYPE}_group`; + static LINE_NAME = `${TYPE}_line`; + + readonly type = TYPE; id: string; + path: string[]; parent: CanvasObjectRenderer; manager: CanvasManager; log: Logger; - getLoggingContext: GetLoggingContext; state: CanvasEraserLineState; konva: { @@ -29,7 +33,7 @@ export class CanvasEraserLineRenderer { this.id = id; this.parent = parent; this.manager = parent.manager; - this.getLoggingContext = this.manager.buildGetLoggingContext(this); + this.path = this.parent.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); this.log.trace({ state }, 'Creating eraser line'); @@ -88,9 +92,13 @@ export class CanvasEraserLineRenderer { repr() { return { id: this.id, - type: CanvasEraserLineRenderer.TYPE, + type: this.type, parent: this.parent.id, state: deepClone(this.state), }; } + + getLoggingContext = (): JSONObject => { + return { ...this.parent.getLoggingContext(), path: this.path.join('.') }; + }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts index 915510fe2ac..30b9c75df65 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -1,29 +1,33 @@ import { Mutex } from 'async-mutex'; +import type { JSONObject } from 'common/types'; import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import type { CanvasStagingArea } from 'features/controlLayers/konva/CanvasStagingArea'; import { FILTER_MAP } from 'features/controlLayers/konva/filters'; import { loadImage } from 'features/controlLayers/konva/util'; -import type { CanvasImageState, GetLoggingContext } from 'features/controlLayers/store/types'; +import type { CanvasImageState } from 'features/controlLayers/store/types'; import { t } from 'i18next'; import Konva from 'konva'; import type { Logger } from 'roarr'; import { getImageDTO } from 'services/api/endpoints/images'; +const TYPE = 'image'; + export class CanvasImageRenderer { - static TYPE = 'image'; - static GROUP_NAME = `${CanvasImageRenderer.TYPE}_group`; - static IMAGE_NAME = `${CanvasImageRenderer.TYPE}_image`; - static PLACEHOLDER_GROUP_NAME = `${CanvasImageRenderer.TYPE}_placeholder-group`; - static PLACEHOLDER_RECT_NAME = `${CanvasImageRenderer.TYPE}_placeholder-rect`; - static PLACEHOLDER_TEXT_NAME = `${CanvasImageRenderer.TYPE}_placeholder-text`; + static GROUP_NAME = `${TYPE}_group`; + static IMAGE_NAME = `${TYPE}_image`; + static PLACEHOLDER_GROUP_NAME = `${TYPE}_placeholder-group`; + static PLACEHOLDER_RECT_NAME = `${TYPE}_placeholder-rect`; + static PLACEHOLDER_TEXT_NAME = `${TYPE}_placeholder-text`; + + readonly type = TYPE; id: string; + path: string[]; parent: CanvasObjectRenderer | CanvasStagingArea; manager: CanvasManager; log: Logger; - getLoggingContext: GetLoggingContext; state: CanvasImageState; konva: { @@ -42,7 +46,7 @@ export class CanvasImageRenderer { this.id = id; this.parent = parent; this.manager = parent.manager; - this.getLoggingContext = this.manager.buildGetLoggingContext(this); + this.path = this.parent.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); this.log.trace({ state }, 'Creating image'); @@ -197,11 +201,15 @@ export class CanvasImageRenderer { repr = () => { return { id: this.id, - type: CanvasImageRenderer.TYPE, + type: this.type, parent: this.parent.id, isLoading: this.isLoading, isError: this.isError, state: deepClone(this.state), }; }; + + getLoggingContext = (): JSONObject => { + return { ...this.parent.getLoggingContext(), path: this.path.join('.') }; + }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts index 2328b5beb26..7b9dd47ea29 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts @@ -1,23 +1,22 @@ +import type { JSONObject } from 'common/types'; import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; -import type { - CanvasEntityIdentifier, - CanvasLayerState, - CanvasV2State, - GetLoggingContext, -} from 'features/controlLayers/store/types'; +import type { CanvasEntityIdentifier, CanvasLayerState, CanvasV2State } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { get } from 'lodash-es'; import type { Logger } from 'roarr'; +const TYPE = 'layer'; + export class CanvasLayerAdapter { + readonly type = TYPE; + id: string; - type: CanvasLayerState['type']; + path: string[]; manager: CanvasManager; log: Logger; - getLoggingContext: GetLoggingContext; state: CanvasLayerState; @@ -31,9 +30,8 @@ export class CanvasLayerAdapter { constructor(state: CanvasLayerAdapter['state'], manager: CanvasLayerAdapter['manager']) { this.id = state.id; - this.type = state.type; this.manager = manager; - this.getLoggingContext = this.manager.buildGetLoggingContext(this); + this.path = this.manager.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); this.log.debug({ state }, 'Creating layer'); @@ -140,6 +138,10 @@ export class CanvasLayerAdapter { }; }; + getLoggingContext = (): JSONObject => { + return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; + }; + logDebugInfo(msg = 'Debug info') { const info = { repr: this.repr(), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 08c7b6b6ec8..496b08e8fdf 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -2,12 +2,6 @@ import type { Store } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import type { JSONObject } from 'common/types'; -import type { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine'; -import type { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine'; -import type { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; -import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; -import type { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; -import type { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; import { MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/controlLayers/konva/constants'; import { getImageDataTransparency, @@ -17,14 +11,7 @@ import { nanoid, } from 'features/controlLayers/konva/util'; import type { Extents, ExtentsResult, GetBboxTask, WorkerLogMessage } from 'features/controlLayers/konva/worker'; -import type { - CanvasV2State, - Coordinate, - Dimensions, - GenerationMode, - GetLoggingContext, - Rect, -} from 'features/controlLayers/store/types'; +import type { CanvasV2State, Coordinate, Dimensions, GenerationMode, Rect } from 'features/controlLayers/store/types'; import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers'; import type Konva from 'konva'; import { clamp } from 'lodash-es'; @@ -39,13 +26,17 @@ import { CanvasControlAdapter } from './CanvasControlAdapter'; import { CanvasLayerAdapter } from './CanvasLayerAdapter'; import { CanvasMaskAdapter } from './CanvasMaskAdapter'; import { CanvasPreview } from './CanvasPreview'; -import { CanvasStagingArea } from './CanvasStagingArea'; import { CanvasStateApi } from './CanvasStateApi'; import { setStageEventHandlers } from './events'; export const $canvasManager = atom(null); +const TYPE = 'manager'; export class CanvasManager { + readonly type = TYPE; + + id: string; + path: string[]; stage: Konva.Stage; container: HTMLDivElement; controlAdapters: Map; @@ -68,6 +59,8 @@ export class CanvasManager { _tasks: Map void }> = new Map(); constructor(stage: Konva.Stage, container: HTMLDivElement, store: Store) { + this.id = getPrefixedId(this.type); + this.path = [this.id]; this.stage = stage; this.container = container; this._store = store; @@ -79,8 +72,8 @@ export class CanvasManager { return { ...message, context: { - ...message.context, ...this.getLoggingContext(), + ...message.context, }, }; }); @@ -637,13 +630,13 @@ export class CanvasManager { } } - getLoggingContext() { + getLoggingContext = (): JSONObject => { return { - // timestamp: new Date().toISOString(), + path: this.path.join('.'), }; - } + }; - buildLogger(getContext: () => JSONObject): Logger { + buildLogger = (getContext: () => JSONObject): Logger => { return this.log.child((message) => { return { ...message, @@ -653,49 +646,6 @@ export class CanvasManager { }, }; }); - } - - buildGetLoggingContext = ( - instance: - | CanvasBrushLineRenderer - | CanvasEraserLineRenderer - | CanvasRectRenderer - | CanvasImageRenderer - | CanvasTransformer - | CanvasObjectRenderer - | CanvasLayerAdapter - | CanvasMaskAdapter - | CanvasStagingArea - ): GetLoggingContext => { - if ( - instance instanceof CanvasLayerAdapter || - instance instanceof CanvasStagingArea || - instance instanceof CanvasMaskAdapter - ) { - return (extra?: JSONObject): JSONObject => { - return { - ...instance.manager.getLoggingContext(), - entityId: instance.id, - ...extra, - }; - }; - } else if (instance instanceof CanvasObjectRenderer) { - return (extra?: JSONObject): JSONObject => { - return { - ...instance.parent.getLoggingContext(), - rendererId: instance.id, - ...extra, - }; - }; - } else { - return (extra?: JSONObject): JSONObject => { - return { - ...instance.parent.getLoggingContext(), - objectId: instance.id, - ...extra, - }; - }; - } }; logDebugInfo() { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts index eae7ff8ccf4..28b8e9ac0c9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts @@ -1,3 +1,4 @@ +import type { JSONObject } from 'common/types'; import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; @@ -7,7 +8,6 @@ import type { CanvasInpaintMaskState, CanvasRegionalGuidanceState, CanvasV2State, - GetLoggingContext, } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { get } from 'lodash-es'; @@ -15,10 +15,10 @@ import type { Logger } from 'roarr'; export class CanvasMaskAdapter { id: string; + path: string[]; type: CanvasInpaintMaskState['type'] | CanvasRegionalGuidanceState['type']; manager: CanvasManager; log: Logger; - getLoggingContext: GetLoggingContext; state: CanvasInpaintMaskState | CanvasRegionalGuidanceState; maskOpacity: number; @@ -36,7 +36,7 @@ export class CanvasMaskAdapter { this.id = state.id; this.type = state.type; this.manager = manager; - this.getLoggingContext = this.manager.buildGetLoggingContext(this); + this.path = this.manager.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); this.log.debug({ state }, 'Creating mask'); @@ -146,4 +146,8 @@ export class CanvasMaskAdapter { state: deepClone(this.state), }; }; + + getLoggingContext = (): JSONObject => { + return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; + }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index 55aef79d171..acef42c850e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -9,15 +9,15 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; import { getPrefixedId, konvaNodeToBlob, konvaNodeToImageData, previewBlob } from 'features/controlLayers/konva/util'; -import { - type CanvasBrushLineState, - type CanvasEraserLineState, - type CanvasImageState, - type CanvasRectState, - imageDTOToImageObject, - type Rect, - type RgbColor, +import type { + CanvasBrushLineState, + CanvasEraserLineState, + CanvasImageState, + CanvasRectState, + Rect, + RgbColor, } from 'features/controlLayers/store/types'; +import { imageDTOToImageObject } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; import { uploadImage } from 'services/api/endpoints/images'; @@ -33,19 +33,22 @@ type AnyObjectRenderer = CanvasBrushLineRenderer | CanvasEraserLineRenderer | Ca */ type AnyObjectState = CanvasBrushLineState | CanvasEraserLineState | CanvasImageState | CanvasRectState; +const TYPE = 'object_renderer'; + /** * Handles rendering of objects for a canvas entity. */ export class CanvasObjectRenderer { - static TYPE = 'object_renderer'; - static KONVA_OBJECT_GROUP_NAME = `${CanvasObjectRenderer.TYPE}:object_group`; - static KONVA_COMPOSITING_RECT_NAME = `${CanvasObjectRenderer.TYPE}:compositing_rect`; + static KONVA_OBJECT_GROUP_NAME = `${TYPE}:object_group`; + static KONVA_COMPOSITING_RECT_NAME = `${TYPE}:compositing_rect`; + + readonly type = TYPE; id: string; + path: string[]; parent: CanvasLayerAdapter | CanvasMaskAdapter; manager: CanvasManager; log: Logger; - getLoggingContext: (extra?: JSONObject) => JSONObject; /** * A set of subscriptions that should be cleaned up when the transformer is destroyed. @@ -90,10 +93,10 @@ export class CanvasObjectRenderer { }; constructor(parent: CanvasLayerAdapter | CanvasMaskAdapter) { - this.id = getPrefixedId(CanvasObjectRenderer.TYPE); + this.id = getPrefixedId(TYPE); this.parent = parent; + this.path = this.parent.path.concat(this.id); this.manager = parent.manager; - this.getLoggingContext = this.manager.buildGetLoggingContext(this); this.log = this.manager.buildLogger(this.getLoggingContext); this.log.trace('Creating object renderer'); @@ -414,10 +417,14 @@ export class CanvasObjectRenderer { repr = () => { return { id: this.id, - type: CanvasObjectRenderer.TYPE, + type: this.type, parent: this.parent.id, renderers: Array.from(this.renderers.values()).map((renderer) => renderer.repr()), buffer: deepClone(this.buffer), }; }; + + getLoggingContext = (): JSONObject => { + return { ...this.parent.getLoggingContext(), path: this.path.join('.') }; + }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts index 6231e8b23f1..19e8b8bd76e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts @@ -1,21 +1,25 @@ +import type { JSONObject } from 'common/types'; import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; -import type { CanvasRectState, GetLoggingContext } from 'features/controlLayers/store/types'; +import type { CanvasRectState } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; +const TYPE = 'rect'; + export class CanvasRectRenderer { - static TYPE = 'rect'; - static GROUP_NAME = `${CanvasRectRenderer.TYPE}_group`; - static RECT_NAME = `${CanvasRectRenderer.TYPE}_rect`; + static GROUP_NAME = `${TYPE}_group`; + static RECT_NAME = `${TYPE}_rect`; + + readonly type = TYPE; id: string; + path: string[]; parent: CanvasObjectRenderer; manager: CanvasManager; log: Logger; - getLoggingContext: GetLoggingContext; state: CanvasRectState; konva: { @@ -29,7 +33,7 @@ export class CanvasRectRenderer { this.id = id; this.parent = parent; this.manager = parent.manager; - this.getLoggingContext = this.manager.buildGetLoggingContext(this); + this.path = this.parent.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); this.log.trace({ state }, 'Creating rect'); @@ -76,10 +80,14 @@ export class CanvasRectRenderer { repr() { return { id: this.id, - type: CanvasRectRenderer.TYPE, + type: this.type, parent: this.parent.id, isFirstRender: this.isFirstRender, state: deepClone(this.state), }; } + + getLoggingContext = (): JSONObject => { + return { ...this.parent.getLoggingContext(), path: this.path.join('.') }; + }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts index 81c134419c5..ad72042ec47 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts @@ -1,20 +1,24 @@ +import type { JSONObject } from 'common/types'; import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasPreview } from 'features/controlLayers/konva/CanvasPreview'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import type { GetLoggingContext, StagingAreaImage } from 'features/controlLayers/store/types'; +import type { StagingAreaImage } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; +const TYPE = 'staging_area'; + export class CanvasStagingArea { - static TYPE = 'staging_area'; - static GROUP_NAME = `${CanvasStagingArea.TYPE}_group`; + static GROUP_NAME = `${TYPE}_group`; + + readonly type = TYPE; id: string; + path: string[]; parent: CanvasPreview; manager: CanvasManager; log: Logger; - getLoggingContext: GetLoggingContext; konva: { group: Konva.Group }; @@ -27,10 +31,10 @@ export class CanvasStagingArea { subscriptions: Set<() => void> = new Set(); constructor(parent: CanvasPreview) { - this.id = getPrefixedId(CanvasStagingArea.TYPE); + this.id = getPrefixedId(TYPE); this.parent = parent; this.manager = this.parent.manager; - this.getLoggingContext = this.manager.buildGetLoggingContext(this); + this.path = this.manager.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); this.log.debug('Creating staging area'); @@ -103,8 +107,12 @@ export class CanvasStagingArea { repr = () => { return { id: this.id, - type: CanvasStagingArea.TYPE, + type: this.type, selectedImage: this.selectedImage, }; }; + + getLoggingContext = (): JSONObject => { + return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; + }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index ea4d8793ca7..75d4bf95cc6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -1,8 +1,9 @@ +import type { JSONObject } from 'common/types'; import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; import { getEmptyRect, getPrefixedId } from 'features/controlLayers/konva/util'; -import type { Coordinate, GetLoggingContext, Rect } from 'features/controlLayers/store/types'; +import type { Coordinate, Rect } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { debounce, get } from 'lodash-es'; import type { Logger } from 'roarr'; @@ -37,11 +38,13 @@ export class CanvasTransformer { static ROTATE_ANCHOR_STROKE_COLOR = 'hsl(200 76% 40% / 1)'; // invokeBlue.700 static ROTATE_ANCHOR_SIZE = 12; + readonly type = CanvasTransformer.TYPE; + id: string; + path: string[]; parent: CanvasLayerAdapter | CanvasMaskAdapter; manager: CanvasManager; log: Logger; - getLoggingContext: GetLoggingContext; /** * The rect of the parent, _including_ transparent regions. @@ -100,8 +103,7 @@ export class CanvasTransformer { this.id = getPrefixedId(CanvasTransformer.TYPE); this.parent = parent; this.manager = parent.manager; - - this.getLoggingContext = this.manager.buildGetLoggingContext(this); + this.path = this.parent.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); this.konva = { @@ -729,7 +731,7 @@ export class CanvasTransformer { repr = () => { return { id: this.id, - type: CanvasTransformer.TYPE, + type: this.type, mode: this.interactionMode, isTransformEnabled: this.isTransformEnabled, isDragEnabled: this.isDragEnabled, @@ -749,4 +751,8 @@ export class CanvasTransformer { this.konva.transformer.destroy(); this.konva.proxyRect.destroy(); }; + + getLoggingContext = (): JSONObject => { + return { ...this.parent.getLoggingContext(), path: this.path.join('.') }; + }; } From 9708fc5d6c9d48ddf9151774c992c54f0937cafb Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 12 Aug 2024 22:02:46 +1000 Subject: [PATCH 350/678] feat(ui): more better logging & naming --- .../common/CanvasEntityContainer.tsx | 2 +- .../controlLayers/konva/CanvasBackground.ts | 10 +++- .../controlLayers/konva/CanvasBbox.ts | 56 ++++++++++++------- .../controlLayers/konva/CanvasBrushLine.ts | 12 +--- .../controlLayers/konva/CanvasEraserLine.ts | 11 +--- .../controlLayers/konva/CanvasImage.ts | 25 +++------ .../controlLayers/konva/CanvasLayerAdapter.ts | 9 +-- .../controlLayers/konva/CanvasMaskAdapter.ts | 8 +-- .../konva/CanvasObjectRenderer.ts | 15 ++--- .../konva/CanvasProgressImage.ts | 24 ++++++-- .../controlLayers/konva/CanvasRect.ts | 11 +--- .../controlLayers/konva/CanvasStagingArea.ts | 10 +--- .../controlLayers/konva/CanvasStateApi.ts | 2 +- .../controlLayers/konva/CanvasTool.ts | 50 ++++++++--------- .../controlLayers/konva/CanvasTransformer.ts | 15 ++--- 15 files changed, 125 insertions(+), 135 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx index 5cc84f8a327..2e29a498176 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx @@ -1,8 +1,8 @@ import { Flex } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { useEntitySelectionColor } from 'features/controlLayers/hooks/useEntitySelectionColor'; import { useEntityIsSelected } from 'features/controlLayers/hooks/useEntityIsSelected'; +import { useEntitySelectionColor } from 'features/controlLayers/hooks/useEntitySelectionColor'; import { entitySelected } from 'features/controlLayers/store/canvasV2Slice'; import type { PropsWithChildren } from 'react'; import { memo, useCallback } from 'react'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts index 1143b5f1303..6dc5462ff98 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts @@ -1,13 +1,15 @@ import { getArbitraryBaseColor } from '@invoke-ai/ui-library'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { getPrefixedId } from 'features/controlLayers/konva/util'; import Konva from 'konva'; export class CanvasBackground { - static BASE_NAME = 'background'; - static LAYER_NAME = `${CanvasBackground.BASE_NAME}_layer`; + readonly type = 'background_grid'; + static GRID_LINE_COLOR_COARSE = getArbitraryBaseColor(27); static GRID_LINE_COLOR_FINE = getArbitraryBaseColor(18); + id: string; manager: CanvasManager; konva: { @@ -20,8 +22,10 @@ export class CanvasBackground { subscriptions: Set<() => void> = new Set(); constructor(manager: CanvasManager) { + this.id = getPrefixedId(this.type); this.manager = manager; - this.konva = { layer: new Konva.Layer({ name: CanvasBackground.LAYER_NAME, listening: false }) }; + this.konva = { layer: new Konva.Layer({ name: `${this.type}:layer`, listening: false }) }; + this.subscriptions.add( this.manager.stateApi.$stageAttrs.listen(() => { this.render(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts index 18defa079f4..983e574b639 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts @@ -1,31 +1,35 @@ +import type { JSONObject } from 'common/types'; import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMultiple'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasPreview } from 'features/controlLayers/konva/CanvasPreview'; +import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { Rect } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { atom } from 'nanostores'; +import type { Logger } from 'roarr'; import { assert } from 'tsafe'; +const ALL_ANCHORS: string[] = [ + 'top-left', + 'top-center', + 'top-right', + 'middle-right', + 'middle-left', + 'bottom-left', + 'bottom-center', + 'bottom-right', +]; +const CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right']; +const NO_ANCHORS: string[] = []; + export class CanvasBbox { - static BASE_NAME = 'bbox'; - static GROUP_NAME = `${CanvasBbox.BASE_NAME}_group`; - static RECT_NAME = `${CanvasBbox.BASE_NAME}_rect`; - static TRANSFORMER_NAME = `${CanvasBbox.BASE_NAME}_transformer`; - static ALL_ANCHORS: string[] = [ - 'top-left', - 'top-center', - 'top-right', - 'middle-right', - 'middle-left', - 'bottom-left', - 'bottom-center', - 'bottom-right', - ]; - static CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right']; - static NO_ANCHORS: string[] = []; + readonly type = 'generation_bbox'; + id: string; + path: string[]; parent: CanvasPreview; manager: CanvasManager; + log: Logger; konva: { group: Konva.Group; @@ -34,19 +38,25 @@ export class CanvasBbox { }; constructor(parent: CanvasPreview) { + this.id = getPrefixedId(this.type); this.parent = parent; this.manager = this.parent.manager; + this.path = this.manager.path.concat(this.id); + this.log = this.manager.buildLogger(this.getLoggingContext); + + this.log.trace('Creating bbox'); + // Create a stash to hold onto the last aspect ratio of the bbox - this allows for locking the aspect ratio when // transforming the bbox. const bbox = this.manager.stateApi.getBbox(); const $aspectRatioBuffer = atom(bbox.rect.width / bbox.rect.height); this.konva = { - group: new Konva.Group({ name: CanvasBbox.GROUP_NAME, listening: false }), + group: new Konva.Group({ name: `${this.type}:group`, listening: false }), // Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully // transparent rect for this purpose. rect: new Konva.Rect({ - name: CanvasBbox.RECT_NAME, + name: `${this.type}:rect`, listening: false, strokeEnabled: false, draggable: true, @@ -56,7 +66,7 @@ export class CanvasBbox { height: bbox.rect.height, }), transformer: new Konva.Transformer({ - name: CanvasBbox.TRANSFORMER_NAME, + name: `${this.type}:transformer`, borderDash: [5, 5], borderStroke: 'rgba(212,216,234,1)', borderEnabled: true, @@ -160,7 +170,7 @@ export class CanvasBbox { // If shift is held and we are resizing from a corner, retain aspect ratio - needs special handling. We skip this // if alt/opt is held - this requires math too big for my brain. - if (shift && CanvasBbox.CORNER_ANCHORS.includes(anchor) && !alt) { + if (shift && CORNER_ANCHORS.includes(anchor) && !alt) { // Fit the bbox to the last aspect ratio let fittedWidth = Math.sqrt(width * height * $aspectRatioBuffer.get()); let fittedHeight = fittedWidth / $aspectRatioBuffer.get(); @@ -237,7 +247,11 @@ export class CanvasBbox { }); this.konva.transformer.setAttrs({ listening: toolState.selected === 'bbox', - enabledAnchors: toolState.selected === 'bbox' ? CanvasBbox.ALL_ANCHORS : CanvasBbox.NO_ANCHORS, + enabledAnchors: toolState.selected === 'bbox' ? ALL_ANCHORS : NO_ANCHORS, }); } + + getLoggingContext = (): JSONObject => { + return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; + }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts index 68d4e9ce2f6..4cb6ca52b03 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts @@ -7,13 +7,8 @@ import type { CanvasBrushLineState } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; -const TYPE = 'brush_line'; - export class CanvasBrushLineRenderer { - static GROUP_NAME = `${TYPE}_group`; - static LINE_NAME = `${TYPE}_line`; - - readonly type = TYPE; + readonly type = 'brush_line_renderer'; id: string; path: string[]; @@ -33,19 +28,18 @@ export class CanvasBrushLineRenderer { this.parent = parent; this.manager = parent.manager; this.path = this.parent.path.concat(this.id); - this.log = this.manager.buildLogger(this.getLoggingContext); this.log.trace({ state }, 'Creating brush line'); this.konva = { group: new Konva.Group({ - name: CanvasBrushLineRenderer.GROUP_NAME, + name: `${this.type}:group`, clip, listening: false, }), line: new Konva.Line({ - name: CanvasBrushLineRenderer.LINE_NAME, + name: `${this.type}:line`, listening: false, shadowForStrokeEnabled: false, strokeWidth, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts index 953c94144a3..4417348557e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts @@ -8,13 +8,8 @@ import { RGBA_RED } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; -const TYPE = 'eraser_line'; - export class CanvasEraserLineRenderer { - static GROUP_NAME = `${TYPE}_group`; - static LINE_NAME = `${TYPE}_line`; - - readonly type = TYPE; + readonly type = 'eraser_line_renderer'; id: string; path: string[]; @@ -40,12 +35,12 @@ export class CanvasEraserLineRenderer { this.konva = { group: new Konva.Group({ - name: CanvasEraserLineRenderer.GROUP_NAME, + name: `${this.type}:group`, clip, listening: false, }), line: new Konva.Line({ - name: CanvasEraserLineRenderer.LINE_NAME, + name: `${this.type}:line`, listening: false, shadowForStrokeEnabled: false, strokeWidth, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts index 30b9c75df65..9b691dcdb2d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -1,6 +1,7 @@ import { Mutex } from 'async-mutex'; import type { JSONObject } from 'common/types'; import { deepClone } from 'common/util/deepClone'; +import type { CanvasFilter } from 'features/controlLayers/konva/CanvasFilter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import type { CanvasStagingArea } from 'features/controlLayers/konva/CanvasStagingArea'; @@ -12,20 +13,12 @@ import Konva from 'konva'; import type { Logger } from 'roarr'; import { getImageDTO } from 'services/api/endpoints/images'; -const TYPE = 'image'; - export class CanvasImageRenderer { - static GROUP_NAME = `${TYPE}_group`; - static IMAGE_NAME = `${TYPE}_image`; - static PLACEHOLDER_GROUP_NAME = `${TYPE}_placeholder-group`; - static PLACEHOLDER_RECT_NAME = `${TYPE}_placeholder-rect`; - static PLACEHOLDER_TEXT_NAME = `${TYPE}_placeholder-text`; - - readonly type = TYPE; + readonly type = 'image_renderer'; id: string; path: string[]; - parent: CanvasObjectRenderer | CanvasStagingArea; + parent: CanvasObjectRenderer | CanvasStagingArea | CanvasFilter; manager: CanvasManager; log: Logger; @@ -40,7 +33,7 @@ export class CanvasImageRenderer { isError: boolean = false; mutex = new Mutex(); - constructor(state: CanvasImageState, parent: CanvasObjectRenderer | CanvasStagingArea) { + constructor(state: CanvasImageState, parent: CanvasObjectRenderer | CanvasStagingArea | CanvasFilter) { const { id, image } = state; const { width, height } = image; this.id = id; @@ -52,18 +45,18 @@ export class CanvasImageRenderer { this.log.trace({ state }, 'Creating image'); this.konva = { - group: new Konva.Group({ name: CanvasImageRenderer.GROUP_NAME, listening: false }), + group: new Konva.Group({ name: `${this.type}:group`, listening: false }), placeholder: { - group: new Konva.Group({ name: CanvasImageRenderer.PLACEHOLDER_GROUP_NAME, listening: false }), + group: new Konva.Group({ name: `${this.type}:placeholder_group`, listening: false }), rect: new Konva.Rect({ - name: CanvasImageRenderer.PLACEHOLDER_RECT_NAME, + name: `${this.type}:placeholder_rect`, fill: 'hsl(220 12% 45% / 1)', // 'base.500' width, height, listening: false, }), text: new Konva.Text({ - name: CanvasImageRenderer.PLACEHOLDER_TEXT_NAME, + name: `${this.type}:placeholder_text`, fill: 'hsl(220 12% 10% / 1)', // 'base.900' width, height, @@ -135,7 +128,7 @@ export class CanvasImageRenderer { } else { this.log.trace('Creating new Konva image'); this.konva.image = new Konva.Image({ - name: CanvasImageRenderer.IMAGE_NAME, + name: `${this.type}:image`, listening: false, image: this.imageElement, width, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts index 7b9dd47ea29..5217e8f6878 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts @@ -8,10 +8,8 @@ import Konva from 'konva'; import { get } from 'lodash-es'; import type { Logger } from 'roarr'; -const TYPE = 'layer'; - export class CanvasLayerAdapter { - readonly type = TYPE; + readonly type = 'layer_adapter'; id: string; path: string[]; @@ -34,6 +32,7 @@ export class CanvasLayerAdapter { this.path = this.manager.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); this.log.debug({ state }, 'Creating layer'); + this.state = state; this.konva = { layer: new Konva.Layer({ @@ -48,15 +47,13 @@ export class CanvasLayerAdapter { this.renderer = new CanvasObjectRenderer(this); this.transformer = new CanvasTransformer(this); - - this.state = state; } /** * Get this entity's entity identifier */ getEntityIdentifier = (): CanvasEntityIdentifier => { - return { id: this.id, type: this.type }; + return { id: this.id, type: this.state.type }; }; destroy = (): void => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts index 28b8e9ac0c9..550e15fa002 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts @@ -14,9 +14,10 @@ import { get } from 'lodash-es'; import type { Logger } from 'roarr'; export class CanvasMaskAdapter { + readonly type = 'mask_adapter'; + id: string; path: string[]; - type: CanvasInpaintMaskState['type'] | CanvasRegionalGuidanceState['type']; manager: CanvasManager; log: Logger; @@ -34,11 +35,11 @@ export class CanvasMaskAdapter { constructor(state: CanvasMaskAdapter['state'], manager: CanvasMaskAdapter['manager']) { this.id = state.id; - this.type = state.type; this.manager = manager; this.path = this.manager.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); this.log.debug({ state }, 'Creating mask'); + this.state = state; this.konva = { layer: new Konva.Layer({ @@ -54,7 +55,6 @@ export class CanvasMaskAdapter { this.renderer = new CanvasObjectRenderer(this); this.transformer = new CanvasTransformer(this); - this.state = state; this.maskOpacity = this.manager.stateApi.getMaskOpacity(); } @@ -62,7 +62,7 @@ export class CanvasMaskAdapter { * Get this entity's entity identifier */ getEntityIdentifier = (): CanvasEntityIdentifier => { - return { id: this.id, type: this.type }; + return { id: this.id, type: this.state.type }; }; destroy = (): void => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index acef42c850e..5b95330245e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -33,16 +33,11 @@ type AnyObjectRenderer = CanvasBrushLineRenderer | CanvasEraserLineRenderer | Ca */ type AnyObjectState = CanvasBrushLineState | CanvasEraserLineState | CanvasImageState | CanvasRectState; -const TYPE = 'object_renderer'; - /** * Handles rendering of objects for a canvas entity. */ export class CanvasObjectRenderer { - static KONVA_OBJECT_GROUP_NAME = `${TYPE}:object_group`; - static KONVA_COMPOSITING_RECT_NAME = `${TYPE}:compositing_rect`; - - readonly type = TYPE; + readonly type = 'object_renderer'; id: string; path: string[]; @@ -93,7 +88,7 @@ export class CanvasObjectRenderer { }; constructor(parent: CanvasLayerAdapter | CanvasMaskAdapter) { - this.id = getPrefixedId(TYPE); + this.id = getPrefixedId(this.type); this.parent = parent; this.path = this.parent.path.concat(this.id); this.manager = parent.manager; @@ -101,15 +96,15 @@ export class CanvasObjectRenderer { this.log.trace('Creating object renderer'); this.konva = { - objectGroup: new Konva.Group({ name: CanvasObjectRenderer.KONVA_OBJECT_GROUP_NAME, listening: false }), + objectGroup: new Konva.Group({ name: `${this.type}:object_group`, listening: false }), compositingRect: null, }; this.parent.konva.layer.add(this.konva.objectGroup); - if (this.parent.type === 'inpaint_mask' || this.parent.type === 'regional_guidance') { + if (this.parent.state.type === 'inpaint_mask' || this.parent.state.type === 'regional_guidance') { this.konva.compositingRect = new Konva.Rect({ - name: CanvasObjectRenderer.KONVA_COMPOSITING_RECT_NAME, + name: `${this.type}:compositing_rect`, globalCompositeOperation: 'source-in', listening: false, strokeEnabled: false, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts index 64f16015b05..00c796b2c27 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts @@ -1,18 +1,20 @@ import { Mutex } from 'async-mutex'; +import type { JSONObject } from 'common/types'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasPreview } from 'features/controlLayers/konva/CanvasPreview'; import { getPrefixedId, loadImage } from 'features/controlLayers/konva/util'; import Konva from 'konva'; +import type { Logger } from 'roarr'; import type { InvocationDenoiseProgressEvent } from 'services/events/types'; export class CanvasProgressImage { - static NAME_PREFIX = 'progress-image'; - static GROUP_NAME = `${CanvasProgressImage.NAME_PREFIX}_group`; - static IMAGE_NAME = `${CanvasProgressImage.NAME_PREFIX}_image`; + readonly type = 'progress_image'; id: string; + path: string[]; parent: CanvasPreview; manager: CanvasManager; + log: Logger; /** * A set of subscriptions that should be cleaned up when the transformer is destroyed. @@ -33,11 +35,16 @@ export class CanvasProgressImage { mutex: Mutex = new Mutex(); constructor(parent: CanvasPreview) { - this.id = getPrefixedId(CanvasProgressImage.NAME_PREFIX); + this.id = getPrefixedId(this.type); this.parent = parent; this.manager = parent.manager; + this.path = this.manager.path.concat(this.id); + this.log = this.manager.buildLogger(this.getLoggingContext); + + this.log.trace('Creating progress image'); + this.konva = { - group: new Konva.Group({ name: CanvasProgressImage.GROUP_NAME, listening: false }), + group: new Konva.Group({ name: `${this.type}:group`, listening: false }), image: null, }; @@ -86,7 +93,7 @@ export class CanvasProgressImage { }); } else { this.konva.image = new Konva.Image({ - name: CanvasProgressImage.IMAGE_NAME, + name: `${this.type}:image`, listening: false, image: this.imageElement, x, @@ -106,9 +113,14 @@ export class CanvasProgressImage { }; destroy = () => { + this.log.trace('Destroying progress image'); for (const unsubscribe of this.subscriptions) { unsubscribe(); } this.konva.group.destroy(); }; + + getLoggingContext = (): JSONObject => { + return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; + }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts index 19e8b8bd76e..9bb7340a4f9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts @@ -7,13 +7,8 @@ import type { CanvasRectState } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; -const TYPE = 'rect'; - export class CanvasRectRenderer { - static GROUP_NAME = `${TYPE}_group`; - static RECT_NAME = `${TYPE}_rect`; - - readonly type = TYPE; + readonly type = 'rect_renderer'; id: string; path: string[]; @@ -38,9 +33,9 @@ export class CanvasRectRenderer { this.log.trace({ state }, 'Creating rect'); this.konva = { - group: new Konva.Group({ name: CanvasRectRenderer.GROUP_NAME, listening: false }), + group: new Konva.Group({ name: `${this.type}:group`, listening: false }), rect: new Konva.Rect({ - name: CanvasRectRenderer.RECT_NAME, + name: `${this.type}:rect`, ...rect, listening: false, fill: rgbaColorToString(color), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts index ad72042ec47..c58186a14dd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts @@ -7,12 +7,8 @@ import type { StagingAreaImage } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; -const TYPE = 'staging_area'; - export class CanvasStagingArea { - static GROUP_NAME = `${TYPE}_group`; - - readonly type = TYPE; + readonly type = 'staging_area'; id: string; path: string[]; @@ -31,14 +27,14 @@ export class CanvasStagingArea { subscriptions: Set<() => void> = new Set(); constructor(parent: CanvasPreview) { - this.id = getPrefixedId(TYPE); + this.id = getPrefixedId(this.type); this.parent = parent; this.manager = this.parent.manager; this.path = this.manager.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); this.log.debug('Creating staging area'); - this.konva = { group: new Konva.Group({ name: CanvasStagingArea.GROUP_NAME, listening: false }) }; + this.konva = { group: new Konva.Group({ name: `${this.type}:group`, listening: false }) }; this.image = null; this.selectedImage = null; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index 0027bdf589e..c4397a73367 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -209,7 +209,7 @@ export class CanvasStateApi { entityAdapter = this.manager.inpaintMask; } - if (entityState && entityAdapter && entityState.type === entityAdapter.type) { + if (entityState && entityAdapter) { return { id: entityState.id, type: entityState.type, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts index 9eaa0bcee89..7821d70dc11 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts @@ -1,3 +1,4 @@ +import type { JSONObject } from 'common/types'; import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasPreview } from 'features/controlLayers/konva/CanvasPreview'; @@ -6,28 +7,19 @@ import { BRUSH_BORDER_OUTER_COLOR, BRUSH_ERASER_BORDER_WIDTH, } from 'features/controlLayers/konva/constants'; -import { alignCoordForTool } from 'features/controlLayers/konva/util'; +import { alignCoordForTool, getPrefixedId } from 'features/controlLayers/konva/util'; import Konva from 'konva'; +import type { Logger } from 'roarr'; export class CanvasTool { - static NAME_PREFIX = 'tool'; - - static GROUP_NAME = `${CanvasTool.NAME_PREFIX}_group`; - - static BRUSH_NAME_PREFIX = `${CanvasTool.NAME_PREFIX}_brush`; - static BRUSH_GROUP_NAME = `${CanvasTool.BRUSH_NAME_PREFIX}_group`; - static BRUSH_FILL_CIRCLE_NAME = `${CanvasTool.BRUSH_NAME_PREFIX}_fill-circle`; - static BRUSH_INNER_BORDER_CIRCLE_NAME = `${CanvasTool.BRUSH_NAME_PREFIX}_inner-border-circle`; - static BRUSH_OUTER_BORDER_CIRCLE_NAME = `${CanvasTool.BRUSH_NAME_PREFIX}_outer-border-circle`; - - static ERASER_NAME_PREFIX = `${CanvasTool.NAME_PREFIX}_eraser`; - static ERASER_GROUP_NAME = `${CanvasTool.ERASER_NAME_PREFIX}_group`; - static ERASER_FILL_CIRCLE_NAME = `${CanvasTool.ERASER_NAME_PREFIX}_fill-circle`; - static ERASER_INNER_BORDER_CIRCLE_NAME = `${CanvasTool.ERASER_NAME_PREFIX}_inner-border-circle`; - static ERASER_OUTER_BORDER_CIRCLE_NAME = `${CanvasTool.ERASER_NAME_PREFIX}_outer-border-circle`; + readonly type = 'tool_preview'; + id: string; + path: string[]; parent: CanvasPreview; manager: CanvasManager; + log: Logger; + konva: { group: Konva.Group; brush: { @@ -50,26 +42,29 @@ export class CanvasTool { subscriptions: Set<() => void> = new Set(); constructor(parent: CanvasPreview) { + this.id = getPrefixedId(this.type); this.parent = parent; this.manager = this.parent.manager; + this.path = this.manager.path.concat(this.id); + this.log = this.manager.buildLogger(this.getLoggingContext); this.konva = { - group: new Konva.Group({ name: CanvasTool.GROUP_NAME }), + group: new Konva.Group({ name: `${this.type}:group` }), brush: { - group: new Konva.Group({ name: CanvasTool.BRUSH_GROUP_NAME }), + group: new Konva.Group({ name: `${this.type}:brush_group` }), fillCircle: new Konva.Circle({ - name: CanvasTool.BRUSH_FILL_CIRCLE_NAME, + name: `${this.type}:brush_fill_circle`, listening: false, strokeEnabled: false, }), innerBorderCircle: new Konva.Circle({ - name: CanvasTool.BRUSH_INNER_BORDER_CIRCLE_NAME, + name: `${this.type}:brush_inner_border_circle`, listening: false, stroke: BRUSH_BORDER_INNER_COLOR, strokeWidth: BRUSH_ERASER_BORDER_WIDTH, strokeEnabled: true, }), outerBorderCircle: new Konva.Circle({ - name: CanvasTool.BRUSH_OUTER_BORDER_CIRCLE_NAME, + name: `${this.type}:brush_outer_border_circle`, listening: false, stroke: BRUSH_BORDER_OUTER_COLOR, strokeWidth: BRUSH_ERASER_BORDER_WIDTH, @@ -77,23 +72,23 @@ export class CanvasTool { }), }, eraser: { - group: new Konva.Group({ name: CanvasTool.ERASER_GROUP_NAME }), + group: new Konva.Group({ name: `${this.type}:eraser_group` }), fillCircle: new Konva.Circle({ - name: CanvasTool.ERASER_FILL_CIRCLE_NAME, + name: `${this.type}:eraser_fill_circle`, listening: false, strokeEnabled: false, fill: 'white', globalCompositeOperation: 'destination-out', }), innerBorderCircle: new Konva.Circle({ - name: CanvasTool.ERASER_INNER_BORDER_CIRCLE_NAME, + name: `${this.type}:eraser_inner_border_circle`, listening: false, stroke: BRUSH_BORDER_INNER_COLOR, strokeWidth: BRUSH_ERASER_BORDER_WIDTH, strokeEnabled: true, }), outerBorderCircle: new Konva.Circle({ - name: CanvasTool.ERASER_OUTER_BORDER_CIRCLE_NAME, + name: `${this.type}:eraser_outer_border_circle`, listening: false, stroke: BRUSH_BORDER_OUTER_COLOR, strokeWidth: BRUSH_ERASER_BORDER_WIDTH, @@ -160,6 +155,7 @@ export class CanvasTool { const isMouseDown = this.manager.stateApi.$isMouseDown.get(); const tool = toolState.selected; + console.log(selectedEntity); const isDrawableEntity = selectedEntity?.state.type === 'regional_guidance' || selectedEntity?.state.type === 'layer' || @@ -258,4 +254,8 @@ export class CanvasTool { } } } + + getLoggingContext = (): JSONObject => { + return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; + }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index 75d4bf95cc6..7b71014c35f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -17,10 +17,7 @@ import type { Logger } from 'roarr'; * It renders an outline when dragging and resizing the entity, with transform anchors for resizing and rotation. */ export class CanvasTransformer { - static TYPE = 'entity_transformer'; - static KONVA_TRANSFORMER_NAME = `${CanvasTransformer.TYPE}:transformer`; - static KONVA_PROXY_RECT_NAME = `${CanvasTransformer.TYPE}:proxy_rect`; - static KONVA_OUTLINE_RECT_NAME = `${CanvasTransformer.TYPE}:outline_rect`; + readonly type = 'entity_transformer'; static RECT_CALC_DEBOUNCE_MS = 300; static OUTLINE_PADDING = 0; @@ -38,8 +35,6 @@ export class CanvasTransformer { static ROTATE_ANCHOR_STROKE_COLOR = 'hsl(200 76% 40% / 1)'; // invokeBlue.700 static ROTATE_ANCHOR_SIZE = 12; - readonly type = CanvasTransformer.TYPE; - id: string; path: string[]; parent: CanvasLayerAdapter | CanvasMaskAdapter; @@ -100,7 +95,7 @@ export class CanvasTransformer { }; constructor(parent: CanvasLayerAdapter | CanvasMaskAdapter) { - this.id = getPrefixedId(CanvasTransformer.TYPE); + this.id = getPrefixedId(this.type); this.parent = parent; this.manager = parent.manager; this.path = this.parent.path.concat(this.id); @@ -110,13 +105,13 @@ export class CanvasTransformer { outlineRect: new Konva.Rect({ listening: false, draggable: false, - name: CanvasTransformer.KONVA_OUTLINE_RECT_NAME, + name: `${this.type}:outline_rect`, stroke: CanvasTransformer.OUTLINE_COLOR, perfectDrawEnabled: false, strokeHitEnabled: false, }), transformer: new Konva.Transformer({ - name: CanvasTransformer.KONVA_TRANSFORMER_NAME, + name: `${this.type}:transformer`, // Visibility and listening are managed via activate() and deactivate() visible: false, listening: false, @@ -235,7 +230,7 @@ export class CanvasTransformer { }, }), proxyRect: new Konva.Rect({ - name: CanvasTransformer.KONVA_PROXY_RECT_NAME, + name: `${this.type}:proxy_rect`, listening: false, draggable: true, }), From 500f151d96e59802240209ddcd4e00082e2ec0dd Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 13 Aug 2024 15:37:33 +1000 Subject: [PATCH 351/678] feat(ui): add contextmenu for canvas entities --- .../controlLayers/components/Layer/Layer.tsx | 7 ------ .../components/common/CanvasEntityHeader.tsx | 23 +++++++++++++++---- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx index d8f73cbaaa0..d68ae0dea08 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx @@ -1,5 +1,4 @@ import { Spacer, useDisclosure } from '@invoke-ai/ui-library'; -import IAIDroppable from 'common/components/IAIDroppable'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; @@ -9,7 +8,6 @@ import { LayerActionsMenu } from 'features/controlLayers/components/Layer/LayerA import { LayerSettings } from 'features/controlLayers/components/Layer/LayerSettings'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; -import type { AddLayerFromImageDropData } from 'features/dnd/types'; import { memo, useMemo } from 'react'; import { LayerOpacity } from './LayerOpacity'; @@ -21,10 +19,6 @@ type Props = { export const Layer = memo(({ id }: Props) => { const entityIdentifier = useMemo(() => ({ id, type: 'layer' }), [id]); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: false }); - const droppableData = useMemo( - () => ({ id, actionType: 'ADD_LAYER_IMAGE', context: { id } }), - [id] - ); return ( @@ -38,7 +32,6 @@ export const Layer = memo(({ id }: Props) => { {isOpen && } - ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx index e8aca8057ed..5fe02038614 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx @@ -1,12 +1,25 @@ import type { FlexProps } from '@invoke-ai/ui-library'; -import { Flex } from '@invoke-ai/ui-library'; -import { memo } from 'react'; +import { ContextMenu, Flex, MenuList } from '@invoke-ai/ui-library'; +import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/common/CanvasEntityActionMenuItems'; +import { memo, useCallback } from 'react'; export const CanvasEntityHeader = memo(({ children, ...rest }: FlexProps) => { + const renderMenu = useCallback(() => { + return ( + + + + ); + }, []); + return ( - - {children} - + + {(ref) => ( + + {children} + + )} + ); }); From c9dc61c3116cf334341f737beabef877f44d9a70 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 14 Aug 2024 18:10:51 +1000 Subject: [PATCH 352/678] feat(ui, app): use layer as control (wip) --- invokeai/app/services/events/events_common.py | 5 +- .../session_queue/session_queue_common.py | 8 +- .../frontend/web/src/app/hooks/useSocketIO.ts | 12 +- .../listeners/controlAdapterPreprocessor.ts | 4 +- .../web/src/app/store/nanostores/store.ts | 13 +- invokeai/frontend/web/src/app/store/store.ts | 3 +- .../frontend/web/src/app/store/storeHooks.ts | 6 +- .../frontend/web/src/app/types/invokeai.ts | 4 +- .../ControlAdapterProcessorConfig.tsx | 6 +- .../ControlAdapterProcessorTypeSelect.tsx | 14 +- .../ControlAdapter/ControlAdapterSettings.tsx | 4 +- .../processors/CannyProcessor.tsx | 4 +- .../processors/ColorMapProcessor.tsx | 4 +- .../processors/ContentShuffleProcessor.tsx | 4 +- .../processors/DWOpenposeProcessor.tsx | 4 +- .../processors/MediapipeFaceProcessor.tsx | 4 +- .../processors/MidasDepthProcessor.tsx | 4 +- .../processors/MlsdImageProcessor.tsx | 4 +- .../ControlAdapter/processors/types.ts | 4 +- .../components/ControlLayersPanelContent.tsx | 32 ++- .../components/ControlLayersToolbar.tsx | 15 ++ .../components/Filters/Filter.tsx | 76 ++++++ .../components/Filters/FilterCanny.tsx | 67 +++++ .../components/Filters/FilterColorMap.tsx | 47 ++++ .../Filters/FilterContentShuffle.tsx | 78 ++++++ .../components/Filters/FilterDWOpenpose.tsx | 61 +++++ .../Filters/FilterDepthAnything.tsx | 46 ++++ .../components/Filters/FilterHed.tsx | 31 +++ .../components/Filters/FilterLineart.tsx | 31 +++ .../Filters/FilterMediapipeFace.tsx | 73 +++++ .../components/Filters/FilterMidasDepth.tsx | 75 ++++++ .../components/Filters/FilterMlsdImage.tsx | 75 ++++++ .../components/Filters/FilterPidi.tsx | 42 +++ .../components/Filters/FilterSettings.tsx | 77 ++++++ .../components/Filters/FilterTypeSelect.tsx | 54 ++++ .../components/Filters/FilterWrapper.tsx | 17 ++ .../controlLayers/components/Filters/types.ts | 6 + .../controlLayers/components/Layer/Layer.tsx | 7 +- .../components/Layer/LayerControlAdapter.tsx | 66 +++++ .../components/Layer/LayerSettings.tsx | 14 +- .../components/StageComponent.tsx | 14 +- .../components/TransformToolButton.tsx | 16 +- .../common/CanvasEntityActionMenuItems.tsx | 61 ++++- .../common/CanvasEntityEnabledToggle.tsx | 1 + .../controlLayers/hooks/addLayerHooks.ts | 8 +- .../controlLayers/hooks/useEntityAdapter.ts | 8 + .../hooks/useLayerControlAdapter.ts | 57 ++++ .../controlLayers/konva/CanvasFilter.ts | 132 +++++++++ .../controlLayers/konva/CanvasLayerAdapter.ts | 3 + .../controlLayers/konva/CanvasManager.ts | 14 +- .../konva/CanvasObjectRenderer.ts | 59 +++- .../controlLayers/konva/CanvasStateApi.ts | 16 +- .../controlLayers/konva/CanvasTool.ts | 2 +- .../controlLayers/store/canvasV2Slice.ts | 26 +- .../store/controlAdaptersReducers.ts | 4 +- .../controlLayers/store/layersReducers.ts | 78 +++++- .../controlLayers/store/regionsReducers.ts | 4 +- .../controlLayers/store/types.test.ts | 6 +- .../src/features/controlLayers/store/types.ts | 251 +++++++++--------- .../web/src/features/metadata/util/parsers.ts | 12 +- .../graph/generation/addControlAdapters.ts | 92 ++++--- .../nodes/util/graph/generation/addLayers.ts | 7 +- .../util/graph/generation/buildSD1Graph.ts | 2 +- .../util/graph/generation/buildSDXLGraph.ts | 2 +- .../frontend/web/src/services/api/schema.ts | 46 +++- 65 files changed, 1732 insertions(+), 290 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterCanny.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterColorMap.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterContentShuffle.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterDWOpenpose.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterDepthAnything.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterHed.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterLineart.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterMediapipeFace.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterMidasDepth.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterMlsdImage.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterPidi.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterSettings.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterTypeSelect.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterWrapper.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Filters/types.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerControlAdapter.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/useEntityAdapter.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilter.ts diff --git a/invokeai/app/services/events/events_common.py b/invokeai/app/services/events/events_common.py index a4570fa8e5a..c348611bab4 100644 --- a/invokeai/app/services/events/events_common.py +++ b/invokeai/app/services/events/events_common.py @@ -10,7 +10,6 @@ QUEUE_ITEM_STATUS, BatchStatus, EnqueueBatchResult, - QueueItemOrigin, SessionQueueItem, SessionQueueStatus, ) @@ -89,7 +88,7 @@ class QueueItemEventBase(QueueEventBase): item_id: int = Field(description="The ID of the queue item") batch_id: str = Field(description="The ID of the queue batch") - origin: QueueItemOrigin | None = Field(default=None, description="The origin of the batch") + origin: str | None = Field(default=None, description="The origin of the batch") class InvocationEventBase(QueueItemEventBase): @@ -284,7 +283,7 @@ class BatchEnqueuedEvent(QueueEventBase): description="The number of invocations initially requested to be enqueued (may be less than enqueued if queue was full)" ) priority: int = Field(description="The priority of the batch") - origin: QueueItemOrigin | None = Field(default=None, description="The origin of the batch") + origin: str | None = Field(default=None, description="The origin of the batch") @classmethod def build(cls, enqueue_result: EnqueueBatchResult) -> "BatchEnqueuedEvent": diff --git a/invokeai/app/services/session_queue/session_queue_common.py b/invokeai/app/services/session_queue/session_queue_common.py index 5348339e712..a87684cbedc 100644 --- a/invokeai/app/services/session_queue/session_queue_common.py +++ b/invokeai/app/services/session_queue/session_queue_common.py @@ -86,7 +86,7 @@ class BatchDatum(BaseModel): class Batch(BaseModel): batch_id: str = Field(default_factory=uuid_string, description="The ID of the batch") - origin: QueueItemOrigin | None = Field(default=None, description="The origin of this batch.") + origin: str | None = Field(default=None, description="The origin of this batch.") data: Optional[BatchDataCollection] = Field(default=None, description="The batch data collection.") graph: Graph = Field(description="The graph to initialize the session with") workflow: Optional[WorkflowWithoutID] = Field( @@ -205,7 +205,7 @@ class SessionQueueItemWithoutGraph(BaseModel): status: QUEUE_ITEM_STATUS = Field(default="pending", description="The status of this queue item") priority: int = Field(default=0, description="The priority of this queue item") batch_id: str = Field(description="The ID of the batch associated with this queue item") - origin: QueueItemOrigin | None = Field(default=None, description="The origin of this queue item. ") + origin: str | None = Field(default=None, description="The origin of this queue item. ") session_id: str = Field( description="The ID of the session associated with this queue item. The session doesn't exist in graph_executions until the queue item is executed." ) @@ -305,7 +305,7 @@ class SessionQueueStatus(BaseModel): class BatchStatus(BaseModel): queue_id: str = Field(..., description="The ID of the queue") batch_id: str = Field(..., description="The ID of the batch") - origin: QueueItemOrigin | None = Field(..., description="The origin of the batch") + origin: str | None = Field(..., description="The origin of the batch") pending: int = Field(..., description="Number of queue items with status 'pending'") in_progress: int = Field(..., description="Number of queue items with status 'in_progress'") completed: int = Field(..., description="Number of queue items with status 'complete'") @@ -451,7 +451,7 @@ class SessionQueueValueToInsert(NamedTuple): field_values: Optional[str] # field_values json priority: int # priority workflow: Optional[str] # workflow json - origin: QueueItemOrigin | None + origin: str | None ValuesToInsert: TypeAlias = list[SessionQueueValueToInsert] diff --git a/invokeai/frontend/web/src/app/hooks/useSocketIO.ts b/invokeai/frontend/web/src/app/hooks/useSocketIO.ts index d3baf5f4524..8a530b8229b 100644 --- a/invokeai/frontend/web/src/app/hooks/useSocketIO.ts +++ b/invokeai/frontend/web/src/app/hooks/useSocketIO.ts @@ -10,6 +10,7 @@ import { setEventListeners } from 'services/events/setEventListeners'; import type { ClientToServerEvents, ServerToClientEvents } from 'services/events/types'; import type { ManagerOptions, Socket, SocketOptions } from 'socket.io-client'; import { io } from 'socket.io-client'; +import { assert } from 'tsafe'; // Inject socket options and url into window for debugging declare global { @@ -18,6 +19,14 @@ declare global { } } +export type AppSocket = Socket; + +export const $socket = atom(null); +export const getSocket = () => { + const socket = $socket.get(); + assert(socket !== null, 'Socket is not initialized'); + return socket; +}; export const $socketOptions = map>({}); const $isSocketInitialized = atom(false); @@ -61,7 +70,8 @@ export const useSocketIO = () => { return; } - const socket: Socket = io(socketUrl, socketOptions); + const socket: AppSocket = io(socketUrl, socketOptions); + $socket.set(socket); setEventListeners({ dispatch, socket }); socket.connect(); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts index d4611d23eb7..707820bda39 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts @@ -12,7 +12,7 @@ import { caRecalled, } from 'features/controlLayers/store/canvasV2Slice'; import { selectCA } from 'features/controlLayers/store/controlAdaptersReducers'; -import { CA_PROCESSOR_DATA } from 'features/controlLayers/store/types'; +import { IMAGE_FILTERS } from 'features/controlLayers/store/types'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { isEqual } from 'lodash-es'; @@ -95,7 +95,7 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni } // TODO(psyche): I can't get TS to be happy, it thinkgs `config` is `never` but it should be inferred from the generic... I'll just cast it for now - const processorNode = CA_PROCESSOR_DATA[config.type].buildNode(image.image, config as never); + const processorNode = IMAGE_FILTERS[config.type].buildNode(image.image, config as never); const enqueueBatchArg: BatchConfig = { prepend: true, batch: { diff --git a/invokeai/frontend/web/src/app/store/nanostores/store.ts b/invokeai/frontend/web/src/app/store/nanostores/store.ts index 65c59dad5d3..00adc9a34f3 100644 --- a/invokeai/frontend/web/src/app/store/nanostores/store.ts +++ b/invokeai/frontend/web/src/app/store/nanostores/store.ts @@ -1,4 +1,5 @@ -import type { createStore } from 'app/store/store'; +import { useStore } from '@nanostores/react'; +import type { AppStore } from 'app/store/store'; import { atom } from 'nanostores'; // Inject socket options and url into window for debugging @@ -22,7 +23,7 @@ class ReduxStoreNotInitialized extends Error { } } -export const $store = atom> | undefined>(); +export const $store = atom>(); export const getStore = () => { const store = $store.get(); @@ -31,3 +32,11 @@ export const getStore = () => { } return store; }; + +export const useAppStore = () => { + const store = useStore($store); + if (!store) { + throw new ReduxStoreNotInitialized(); + } + return store; +}; diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index f41d6273e9b..fae121c547e 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -180,7 +180,8 @@ export const createStore = (uniqueStoreKey?: string, persist = true) => }, }); -export type RootState = ReturnType['getState']>; +export type AppStore = ReturnType; +export type RootState = ReturnType; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type AppThunkDispatch = ThunkDispatch; export type AppDispatch = ReturnType['dispatch']; diff --git a/invokeai/frontend/web/src/app/store/storeHooks.ts b/invokeai/frontend/web/src/app/store/storeHooks.ts index 6bc904acb31..632ea76332e 100644 --- a/invokeai/frontend/web/src/app/store/storeHooks.ts +++ b/invokeai/frontend/web/src/app/store/storeHooks.ts @@ -1,8 +1,8 @@ -import type { AppThunkDispatch, RootState } from 'app/store/store'; +import type { AppStore, AppThunkDispatch, RootState } from 'app/store/store'; import type { TypedUseSelectorHook } from 'react-redux'; -import { useDispatch, useSelector, useStore } from 'react-redux'; +import {useDispatch, useSelector, useStore } from 'react-redux'; // Use throughout your app instead of plain `useDispatch` and `useSelector` export const useAppDispatch = () => useDispatch(); export const useAppSelector: TypedUseSelectorHook = useSelector; -export const useAppStore = () => useStore(); +export const useAppStore = () => useStore(); diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index 4268bc5411d..dc51e6ee2eb 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -1,4 +1,4 @@ -import type { ProcessorTypeV2 } from 'features/controlLayers/store/types'; +import type { FilterType } from 'features/controlLayers/store/types'; import type { ParameterPrecision, ParameterScheduler } from 'features/parameters/types/parameterSchemas'; import type { InvokeTabName } from 'features/ui/store/tabMap'; import type { O } from 'ts-toolbelt'; @@ -83,7 +83,7 @@ export type AppConfig = { sd: { defaultModel?: string; disabledControlNetModels: string[]; - disabledControlNetProcessors: ProcessorTypeV2[]; + disabledControlNetProcessors: FilterType[]; // Core parameters iterations: NumericalParameterConfig; width: NumericalParameterConfig; // initial value comes from model diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterProcessorConfig.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterProcessorConfig.tsx index bd7d96d5024..6836584ba36 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterProcessorConfig.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterProcessorConfig.tsx @@ -9,12 +9,12 @@ import { MediapipeFaceProcessor } from 'features/controlLayers/components/Contro import { MidasDepthProcessor } from 'features/controlLayers/components/ControlAdapter/processors/MidasDepthProcessor'; import { MlsdImageProcessor } from 'features/controlLayers/components/ControlAdapter/processors/MlsdImageProcessor'; import { PidiProcessor } from 'features/controlLayers/components/ControlAdapter/processors/PidiProcessor'; -import type { ProcessorConfig } from 'features/controlLayers/store/types'; +import type { FilterConfig } from 'features/controlLayers/store/types'; import { memo } from 'react'; type Props = { - config: ProcessorConfig | null; - onChange: (config: ProcessorConfig | null) => void; + config: FilterConfig | null; + onChange: (config: FilterConfig | null) => void; }; export const ControlAdapterProcessorConfig = memo(({ config, onChange }: Props) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterProcessorTypeSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterProcessorTypeSelect.tsx index c936ff8a09a..e4d0d1878d1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterProcessorTypeSelect.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterProcessorTypeSelect.tsx @@ -3,8 +3,8 @@ import { Combobox, Flex, FormControl, FormLabel, IconButton } from '@invoke-ai/u import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import type {ProcessorConfig } from 'features/controlLayers/store/types'; -import { CA_PROCESSOR_DATA, isProcessorTypeV2 } from 'features/controlLayers/store/types'; +import type {FilterConfig } from 'features/controlLayers/store/types'; +import { IMAGE_FILTERS, isFilterType } from 'features/controlLayers/store/types'; import { configSelector } from 'features/system/store/configSelectors'; import { includes, map } from 'lodash-es'; import { memo, useCallback, useMemo } from 'react'; @@ -13,8 +13,8 @@ import { PiXBold } from 'react-icons/pi'; import { assert } from 'tsafe'; type Props = { - config: ProcessorConfig | null; - onChange: (config: ProcessorConfig | null) => void; + config: FilterConfig | null; + onChange: (config: FilterConfig | null) => void; }; const selectDisabledProcessors = createMemoizedSelector( @@ -26,7 +26,7 @@ export const ControlAdapterProcessorTypeSelect = memo(({ config, onChange }: Pro const { t } = useTranslation(); const disabledProcessors = useAppSelector(selectDisabledProcessors); const options = useMemo(() => { - return map(CA_PROCESSOR_DATA, ({ labelTKey }, type) => ({ value: type, label: t(labelTKey) })).filter( + return map(IMAGE_FILTERS, ({ labelTKey }, type) => ({ value: type, label: t(labelTKey) })).filter( (o) => !includes(disabledProcessors, o.value) ); }, [disabledProcessors, t]); @@ -36,8 +36,8 @@ export const ControlAdapterProcessorTypeSelect = memo(({ config, onChange }: Pro if (!v) { onChange(null); } else { - assert(isProcessorTypeV2(v.value)); - onChange(CA_PROCESSOR_DATA[v.value].buildDefaults()); + assert(isFilterType(v.value)); + onChange(IMAGE_FILTERS[v.value].buildDefaults()); } }, [onChange] diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterSettings.tsx index 96a505cf7e0..ef2379d1171 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterSettings.tsx @@ -19,7 +19,7 @@ import { caWeightChanged, } from 'features/controlLayers/store/canvasV2Slice'; import { selectCAOrThrow } from 'features/controlLayers/store/controlAdaptersReducers'; -import type { ControlModeV2, ProcessorConfig } from 'features/controlLayers/store/types'; +import type { ControlModeV2, FilterConfig } from 'features/controlLayers/store/types'; import type { CAImageDropData } from 'features/dnd/types'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -62,7 +62,7 @@ export const ControlAdapterSettings = memo(() => { ); const onChangeProcessorConfig = useCallback( - (processorConfig: ProcessorConfig | null) => { + (processorConfig: FilterConfig | null) => { dispatch(caProcessorConfigChanged({ id, processorConfig })); }, [dispatch, id] diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/CannyProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/CannyProcessor.tsx index d05d1897127..d1b15c6645b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/CannyProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/CannyProcessor.tsx @@ -1,14 +1,14 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; import type { CannyProcessorConfig } from 'features/controlLayers/store/types'; -import { CA_PROCESSOR_DATA } from 'features/controlLayers/store/types'; +import { IMAGE_FILTERS } from 'features/controlLayers/store/types'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import ProcessorWrapper from './ProcessorWrapper'; type Props = ProcessorComponentProps; -const DEFAULTS = CA_PROCESSOR_DATA['canny_image_processor'].buildDefaults(); +const DEFAULTS = IMAGE_FILTERS['canny_image_processor'].buildDefaults(); export const CannyProcessor = ({ onChange, config }: Props) => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ColorMapProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ColorMapProcessor.tsx index 951e4c36dbf..3261703ded6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ColorMapProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ColorMapProcessor.tsx @@ -1,14 +1,14 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; import type { ColorMapProcessorConfig } from 'features/controlLayers/store/types'; -import { CA_PROCESSOR_DATA } from 'features/controlLayers/store/types'; +import { IMAGE_FILTERS } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import ProcessorWrapper from './ProcessorWrapper'; type Props = ProcessorComponentProps; -const DEFAULTS = CA_PROCESSOR_DATA['color_map_image_processor'].buildDefaults(); +const DEFAULTS = IMAGE_FILTERS['color_map_image_processor'].buildDefaults(); export const ColorMapProcessor = memo(({ onChange, config }: Props) => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ContentShuffleProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ContentShuffleProcessor.tsx index 1b7b173287e..b86387097a1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ContentShuffleProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ContentShuffleProcessor.tsx @@ -1,14 +1,14 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; import type { ContentShuffleProcessorConfig } from 'features/controlLayers/store/types'; -import { CA_PROCESSOR_DATA } from 'features/controlLayers/store/types'; +import { IMAGE_FILTERS } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import ProcessorWrapper from './ProcessorWrapper'; type Props = ProcessorComponentProps; -const DEFAULTS = CA_PROCESSOR_DATA['content_shuffle_image_processor'].buildDefaults(); +const DEFAULTS = IMAGE_FILTERS['content_shuffle_image_processor'].buildDefaults(); export const ContentShuffleProcessor = memo(({ onChange, config }: Props) => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DWOpenposeProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DWOpenposeProcessor.tsx index 1e157adb2aa..4b197eb361e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DWOpenposeProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DWOpenposeProcessor.tsx @@ -1,7 +1,7 @@ import { Flex, FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; import type { DWOpenposeProcessorConfig } from 'features/controlLayers/store/types'; -import { CA_PROCESSOR_DATA } from 'features/controlLayers/store/types'; +import { IMAGE_FILTERS } from 'features/controlLayers/store/types'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next'; import ProcessorWrapper from './ProcessorWrapper'; type Props = ProcessorComponentProps; -const DEFAULTS = CA_PROCESSOR_DATA['dw_openpose_image_processor'].buildDefaults(); +const DEFAULTS = IMAGE_FILTERS['dw_openpose_image_processor'].buildDefaults(); export const DWOpenposeProcessor = memo(({ onChange, config }: Props) => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MediapipeFaceProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MediapipeFaceProcessor.tsx index 2cde63791e5..ad59e9f8d78 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MediapipeFaceProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MediapipeFaceProcessor.tsx @@ -1,14 +1,14 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; import type { MediapipeFaceProcessorConfig } from 'features/controlLayers/store/types'; -import { CA_PROCESSOR_DATA } from 'features/controlLayers/store/types'; +import { IMAGE_FILTERS } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import ProcessorWrapper from './ProcessorWrapper'; type Props = ProcessorComponentProps; -const DEFAULTS = CA_PROCESSOR_DATA['mediapipe_face_processor'].buildDefaults(); +const DEFAULTS = IMAGE_FILTERS['mediapipe_face_processor'].buildDefaults(); export const MediapipeFaceProcessor = memo(({ onChange, config }: Props) => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MidasDepthProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MidasDepthProcessor.tsx index 4f66f31a7f3..c9740c23d38 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MidasDepthProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MidasDepthProcessor.tsx @@ -1,14 +1,14 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; import type { MidasDepthProcessorConfig } from 'features/controlLayers/store/types'; -import { CA_PROCESSOR_DATA } from 'features/controlLayers/store/types'; +import { IMAGE_FILTERS } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import ProcessorWrapper from './ProcessorWrapper'; type Props = ProcessorComponentProps; -const DEFAULTS = CA_PROCESSOR_DATA['midas_depth_image_processor'].buildDefaults(); +const DEFAULTS = IMAGE_FILTERS['midas_depth_image_processor'].buildDefaults(); export const MidasDepthProcessor = memo(({ onChange, config }: Props) => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MlsdImageProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MlsdImageProcessor.tsx index d578fc8ef32..d907cbe705a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MlsdImageProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MlsdImageProcessor.tsx @@ -1,14 +1,14 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; import type { MlsdProcessorConfig } from 'features/controlLayers/store/types'; -import { CA_PROCESSOR_DATA } from 'features/controlLayers/store/types'; +import { IMAGE_FILTERS } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import ProcessorWrapper from './ProcessorWrapper'; type Props = ProcessorComponentProps; -const DEFAULTS = CA_PROCESSOR_DATA['mlsd_image_processor'].buildDefaults(); +const DEFAULTS = IMAGE_FILTERS['mlsd_image_processor'].buildDefaults(); export const MlsdImageProcessor = memo(({ onChange, config }: Props) => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/types.ts b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/types.ts index a9667437a49..a635e1f90f6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/types.ts @@ -1,6 +1,6 @@ -import type { ProcessorConfig } from 'features/controlLayers/store/types'; +import type { FilterConfig } from 'features/controlLayers/store/types'; -export type ProcessorComponentProps = { +export type ProcessorComponentProps = { onChange: (config: T) => void; config: T; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index f9f7c078113..ddaa0090555 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -1,19 +1,37 @@ /* eslint-disable i18next/no-literal-string */ import { Flex } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { AddLayerButton } from 'features/controlLayers/components/AddLayerButton'; import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList'; import { DeleteAllLayersButton } from 'features/controlLayers/components/DeleteAllLayersButton'; +import { Filter } from 'features/controlLayers/components/Filters/Filter'; +import { $filteringEntity } from 'features/controlLayers/store/canvasV2Slice'; +import ResizeHandle from 'features/ui/components/tabs/ResizeHandle'; import { memo } from 'react'; +import { Panel, PanelGroup } from 'react-resizable-panels'; export const ControlLayersPanelContent = memo(() => { + const filteringEntity = useStore($filteringEntity); return ( - - - - - - - + + + + + + + + + + + {Boolean(filteringEntity) && ( + <> + + + + + + )} + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index 511c0b1368d..c22d15ed82b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -12,11 +12,25 @@ import { ResetCanvasButton } from 'features/controlLayers/components/ResetCanvas import { ToolChooser } from 'features/controlLayers/components/ToolChooser'; import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup'; import { $canvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { nanoid } from 'features/controlLayers/konva/util'; import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton'; import { ViewerToggleMenu } from 'features/gallery/components/ImageViewer/ViewerToggleMenu'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; +const filter = () => { + const entity = $canvasManager.get()?.stateApi.getSelectedEntity(); + if (!entity || entity.type !== 'layer') { + return; + } + entity.adapter.filter.previewFilter({ + type: 'canny_image_processor', + id: nanoid(), + low_threshold: 50, + high_threshold: 50, + }); +}; + export const ControlLayersToolbar = memo(() => { const tool = useAppSelector((s) => s.canvasV2.tool.selected); const canvasManager = useStore($canvasManager); @@ -47,6 +61,7 @@ export const ControlLayersToolbar = memo(() => { + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx new file mode 100644 index 00000000000..98536318a5d --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx @@ -0,0 +1,76 @@ +import { Button, ButtonGroup, Flex } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { FilterSettings } from 'features/controlLayers/components/Filters/FilterSettings'; +import { FilterTypeSelect } from 'features/controlLayers/components/Filters/FilterTypeSelect'; +import { $canvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { $filteringEntity } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; + +export const Filter = memo(() => { + const filteringEntity = useStore($filteringEntity); + + const preview = useCallback(() => { + if (!filteringEntity) { + return; + } + const canvasManager = $canvasManager.get(); + if (!canvasManager) { + return; + } + const entity = canvasManager.stateApi.getEntity(filteringEntity); + if (!entity || entity.type !== 'layer') { + return; + } + entity.adapter.filter.previewFilter(); + }, [filteringEntity]); + + const apply = useCallback(() => { + if (!filteringEntity) { + return; + } + const canvasManager = $canvasManager.get(); + if (!canvasManager) { + return; + } + const entity = canvasManager.stateApi.getEntity(filteringEntity); + if (!entity || entity.type !== 'layer') { + return; + } + entity.adapter.filter.applyFilter(); + }, [filteringEntity]); + + const cancel = useCallback(() => { + if (!filteringEntity) { + return; + } + const canvasManager = $canvasManager.get(); + if (!canvasManager) { + return; + } + const entity = canvasManager.stateApi.getEntity(filteringEntity); + if (!entity || entity.type !== 'layer') { + return; + } + entity.adapter.filter.cancelFilter(); + }, [filteringEntity]); + + return ( + + + + + + + + + + ); +}); + +Filter.displayName = 'Filter'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterCanny.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterCanny.tsx new file mode 100644 index 00000000000..781053e95ca --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterCanny.tsx @@ -0,0 +1,67 @@ +import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import type { CannyProcessorConfig } from 'features/controlLayers/store/types'; +import { IMAGE_FILTERS } from 'features/controlLayers/store/types'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import type { FilterComponentProps } from './types'; + +type Props = FilterComponentProps; +const DEFAULTS = IMAGE_FILTERS['canny_image_processor'].buildDefaults(); + +export const FilterCanny = ({ onChange, config }: Props) => { + const { t } = useTranslation(); + const handleLowThresholdChanged = useCallback( + (v: number) => { + onChange({ ...config, low_threshold: v }); + }, + [onChange, config] + ); + const handleHighThresholdChanged = useCallback( + (v: number) => { + onChange({ ...config, high_threshold: v }); + }, + [onChange, config] + ); + + return ( + <> + + {t('controlnet.lowThreshold')} + + + + + {t('controlnet.highThreshold')} + + + + + ); +}; + +FilterCanny.displayName = 'FilterCanny'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterColorMap.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterColorMap.tsx new file mode 100644 index 00000000000..785af042234 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterColorMap.tsx @@ -0,0 +1,47 @@ +import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import type { ColorMapProcessorConfig } from 'features/controlLayers/store/types'; +import { IMAGE_FILTERS } from 'features/controlLayers/store/types'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import type { FilterComponentProps } from './types'; + +type Props = FilterComponentProps; +const DEFAULTS = IMAGE_FILTERS['color_map_image_processor'].buildDefaults(); + +export const FilterColorMap = memo(({ onChange, config }: Props) => { + const { t } = useTranslation(); + const handleColorMapTileSizeChanged = useCallback( + (v: number) => { + onChange({ ...config, color_map_tile_size: v }); + }, + [config, onChange] + ); + + return ( + <> + + {t('controlnet.colorMapTileSize')} + + + + + ); +}); + +FilterColorMap.displayName = 'FilterColorMap'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterContentShuffle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterContentShuffle.tsx new file mode 100644 index 00000000000..1cc81c50e1e --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterContentShuffle.tsx @@ -0,0 +1,78 @@ +import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import type { ContentShuffleProcessorConfig } from 'features/controlLayers/store/types'; +import { IMAGE_FILTERS } from 'features/controlLayers/store/types'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import type { FilterComponentProps } from './types'; + +type Props = FilterComponentProps; +const DEFAULTS = IMAGE_FILTERS['content_shuffle_image_processor'].buildDefaults(); + +export const FilterContentShuffle = memo(({ onChange, config }: Props) => { + const { t } = useTranslation(); + + const handleWChanged = useCallback( + (v: number) => { + onChange({ ...config, w: v }); + }, + [config, onChange] + ); + + const handleHChanged = useCallback( + (v: number) => { + onChange({ ...config, h: v }); + }, + [config, onChange] + ); + + const handleFChanged = useCallback( + (v: number) => { + onChange({ ...config, f: v }); + }, + [config, onChange] + ); + + return ( + <> + + {t('controlnet.w')} + + + + + {t('controlnet.h')} + + + + + {t('controlnet.f')} + + + + + ); +}); + +FilterContentShuffle.displayName = 'FilterContentShuffle'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterDWOpenpose.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterDWOpenpose.tsx new file mode 100644 index 00000000000..d0f22bae203 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterDWOpenpose.tsx @@ -0,0 +1,61 @@ +import { Flex, FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; +import type { DWOpenposeProcessorConfig } from 'features/controlLayers/store/types'; +import { IMAGE_FILTERS } from 'features/controlLayers/store/types'; +import type { ChangeEvent } from 'react'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import type { FilterComponentProps } from './types'; + +type Props = FilterComponentProps; +const DEFAULTS = IMAGE_FILTERS['dw_openpose_image_processor'].buildDefaults(); + +export const FilterDWOpenpose = memo(({ onChange, config }: Props) => { + const { t } = useTranslation(); + + const handleDrawBodyChanged = useCallback( + (e: ChangeEvent) => { + onChange({ ...config, draw_body: e.target.checked }); + }, + [config, onChange] + ); + + const handleDrawFaceChanged = useCallback( + (e: ChangeEvent) => { + onChange({ ...config, draw_face: e.target.checked }); + }, + [config, onChange] + ); + + const handleDrawHandsChanged = useCallback( + (e: ChangeEvent) => { + onChange({ ...config, draw_hands: e.target.checked }); + }, + [config, onChange] + ); + + return ( + <> + + + {t('controlnet.body')} + + + + {t('controlnet.face')} + + + + {t('controlnet.hands')} + + + + + ); +}); + +FilterDWOpenpose.displayName = 'FilterDWOpenpose'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterDepthAnything.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterDepthAnything.tsx new file mode 100644 index 00000000000..46abf1da4e6 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterDepthAnything.tsx @@ -0,0 +1,46 @@ +import type { ComboboxOnChange } from '@invoke-ai/ui-library'; +import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import type { DepthAnythingModelSize, DepthAnythingProcessorConfig } from 'features/controlLayers/store/types'; +import { isDepthAnythingModelSize } from 'features/controlLayers/store/types'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import type { FilterComponentProps } from './types'; + +type Props = FilterComponentProps; + +export const FilterDepthAnything = memo(({ onChange, config }: Props) => { + const { t } = useTranslation(); + const handleModelSizeChange = useCallback( + (v) => { + if (!isDepthAnythingModelSize(v?.value)) { + return; + } + onChange({ ...config, model_size: v.value }); + }, + [config, onChange] + ); + + const options: { label: string; value: DepthAnythingModelSize }[] = useMemo( + () => [ + { label: t('controlnet.depthAnythingSmallV2'), value: 'small_v2' }, + { label: t('controlnet.small'), value: 'small' }, + { label: t('controlnet.base'), value: 'base' }, + { label: t('controlnet.large'), value: 'large' }, + ], + [t] + ); + + const value = useMemo(() => options.filter((o) => o.value === config.model_size)[0], [options, config.model_size]); + + return ( + <> + + {t('controlnet.modelSize')} + + + + ); +}); + +FilterDepthAnything.displayName = 'FilterDepthAnything'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterHed.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterHed.tsx new file mode 100644 index 00000000000..50ed535da19 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterHed.tsx @@ -0,0 +1,31 @@ +import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; +import type { HedProcessorConfig } from 'features/controlLayers/store/types'; +import type { ChangeEvent } from 'react'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import type { FilterComponentProps } from './types'; + +type Props = FilterComponentProps; + +export const FilterHed = memo(({ onChange, config }: Props) => { + const { t } = useTranslation(); + + const handleScribbleChanged = useCallback( + (e: ChangeEvent) => { + onChange({ ...config, scribble: e.target.checked }); + }, + [config, onChange] + ); + + return ( + <> + + {t('controlnet.scribble')} + + + + ); +}); + +FilterHed.displayName = 'FilterHed'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterLineart.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterLineart.tsx new file mode 100644 index 00000000000..9b6f57f9d8f --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterLineart.tsx @@ -0,0 +1,31 @@ +import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; +import type { LineartProcessorConfig } from 'features/controlLayers/store/types'; +import type { ChangeEvent } from 'react'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import type { FilterComponentProps } from './types'; + +type Props = FilterComponentProps; + +export const FilterLineart = memo(({ onChange, config }: Props) => { + const { t } = useTranslation(); + + const handleCoarseChanged = useCallback( + (e: ChangeEvent) => { + onChange({ ...config, coarse: e.target.checked }); + }, + [config, onChange] + ); + + return ( + <> + + {t('controlnet.coarse')} + + + + ); +}); + +FilterLineart.displayName = 'FilterLineart'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterMediapipeFace.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterMediapipeFace.tsx new file mode 100644 index 00000000000..6674434d0a4 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterMediapipeFace.tsx @@ -0,0 +1,73 @@ +import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import type { MediapipeFaceProcessorConfig } from 'features/controlLayers/store/types'; +import { IMAGE_FILTERS } from 'features/controlLayers/store/types'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import type { FilterComponentProps } from './types'; + +type Props = FilterComponentProps; +const DEFAULTS = IMAGE_FILTERS['mediapipe_face_processor'].buildDefaults(); + +export const FilterMediapipeFace = memo(({ onChange, config }: Props) => { + const { t } = useTranslation(); + + const handleMaxFacesChanged = useCallback( + (v: number) => { + onChange({ ...config, max_faces: v }); + }, + [config, onChange] + ); + + const handleMinConfidenceChanged = useCallback( + (v: number) => { + onChange({ ...config, min_confidence: v }); + }, + [config, onChange] + ); + + return ( + <> + + {t('controlnet.maxFaces')} + + + + + {t('controlnet.minConfidence')} + + + + + ); +}); + +FilterMediapipeFace.displayName = 'FilterMediapipeFace'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterMidasDepth.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterMidasDepth.tsx new file mode 100644 index 00000000000..9024b45a889 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterMidasDepth.tsx @@ -0,0 +1,75 @@ +import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import type { MidasDepthProcessorConfig } from 'features/controlLayers/store/types'; +import { IMAGE_FILTERS } from 'features/controlLayers/store/types'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import type { FilterComponentProps } from './types'; + +type Props = FilterComponentProps; +const DEFAULTS = IMAGE_FILTERS['midas_depth_image_processor'].buildDefaults(); + +export const FilterMidasDepth = memo(({ onChange, config }: Props) => { + const { t } = useTranslation(); + + const handleAMultChanged = useCallback( + (v: number) => { + onChange({ ...config, a_mult: v }); + }, + [config, onChange] + ); + + const handleBgThChanged = useCallback( + (v: number) => { + onChange({ ...config, bg_th: v }); + }, + [config, onChange] + ); + + return ( + <> + + {t('controlnet.amult')} + + + + + {t('controlnet.bgth')} + + + + + ); +}); + +FilterMidasDepth.displayName = 'FilterMidasDepth'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterMlsdImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterMlsdImage.tsx new file mode 100644 index 00000000000..e16d77990a5 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterMlsdImage.tsx @@ -0,0 +1,75 @@ +import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import type { MlsdProcessorConfig } from 'features/controlLayers/store/types'; +import { IMAGE_FILTERS } from 'features/controlLayers/store/types'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import type { FilterComponentProps } from './types'; + +type Props = FilterComponentProps; +const DEFAULTS = IMAGE_FILTERS['mlsd_image_processor'].buildDefaults(); + +export const FilterMlsdImage = memo(({ onChange, config }: Props) => { + const { t } = useTranslation(); + + const handleThrDChanged = useCallback( + (v: number) => { + onChange({ ...config, thr_d: v }); + }, + [config, onChange] + ); + + const handleThrVChanged = useCallback( + (v: number) => { + onChange({ ...config, thr_v: v }); + }, + [config, onChange] + ); + + return ( + <> + + {t('controlnet.w')} + + + + + {t('controlnet.h')} + + + + + ); +}); + +FilterMlsdImage.displayName = 'FilterMlsdImage'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterPidi.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterPidi.tsx new file mode 100644 index 00000000000..1814edc0140 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterPidi.tsx @@ -0,0 +1,42 @@ +import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; +import type { PidiProcessorConfig } from 'features/controlLayers/store/types'; +import type { ChangeEvent } from 'react'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import type { FilterComponentProps } from './types'; + +type Props = FilterComponentProps; + +export const FilterPidi = ({ onChange, config }: Props) => { + const { t } = useTranslation(); + + const handleScribbleChanged = useCallback( + (e: ChangeEvent) => { + onChange({ ...config, scribble: e.target.checked }); + }, + [config, onChange] + ); + + const handleSafeChanged = useCallback( + (e: ChangeEvent) => { + onChange({ ...config, safe: e.target.checked }); + }, + [config, onChange] + ); + + return ( + <> + + {t('controlnet.scribble')} + + + + {t('controlnet.safe')} + + + + ); +}; + +FilterPidi.displayName = 'FilterPidi'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterSettings.tsx new file mode 100644 index 00000000000..6b90ab9c149 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterSettings.tsx @@ -0,0 +1,77 @@ +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { IAINoContentFallback } from 'common/components/IAIImageFallback'; +import { FilterCanny } from 'features/controlLayers/components/Filters/FilterCanny'; +import { FilterColorMap } from 'features/controlLayers/components/Filters/FilterColorMap'; +import { FilterContentShuffle } from 'features/controlLayers/components/Filters/FilterContentShuffle'; +import { FilterDepthAnything } from 'features/controlLayers/components/Filters/FilterDepthAnything'; +import { FilterDWOpenpose } from 'features/controlLayers/components/Filters/FilterDWOpenpose'; +import { FilterHed } from 'features/controlLayers/components/Filters/FilterHed'; +import { FilterLineart } from 'features/controlLayers/components/Filters/FilterLineart'; +import { FilterMediapipeFace } from 'features/controlLayers/components/Filters/FilterMediapipeFace'; +import { FilterMidasDepth } from 'features/controlLayers/components/Filters/FilterMidasDepth'; +import { FilterMlsdImage } from 'features/controlLayers/components/Filters/FilterMlsdImage'; +import { FilterPidi } from 'features/controlLayers/components/Filters/FilterPidi'; +import { filterConfigChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { type FilterConfig, IMAGE_FILTERS } from 'features/controlLayers/store/types'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const FilterSettings = memo(() => { + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + const config = useAppSelector((s) => s.canvasV2.filter.config); + const updateFilter = useCallback( + (config: FilterConfig) => { + dispatch(filterConfigChanged({ config })); + }, + [dispatch] + ); + + if (config.type === 'canny_image_processor') { + return ; + } + + if (config.type === 'color_map_image_processor') { + return ; + } + + if (config.type === 'content_shuffle_image_processor') { + return ; + } + + if (config.type === 'depth_anything_image_processor') { + return ; + } + + if (config.type === 'dw_openpose_image_processor') { + return ; + } + + if (config.type === 'hed_image_processor') { + return ; + } + + if (config.type === 'lineart_image_processor') { + return ; + } + + if (config.type === 'mediapipe_face_processor') { + return ; + } + + if (config.type === 'midas_depth_image_processor') { + return ; + } + + if (config.type === 'mlsd_image_processor') { + return ; + } + + if (config.type === 'pidi_image_processor') { + return ; + } + + return ; +}); + +FilterSettings.displayName = 'Filter'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterTypeSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterTypeSelect.tsx new file mode 100644 index 00000000000..2e765848bd3 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterTypeSelect.tsx @@ -0,0 +1,54 @@ +import type { ComboboxOnChange } from '@invoke-ai/ui-library'; +import { Combobox, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; +import { filterSelected } from 'features/controlLayers/store/canvasV2Slice'; +import { IMAGE_FILTERS, isFilterType } from 'features/controlLayers/store/types'; +import { configSelector } from 'features/system/store/configSelectors'; +import { includes, map } from 'lodash-es'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { assert } from 'tsafe'; + +const selectDisabledProcessors = createMemoizedSelector( + configSelector, + (config) => config.sd.disabledControlNetProcessors +); + +export const FilterTypeSelect = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const filterType = useAppSelector((s) => s.canvasV2.filter.config.type); + const disabledProcessors = useAppSelector(selectDisabledProcessors); + const options = useMemo(() => { + return map(IMAGE_FILTERS, ({ labelTKey }, type) => ({ value: type, label: t(labelTKey) })).filter( + (o) => !includes(disabledProcessors, o.value) + ); + }, [disabledProcessors, t]); + + const _onChange = useCallback( + (v) => { + if (!v) { + return; + } + assert(isFilterType(v.value)); + dispatch(filterSelected({ type: v.value })); + }, + [dispatch] + ); + const value = useMemo(() => options.find((o) => o.value === filterType) ?? null, [options, filterType]); + + return ( + + + + {t('controlLayers.filter')} + + + + + ); +}); + +FilterTypeSelect.displayName = 'FilterTypeSelect'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterWrapper.tsx new file mode 100644 index 00000000000..6f20a641cc2 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterWrapper.tsx @@ -0,0 +1,17 @@ +import { Flex } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useFilter } from 'features/controlLayers/components/Filters/Filter'; +import type { PropsWithChildren } from 'react'; +import { memo } from 'react'; + +const FilterWrapper = (props: PropsWithChildren) => { + const isPreviewDisabled = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.type !== 'layer'); + const filter = useFilter(); + return ( + + {props.children} + + ); +}; + +export default memo(FilterWrapper); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/types.ts b/invokeai/frontend/web/src/features/controlLayers/components/Filters/types.ts new file mode 100644 index 00000000000..e4132640a58 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Filters/types.ts @@ -0,0 +1,6 @@ +import type { FilterConfig } from 'features/controlLayers/store/types'; + +export type FilterComponentProps = { + onChange: (config: T) => void; + config: T; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx index d68ae0dea08..65c5501039a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx @@ -1,4 +1,4 @@ -import { Spacer, useDisclosure } from '@invoke-ai/ui-library'; +import { Spacer } from '@invoke-ai/ui-library'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; @@ -18,12 +18,11 @@ type Props = { export const Layer = memo(({ id }: Props) => { const entityIdentifier = useMemo(() => ({ id, type: 'layer' }), [id]); - const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: false }); return ( - + @@ -31,7 +30,7 @@ export const Layer = memo(({ id }: Props) => { - {isOpen && } + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerControlAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerControlAdapter.tsx new file mode 100644 index 00000000000..378a411fb18 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerControlAdapter.tsx @@ -0,0 +1,66 @@ +import { Flex } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct'; +import { Weight } from 'features/controlLayers/components/common/Weight'; +import { ControlAdapterControlModeSelect } from 'features/controlLayers/components/ControlAdapter/ControlAdapterControlModeSelect'; +import { ControlAdapterModel } from 'features/controlLayers/components/ControlAdapter/ControlAdapterModel'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { + layerControlAdapterBeginEndStepPctChanged, + layerControlAdapterControlModeChanged, + layerControlAdapterModelChanged, + layerControlAdapterWeightChanged, +} from 'features/controlLayers/store/canvasV2Slice'; +import type { ControlModeV2, ControlNetConfig, T2IAdapterConfig } from 'features/controlLayers/store/types'; +import { memo, useCallback } from 'react'; +import type { ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types'; + +type Props = { + controlAdapter: ControlNetConfig | T2IAdapterConfig; +}; + +export const LayerControlAdapter = memo(({ controlAdapter }: Props) => { + const dispatch = useAppDispatch(); + const { id } = useEntityIdentifierContext(); + + const onChangeBeginEndStepPct = useCallback( + (beginEndStepPct: [number, number]) => { + dispatch(layerControlAdapterBeginEndStepPctChanged({ id, beginEndStepPct })); + }, + [dispatch, id] + ); + + const onChangeControlMode = useCallback( + (controlMode: ControlModeV2) => { + dispatch(layerControlAdapterControlModeChanged({ id, controlMode })); + }, + [dispatch, id] + ); + + const onChangeWeight = useCallback( + (weight: number) => { + dispatch(layerControlAdapterWeightChanged({ id, weight })); + }, + [dispatch, id] + ); + + const onChangeModel = useCallback( + (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => { + dispatch(layerControlAdapterModelChanged({ id, modelConfig })); + }, + [dispatch, id] + ); + + return ( + + + + + {controlAdapter.type === 'controlnet' && ( + + )} + + ); +}); + +LayerControlAdapter.displayName = 'LayerControlAdapter'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerSettings.tsx index 111e30d96f5..d29278460be 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerSettings.tsx @@ -1,10 +1,22 @@ import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; +import { LayerControlAdapter } from 'features/controlLayers/components/Layer/LayerControlAdapter'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useLayerControlAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter'; import { memo } from 'react'; export const LayerSettings = memo(() => { const entityIdentifier = useEntityIdentifierContext(); - return PLACEHOLDER; + const controlAdapter = useLayerControlAdapter(entityIdentifier); + + if (!controlAdapter) { + return null; + } + + return ( + + + + ); }); LayerSettings.displayName = 'LayerSettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index d6d63e1d0c7..ae8e217df81 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -1,6 +1,8 @@ import { Flex } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { $socket } from 'app/hooks/useSocketIO'; import { logger } from 'app/logging/logger'; -import { useAppStore } from 'app/store/storeHooks'; +import { useAppStore } from 'app/store/nanostores/store'; import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay'; import { $canvasManager, CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import Konva from 'konva'; @@ -17,6 +19,7 @@ Konva.showWarnings = false; const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, asPreview: boolean) => { const store = useAppStore(); + const socket = useStore($socket); const dpr = useDevicePixelRatio({ round: false }); useLayoutEffect(() => { @@ -27,12 +30,17 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, return () => {}; } - const manager = new CanvasManager(stage, container, store); + if (!socket) { + log.debug('Socket not connected, skipping initialization'); + return () => {}; + } + + const manager = new CanvasManager(stage, container, store, socket); $canvasManager.set(manager); console.log(manager); const cleanup = manager.initialize(); return cleanup; - }, [asPreview, container, stage, store]); + }, [asPreview, container, socket, stage, store]); useLayoutEffect(() => { Konva.pixelRatio = dpr; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx index bbabeb09e36..e26635f1ad1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx @@ -2,7 +2,8 @@ import { Button, IconButton } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; import { $canvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { memo, useCallback, useEffect, useState } from 'react'; +import { $transformingEntity } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiResizeBold } from 'react-icons/pi'; @@ -10,20 +11,11 @@ import { PiResizeBold } from 'react-icons/pi'; export const TransformToolButton = memo(() => { const { t } = useTranslation(); const canvasManager = useStore($canvasManager); - const [isTransforming, setIsTransforming] = useState(false); + const transformingEntity = useStore($transformingEntity); const isDisabled = useAppSelector( (s) => s.canvasV2.selectedEntityIdentifier === null || s.canvasV2.session.isStaging ); - useEffect(() => { - if (!canvasManager) { - return; - } - return canvasManager.stateApi.$transformingEntity.listen((newValue) => { - setIsTransforming(Boolean(newValue)); - }); - }, [canvasManager]); - const onTransform = useCallback(() => { if (!canvasManager) { return; @@ -47,7 +39,7 @@ export const TransformToolButton = memo(() => { useHotkeys(['ctrl+t', 'meta+t'], onTransform, { enabled: !isDisabled }, [isDisabled, onTransform]); - if (isTransforming) { + if (transformingEntity) { return ( <> diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx index cb0094fd56d..b75e87d8287 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx @@ -1,8 +1,12 @@ -import { MenuItem } from '@invoke-ai/ui-library'; +import { MenuDivider, MenuItem } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useLayerUseAsControl } from 'features/controlLayers/hooks/useLayerControlAdapter'; +import { $canvasManager } from 'features/controlLayers/konva/CanvasManager'; import { + $filteringEntity, entityArrangedBackwardOne, entityArrangedForwardOne, entityArrangedToBack, @@ -20,7 +24,11 @@ import { PiArrowLineDownBold, PiArrowLineUpBold, PiArrowUpBold, + PiCheckBold, + PiQuestionMarkBold, + PiStarHalfBold, PiTrashSimpleBold, + PiXBold, } from 'react-icons/pi'; const getIndexAndCount = ( @@ -52,18 +60,15 @@ const getIndexAndCount = ( export const CanvasEntityActionMenuItems = memo(() => { const { t } = useTranslation(); + const canvasManager = useStore($canvasManager); const dispatch = useAppDispatch(); const entityIdentifier = useEntityIdentifierContext(); + const useAsControl = useLayerUseAsControl(entityIdentifier); const selectValidActions = useMemo( () => createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { const { index, count } = getIndexAndCount(canvasV2, entityIdentifier); return { - isArrangeable: - entityIdentifier.type === 'layer' || - entityIdentifier.type === 'control_adapter' || - entityIdentifier.type === 'regional_guidance', - isDeleteable: entityIdentifier.type !== 'inpaint_mask', canMoveForwardOne: index < count - 1, canMoveBackwardOne: index > 0, canMoveToFront: index < count - 1, @@ -75,6 +80,18 @@ export const CanvasEntityActionMenuItems = memo(() => { const validActions = useAppSelector(selectValidActions); + const isArrangeable = useMemo( + () => entityIdentifier.type === 'layer' || entityIdentifier.type === 'regional_guidance', + [entityIdentifier.type] + ); + + const isDeleteable = useMemo( + () => entityIdentifier.type === 'layer' || entityIdentifier.type === 'regional_guidance', + [entityIdentifier.type] + ); + const isFilterable = useMemo(() => entityIdentifier.type === 'layer', [entityIdentifier.type]); + const isUseAsControlable = useMemo(() => entityIdentifier.type === 'layer', [entityIdentifier.type]); + const deleteEntity = useCallback(() => { dispatch(entityDeleted({ entityIdentifier })); }, [dispatch, entityIdentifier]); @@ -93,10 +110,23 @@ export const CanvasEntityActionMenuItems = memo(() => { const moveToBack = useCallback(() => { dispatch(entityArrangedToBack({ entityIdentifier })); }, [dispatch, entityIdentifier]); + const filter = useCallback(() => { + $filteringEntity.set(entityIdentifier); + }, [entityIdentifier]); + const debug = useCallback(() => { + if (!canvasManager) { + return; + } + const entity = canvasManager.stateApi.getEntity(entityIdentifier); + if (!entity) { + return; + } + console.debug(entity); + }, [canvasManager, entityIdentifier]); return ( <> - {validActions.isArrangeable && ( + {isArrangeable && ( <> }> {t('controlLayers.moveToFront')} @@ -112,14 +142,29 @@ export const CanvasEntityActionMenuItems = memo(() => { )} + {isFilterable && ( + }> + {t('common.filter')} + + )} + {isUseAsControlable && ( + : }> + {useAsControl.hasControlAdapter ? t('common.removeControl') : t('common.useAsControl')} + + )} + }> {t('accessibility.reset')} - {validActions.isDeleteable && ( + {isDeleteable && ( } color="error.300"> {t('common.delete')} )} + + } color="warn.300"> + {t('common.debug')} + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx index e33a5ce9c32..df33252be90 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx @@ -11,6 +11,7 @@ import { PiCheckBold } from 'react-icons/pi'; export const CanvasEntityEnabledToggle = memo(() => { const { t } = useTranslation(); const entityIdentifier = useEntityIdentifierContext(); + const isEnabled = useEntityIsEnabled(entityIdentifier); const dispatch = useAppDispatch(); const onClick = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts index 803369ad15c..c41a95384c5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts @@ -2,11 +2,11 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { deepClone } from 'common/util/deepClone'; import { caAdded, ipaAdded, rgIPAdapterAdded } from 'features/controlLayers/store/canvasV2Slice'; import { - CA_PROCESSOR_DATA, + IMAGE_FILTERS, initialControlNetV2, initialIPAdapterV2, initialT2IAdapterV2, - isProcessorTypeV2, + isFilterType, } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { useCallback, useMemo } from 'react'; @@ -30,8 +30,8 @@ export const useAddCALayer = () => { } const defaultPreprocessor = model.default_settings?.preprocessor; - const processorConfig = isProcessorTypeV2(defaultPreprocessor) - ? CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults(baseModel) + const processorConfig = isFilterType(defaultPreprocessor) + ? IMAGE_FILTERS[defaultPreprocessor].buildDefaults(baseModel) : null; const initialConfig = deepClone(model.type === 'controlnet' ? initialControlNetV2 : initialT2IAdapterV2); diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityAdapter.ts new file mode 100644 index 00000000000..210c5cb092b --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityAdapter.ts @@ -0,0 +1,8 @@ +import { useStore } from '@nanostores/react'; +import { $canvasManager } from 'features/controlLayers/konva/CanvasManager'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; + +export const useEntityAdapter = (entityIdentifier: CanvasEntityIdentifier) => { + const canvasManager = useStore($canvasManager); + console.log(canvasManager); +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts new file mode 100644 index 00000000000..71bb3470226 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts @@ -0,0 +1,57 @@ +import { createMemoizedAppSelector } from 'app/store/createMemoizedSelector'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { deepClone } from 'common/util/deepClone'; +import { layerUsedAsControlChanged, selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { selectLayer } from 'features/controlLayers/store/layersReducers'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { initialControlNetV2, initialT2IAdapterV2 } from 'features/controlLayers/store/types'; +import { zModelIdentifierField } from 'features/nodes/types/common'; +import { useCallback, useMemo } from 'react'; +import { useControlNetAndT2IAdapterModels } from 'services/api/hooks/modelsByType'; +import type { ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types'; + +export const useLayerControlAdapter = (entityIdentifier: CanvasEntityIdentifier) => { + const selectControlAdapter = useMemo( + () => + createMemoizedAppSelector(selectCanvasV2Slice, (canvasV2) => { + const layer = selectLayer(canvasV2, entityIdentifier.id); + if (!layer) { + return null; + } + return layer.controlAdapter; + }), + [entityIdentifier] + ); + const controlAdapter = useAppSelector(selectControlAdapter); + return controlAdapter; +}; + +export const useLayerUseAsControl = (entityIdentifier: CanvasEntityIdentifier) => { + const dispatch = useAppDispatch(); + const [modelConfigs] = useControlNetAndT2IAdapterModels(); + + const baseModel = useAppSelector((s) => s.canvasV2.params.model?.base); + const controlAdapter = useLayerControlAdapter(entityIdentifier); + + const model: ControlNetModelConfig | T2IAdapterModelConfig | null = useMemo(() => { + // prefer to use a model that matches the base model + const compatibleModels = modelConfigs.filter((m) => (baseModel ? m.base === baseModel : true)); + return compatibleModels[0] ?? modelConfigs[0] ?? null; + }, [baseModel, modelConfigs]); + + const toggle = useCallback(() => { + if (controlAdapter) { + dispatch(layerUsedAsControlChanged({ id: entityIdentifier.id, controlAdapter: null })); + return; + } + const newControlAdapter = deepClone(model?.type === 't2i_adapter' ? initialT2IAdapterV2 : initialControlNetV2); + + if (model) { + newControlAdapter.model = zModelIdentifierField.parse(model); + } + + dispatch(layerUsedAsControlChanged({ id: entityIdentifier.id, controlAdapter: newControlAdapter })); + }, [controlAdapter, dispatch, entityIdentifier.id, model]); + + return { hasControlAdapter: Boolean(controlAdapter), toggle }; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilter.ts new file mode 100644 index 00000000000..dd1581612fc --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilter.ts @@ -0,0 +1,132 @@ +import type { JSONObject } from 'common/types'; +import { parseify } from 'common/util/serialize'; +import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { getPrefixedId } from 'features/controlLayers/konva/util'; +import type { CanvasImageState } from 'features/controlLayers/store/types'; +import { IMAGE_FILTERS, imageDTOToImageObject } from 'features/controlLayers/store/types'; +import type { Logger } from 'roarr'; +import { getImageDTO } from 'services/api/endpoints/images'; +import { queueApi } from 'services/api/endpoints/queue'; +import type { BatchConfig } from 'services/api/types'; +import type { InvocationCompleteEvent } from 'services/events/types'; +import { assert } from 'tsafe'; + +const TYPE = 'entity_filter_preview'; + +export class CanvasFilter { + readonly type = TYPE; + + id: string; + path: string[]; + parent: CanvasLayerAdapter; + manager: CanvasManager; + log: Logger; + + imageState: CanvasImageState | null = null; + + constructor(parent: CanvasLayerAdapter) { + this.id = getPrefixedId(this.type); + this.parent = parent; + this.manager = parent.manager; + this.path = this.parent.path.concat(this.id); + this.log = this.manager.buildLogger(this.getLoggingContext); + this.log.trace('Creating filter'); + } + + previewFilter = async () => { + const { config } = this.manager.stateApi.getFilterState(); + this.log.trace({ config }, 'Previewing filter'); + const dispatch = this.manager.stateApi._store.dispatch; + + const imageDTO = await this.parent.renderer.rasterize(); + // TODO(psyche): I can't get TS to be happy, it thinkgs `config` is `never` but it should be inferred from the generic... I'll just cast it for now + const filterNode = IMAGE_FILTERS[config.type].buildNode(imageDTO, config as never); + const enqueueBatchArg: BatchConfig = { + prepend: true, + batch: { + graph: { + nodes: { + [filterNode.id]: { + ...filterNode, + // Control images are always intermediate - do not save to gallery + // is_intermediate: true, + is_intermediate: false, // false for testing + }, + }, + edges: [], + }, + origin: this.id, + runs: 1, + }, + }; + + // Listen for the filter processing completion event + const listener = async (event: InvocationCompleteEvent) => { + if (event.origin !== this.id || event.invocation_source_id !== filterNode.id) { + return; + } + this.log.trace({ event: parseify(event) }, 'Handling filter processing completion'); + const { result } = event; + assert(result.type === 'image_output', `Processor did not return an image output, got: ${result}`); + const imageDTO = await getImageDTO(result.image.image_name); + assert(imageDTO, "Failed to fetch processor output's image DTO"); + this.imageState = imageDTOToImageObject(imageDTO); + this.parent.renderer.clearBuffer(); + await this.parent.renderer.setBuffer(this.imageState); + this.parent.renderer.hideObjects([this.imageState.id]); + this.manager.socket.off('invocation_complete', listener); + }; + + this.manager.socket.on('invocation_complete', listener); + + this.log.trace({ enqueueBatchArg: parseify(enqueueBatchArg) }, 'Enqueuing filter batch'); + + dispatch( + queueApi.endpoints.enqueueBatch.initiate(enqueueBatchArg, { + fixedCacheKey: 'enqueueBatch', + }) + ); + }; + + applyFilter = () => { + this.log.trace('Applying filter'); + if (!this.imageState) { + this.log.warn('No image state to apply filter to'); + return; + } + this.parent.renderer.commitBuffer(); + const rect = this.parent.transformer.getRelativeRect(); + this.manager.stateApi.rasterizeEntity({ + entityIdentifier: this.parent.getEntityIdentifier(), + imageObject: this.imageState, + position: { x: Math.round(rect.x), y: Math.round(rect.y) }, + }); + this.parent.renderer.showObjects(); + this.manager.stateApi.$filteringEntity.set(null); + this.imageState = null; + }; + + cancelFilter = () => { + this.log.trace('Cancelling filter'); + this.parent.renderer.clearBuffer(); + this.parent.renderer.showObjects(); + this.manager.stateApi.$filteringEntity.set(null); + this.imageState = null; + }; + + destroy = () => { + this.log.trace('Destroying filter'); + }; + + repr = () => { + return { + id: this.id, + type: this.type, + }; + }; + + getLoggingContext = (): JSONObject => { + return { ...this.parent.getLoggingContext(), path: this.path.join('.') }; + }; +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts index 5217e8f6878..c2b0d7c51a0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts @@ -1,5 +1,6 @@ import type { JSONObject } from 'common/types'; import { deepClone } from 'common/util/deepClone'; +import { CanvasFilter } from 'features/controlLayers/konva/CanvasFilter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; @@ -23,6 +24,7 @@ export class CanvasLayerAdapter { }; transformer: CanvasTransformer; renderer: CanvasObjectRenderer; + filter: CanvasFilter; isFirstRender: boolean = true; @@ -47,6 +49,7 @@ export class CanvasLayerAdapter { this.renderer = new CanvasObjectRenderer(this); this.transformer = new CanvasTransformer(this); + this.filter = new CanvasFilter(this); } /** diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 496b08e8fdf..6777bbd3ffe 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -1,6 +1,6 @@ -import type { Store } from '@reduxjs/toolkit'; +import type { AppSocket } from 'app/hooks/useSocketIO'; import { logger } from 'app/logging/logger'; -import type { RootState } from 'app/store/store'; +import type { AppStore } from 'app/store/store'; import type { JSONObject } from 'common/types'; import { MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/controlLayers/konva/constants'; import { @@ -12,7 +12,7 @@ import { } from 'features/controlLayers/konva/util'; import type { Extents, ExtentsResult, GetBboxTask, WorkerLogMessage } from 'features/controlLayers/konva/worker'; import type { CanvasV2State, Coordinate, Dimensions, GenerationMode, Rect } from 'features/controlLayers/store/types'; -import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers'; +import { isValidLayerWithoutControlAdapter } from 'features/nodes/util/graph/generation/addLayers'; import type Konva from 'konva'; import { clamp } from 'lodash-es'; import { atom } from 'nanostores'; @@ -49,8 +49,9 @@ export class CanvasManager { log: Logger; workerLog: Logger; + socket: AppSocket; - _store: Store; + _store: AppStore; _prevState: CanvasV2State; _isFirstRender: boolean = true; _isDebugging: boolean = false; @@ -58,12 +59,13 @@ export class CanvasManager { _worker: Worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module', name: 'worker' }); _tasks: Map void }> = new Map(); - constructor(stage: Konva.Stage, container: HTMLDivElement, store: Store) { + constructor(stage: Konva.Stage, container: HTMLDivElement, store: AppStore, socket: AppSocket) { this.id = getPrefixedId(this.type); this.path = [this.id]; this.stage = stage; this.container = container; this._store = store; + this.socket = socket; this.stateApi = new CanvasStateApi(this._store, this); this._prevState = this.stateApi.getState(); @@ -547,7 +549,7 @@ export class CanvasManager { stageClone.x(0); stageClone.y(0); - const validLayers = layersState.entities.filter(isValidLayer); + const validLayers = layersState.entities.filter(isValidLayerWithoutControlAdapter); // getLayers() returns the internal `children` array of the stage directly - calling destroy on a layer will // mutate that array. We need to clone the array to avoid mutating the original. for (const konvaLayer of stageClone.getLayers().slice()) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index 5b95330245e..4f6b16cda30 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -20,7 +20,7 @@ import type { import { imageDTOToImageObject } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; -import { uploadImage } from 'services/api/endpoints/images'; +import { getImageDTO, uploadImage } from 'services/api/endpoints/images'; import type { ImageCategory, ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; @@ -62,6 +62,12 @@ export class CanvasObjectRenderer { */ renderers: Map = new Map(); + /** + * A cache of the rasterized image data URL. If the cache is null, the parent has not been rasterized since its last + * change. + */ + rasterizedImageCache: string | null = null; + /** * A object containing singleton Konva nodes. */ @@ -162,6 +168,19 @@ export class CanvasObjectRenderer { didRender = (await this.renderObject(this.buffer)) || didRender; } + if (didRender && this.rasterizedImageCache) { + const hasOneObject = this.renderers.size === 1; + const firstObject = Array.from(this.renderers.values())[0]; + if ( + hasOneObject && + firstObject && + firstObject.state.type === 'image' && + firstObject.state.image.image_name !== this.rasterizedImageCache + ) { + this.rasterizedImageCache = null; + } + } + return didRender; }; @@ -313,6 +332,18 @@ export class CanvasObjectRenderer { this.buffer = null; }; + hideObjects = (except: string[] = []) => { + for (const renderer of this.renderers.values()) { + renderer.setVisibility(except.includes(renderer.id)); + } + }; + + showObjects = (except: string[] = []) => { + for (const renderer of this.renderers.values()) { + renderer.setVisibility(!except.includes(renderer.id)); + } + }; + /** * Determines if the objects in the renderer require a pixel bbox calculation. * @@ -345,15 +376,33 @@ export class CanvasObjectRenderer { return this.renderers.size > 0 || this.buffer !== null; }; - rasterize = async () => { + /** + * Rasterizes the parent entity. If the entity has a rasterization cache, the cached image is returned after + * validating that it exists on the server. + * + * The rasterization cache is reset when the entity's objects change. The buffer object is not considered part of the + * entity's objects for this purpose. + * + * @returns A promise that resolves to the rasterized image DTO. + */ + rasterize = async (): Promise => { this.log.debug('Rasterizing entity'); + let imageDTO: ImageDTO | null = null; + if (this.rasterizedImageCache) { + imageDTO = await getImageDTO(this.rasterizedImageCache); + } + + if (imageDTO) { + return imageDTO; + } + const rect = this.parent.transformer.getRelativeRect(); const blob = await this.getBlob({ rect }); if (this.manager._isDebugging) { previewBlob(blob, 'Rasterized entity'); } - const imageDTO = await uploadImage(blob, `${this.id}_rasterized.png`, 'other', true); + imageDTO = await uploadImage(blob, `${this.id}_rasterized.png`, 'other', true); const imageObject = imageDTOToImageObject(imageDTO); await this.renderObject(imageObject, true); this.manager.stateApi.rasterizeEntity({ @@ -361,6 +410,10 @@ export class CanvasObjectRenderer { imageObject, position: { x: Math.round(rect.x), y: Math.round(rect.y) }, }); + + this.rasterizedImageCache = imageDTO.image_name; + + return imageDTO; }; getBlob = ({ rect }: { rect?: Rect }): Promise => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index c4397a73367..c880f60f59d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -1,11 +1,11 @@ import { $alt, $ctrl, $meta, $shift } from '@invoke-ai/ui-library'; -import type { Store } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; -import type { RootState } from 'app/store/store'; +import type { AppStore } from 'app/store/store'; import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; import { + $filteringEntity, $isDrawing, $isMouseDown, $lastAddedPoint, @@ -15,6 +15,7 @@ import { $shouldShowStagedImage, $spaceKey, $stageAttrs, + $transformingEntity, bboxChanged, brushWidthChanged, entityBrushLineAdded, @@ -81,10 +82,10 @@ type EntityStateAndAdapter = const log = logger('canvas'); export class CanvasStateApi { - _store: Store; + _store: AppStore; manager: CanvasManager; - constructor(store: Store, manager: CanvasManager) { + constructor(store: AppStore, manager: CanvasManager) { this._store = store; this.manager = manager; } @@ -188,6 +189,9 @@ export class CanvasStateApi { getLogLevel = () => { return this._store.getState().system.consoleLogLevel; }; + getFilterState = () => { + return this._store.getState().canvasV2.filter; + }; getEntity(identifier: CanvasEntityIdentifier): EntityStateAndAdapter | null { const state = this.getState(); @@ -256,7 +260,9 @@ export class CanvasStateApi { return currentFill; }; - $transformingEntity: WritableAtom = atom(); + $transformingEntity = $transformingEntity; + $filteringEntity = $filteringEntity; + $toolState: WritableAtom = atom(); $currentFill: WritableAtom = atom(); $selectedEntity: WritableAtom = atom(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts index 7821d70dc11..32307fd015d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts @@ -155,7 +155,7 @@ export class CanvasTool { const isMouseDown = this.manager.stateApi.$isMouseDown.get(); const tool = toolState.selected; - console.log(selectedEntity); + const isDrawableEntity = selectedEntity?.state.type === 'regional_guidance' || selectedEntity?.state.type === 'layer' || diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 7d5ac7a91a3..0470b362816 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -33,9 +33,10 @@ import type { EntityMovedPayload, EntityRasterizedPayload, EntityRectAddedPayload, + FilterConfig, StageAttrs, } from './types'; -import { RGBA_RED } from './types'; +import { IMAGE_FILTERS, RGBA_RED } from './types'; const initialState: CanvasV2State = { _version: 3, @@ -133,6 +134,10 @@ const initialState: CanvasV2State = { stagedImages: [], selectedStagedImageIndex: 0, }, + filter: { + autoProcess: true, + config: IMAGE_FILTERS.canny_image_processor.buildDefaults(), + }, }; export function selectEntity(state: CanvasV2State, { id, type }: CanvasEntityIdentifier) { @@ -222,11 +227,12 @@ export const canvasV2Slice = createSlice({ } else if (entity.type === 'layer') { entity.objects = [imageObject]; entity.position = position; + entity.imageCache = imageObject.image.image_name; state.layers.imageCache = null; } else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { entity.objects = [imageObject]; entity.position = position; - entity.imageCache = null; + entity.imageCache = imageObject.image.image_name; } else { assert(false, 'Not implemented'); } @@ -354,6 +360,12 @@ export const canvasV2Slice = createSlice({ state.ipAdapters.entities = []; state.controlAdapters.entities = []; }, + filterSelected: (state, action: PayloadAction<{ type: FilterConfig['type'] }>) => { + state.filter.config = IMAGE_FILTERS[action.payload.type].buildDefaults(); + }, + filterConfigChanged: (state, action: PayloadAction<{ config: FilterConfig }>) => { + state.filter.config = action.payload.config; + }, canvasReset: (state) => { state.bbox = deepClone(initialState.bbox); const optimalDimension = getOptimalDimension(state.params.model); @@ -415,6 +427,11 @@ export const { layerOpacityChanged, layerAllDeleted, layerImageCacheChanged, + layerUsedAsControlChanged, + layerControlAdapterModelChanged, + layerControlAdapterControlModeChanged, + layerControlAdapterWeightChanged, + layerControlAdapterBeginEndStepPctChanged, // IP Adapters ipaAdded, ipaRecalled, @@ -513,6 +530,9 @@ export const { sessionStagingAreaReset, sessionNextStagedImageSelected, sessionPrevStagedImageSelected, + // Filter + filterSelected, + filterConfigChanged, } = canvasV2Slice.actions; export const selectCanvasV2Slice = (state: RootState) => state.canvasV2; @@ -539,6 +559,8 @@ export const $lastAddedPoint = atom(null); export const $lastMouseDownPos = atom(null); export const $lastCursorPos = atom(null); export const $spaceKey = atom(false); +export const $transformingEntity = atom(null); +export const $filteringEntity = atom(null); export const canvasV2PersistConfig: PersistConfig = { name: canvasV2Slice.name, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts index ac819399dae..a71b2fcedbc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts @@ -13,7 +13,7 @@ import type { ControlModeV2, ControlNetConfig, Filter, - ProcessorConfig, + FilterConfig, T2IAdapterConfig, } from './types'; import { buildControlAdapterProcessorV2, imageDTOToImageObject } from './types'; @@ -145,7 +145,7 @@ export const controlAdaptersReducers = { } ca.controlMode = controlMode; }, - caProcessorConfigChanged: (state, action: PayloadAction<{ id: string; processorConfig: ProcessorConfig | null }>) => { + caProcessorConfigChanged: (state, action: PayloadAction<{ id: string; processorConfig: FilterConfig | null }>) => { const { id, processorConfig } = action.payload; const ca = selectCA(state, id); if (!ca) { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index 133e1213390..1d0407ec943 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -1,10 +1,11 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { getPrefixedId } from 'features/controlLayers/konva/util'; +import { zModelIdentifierField } from 'features/nodes/types/common'; import { merge } from 'lodash-es'; -import type { ImageDTO } from 'services/api/types'; +import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; -import type { CanvasLayerState, CanvasV2State } from './types'; +import type { CanvasLayerState, CanvasV2State, ControlModeV2, ControlNetConfig, T2IAdapterConfig } from './types'; import { imageDTOToImageWithDims } from './types'; export const selectLayer = (state: CanvasV2State, id: string) => state.layers.entities.find((layer) => layer.id === id); @@ -29,6 +30,7 @@ export const layersReducers = { opacity: 1, position: { x: 0, y: 0 }, imageCache: null, + controlAdapter: null, }; merge(layer, overrides); state.layers.entities.push(layer); @@ -64,4 +66,76 @@ export const layersReducers = { const { imageDTO } = action.payload; state.layers.imageCache = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; }, + layerUsedAsControlChanged: ( + state, + action: PayloadAction<{ id: string; controlAdapter: ControlNetConfig | T2IAdapterConfig | null }> + ) => { + const { id, controlAdapter } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + layer.controlAdapter = controlAdapter; + }, + layerControlAdapterModelChanged: ( + state, + action: PayloadAction<{ + id: string; + modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null; + }> + ) => { + const { id, modelConfig } = action.payload; + const layer = selectLayer(state, id); + if (!layer || !layer.controlAdapter) { + return; + } + if (!modelConfig) { + layer.controlAdapter.model = null; + return; + } + layer.controlAdapter.model = zModelIdentifierField.parse(modelConfig); + + // We may need to convert the CA to match the model + if (layer.controlAdapter.type === 't2i_adapter' && layer.controlAdapter.model.type === 'controlnet') { + // Converting from T2I Adapter to ControlNet - add `controlMode` + const controlNetConfig: ControlNetConfig = { + ...layer.controlAdapter, + type: 'controlnet', + controlMode: 'balanced', + }; + layer.controlAdapter = controlNetConfig; + } else if (layer.controlAdapter.type === 'controlnet' && layer.controlAdapter.model.type === 't2i_adapter') { + // Converting from ControlNet to T2I Adapter - remove `controlMode` + const { controlMode: _, ...rest } = layer.controlAdapter; + const t2iAdapterConfig: T2IAdapterConfig = { ...rest, type: 't2i_adapter' }; + layer.controlAdapter = t2iAdapterConfig; + } + }, + layerControlAdapterControlModeChanged: (state, action: PayloadAction<{ id: string; controlMode: ControlModeV2 }>) => { + const { id, controlMode } = action.payload; + const layer = selectLayer(state, id); + if (!layer || !layer.controlAdapter || layer.controlAdapter.type !== 'controlnet') { + return; + } + layer.controlAdapter.controlMode = controlMode; + }, + layerControlAdapterWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { + const { id, weight } = action.payload; + const layer = selectLayer(state, id); + if (!layer || !layer.controlAdapter) { + return; + } + layer.controlAdapter.weight = weight; + }, + layerControlAdapterBeginEndStepPctChanged: ( + state, + action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }> + ) => { + const { id, beginEndStepPct } = action.payload; + const layer = selectLayer(state, id); + if (!layer || !layer.controlAdapter) { + return; + } + layer.controlAdapter.beginEndStepPct = beginEndStepPct; + }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index 2b3a96be8d0..372d570e535 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -1,7 +1,7 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { CanvasV2State, CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types'; -import { imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/types'; +import { imageDTOToImageObject } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas'; import { isEqual } from 'lodash-es'; @@ -99,7 +99,7 @@ export const regionsReducers = { if (!rg) { return; } - rg.imageCache = imageDTOToImageWithDims(imageDTO); + rg.imageCache = imageDTO.image_name; }, rgAutoNegativeChanged: (state, action: PayloadAction<{ id: string; autoNegative: ParameterAutoNegative }>) => { const { id, autoNegative } = action.payload; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.test.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.test.ts index 6d95cac1217..9ee2dd975c5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.test.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.test.ts @@ -21,14 +21,14 @@ import type { MlsdProcessorConfig, NormalbaeProcessorConfig, PidiProcessorConfig, - ProcessorConfig, - ProcessorTypeV2, + FilterConfig, + FilterType, ZoeDepthProcessorConfig, } from './types'; describe('Control Adapter Types', () => { test('ProcessorType', () => { - assert>(); + assert>(); }); test('IP Adapter Method', () => { assert['method']>, IPMethodV2>>(); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 995d0e2d3bb..1705d5a3c68 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,4 +1,3 @@ -import type { JSONObject } from 'common/types'; import type { CanvasControlAdapter } from 'features/controlLayers/konva/CanvasControlAdapter'; import { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter'; import { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; @@ -36,6 +35,7 @@ import type { BaseModelType, ControlNetModelConfig, ImageDTO, + S, T2IAdapterModelConfig, } from 'services/api/types'; import { z } from 'zod'; @@ -175,7 +175,7 @@ const zZoeDepthProcessorConfig = z.object({ }); export type ZoeDepthProcessorConfig = z.infer; -export const zProcessorConfig = z.discriminatedUnion('type', [ +export const zFilterConfig = z.discriminatedUnion('type', [ zCannyProcessorConfig, zColorMapProcessorConfig, zContentShuffleProcessorConfig, @@ -191,9 +191,9 @@ export const zProcessorConfig = z.discriminatedUnion('type', [ zPidiProcessorConfig, zZoeDepthProcessorConfig, ]); -export type ProcessorConfig = z.infer; +export type FilterConfig = z.infer; -const zProcessorTypeV2 = z.enum([ +const zFilterType = z.enum([ 'canny_image_processor', 'color_map_image_processor', 'content_shuffle_image_processor', @@ -209,22 +209,19 @@ const zProcessorTypeV2 = z.enum([ 'pidi_image_processor', 'zoe_depth_image_processor', ]); -export type ProcessorTypeV2 = z.infer; -export const isProcessorTypeV2 = (v: unknown): v is ProcessorTypeV2 => zProcessorTypeV2.safeParse(v).success; +export type FilterType = z.infer; +export const isFilterType = (v: unknown): v is FilterType => zFilterType.safeParse(v).success; -type ProcessorData = { +const minDim = (image: ImageWithDims): number => Math.min(image.width, image.height); + +type ImageFilterData = { type: T; labelTKey: string; descriptionTKey: string; - buildDefaults(baseModel?: BaseModelType): Extract; - buildNode(image: ImageWithDims, config: Extract): Extract; + buildDefaults(baseModel?: BaseModelType): Extract; + buildNode(imageDTO: ImageWithDims, config: Extract): Extract; }; -const minDim = (image: ImageWithDims): number => Math.min(image.width, image.height); - -type CAProcessorsData = { - [key in ProcessorTypeV2]: ProcessorData; -}; /** * A dict of ControlNet processors, including: * - label translation key @@ -234,234 +231,243 @@ type CAProcessorsData = { * * TODO: Generate from the OpenAPI schema */ -export const CA_PROCESSOR_DATA: CAProcessorsData = { +export const IMAGE_FILTERS: { [key in FilterConfig['type']]: ImageFilterData } = { canny_image_processor: { type: 'canny_image_processor', labelTKey: 'controlnet.canny', descriptionTKey: 'controlnet.cannyDescription', - buildDefaults: () => ({ + buildDefaults: (): CannyProcessorConfig => ({ id: 'canny_image_processor', type: 'canny_image_processor', low_threshold: 100, high_threshold: 200, }), - buildNode: (image, config) => ({ + buildNode: (imageDTO: ImageDTO, config: CannyProcessorConfig): S['CannyImageProcessorInvocation'] => ({ ...config, type: 'canny_image_processor', - image: { image_name: image.image_name }, - detect_resolution: minDim(image), - image_resolution: minDim(image), + image: { image_name: imageDTO.image_name }, + detect_resolution: minDim(imageDTO), + image_resolution: minDim(imageDTO), }), }, color_map_image_processor: { type: 'color_map_image_processor', labelTKey: 'controlnet.colorMap', descriptionTKey: 'controlnet.colorMapDescription', - buildDefaults: () => ({ + buildDefaults: (): ColorMapProcessorConfig => ({ id: 'color_map_image_processor', type: 'color_map_image_processor', color_map_tile_size: 64, }), - buildNode: (image, config) => ({ + buildNode: (imageDTO: ImageDTO, config: ColorMapProcessorConfig): S['ColorMapImageProcessorInvocation'] => ({ ...config, type: 'color_map_image_processor', - image: { image_name: image.image_name }, + image: { image_name: imageDTO.image_name }, }), }, content_shuffle_image_processor: { type: 'content_shuffle_image_processor', labelTKey: 'controlnet.contentShuffle', descriptionTKey: 'controlnet.contentShuffleDescription', - buildDefaults: (baseModel) => ({ + buildDefaults: (baseModel: BaseModelType): ContentShuffleProcessorConfig => ({ id: 'content_shuffle_image_processor', type: 'content_shuffle_image_processor', h: baseModel === 'sdxl' ? 1024 : 512, w: baseModel === 'sdxl' ? 1024 : 512, f: baseModel === 'sdxl' ? 512 : 256, }), - buildNode: (image, config) => ({ + buildNode: ( + imageDTO: ImageDTO, + config: ContentShuffleProcessorConfig + ): S['ContentShuffleImageProcessorInvocation'] => ({ ...config, - image: { image_name: image.image_name }, - detect_resolution: minDim(image), - image_resolution: minDim(image), + image: { image_name: imageDTO.image_name }, + detect_resolution: minDim(imageDTO), + image_resolution: minDim(imageDTO), }), }, depth_anything_image_processor: { type: 'depth_anything_image_processor', labelTKey: 'controlnet.depthAnything', descriptionTKey: 'controlnet.depthAnythingDescription', - buildDefaults: () => ({ + buildDefaults: (): DepthAnythingProcessorConfig => ({ id: 'depth_anything_image_processor', type: 'depth_anything_image_processor', model_size: 'small_v2', }), - buildNode: (image, config) => ({ + buildNode: ( + imageDTO: ImageDTO, + config: DepthAnythingProcessorConfig + ): S['DepthAnythingImageProcessorInvocation'] => ({ ...config, - image: { image_name: image.image_name }, - resolution: minDim(image), + image: { image_name: imageDTO.image_name }, + resolution: minDim(imageDTO), }), }, hed_image_processor: { type: 'hed_image_processor', labelTKey: 'controlnet.hed', descriptionTKey: 'controlnet.hedDescription', - buildDefaults: () => ({ + buildDefaults: (): HedProcessorConfig => ({ id: 'hed_image_processor', type: 'hed_image_processor', scribble: false, }), - buildNode: (image, config) => ({ + buildNode: (imageDTO: ImageDTO, config: HedProcessorConfig): S['HedImageProcessorInvocation'] => ({ ...config, - image: { image_name: image.image_name }, - detect_resolution: minDim(image), - image_resolution: minDim(image), + image: { image_name: imageDTO.image_name }, + detect_resolution: minDim(imageDTO), + image_resolution: minDim(imageDTO), }), }, lineart_anime_image_processor: { type: 'lineart_anime_image_processor', labelTKey: 'controlnet.lineartAnime', descriptionTKey: 'controlnet.lineartAnimeDescription', - buildDefaults: () => ({ + buildDefaults: (): LineartAnimeProcessorConfig => ({ id: 'lineart_anime_image_processor', type: 'lineart_anime_image_processor', }), - buildNode: (image, config) => ({ + buildNode: ( + imageDTO: ImageDTO, + config: LineartAnimeProcessorConfig + ): S['LineartAnimeImageProcessorInvocation'] => ({ ...config, - image: { image_name: image.image_name }, - detect_resolution: minDim(image), - image_resolution: minDim(image), + image: { image_name: imageDTO.image_name }, + detect_resolution: minDim(imageDTO), + image_resolution: minDim(imageDTO), }), }, lineart_image_processor: { type: 'lineart_image_processor', labelTKey: 'controlnet.lineart', descriptionTKey: 'controlnet.lineartDescription', - buildDefaults: () => ({ + buildDefaults: (): LineartProcessorConfig => ({ id: 'lineart_image_processor', type: 'lineart_image_processor', coarse: false, }), - buildNode: (image, config) => ({ + buildNode: (imageDTO: ImageDTO, config: LineartProcessorConfig): S['LineartImageProcessorInvocation'] => ({ ...config, - image: { image_name: image.image_name }, - detect_resolution: minDim(image), - image_resolution: minDim(image), + image: { image_name: imageDTO.image_name }, + detect_resolution: minDim(imageDTO), + image_resolution: minDim(imageDTO), }), }, mediapipe_face_processor: { type: 'mediapipe_face_processor', labelTKey: 'controlnet.mediapipeFace', descriptionTKey: 'controlnet.mediapipeFaceDescription', - buildDefaults: () => ({ + buildDefaults: (): MediapipeFaceProcessorConfig => ({ id: 'mediapipe_face_processor', type: 'mediapipe_face_processor', max_faces: 1, min_confidence: 0.5, }), - buildNode: (image, config) => ({ + buildNode: (imageDTO: ImageDTO, config: MediapipeFaceProcessorConfig): S['MediapipeFaceProcessorInvocation'] => ({ ...config, - image: { image_name: image.image_name }, - detect_resolution: minDim(image), - image_resolution: minDim(image), + image: { image_name: imageDTO.image_name }, + detect_resolution: minDim(imageDTO), + image_resolution: minDim(imageDTO), }), }, midas_depth_image_processor: { type: 'midas_depth_image_processor', labelTKey: 'controlnet.depthMidas', descriptionTKey: 'controlnet.depthMidasDescription', - buildDefaults: () => ({ + buildDefaults: (): MidasDepthProcessorConfig => ({ id: 'midas_depth_image_processor', type: 'midas_depth_image_processor', a_mult: 2, bg_th: 0.1, }), - buildNode: (image, config) => ({ + buildNode: (imageDTO: ImageDTO, config: MidasDepthProcessorConfig): S['MidasDepthImageProcessorInvocation'] => ({ ...config, - image: { image_name: image.image_name }, - detect_resolution: minDim(image), - image_resolution: minDim(image), + image: { image_name: imageDTO.image_name }, + detect_resolution: minDim(imageDTO), + image_resolution: minDim(imageDTO), }), }, mlsd_image_processor: { type: 'mlsd_image_processor', labelTKey: 'controlnet.mlsd', descriptionTKey: 'controlnet.mlsdDescription', - buildDefaults: () => ({ + buildDefaults: (): MlsdProcessorConfig => ({ id: 'mlsd_image_processor', type: 'mlsd_image_processor', thr_d: 0.1, thr_v: 0.1, }), - buildNode: (image, config) => ({ + buildNode: (imageDTO: ImageDTO, config: MlsdProcessorConfig): S['MlsdImageProcessorInvocation'] => ({ ...config, - image: { image_name: image.image_name }, - detect_resolution: minDim(image), - image_resolution: minDim(image), + image: { image_name: imageDTO.image_name }, + detect_resolution: minDim(imageDTO), + image_resolution: minDim(imageDTO), }), }, normalbae_image_processor: { type: 'normalbae_image_processor', labelTKey: 'controlnet.normalBae', descriptionTKey: 'controlnet.normalBaeDescription', - buildDefaults: () => ({ + buildDefaults: (): NormalbaeProcessorConfig => ({ id: 'normalbae_image_processor', type: 'normalbae_image_processor', }), - buildNode: (image, config) => ({ + buildNode: (imageDTO: ImageDTO, config: NormalbaeProcessorConfig): S['NormalbaeImageProcessorInvocation'] => ({ ...config, - image: { image_name: image.image_name }, - detect_resolution: minDim(image), - image_resolution: minDim(image), + image: { image_name: imageDTO.image_name }, + detect_resolution: minDim(imageDTO), + image_resolution: minDim(imageDTO), }), }, dw_openpose_image_processor: { type: 'dw_openpose_image_processor', labelTKey: 'controlnet.dwOpenpose', descriptionTKey: 'controlnet.dwOpenposeDescription', - buildDefaults: () => ({ + buildDefaults: (): DWOpenposeProcessorConfig => ({ id: 'dw_openpose_image_processor', type: 'dw_openpose_image_processor', draw_body: true, draw_face: false, draw_hands: false, }), - buildNode: (image, config) => ({ + buildNode: (imageDTO: ImageDTO, config: DWOpenposeProcessorConfig): S['DWOpenposeImageProcessorInvocation'] => ({ ...config, - image: { image_name: image.image_name }, - image_resolution: minDim(image), + image: { image_name: imageDTO.image_name }, + image_resolution: minDim(imageDTO), }), }, pidi_image_processor: { type: 'pidi_image_processor', labelTKey: 'controlnet.pidi', descriptionTKey: 'controlnet.pidiDescription', - buildDefaults: () => ({ + buildDefaults: (): PidiProcessorConfig => ({ id: 'pidi_image_processor', type: 'pidi_image_processor', scribble: false, safe: false, }), - buildNode: (image, config) => ({ + buildNode: (imageDTO: ImageDTO, config: PidiProcessorConfig): S['PidiImageProcessorInvocation'] => ({ ...config, - image: { image_name: image.image_name }, - detect_resolution: minDim(image), - image_resolution: minDim(image), + image: { image_name: imageDTO.image_name }, + detect_resolution: minDim(imageDTO), + image_resolution: minDim(imageDTO), }), }, zoe_depth_image_processor: { type: 'zoe_depth_image_processor', labelTKey: 'controlnet.depthZoe', descriptionTKey: 'controlnet.depthZoeDescription', - buildDefaults: () => ({ + buildDefaults: (): ZoeDepthProcessorConfig => ({ id: 'zoe_depth_image_processor', type: 'zoe_depth_image_processor', }), - buildNode: (image, config) => ({ + buildNode: (imageDTO: ImageDTO, config: ZoeDepthProcessorConfig): S['ZoeDepthImageProcessorInvocation'] => ({ ...config, - image: { image_name: image.image_name }, + image: { image_name: imageDTO.image_name }, }), }, -}; +} as const; const zTool = z.enum(['brush', 'eraser', 'move', 'rect', 'view', 'bbox']); export type Tool = z.infer; @@ -575,17 +581,6 @@ export function isCanvasBrushLineState(obj: CanvasObjectState): obj is CanvasBru return obj.type === 'brush_line'; } -export const zCanvasLayerState = z.object({ - id: zId, - type: z.literal('layer'), - isEnabled: z.boolean(), - position: zCoordinate, - opacity: zOpacity, - objects: z.array(zCanvasObjectState), - imageCache: z.string().min(1).nullable(), -}); -export type CanvasLayerState = z.infer; - export const zCanvasIPAdapterState = z.object({ id: zId, type: z.literal('ip_adapter'), @@ -689,7 +684,7 @@ const zCanvasControlAdapterStateBase = z.object({ weight: z.number().gte(-1).lte(2), imageObject: zCanvasImageState.nullable(), processedImageObject: zCanvasImageState.nullable(), - processorConfig: zProcessorConfig.nullable(), + processorConfig: zFilterConfig.nullable(), processorPendingBatchId: z.string().nullable().default(null), beginEndStepPct: zBeginEndStepPct, model: zModelIdentifierField.nullable(), @@ -709,41 +704,55 @@ export const zCanvasControlAdapterState = z.discriminatedUnion('adapterType', [ zCanvasT2IAdapteState, ]); export type CanvasControlAdapterState = z.infer; -export type ControlNetConfig = Pick< - CanvasControlNetState, - | 'adapterType' - | 'weight' - | 'imageObject' - | 'processedImageObject' - | 'processorConfig' - | 'beginEndStepPct' - | 'model' - | 'controlMode' ->; -export type T2IAdapterConfig = Pick< - CanvasT2IAdapterState, - 'adapterType' | 'weight' | 'imageObject' | 'processedImageObject' | 'processorConfig' | 'beginEndStepPct' | 'model' ->; + +const zControlNetConfig = z.object({ + type: z.literal('controlnet'), + model: zModelIdentifierField.nullable(), + weight: z.number().gte(-1).lte(2), + beginEndStepPct: zBeginEndStepPct, + controlMode: zControlModeV2, +}); +export type ControlNetConfig = z.infer; + +const zT2IAdapterConfig = z.object({ + type: z.literal('t2i_adapter'), + model: zModelIdentifierField.nullable(), + weight: z.number().gte(-1).lte(2), + beginEndStepPct: zBeginEndStepPct, +}); +export type T2IAdapterConfig = z.infer; + +export const zCanvasLayerState = z.object({ + id: zId, + type: z.literal('layer'), + isEnabled: z.boolean(), + position: zCoordinate, + opacity: zOpacity, + objects: z.array(zCanvasObjectState), + imageCache: z.string().min(1).nullable(), + controlAdapter: z.discriminatedUnion('type', [zControlNetConfig, zT2IAdapterConfig]).nullable(), +}); +export type CanvasLayerState = z.infer; +export type CanvasLayerStateWithValidControlNet = Omit & { + controlAdapter: Omit & { model: ControlNetModelConfig }; +}; +export type CanvasLayerStateWithValidT2IAdapter = Omit & { + controlAdapter: Omit & { model: T2IAdapterModelConfig }; +}; export const initialControlNetV2: ControlNetConfig = { - adapterType: 'controlnet', + type: 'controlnet', model: null, weight: 1, beginEndStepPct: [0, 1], controlMode: 'balanced', - imageObject: null, - processedImageObject: null, - processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(), }; export const initialT2IAdapterV2: T2IAdapterConfig = { - adapterType: 't2i_adapter', + type: 't2i_adapter', model: null, weight: 1, beginEndStepPct: [0, 1], - imageObject: null, - processedImageObject: null, - processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(), }; export const initialIPAdapterV2: IPAdapterConfig = { @@ -757,12 +766,12 @@ export const initialIPAdapterV2: IPAdapterConfig = { export const buildControlAdapterProcessorV2 = ( modelConfig: ControlNetModelConfig | T2IAdapterModelConfig -): ProcessorConfig | null => { +): FilterConfig | null => { const defaultPreprocessor = modelConfig.default_settings?.preprocessor; - if (!isProcessorTypeV2(defaultPreprocessor)) { + if (!isFilterType(defaultPreprocessor)) { return null; } - const processorConfig = CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults(modelConfig.base); + const processorConfig = IMAGE_FILTERS[defaultPreprocessor].buildDefaults(modelConfig.base); return processorConfig; }; @@ -901,6 +910,10 @@ export type CanvasV2State = { stagedImages: StagingAreaImage[]; selectedStagedImageIndex: number; }; + filter: { + autoProcess: boolean; + config: FilterConfig; + }; }; export type StageAttrs = { @@ -964,5 +977,3 @@ export function isDrawableEntityType( ): entityType is 'layer' | 'regional_guidance' | 'inpaint_mask' { return entityType === 'layer' || entityType === 'regional_guidance' || entityType === 'inpaint_mask'; } - -export type GetLoggingContext = (extra?: JSONObject) => JSONObject; diff --git a/invokeai/frontend/web/src/features/metadata/util/parsers.ts b/invokeai/frontend/web/src/features/metadata/util/parsers.ts index 0d85d90af5b..5ee43d344aa 100644 --- a/invokeai/frontend/web/src/features/metadata/util/parsers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/parsers.ts @@ -2,12 +2,12 @@ import { getCAId, getImageObjectId, getIPAId, getLayerId } from 'features/contro import { defaultLoRAConfig } from 'features/controlLayers/store/lorasReducers'; import type { CanvasControlAdapterState, CanvasIPAdapterState, CanvasLayerState, LoRA } from 'features/controlLayers/store/types'; import { - CA_PROCESSOR_DATA, + IMAGE_FILTERS, imageDTOToImageWithDims, initialControlNetV2, initialIPAdapterV2, initialT2IAdapterV2, - isProcessorTypeV2, + isFilterType, zCanvasLayerState, } from 'features/controlLayers/store/types'; import type { @@ -559,8 +559,8 @@ const parseControlNetToControlAdapterLayer: MetadataParseFunc, base: BaseModelType -): Promise => { - const validControlAdapters = controlAdapters.filter((ca) => isValidControlAdapter(ca, base)); - for (const ca of validControlAdapters) { - if (ca.adapterType === 'controlnet') { - await addControlNetToGraph(manager, ca, g, bbox, denoise); +): Promise<(CanvasLayerStateWithValidControlNet | CanvasLayerStateWithValidT2IAdapter)[]> => { + const layersWithValidControlAdapters = layers + .filter((layer) => layer.isEnabled) + .filter((layer) => doesLayerHaveValidControlAdapter(layer, base)); + for (const layer of layersWithValidControlAdapters) { + const adapter = manager.layers.get(layer.id); + assert(adapter, 'Adapter not found'); + const imageDTO = await adapter.renderer.getImageDTO({ rect: bbox, is_intermediate: true, category: 'control' }); + if (layer.controlAdapter.type === 'controlnet') { + await addControlNetToGraph(g, layer, imageDTO, denoise); } else { - await addT2IAdapterToGraph(manager, ca, g, bbox, denoise); + await addT2IAdapterToGraph(g, layer, imageDTO, denoise); } } - return validControlAdapters; + return layersWithValidControlAdapters; }; const addControlNetCollectorSafe = (g: Graph, denoise: Invocation<'denoise_latents'>): Invocation<'collect'> => { @@ -49,16 +56,15 @@ const addControlNetCollectorSafe = (g: Graph, denoise: Invocation<'denoise_laten } }; -const addControlNetToGraph = async ( - manager: CanvasManager, - ca: CanvasControlNetState, +const addControlNetToGraph = ( g: Graph, - bbox: Rect, + layer: CanvasLayerStateWithValidControlNet, + imageDTO: ImageDTO, denoise: Invocation<'denoise_latents'> ) => { - const { id, beginEndStepPct, controlMode, model, weight } = ca; - assert(model, 'ControlNet model is required'); - const { image_name } = await manager.getControlAdapterImage({ id: ca.id, bbox, preview: true }); + const { id, controlAdapter } = layer; + const { beginEndStepPct, model, weight, controlMode } = controlAdapter; + const { image_name } = imageDTO; const controlNetCollect = addControlNetCollectorSafe(g, denoise); @@ -94,16 +100,15 @@ const addT2IAdapterCollectorSafe = (g: Graph, denoise: Invocation<'denoise_laten } }; -const addT2IAdapterToGraph = async ( - manager: CanvasManager, - ca: CanvasT2IAdapterState, +const addT2IAdapterToGraph = ( g: Graph, - bbox: Rect, + layer: CanvasLayerStateWithValidT2IAdapter, + imageDTO: ImageDTO, denoise: Invocation<'denoise_latents'> ) => { - const { id, beginEndStepPct, model, weight } = ca; - assert(model, 'T2I Adapter model is required'); - const { image_name } = await manager.getControlAdapterImage({ id: ca.id, bbox, preview: true }); + const { id, controlAdapter } = layer; + const { beginEndStepPct, model, weight } = controlAdapter; + const { image_name } = imageDTO; const t2iAdapterCollect = addT2IAdapterCollectorSafe(g, denoise); @@ -124,7 +129,7 @@ const addT2IAdapterToGraph = async ( const buildControlImage = ( image: ImageWithDims | null, processedImage: ImageWithDims | null, - processorConfig: ProcessorConfig | null + processorConfig: FilterConfig | null ): ImageField => { if (processedImage && processorConfig) { // We've processed the image in the app - use it for the control image. @@ -140,10 +145,29 @@ const buildControlImage = ( assert(false, 'Attempted to add unprocessed control image'); }; -const isValidControlAdapter = (ca: CanvasControlAdapterState, base: BaseModelType): boolean => { - // Must be have a model that matches the current base and must have a control image - const hasModel = Boolean(ca.model); - const modelMatchesBase = ca.model?.base === base; - const hasControlImage = Boolean(ca.imageObject || (ca.processedImageObject && ca.processorConfig)); - return hasModel && modelMatchesBase && hasControlImage; +const isValidControlAdapter = (controlAdapter: ControlNetConfig | T2IAdapterConfig, base: BaseModelType): boolean => { + // Must be have a model + const hasModel = Boolean(controlAdapter.model); + // Model must match the current base model + const modelMatchesBase = controlAdapter.model?.base === base; + return hasModel && modelMatchesBase; +}; + +const doesLayerHaveValidControlAdapter = ( + layer: CanvasLayerState, + base: BaseModelType +): layer is CanvasLayerStateWithValidControlNet | CanvasLayerStateWithValidT2IAdapter => { + if (!layer.controlAdapter) { + // Must have a control adapter + return false; + } + if (!layer.controlAdapter.model) { + // Control adapter must have a model selected + return false; + } + if (layer.controlAdapter.model.base !== base) { + // Selected model must match current base model + return false; + } + return true; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts index 157e0e96dc7..fad839efad4 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts @@ -1,9 +1,10 @@ import type { CanvasLayerState } from 'features/controlLayers/store/types'; -export const isValidLayer = (entity: CanvasLayerState) => { +export const isValidLayerWithoutControlAdapter = (layer: CanvasLayerState) => { return ( - entity.isEnabled && + layer.isEnabled && // Boolean(entity.bbox) && TODO(psyche): Re-enable this check when we have a way to calculate bbox for all layers - entity.objects.length > 0 + layer.objects.length > 0 && + layer.controlAdapter === null ); }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index 7f99ce6debf..8c9e198d6d3 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -215,7 +215,7 @@ export const buildSD1Graph = async (state: RootState, manager: CanvasManager): P const _addedCAs = await addControlAdapters( manager, - state.canvasV2.controlAdapters.entities, + state.canvasV2.layers.entities, g, state.canvasV2.bbox.rect, denoise, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index 34868e86025..56b4292c1e5 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -219,7 +219,7 @@ export const buildSDXLGraph = async (state: RootState, manager: CanvasManager): const _addedCAs = await addControlAdapters( manager, - state.canvasV2.controlAdapters.entities, + state.canvasV2.layers.entities, g, state.canvasV2.bbox.rect, denoise, diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index cebfdda5b5b..7d0011cd70e 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -1679,8 +1679,11 @@ export type components = { * @description The ID of the batch */ batch_id?: string; - /** @description The origin of this batch. */ - origin?: components["schemas"]["QueueItemOrigin"] | null; + /** + * Origin + * @description The origin of this batch. + */ + origin?: string | null; /** * Data * @description The batch data collection. @@ -1751,10 +1754,11 @@ export type components = { */ priority: number; /** + * Origin * @description The origin of the batch * @default null */ - origin: components["schemas"]["QueueItemOrigin"] | null; + origin: string | null; }; /** BatchStatus */ BatchStatus: { @@ -1768,8 +1772,11 @@ export type components = { * @description The ID of the batch */ batch_id: string; - /** @description The origin of the batch */ - origin: components["schemas"]["QueueItemOrigin"] | null; + /** + * Origin + * @description The origin of the batch + */ + origin: string | null; /** * Pending * @description Number of queue items with status 'pending' @@ -8844,10 +8851,11 @@ export type components = { */ batch_id: string; /** + * Origin * @description The origin of the batch * @default null */ - origin: components["schemas"]["QueueItemOrigin"] | null; + origin: string | null; /** * Session Id * @description The ID of the session (aka graph execution state) @@ -8895,10 +8903,11 @@ export type components = { */ batch_id: string; /** + * Origin * @description The origin of the batch * @default null */ - origin: components["schemas"]["QueueItemOrigin"] | null; + origin: string | null; /** * Session Id * @description The ID of the session (aka graph execution state) @@ -8963,10 +8972,11 @@ export type components = { */ batch_id: string; /** + * Origin * @description The origin of the batch * @default null */ - origin: components["schemas"]["QueueItemOrigin"] | null; + origin: string | null; /** * Session Id * @description The ID of the session (aka graph execution state) @@ -9189,10 +9199,11 @@ export type components = { */ batch_id: string; /** + * Origin * @description The origin of the batch * @default null */ - origin: components["schemas"]["QueueItemOrigin"] | null; + origin: string | null; /** * Session Id * @description The ID of the session (aka graph execution state) @@ -12268,10 +12279,11 @@ export type components = { */ batch_id: string; /** + * Origin * @description The origin of the batch * @default null */ - origin: components["schemas"]["QueueItemOrigin"] | null; + origin: string | null; /** * Status * @description The new status of the queue item @@ -13628,8 +13640,11 @@ export type components = { * @description The ID of the batch associated with this queue item */ batch_id: string; - /** @description The origin of this queue item. */ - origin?: components["schemas"]["QueueItemOrigin"] | null; + /** + * Origin + * @description The origin of this queue item. + */ + origin?: string | null; /** * Session Id * @description The ID of the session associated with this queue item. The session doesn't exist in graph_executions until the queue item is executed. @@ -13710,8 +13725,11 @@ export type components = { * @description The ID of the batch associated with this queue item */ batch_id: string; - /** @description The origin of this queue item. */ - origin?: components["schemas"]["QueueItemOrigin"] | null; + /** + * Origin + * @description The origin of this queue item. + */ + origin?: string | null; /** * Session Id * @description The ID of the session associated with this queue item. The session doesn't exist in graph_executions until the queue item is executed. From 3a2efb351d7e1ae142a09460a3dc0b6e4d67024f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 14 Aug 2024 20:35:19 +1000 Subject: [PATCH 353/678] feat(ui): implement cache for image rasterization, rip out some old controladapters code --- .../middleware/listenerMiddleware/index.ts | 3 +- .../listeners/boardAndImagesDeleted.ts | 12 +- .../listeners/imageDeletionListeners.ts | 30 ++- .../listeners/imageDropped.ts | 25 +- .../listeners/imageUploaded.ts | 14 +- .../listeners/modelSelected.ts | 17 +- .../listeners/modelsLoaded.ts | 18 +- .../src/common/hooks/useIsReadyToEnqueue.ts | 70 +++--- .../components/CanvasEntityList.tsx | 2 - .../ControlAdapter/ControlAdapter.tsx | 39 --- .../ControlAdapterActionsMenu.tsx | 17 -- .../ControlAdapterImagePreview.tsx | 229 ------------------ .../ControlAdapter/ControlAdapterList.tsx | 38 --- .../ControlAdapterOpacityAndFilter.tsx | 99 -------- .../ControlAdapterProcessorConfig.tsx | 84 ------- .../ControlAdapterProcessorTypeSelect.tsx | 70 ------ .../ControlAdapter/ControlAdapterSettings.tsx | 154 ------------ .../processors/CannyProcessor.tsx | 68 ------ .../processors/ColorMapProcessor.tsx | 48 ---- .../processors/ContentShuffleProcessor.tsx | 79 ------ .../processors/DWOpenposeProcessor.tsx | 62 ----- .../processors/DepthAnythingProcessor.tsx | 47 ---- .../processors/HedProcessor.tsx | 32 --- .../processors/LineartProcessor.tsx | 32 --- .../processors/MediapipeFaceProcessor.tsx | 74 ------ .../processors/MidasDepthProcessor.tsx | 76 ------ .../processors/MlsdImageProcessor.tsx | 76 ------ .../processors/PidiProcessor.tsx | 43 ---- .../processors/ProcessorWrapper.tsx | 15 -- .../ControlAdapter/processors/types.ts | 6 - .../ControlLayersSettingsPopover.tsx | 13 +- .../components/DeleteAllLayersButton.tsx | 2 +- .../controlLayers/components/Layer/Layer.tsx | 3 - .../components/Layer/LayerOpacity.tsx | 82 ------- .../common/CanvasEntityActionMenuItems.tsx | 5 - .../controlLayers/hooks/addLayerHooks.ts | 4 +- .../controlLayers/konva/CanvasFilter.ts | 7 +- .../controlLayers/konva/CanvasManager.ts | 120 ++++----- .../konva/CanvasObjectRenderer.ts | 86 +++---- .../controlLayers/konva/CanvasStateApi.ts | 27 +-- .../controlLayers/store/canvasV2Slice.ts | 173 ++++++------- .../store/controlAdaptersReducers.ts | 197 --------------- .../store/inpaintMaskReducers.ts | 6 - .../controlLayers/store/layersReducers.ts | 41 ++-- .../controlLayers/store/regionsReducers.ts | 10 +- .../features/controlLayers/store/selectors.ts | 2 +- .../src/features/controlLayers/store/types.ts | 20 +- .../src/features/metadata/util/recallers.ts | 6 +- .../graph/generation/addControlAdapters.ts | 3 +- .../nodes/util/graph/generation/addInpaint.ts | 2 +- .../util/graph/generation/addOutpaint.ts | 2 +- .../nodes/util/graph/generation/addRegions.ts | 7 +- 52 files changed, 322 insertions(+), 2075 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapter.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterActionsMenu.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterImagePreview.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterList.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterOpacityAndFilter.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterProcessorConfig.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterProcessorTypeSelect.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterSettings.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/CannyProcessor.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ColorMapProcessor.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ContentShuffleProcessor.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DWOpenposeProcessor.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DepthAnythingProcessor.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/HedProcessor.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/LineartProcessor.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MediapipeFaceProcessor.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MidasDepthProcessor.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MlsdImageProcessor.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/PidiProcessor.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ProcessorWrapper.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/types.ts delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerOpacity.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts index 29df0bf5422..c9dd883c6d8 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -9,7 +9,6 @@ import { addBatchEnqueuedListener } from 'app/store/middleware/listenerMiddlewar import { addDeleteBoardAndImagesFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted'; import { addBoardIdSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/boardIdSelected'; import { addBulkDownloadListeners } from 'app/store/middleware/listenerMiddleware/listeners/bulkDownload'; -import { addControlAdapterPreprocessor } from 'app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor'; import { addEnqueueRequestedLinear } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear'; import { addEnqueueRequestedNodes } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes'; import { addGalleryImageClickedListener } from 'app/store/middleware/listenerMiddleware/listeners/galleryImageClicked'; @@ -133,4 +132,4 @@ addAdHocPostProcessingRequestedListener(startAppListening); addDynamicPromptsListener(startAppListening); addSetDefaultSettingsListener(startAppListening); -addControlAdapterPreprocessor(startAppListening); +// addControlAdapterPreprocessor(startAppListening); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts index d3f20971b78..f5c3a95537c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts @@ -1,5 +1,5 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { caAllDeleted, ipaAllDeleted, layerAllDeleted } from 'features/controlLayers/store/canvasV2Slice'; +import { ipaAllDeleted, layerAllDeleted } from 'features/controlLayers/store/canvasV2Slice'; import { getImageUsage } from 'features/deleteImageModal/store/selectors'; import { nodeEditorReset } from 'features/nodes/store/nodesSlice'; import { imagesApi } from 'services/api/endpoints/images'; @@ -14,7 +14,7 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS let wereLayersReset = false; let wasNodeEditorReset = false; - let wereControlAdaptersReset = false; + const wereControlAdaptersReset = false; let wereIPAdaptersReset = false; const { nodes, canvasV2 } = getState(); @@ -31,10 +31,10 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS wasNodeEditorReset = true; } - if (imageUsage.isControlAdapterImage && !wereControlAdaptersReset) { - dispatch(caAllDeleted()); - wereControlAdaptersReset = true; - } + // if (imageUsage.isControlAdapterImage && !wereControlAdaptersReset) { + // dispatch(caAllDeleted()); + // wereControlAdaptersReset = true; + // } if (imageUsage.isIPAdapterImage && !wereIPAdaptersReset) { dispatch(ipaAllDeleted()); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts index 4d57cd6b02b..17e65967013 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts @@ -1,12 +1,7 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import type { AppDispatch, RootState } from 'app/store/store'; -import { - caImageChanged, - caProcessedImageChanged, - entityDeleted, - ipaImageChanged, -} from 'features/controlLayers/store/canvasV2Slice'; +import { entityDeleted, ipaImageChanged } from 'features/controlLayers/store/canvasV2Slice'; import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions'; import { isModalOpenChanged } from 'features/deleteImageModal/store/slice'; import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors'; @@ -39,14 +34,17 @@ const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: Im }); }; -const deleteControlAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { - state.canvasV2.controlAdapters.entities.forEach(({ id, imageObject, processedImageObject }) => { - if (imageObject?.image.image_name === imageDTO.image_name || processedImageObject?.image.image_name === imageDTO.image_name) { - dispatch(caImageChanged({ id, imageDTO: null })); - dispatch(caProcessedImageChanged({ id, imageDTO: null })); - } - }); -}; +// const deleteControlAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { +// state.canvasV2.controlAdapters.entities.forEach(({ id, imageObject, processedImageObject }) => { +// if ( +// imageObject?.image.image_name === imageDTO.image_name || +// processedImageObject?.image.image_name === imageDTO.image_name +// ) { +// dispatch(caImageChanged({ id, imageDTO: null })); +// dispatch(caProcessedImageChanged({ id, imageDTO: null })); +// } +// }); +// }; const deleteIPAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { state.canvasV2.ipAdapters.entities.forEach(({ id, imageObject }) => { @@ -120,7 +118,7 @@ export const addImageDeletionListeners = (startAppListening: AppStartListening) } deleteNodesImages(state, dispatch, imageDTO); - deleteControlAdapterImages(state, dispatch, imageDTO); + // deleteControlAdapterImages(state, dispatch, imageDTO); deleteIPAdapterImages(state, dispatch, imageDTO); deleteLayerImages(state, dispatch, imageDTO); } catch { @@ -161,7 +159,7 @@ export const addImageDeletionListeners = (startAppListening: AppStartListening) imageDTOs.forEach((imageDTO) => { deleteNodesImages(state, dispatch, imageDTO); - deleteControlAdapterImages(state, dispatch, imageDTO); + // deleteControlAdapterImages(state, dispatch, imageDTO); deleteIPAdapterImages(state, dispatch, imageDTO); deleteLayerImages(state, dispatch, imageDTO); }); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index 80c1f2bebda..39bb31ce634 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -3,7 +3,6 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { parseify } from 'common/util/serialize'; import { - caImageChanged, ipaImageChanged, layerAdded, rgIPAdapterImageChanged, @@ -60,18 +59,18 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => return; } - /** - * Image dropped on Control Adapter Layer - */ - if ( - overData.actionType === 'SET_CA_IMAGE' && - activeData.payloadType === 'IMAGE_DTO' && - activeData.payload.imageDTO - ) { - const { id } = overData.context; - dispatch(caImageChanged({ id, imageDTO: activeData.payload.imageDTO })); - return; - } + // /** + // * Image dropped on Control Adapter Layer + // */ + // if ( + // overData.actionType === 'SET_CA_IMAGE' && + // activeData.payloadType === 'IMAGE_DTO' && + // activeData.payload.imageDTO + // ) { + // const { id } = overData.context; + // dispatch(caImageChanged({ id, imageDTO: activeData.payload.imageDTO })); + // return; + // } /** * Image dropped on IP Adapter Layer diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts index fa23cdfc064..b0b26fbd1a6 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts @@ -1,6 +1,6 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { caImageChanged, ipaImageChanged, rgIPAdapterImageChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { ipaImageChanged, rgIPAdapterImageChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectListBoardsQueryArgs } from 'features/gallery/store/gallerySelectors'; import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice'; @@ -79,12 +79,12 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis return; } - if (postUploadAction?.type === 'SET_CA_IMAGE') { - const { id } = postUploadAction; - dispatch(caImageChanged({ id, imageDTO })); - toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage') }); - return; - } + // if (postUploadAction?.type === 'SET_CA_IMAGE') { + // const { id } = postUploadAction; + // dispatch(caImageChanged({ id, imageDTO })); + // toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage') }); + // return; + // } if (postUploadAction?.type === 'SET_IPA_IMAGE') { const { id } = postUploadAction; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts index e07dcdc8440..bbb9cd1cef4 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts @@ -1,7 +1,6 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { - entityIsEnabledToggled, loraDeleted, modelChanged, vaeSelected, @@ -50,14 +49,14 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = } // handle incompatible controlnets - state.canvasV2.controlAdapters.entities.forEach((ca) => { - if (ca.model?.base !== newBaseModel) { - modelsCleared += 1; - if (ca.isEnabled) { - dispatch(entityIsEnabledToggled({ entityIdentifier: { id: ca.id, type: 'control_adapter' } })); - } - } - }); + // state.canvasV2.controlAdapters.entities.forEach((ca) => { + // if (ca.model?.base !== newBaseModel) { + // modelsCleared += 1; + // if (ca.isEnabled) { + // dispatch(entityIsEnabledToggled({ entityIdentifier: { id: ca.id, type: 'control_adapter' } })); + // } + // } + // }); if (modelsCleared > 0) { toast({ diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts index 87967544a8c..86daeab55a3 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts @@ -5,7 +5,6 @@ import type { JSONObject } from 'common/types'; import { bboxHeightChanged, bboxWidthChanged, - caModelChanged, ipaModelChanged, loraDeleted, modelChanged, @@ -21,7 +20,6 @@ import type { Logger } from 'roarr'; import { modelConfigsAdapterSelectors, modelsApi } from 'services/api/endpoints/models'; import type { AnyModelConfig } from 'services/api/types'; import { - isControlNetOrT2IAdapterModelConfig, isIPAdapterModelConfig, isLoRAModelConfig, isNonRefinerMainModelConfig, @@ -171,14 +169,14 @@ const handleLoRAModels: ModelHandler = (models, state, dispatch, _log) => { }; const handleControlAdapterModels: ModelHandler = (models, state, dispatch, _log) => { - const caModels = models.filter(isControlNetOrT2IAdapterModelConfig); - state.canvasV2.controlAdapters.entities.forEach((ca) => { - const isModelAvailable = caModels.some((m) => m.key === ca.model?.key); - if (isModelAvailable) { - return; - } - dispatch(caModelChanged({ id: ca.id, modelConfig: null })); - }); + // const caModels = models.filter(isControlNetOrT2IAdapterModelConfig); + // state.canvasV2.controlAdapters.entities.forEach((ca) => { + // const isModelAvailable = caModels.some((m) => m.key === ca.model?.key); + // if (isModelAvailable) { + // return; + // } + // dispatch(caModelChanged({ id: ca.id, modelConfig: null })); + // }); }; const handleIPAdapterModels: ModelHandler = (models, state, dispatch, _log) => { diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index bf8bf22d08c..48df41fc484 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -125,41 +125,41 @@ const createSelector = (templates: Templates) => reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') }); } - canvasV2.controlAdapters.entities - .filter((ca) => ca.isEnabled) - .forEach((ca, i) => { - const layerLiteral = i18n.t('controlLayers.layers_one'); - const layerNumber = i + 1; - const layerType = i18n.t(LAYER_TYPE_TO_TKEY[ca.type]); - const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; - const problems: string[] = []; - // Must have model - if (!ca.model) { - problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoModelSelected')); - } - // Model base must match - if (ca.model?.base !== model?.base) { - problems.push(i18n.t('parameters.invoke.layer.controlAdapterIncompatibleBaseModel')); - } - // Must have a control image OR, if it has a processor, it must have a processed image - if (!ca.imageObject) { - problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoImageSelected')); - } else if (ca.processorConfig && !ca.processedImageObject) { - problems.push(i18n.t('parameters.invoke.layer.controlAdapterImageNotProcessed')); - } - // T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL) - if (ca.adapterType === 't2i_adapter') { - const multiple = model?.base === 'sdxl' ? 32 : 64; - if (bbox.rect.width % multiple !== 0 || bbox.rect.height % multiple !== 0) { - problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions', { multiple })); - } - } - - if (problems.length) { - const content = upperFirst(problems.join(', ')); - reasons.push({ prefix, content }); - } - }); + // canvasV2.controlAdapters.entities + // .filter((ca) => ca.isEnabled) + // .forEach((ca, i) => { + // const layerLiteral = i18n.t('controlLayers.layers_one'); + // const layerNumber = i + 1; + // const layerType = i18n.t(LAYER_TYPE_TO_TKEY[ca.type]); + // const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; + // const problems: string[] = []; + // // Must have model + // if (!ca.model) { + // problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoModelSelected')); + // } + // // Model base must match + // if (ca.model?.base !== model?.base) { + // problems.push(i18n.t('parameters.invoke.layer.controlAdapterIncompatibleBaseModel')); + // } + // // Must have a control image OR, if it has a processor, it must have a processed image + // if (!ca.imageObject) { + // problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoImageSelected')); + // } else if (ca.processorConfig && !ca.processedImageObject) { + // problems.push(i18n.t('parameters.invoke.layer.controlAdapterImageNotProcessed')); + // } + // // T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL) + // if (ca.adapterType === 't2i_adapter') { + // const multiple = model?.base === 'sdxl' ? 32 : 64; + // if (bbox.rect.width % multiple !== 0 || bbox.rect.height % multiple !== 0) { + // problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions', { multiple })); + // } + // } + + // if (problems.length) { + // const content = upperFirst(problems.join(', ')); + // reasons.push({ prefix, content }); + // } + // }); canvasV2.ipAdapters.entities .filter((ipa) => ipa.isEnabled) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx index 9b3c9d3bc74..edb1708aae6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx @@ -1,6 +1,5 @@ import { Flex } from '@invoke-ai/ui-library'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; -import { ControlAdapterList } from 'features/controlLayers/components/ControlAdapter/ControlAdapterList'; import { InpaintMask } from 'features/controlLayers/components/InpaintMask/InpaintMask'; import { IPAdapterList } from 'features/controlLayers/components/IPAdapter/IPAdapterList'; import { LayerEntityList } from 'features/controlLayers/components/Layer/LayerEntityList'; @@ -13,7 +12,6 @@ export const CanvasEntityList = memo(() => { - diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapter.tsx deleted file mode 100644 index 280fd742880..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapter.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Spacer, useDisclosure } from '@invoke-ai/ui-library'; -import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; -import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; -import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; -import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; -import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; -import { ControlAdapterActionsMenu } from 'features/controlLayers/components/ControlAdapter/ControlAdapterActionsMenu'; -import { ControlAdapterOpacityAndFilter } from 'features/controlLayers/components/ControlAdapter/ControlAdapterOpacityAndFilter'; -import { ControlAdapterSettings } from 'features/controlLayers/components/ControlAdapter/ControlAdapterSettings'; -import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; -import { memo, useMemo } from 'react'; - -type Props = { - id: string; -}; - -export const ControlAdapter = memo(({ id }: Props) => { - const entityIdentifier = useMemo(() => ({ id, type: 'control_adapter' }), [id]); - const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); - - return ( - - - - - - - - - - - {isOpen && } - - - ); -}); - -ControlAdapter.displayName = 'ControlAdapter'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterActionsMenu.tsx deleted file mode 100644 index a9b5815a95d..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterActionsMenu.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Menu, MenuList } from '@invoke-ai/ui-library'; -import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/common/CanvasEntityActionMenuItems'; -import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; -import { memo } from 'react'; - -export const ControlAdapterActionsMenu = memo(() => { - return ( - - - - - - - ); -}); - -ControlAdapterActionsMenu.displayName = 'ControlAdapterActionsMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterImagePreview.tsx deleted file mode 100644 index 8faab377805..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterImagePreview.tsx +++ /dev/null @@ -1,229 +0,0 @@ -import { Box, Flex, Spinner, useShiftModifier } from '@invoke-ai/ui-library'; -import { skipToken } from '@reduxjs/toolkit/query'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIDndImage from 'common/components/IAIDndImage'; -import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; -import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; -import type { CanvasControlAdapterState } from 'features/controlLayers/store/types'; -import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types'; -import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; -import { memo, useCallback, useEffect, useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiArrowCounterClockwiseBold, PiFloppyDiskBold, PiRulerBold } from 'react-icons/pi'; -import { - useAddImageToBoardMutation, - useChangeImageIsIntermediateMutation, - useGetImageDTOQuery, - useRemoveImageFromBoardMutation, -} from 'services/api/endpoints/images'; -import type { ImageDTO, PostUploadAction } from 'services/api/types'; - -type Props = { - controlAdapter: CanvasControlAdapterState; - onChangeImage: (imageDTO: ImageDTO | null) => void; - droppableData: TypesafeDroppableData; - postUploadAction: PostUploadAction; - onErrorLoadingImage: () => void; - onErrorLoadingProcessedImage: () => void; -}; - -export const ControlAdapterImagePreview = memo( - ({ - controlAdapter, - onChangeImage, - droppableData, - postUploadAction, - onErrorLoadingImage, - onErrorLoadingProcessedImage, - }: Props) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId); - const isConnected = useAppSelector((s) => s.system.isConnected); - const optimalDimension = useAppSelector(selectOptimalDimension); - const shift = useShiftModifier(); - - const [isMouseOverImage, setIsMouseOverImage] = useState(false); - - const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery( - controlAdapter.imageObject?.image.image_name ?? skipToken - ); - const { currentData: processedControlImage, isError: isErrorProcessedControlImage } = useGetImageDTOQuery( - controlAdapter.processedImageObject?.image.image_name ?? skipToken - ); - - const [changeIsIntermediate] = useChangeImageIsIntermediateMutation(); - const [addToBoard] = useAddImageToBoardMutation(); - const [removeFromBoard] = useRemoveImageFromBoardMutation(); - const handleResetControlImage = useCallback(() => { - onChangeImage(null); - }, [onChangeImage]); - - const handleSaveControlImage = useCallback(async () => { - if (!processedControlImage) { - return; - } - - await changeIsIntermediate({ - imageDTO: processedControlImage, - is_intermediate: false, - }).unwrap(); - - if (autoAddBoardId !== 'none') { - addToBoard({ - imageDTO: processedControlImage, - board_id: autoAddBoardId, - }); - } else { - removeFromBoard({ imageDTO: processedControlImage }); - } - }, [processedControlImage, changeIsIntermediate, autoAddBoardId, addToBoard, removeFromBoard]); - - const handleSetControlImageToDimensions = useCallback(() => { - if (!controlImage) { - return; - } - - const options = { updateAspectRatio: true, clamp: true }; - - if (shift) { - const { width, height } = controlImage; - dispatch(bboxWidthChanged({ width, ...options })); - dispatch(bboxHeightChanged({ height, ...options })); - } else { - const { width, height } = calculateNewSize( - controlImage.width / controlImage.height, - optimalDimension * optimalDimension - ); - dispatch(bboxWidthChanged({ width, ...options })); - dispatch(bboxHeightChanged({ height, ...options })); - } - }, [controlImage, dispatch, optimalDimension, shift]); - - const handleMouseEnter = useCallback(() => { - setIsMouseOverImage(true); - }, []); - - const handleMouseLeave = useCallback(() => { - setIsMouseOverImage(false); - }, []); - - const draggableData = useMemo(() => { - if (controlImage) { - return { - id: controlAdapter.id, - payloadType: 'IMAGE_DTO', - payload: { imageDTO: controlImage }, - }; - } - }, [controlImage, controlAdapter.id]); - - const shouldShowProcessedImage = - controlImage && - processedControlImage && - !isMouseOverImage && - !controlAdapter.processorPendingBatchId && - controlAdapter.processorConfig !== null; - - useEffect(() => { - if (!isConnected) { - return; - } - if (isErrorControlImage) { - onErrorLoadingImage(); - } - if (isErrorProcessedControlImage) { - onErrorLoadingProcessedImage(); - } - }, [ - handleResetControlImage, - isConnected, - isErrorControlImage, - isErrorProcessedControlImage, - onErrorLoadingImage, - onErrorLoadingProcessedImage, - ]); - - return ( - - - - - - - - {controlImage && ( - - } - tooltip={t('controlnet.resetControlImage')} - /> - } - tooltip={t('controlnet.saveControlImage')} - /> - } - tooltip={ - shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions') - } - /> - - )} - - {controlAdapter.processorPendingBatchId !== null && ( - - - - )} - - ); - } -); - -ControlAdapterImagePreview.displayName = 'ControlAdapterImagePreview'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterList.tsx deleted file mode 100644 index 694dade9a80..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterList.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { CanvasEntityGroupTitle } from 'features/controlLayers/components/common/CanvasEntityGroupTitle'; -import { ControlAdapter } from 'features/controlLayers/components/ControlAdapter/ControlAdapter'; -import { mapId } from 'features/controlLayers/konva/util'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; - -const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { - return canvasV2.controlAdapters.entities.map(mapId).reverse(); -}); - -export const ControlAdapterList = memo(() => { - const { t } = useTranslation(); - const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'control_adapter')); - const caIds = useAppSelector(selectEntityIds); - - if (caIds.length === 0) { - return null; - } - - if (caIds.length > 0) { - return ( - <> - - {caIds.map((id) => ( - - ))} - - ); - } -}); - -ControlAdapterList.displayName = 'ControlAdapterList'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterOpacityAndFilter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterOpacityAndFilter.tsx deleted file mode 100644 index ba4f85a1e10..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterOpacityAndFilter.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { - CompositeNumberInput, - CompositeSlider, - Flex, - FormControl, - FormLabel, - IconButton, - Popover, - PopoverArrow, - PopoverBody, - PopoverContent, - PopoverTrigger, - Switch, -} from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { stopPropagation } from 'common/util/stopPropagation'; -import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { caFilterChanged, caOpacityChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { selectCAOrThrow } from 'features/controlLayers/store/controlAdaptersReducers'; -import type { ChangeEvent } from 'react'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiDropHalfFill } from 'react-icons/pi'; - -const marks = [0, 25, 50, 75, 100]; -const formatPct = (v: number | string) => `${v} %`; - -export const ControlAdapterOpacityAndFilter = memo(() => { - const { id } = useEntityIdentifierContext(); - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const opacity = useAppSelector((s) => Math.round(selectCAOrThrow(s.canvasV2, id).opacity * 100)); - const isFilterEnabled = useAppSelector((s) => - selectCAOrThrow(s.canvasV2, id).filters.includes('LightnessToAlphaFilter') - ); - const onChangeOpacity = useCallback( - (v: number) => { - dispatch(caOpacityChanged({ id, opacity: v / 100 })); - }, - [dispatch, id] - ); - const onChangeFilter = useCallback( - (e: ChangeEvent) => { - dispatch(caFilterChanged({ id, filters: e.target.checked ? ['LightnessToAlphaFilter'] : [] })); - }, - [dispatch, id] - ); - return ( - - - } - variant="ghost" - onDoubleClick={stopPropagation} - /> - - - - - - - - {t('controlLayers.opacityFilter')} - - - - - {t('controlLayers.opacity')} - - - - - - - - ); -}); - -ControlAdapterOpacityAndFilter.displayName = 'ControlAdapterOpacityAndFilter'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterProcessorConfig.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterProcessorConfig.tsx deleted file mode 100644 index 6836584ba36..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterProcessorConfig.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { CannyProcessor } from 'features/controlLayers/components/ControlAdapter/processors/CannyProcessor'; -import { ColorMapProcessor } from 'features/controlLayers/components/ControlAdapter/processors/ColorMapProcessor'; -import { ContentShuffleProcessor } from 'features/controlLayers/components/ControlAdapter/processors/ContentShuffleProcessor'; -import { DepthAnythingProcessor } from 'features/controlLayers/components/ControlAdapter/processors/DepthAnythingProcessor'; -import { DWOpenposeProcessor } from 'features/controlLayers/components/ControlAdapter/processors/DWOpenposeProcessor'; -import { HedProcessor } from 'features/controlLayers/components/ControlAdapter/processors/HedProcessor'; -import { LineartProcessor } from 'features/controlLayers/components/ControlAdapter/processors/LineartProcessor'; -import { MediapipeFaceProcessor } from 'features/controlLayers/components/ControlAdapter/processors/MediapipeFaceProcessor'; -import { MidasDepthProcessor } from 'features/controlLayers/components/ControlAdapter/processors/MidasDepthProcessor'; -import { MlsdImageProcessor } from 'features/controlLayers/components/ControlAdapter/processors/MlsdImageProcessor'; -import { PidiProcessor } from 'features/controlLayers/components/ControlAdapter/processors/PidiProcessor'; -import type { FilterConfig } from 'features/controlLayers/store/types'; -import { memo } from 'react'; - -type Props = { - config: FilterConfig | null; - onChange: (config: FilterConfig | null) => void; -}; - -export const ControlAdapterProcessorConfig = memo(({ config, onChange }: Props) => { - if (!config) { - return null; - } - - if (config.type === 'canny_image_processor') { - return ; - } - - if (config.type === 'color_map_image_processor') { - return ; - } - - if (config.type === 'depth_anything_image_processor') { - return ; - } - - if (config.type === 'hed_image_processor') { - return ; - } - - if (config.type === 'lineart_image_processor') { - return ; - } - - if (config.type === 'content_shuffle_image_processor') { - return ; - } - - if (config.type === 'lineart_anime_image_processor') { - // No configurable options for this processor - return null; - } - - if (config.type === 'mediapipe_face_processor') { - return ; - } - - if (config.type === 'midas_depth_image_processor') { - return ; - } - - if (config.type === 'mlsd_image_processor') { - return ; - } - - if (config.type === 'normalbae_image_processor') { - // No configurable options for this processor - return null; - } - - if (config.type === 'dw_openpose_image_processor') { - return ; - } - - if (config.type === 'pidi_image_processor') { - return ; - } - - if (config.type === 'zoe_depth_image_processor') { - return null; - } -}); - -ControlAdapterProcessorConfig.displayName = 'ControlAdapterProcessorConfig'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterProcessorTypeSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterProcessorTypeSelect.tsx deleted file mode 100644 index e4d0d1878d1..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterProcessorTypeSelect.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import type { ComboboxOnChange } from '@invoke-ai/ui-library'; -import { Combobox, Flex, FormControl, FormLabel, IconButton } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import type {FilterConfig } from 'features/controlLayers/store/types'; -import { IMAGE_FILTERS, isFilterType } from 'features/controlLayers/store/types'; -import { configSelector } from 'features/system/store/configSelectors'; -import { includes, map } from 'lodash-es'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiXBold } from 'react-icons/pi'; -import { assert } from 'tsafe'; - -type Props = { - config: FilterConfig | null; - onChange: (config: FilterConfig | null) => void; -}; - -const selectDisabledProcessors = createMemoizedSelector( - configSelector, - (config) => config.sd.disabledControlNetProcessors -); - -export const ControlAdapterProcessorTypeSelect = memo(({ config, onChange }: Props) => { - const { t } = useTranslation(); - const disabledProcessors = useAppSelector(selectDisabledProcessors); - const options = useMemo(() => { - return map(IMAGE_FILTERS, ({ labelTKey }, type) => ({ value: type, label: t(labelTKey) })).filter( - (o) => !includes(disabledProcessors, o.value) - ); - }, [disabledProcessors, t]); - - const _onChange = useCallback( - (v) => { - if (!v) { - onChange(null); - } else { - assert(isFilterType(v.value)); - onChange(IMAGE_FILTERS[v.value].buildDefaults()); - } - }, - [onChange] - ); - const clearProcessor = useCallback(() => { - onChange(null); - }, [onChange]); - const value = useMemo(() => options.find((o) => o.value === config?.type) ?? null, [options, config?.type]); - - return ( - - - - {t('controlnet.processor')} - - - - } - variant="ghost" - size="sm" - /> - - ); -}); - -ControlAdapterProcessorTypeSelect.displayName = 'ControlAdapterProcessorTypeSelect'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterSettings.tsx deleted file mode 100644 index ef2379d1171..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterSettings.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { Box, Divider, Flex, Icon, IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct'; -import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; -import { Weight } from 'features/controlLayers/components/common/Weight'; -import { ControlAdapterControlModeSelect } from 'features/controlLayers/components/ControlAdapter/ControlAdapterControlModeSelect'; -import { ControlAdapterImagePreview } from 'features/controlLayers/components/ControlAdapter/ControlAdapterImagePreview'; -import { ControlAdapterModel } from 'features/controlLayers/components/ControlAdapter/ControlAdapterModel'; -import { ControlAdapterProcessorConfig } from 'features/controlLayers/components/ControlAdapter/ControlAdapterProcessorConfig'; -import { ControlAdapterProcessorTypeSelect } from 'features/controlLayers/components/ControlAdapter/ControlAdapterProcessorTypeSelect'; -import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { - caBeginEndStepPctChanged, - caControlModeChanged, - caImageChanged, - caModelChanged, - caProcessedImageChanged, - caProcessorConfigChanged, - caWeightChanged, -} from 'features/controlLayers/store/canvasV2Slice'; -import { selectCAOrThrow } from 'features/controlLayers/store/controlAdaptersReducers'; -import type { ControlModeV2, FilterConfig } from 'features/controlLayers/store/types'; -import type { CAImageDropData } from 'features/dnd/types'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiCaretUpBold } from 'react-icons/pi'; -import { useToggle } from 'react-use'; -import type { - CAImagePostUploadAction, - ControlNetModelConfig, - ImageDTO, - T2IAdapterModelConfig, -} from 'services/api/types'; - -export const ControlAdapterSettings = memo(() => { - const { id } = useEntityIdentifierContext(); - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - const [isExpanded, toggleIsExpanded] = useToggle(false); - - const controlAdapter = useAppSelector((s) => selectCAOrThrow(s.canvasV2, id)); - - const onChangeBeginEndStepPct = useCallback( - (beginEndStepPct: [number, number]) => { - dispatch(caBeginEndStepPctChanged({ id, beginEndStepPct })); - }, - [dispatch, id] - ); - - const onChangeControlMode = useCallback( - (controlMode: ControlModeV2) => { - dispatch(caControlModeChanged({ id, controlMode })); - }, - [dispatch, id] - ); - - const onChangeWeight = useCallback( - (weight: number) => { - dispatch(caWeightChanged({ id, weight })); - }, - [dispatch, id] - ); - - const onChangeProcessorConfig = useCallback( - (processorConfig: FilterConfig | null) => { - dispatch(caProcessorConfigChanged({ id, processorConfig })); - }, - [dispatch, id] - ); - - const onChangeModel = useCallback( - (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => { - dispatch(caModelChanged({ id, modelConfig })); - }, - [dispatch, id] - ); - - const onChangeImage = useCallback( - (imageDTO: ImageDTO | null) => { - dispatch(caImageChanged({ id, imageDTO })); - }, - [dispatch, id] - ); - - const onErrorLoadingImage = useCallback(() => { - dispatch(caImageChanged({ id, imageDTO: null })); - }, [dispatch, id]); - - const onErrorLoadingProcessedImage = useCallback(() => { - dispatch(caProcessedImageChanged({ id, imageDTO: null })); - }, [dispatch, id]); - - const droppableData = useMemo(() => ({ actionType: 'SET_CA_IMAGE', context: { id }, id }), [id]); - const postUploadAction = useMemo(() => ({ id, type: 'SET_CA_IMAGE' }), [id]); - - return ( - - - - - - - - - } - /> - - - - {controlAdapter.adapterType === 'controlnet' && ( - - )} - - - - - - - - {isExpanded && ( - <> - - - - - - - )} - - - ); -}); - -ControlAdapterSettings.displayName = 'ControlAdapterSettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/CannyProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/CannyProcessor.tsx deleted file mode 100644 index d1b15c6645b..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/CannyProcessor.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; -import type { CannyProcessorConfig } from 'features/controlLayers/store/types'; -import { IMAGE_FILTERS } from 'features/controlLayers/store/types'; -import { useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './ProcessorWrapper'; - -type Props = ProcessorComponentProps; -const DEFAULTS = IMAGE_FILTERS['canny_image_processor'].buildDefaults(); - -export const CannyProcessor = ({ onChange, config }: Props) => { - const { t } = useTranslation(); - const handleLowThresholdChanged = useCallback( - (v: number) => { - onChange({ ...config, low_threshold: v }); - }, - [onChange, config] - ); - const handleHighThresholdChanged = useCallback( - (v: number) => { - onChange({ ...config, high_threshold: v }); - }, - [onChange, config] - ); - - return ( - - - {t('controlnet.lowThreshold')} - - - - - {t('controlnet.highThreshold')} - - - - - ); -}; - -CannyProcessor.displayName = 'CannyProcessor'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ColorMapProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ColorMapProcessor.tsx deleted file mode 100644 index 3261703ded6..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ColorMapProcessor.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; -import type { ColorMapProcessorConfig } from 'features/controlLayers/store/types'; -import { IMAGE_FILTERS } from 'features/controlLayers/store/types'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './ProcessorWrapper'; - -type Props = ProcessorComponentProps; -const DEFAULTS = IMAGE_FILTERS['color_map_image_processor'].buildDefaults(); - -export const ColorMapProcessor = memo(({ onChange, config }: Props) => { - const { t } = useTranslation(); - const handleColorMapTileSizeChanged = useCallback( - (v: number) => { - onChange({ ...config, color_map_tile_size: v }); - }, - [config, onChange] - ); - - return ( - - - {t('controlnet.colorMapTileSize')} - - - - - ); -}); - -ColorMapProcessor.displayName = 'ColorMapProcessor'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ContentShuffleProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ContentShuffleProcessor.tsx deleted file mode 100644 index b86387097a1..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ContentShuffleProcessor.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; -import type { ContentShuffleProcessorConfig } from 'features/controlLayers/store/types'; -import { IMAGE_FILTERS } from 'features/controlLayers/store/types'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './ProcessorWrapper'; - -type Props = ProcessorComponentProps; -const DEFAULTS = IMAGE_FILTERS['content_shuffle_image_processor'].buildDefaults(); - -export const ContentShuffleProcessor = memo(({ onChange, config }: Props) => { - const { t } = useTranslation(); - - const handleWChanged = useCallback( - (v: number) => { - onChange({ ...config, w: v }); - }, - [config, onChange] - ); - - const handleHChanged = useCallback( - (v: number) => { - onChange({ ...config, h: v }); - }, - [config, onChange] - ); - - const handleFChanged = useCallback( - (v: number) => { - onChange({ ...config, f: v }); - }, - [config, onChange] - ); - - return ( - - - {t('controlnet.w')} - - - - - {t('controlnet.h')} - - - - - {t('controlnet.f')} - - - - - ); -}); - -ContentShuffleProcessor.displayName = 'ContentShuffleProcessor'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DWOpenposeProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DWOpenposeProcessor.tsx deleted file mode 100644 index 4b197eb361e..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DWOpenposeProcessor.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Flex, FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; -import type { DWOpenposeProcessorConfig } from 'features/controlLayers/store/types'; -import { IMAGE_FILTERS } from 'features/controlLayers/store/types'; -import type { ChangeEvent } from 'react'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './ProcessorWrapper'; - -type Props = ProcessorComponentProps; -const DEFAULTS = IMAGE_FILTERS['dw_openpose_image_processor'].buildDefaults(); - -export const DWOpenposeProcessor = memo(({ onChange, config }: Props) => { - const { t } = useTranslation(); - - const handleDrawBodyChanged = useCallback( - (e: ChangeEvent) => { - onChange({ ...config, draw_body: e.target.checked }); - }, - [config, onChange] - ); - - const handleDrawFaceChanged = useCallback( - (e: ChangeEvent) => { - onChange({ ...config, draw_face: e.target.checked }); - }, - [config, onChange] - ); - - const handleDrawHandsChanged = useCallback( - (e: ChangeEvent) => { - onChange({ ...config, draw_hands: e.target.checked }); - }, - [config, onChange] - ); - - return ( - - - - {t('controlnet.body')} - - - - {t('controlnet.face')} - - - - {t('controlnet.hands')} - - - - - ); -}); - -DWOpenposeProcessor.displayName = 'DWOpenposeProcessor'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DepthAnythingProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DepthAnythingProcessor.tsx deleted file mode 100644 index b4aa76ed6e6..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DepthAnythingProcessor.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import type { ComboboxOnChange } from '@invoke-ai/ui-library'; -import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; -import type { DepthAnythingModelSize, DepthAnythingProcessorConfig } from 'features/controlLayers/store/types'; -import { isDepthAnythingModelSize } from 'features/controlLayers/store/types'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './ProcessorWrapper'; - -type Props = ProcessorComponentProps; - -export const DepthAnythingProcessor = memo(({ onChange, config }: Props) => { - const { t } = useTranslation(); - const handleModelSizeChange = useCallback( - (v) => { - if (!isDepthAnythingModelSize(v?.value)) { - return; - } - onChange({ ...config, model_size: v.value }); - }, - [config, onChange] - ); - - const options: { label: string; value: DepthAnythingModelSize }[] = useMemo( - () => [ - { label: t('controlnet.depthAnythingSmallV2'), value: 'small_v2' }, - { label: t('controlnet.small'), value: 'small' }, - { label: t('controlnet.base'), value: 'base' }, - { label: t('controlnet.large'), value: 'large' }, - ], - [t] - ); - - const value = useMemo(() => options.filter((o) => o.value === config.model_size)[0], [options, config.model_size]); - - return ( - - - {t('controlnet.modelSize')} - - - - ); -}); - -DepthAnythingProcessor.displayName = 'DepthAnythingProcessor'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/HedProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/HedProcessor.tsx deleted file mode 100644 index 6c27e386c55..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/HedProcessor.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; -import type { HedProcessorConfig } from 'features/controlLayers/store/types'; -import type { ChangeEvent } from 'react'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './ProcessorWrapper'; - -type Props = ProcessorComponentProps; - -export const HedProcessor = memo(({ onChange, config }: Props) => { - const { t } = useTranslation(); - - const handleScribbleChanged = useCallback( - (e: ChangeEvent) => { - onChange({ ...config, scribble: e.target.checked }); - }, - [config, onChange] - ); - - return ( - - - {t('controlnet.scribble')} - - - - ); -}); - -HedProcessor.displayName = 'HedProcessor'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/LineartProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/LineartProcessor.tsx deleted file mode 100644 index 4abf7e920c6..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/LineartProcessor.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; -import type { LineartProcessorConfig } from 'features/controlLayers/store/types'; -import type { ChangeEvent } from 'react'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './ProcessorWrapper'; - -type Props = ProcessorComponentProps; - -export const LineartProcessor = memo(({ onChange, config }: Props) => { - const { t } = useTranslation(); - - const handleCoarseChanged = useCallback( - (e: ChangeEvent) => { - onChange({ ...config, coarse: e.target.checked }); - }, - [config, onChange] - ); - - return ( - - - {t('controlnet.coarse')} - - - - ); -}); - -LineartProcessor.displayName = 'LineartProcessor'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MediapipeFaceProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MediapipeFaceProcessor.tsx deleted file mode 100644 index ad59e9f8d78..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MediapipeFaceProcessor.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; -import type { MediapipeFaceProcessorConfig } from 'features/controlLayers/store/types'; -import { IMAGE_FILTERS } from 'features/controlLayers/store/types'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './ProcessorWrapper'; - -type Props = ProcessorComponentProps; -const DEFAULTS = IMAGE_FILTERS['mediapipe_face_processor'].buildDefaults(); - -export const MediapipeFaceProcessor = memo(({ onChange, config }: Props) => { - const { t } = useTranslation(); - - const handleMaxFacesChanged = useCallback( - (v: number) => { - onChange({ ...config, max_faces: v }); - }, - [config, onChange] - ); - - const handleMinConfidenceChanged = useCallback( - (v: number) => { - onChange({ ...config, min_confidence: v }); - }, - [config, onChange] - ); - - return ( - - - {t('controlnet.maxFaces')} - - - - - {t('controlnet.minConfidence')} - - - - - ); -}); - -MediapipeFaceProcessor.displayName = 'MediapipeFaceProcessor'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MidasDepthProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MidasDepthProcessor.tsx deleted file mode 100644 index c9740c23d38..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MidasDepthProcessor.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; -import type { MidasDepthProcessorConfig } from 'features/controlLayers/store/types'; -import { IMAGE_FILTERS } from 'features/controlLayers/store/types'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './ProcessorWrapper'; - -type Props = ProcessorComponentProps; -const DEFAULTS = IMAGE_FILTERS['midas_depth_image_processor'].buildDefaults(); - -export const MidasDepthProcessor = memo(({ onChange, config }: Props) => { - const { t } = useTranslation(); - - const handleAMultChanged = useCallback( - (v: number) => { - onChange({ ...config, a_mult: v }); - }, - [config, onChange] - ); - - const handleBgThChanged = useCallback( - (v: number) => { - onChange({ ...config, bg_th: v }); - }, - [config, onChange] - ); - - return ( - - - {t('controlnet.amult')} - - - - - {t('controlnet.bgth')} - - - - - ); -}); - -MidasDepthProcessor.displayName = 'MidasDepthProcessor'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MlsdImageProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MlsdImageProcessor.tsx deleted file mode 100644 index d907cbe705a..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MlsdImageProcessor.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; -import type { MlsdProcessorConfig } from 'features/controlLayers/store/types'; -import { IMAGE_FILTERS } from 'features/controlLayers/store/types'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './ProcessorWrapper'; - -type Props = ProcessorComponentProps; -const DEFAULTS = IMAGE_FILTERS['mlsd_image_processor'].buildDefaults(); - -export const MlsdImageProcessor = memo(({ onChange, config }: Props) => { - const { t } = useTranslation(); - - const handleThrDChanged = useCallback( - (v: number) => { - onChange({ ...config, thr_d: v }); - }, - [config, onChange] - ); - - const handleThrVChanged = useCallback( - (v: number) => { - onChange({ ...config, thr_v: v }); - }, - [config, onChange] - ); - - return ( - - - {t('controlnet.w')} - - - - - {t('controlnet.h')} - - - - - ); -}); - -MlsdImageProcessor.displayName = 'MlsdImageProcessor'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/PidiProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/PidiProcessor.tsx deleted file mode 100644 index 6605baaadf3..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/PidiProcessor.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; -import type { PidiProcessorConfig } from 'features/controlLayers/store/types'; -import type { ChangeEvent } from 'react'; -import { useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './ProcessorWrapper'; - -type Props = ProcessorComponentProps; - -export const PidiProcessor = ({ onChange, config }: Props) => { - const { t } = useTranslation(); - - const handleScribbleChanged = useCallback( - (e: ChangeEvent) => { - onChange({ ...config, scribble: e.target.checked }); - }, - [config, onChange] - ); - - const handleSafeChanged = useCallback( - (e: ChangeEvent) => { - onChange({ ...config, safe: e.target.checked }); - }, - [config, onChange] - ); - - return ( - - - {t('controlnet.scribble')} - - - - {t('controlnet.safe')} - - - - ); -}; - -PidiProcessor.displayName = 'PidiProcessor'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ProcessorWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ProcessorWrapper.tsx deleted file mode 100644 index 2b2468703be..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ProcessorWrapper.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Flex } from '@invoke-ai/ui-library'; -import type { PropsWithChildren } from 'react'; -import { memo } from 'react'; - -type Props = PropsWithChildren; - -const ProcessorWrapper = (props: Props) => { - return ( - - {props.children} - - ); -}; - -export default memo(ProcessorWrapper); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/types.ts b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/types.ts deleted file mode 100644 index a635e1f90f6..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { FilterConfig } from 'features/controlLayers/store/types'; - -export type ProcessorComponentProps = { - onChange: (config: T) => void; - config: T; -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx index afaafe69f29..0527377d6fd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx @@ -1,4 +1,5 @@ import { + Button, Checkbox, Flex, FormControl, @@ -11,7 +12,11 @@ import { } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { MaskOpacity } from 'features/controlLayers/components/MaskOpacity'; -import { clipToBboxChanged, invertScrollChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { + clipToBboxChanged, + invertScrollChanged, + rasterizationCachesInvalidated, +} from 'features/controlLayers/store/canvasV2Slice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -30,6 +35,9 @@ const ControlLayersSettingsPopover = () => { (e: ChangeEvent) => dispatch(clipToBboxChanged(e.target.checked)), [dispatch] ); + const invalidateRasterizationCaches = useCallback(() => { + dispatch(rasterizationCachesInvalidated()); + }, [dispatch]); return ( @@ -47,6 +55,9 @@ const ControlLayersSettingsPopover = () => { {t('unifiedCanvas.clipToBbox')} + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx index 16a3c88d83f..765608e6d77 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx @@ -11,7 +11,7 @@ export const DeleteAllLayersButton = memo(() => { const entityCount = useAppSelector((s) => { return ( s.canvasV2.regions.entities.length + - s.canvasV2.controlAdapters.entities.length + + // s.canvasV2.controlAdapters.entities.length + s.canvasV2.ipAdapters.entities.length + s.canvasV2.layers.entities.length ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx index 65c5501039a..a91f1044207 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx @@ -10,8 +10,6 @@ import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityI import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; -import { LayerOpacity } from './LayerOpacity'; - type Props = { id: string; }; @@ -26,7 +24,6 @@ export const Layer = memo(({ id }: Props) => { - diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerOpacity.tsx deleted file mode 100644 index 0d42659966c..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerOpacity.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { - CompositeNumberInput, - CompositeSlider, - Flex, - FormControl, - FormLabel, - IconButton, - Popover, - PopoverArrow, - PopoverBody, - PopoverContent, - PopoverTrigger, -} from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { stopPropagation } from 'common/util/stopPropagation'; -import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { layerOpacityChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { selectLayerOrThrow } from 'features/controlLayers/store/layersReducers'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiDropHalfFill } from 'react-icons/pi'; - -const marks = [0, 25, 50, 75, 100]; -const formatPct = (v: number | string) => `${v} %`; - -export const LayerOpacity = memo(() => { - const entityIdentifier = useEntityIdentifierContext(); - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const opacity = useAppSelector((s) => Math.round(selectLayerOrThrow(s.canvasV2, entityIdentifier.id).opacity * 100)); - const onChangeOpacity = useCallback( - (v: number) => { - dispatch(layerOpacityChanged({ id: entityIdentifier.id, opacity: v / 100 })); - }, - [dispatch, entityIdentifier.id] - ); - return ( - - - } - variant="ghost" - onDoubleClick={stopPropagation} - /> - - - - - - - {t('controlLayers.opacity')} - - - - - - - - ); -}); - -LayerOpacity.displayName = 'LayerOpacity'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx index b75e87d8287..0a812ce0c2d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx @@ -40,11 +40,6 @@ const getIndexAndCount = ( index: canvasV2.layers.entities.findIndex((entity) => entity.id === id), count: canvasV2.layers.entities.length, }; - } else if (type === 'control_adapter') { - return { - index: canvasV2.controlAdapters.entities.findIndex((entity) => entity.id === id), - count: canvasV2.controlAdapters.entities.length, - }; } else if (type === 'regional_guidance') { return { index: canvasV2.regions.entities.findIndex((entity) => entity.id === id), diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts index c41a95384c5..c7fd177d755 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts @@ -1,6 +1,6 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { deepClone } from 'common/util/deepClone'; -import { caAdded, ipaAdded, rgIPAdapterAdded } from 'features/controlLayers/store/canvasV2Slice'; +import { ipaAdded, rgIPAdapterAdded } from 'features/controlLayers/store/canvasV2Slice'; import { IMAGE_FILTERS, initialControlNetV2, @@ -37,7 +37,7 @@ export const useAddCALayer = () => { const initialConfig = deepClone(model.type === 'controlnet' ? initialControlNetV2 : initialT2IAdapterV2); const config = { ...initialConfig, model: zModelIdentifierField.parse(model), processorConfig }; - dispatch(caAdded({ config })); + // dispatch(caAdded({ config })); }, [dispatch, model, baseModel]); return [addCALayer, isDisabled] as const; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilter.ts index dd1581612fc..f718b78a61d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilter.ts @@ -100,7 +100,12 @@ export class CanvasFilter { this.manager.stateApi.rasterizeEntity({ entityIdentifier: this.parent.getEntityIdentifier(), imageObject: this.imageState, - position: { x: Math.round(rect.x), y: Math.round(rect.y) }, + rect: { + x: Math.round(rect.x), + y: Math.round(rect.y), + width: this.imageState.image.height, + height: this.imageState.image.width, + }, }); this.parent.renderer.showObjects(); this.manager.stateApi.$filteringEntity.set(null); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 6777bbd3ffe..9d1f76ff4dd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -9,20 +9,27 @@ import { konvaNodeToBlob, konvaNodeToImageData, nanoid, + previewBlob, } from 'features/controlLayers/konva/util'; import type { Extents, ExtentsResult, GetBboxTask, WorkerLogMessage } from 'features/controlLayers/konva/worker'; -import type { CanvasV2State, Coordinate, Dimensions, GenerationMode, Rect } from 'features/controlLayers/store/types'; +import type { + CanvasV2State, + Coordinate, + Dimensions, + GenerationMode, + ImageCache, + Rect, +} from 'features/controlLayers/store/types'; import { isValidLayerWithoutControlAdapter } from 'features/nodes/util/graph/generation/addLayers'; import type Konva from 'konva'; -import { clamp } from 'lodash-es'; +import { clamp, isEqual } from 'lodash-es'; import { atom } from 'nanostores'; import type { Logger } from 'roarr'; import { getImageDTO, uploadImage } from 'services/api/endpoints/images'; import type { ImageDTO } from 'services/api/types'; -import { assert } from 'tsafe'; import { CanvasBackground } from './CanvasBackground'; -import { CanvasControlAdapter } from './CanvasControlAdapter'; +import type { CanvasControlAdapter } from './CanvasControlAdapter'; import { CanvasLayerAdapter } from './CanvasLayerAdapter'; import { CanvasMaskAdapter } from './CanvasMaskAdapter'; import { CanvasPreview } from './CanvasPreview'; @@ -144,40 +151,15 @@ export class CanvasManager { this._worker.postMessage(task, [data.buffer]); } - async renderControlAdapters() { - const { entities } = this.stateApi.getControlAdaptersState(); - - for (const canvasControlAdapter of this.controlAdapters.values()) { - if (!entities.find((ca) => ca.id === canvasControlAdapter.id)) { - canvasControlAdapter.destroy(); - this.controlAdapters.delete(canvasControlAdapter.id); - } - } - - for (const entity of entities) { - let adapter = this.controlAdapters.get(entity.id); - if (!adapter) { - adapter = new CanvasControlAdapter(entity, this); - this.controlAdapters.set(adapter.id, adapter); - this.stage.add(adapter.konva.layer); - } - await adapter.render(entity); - } - } - arrangeEntities() { - const { getLayersState, getControlAdaptersState, getRegionsState } = this.stateApi; + const { getLayersState, getRegionsState } = this.stateApi; const layers = getLayersState().entities; - const controlAdapters = getControlAdaptersState().entities; const regions = getRegionsState().entities; let zIndex = 0; this.background.konva.layer.zIndex(++zIndex); for (const layer of layers) { this.layers.get(layer.id)?.konva.layer.zIndex(++zIndex); } - for (const ca of controlAdapters) { - this.controlAdapters.get(ca.id)?.konva.layer.zIndex(++zIndex); - } for (const rg of regions) { this.regions.get(rg.id)?.konva.layer.zIndex(++zIndex); } @@ -358,16 +340,6 @@ export class CanvasManager { }); } - if ( - this._isFirstRender || - state.controlAdapters.entities !== this._prevState.controlAdapters.entities || - state.tool.selected !== this._prevState.tool.selected || - state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id - ) { - this.log.debug('Rendering control adapters'); - await this.renderControlAdapters(); - } - this.stateApi.$toolState.set(state.tool); this.stateApi.$selectedEntityIdentifier.set(state.selectedEntityIdentifier); this.stateApi.$selectedEntity.set(this.stateApi.getSelectedEntity()); @@ -382,12 +354,7 @@ export class CanvasManager { await this.preview.bbox.render(); } - if ( - this._isFirstRender || - state.layers !== this._prevState.layers || - state.controlAdapters !== this._prevState.controlAdapters || - state.regions !== this._prevState.regions - ) { + if (this._isFirstRender || state.layers !== this._prevState.layers || state.regions !== this._prevState.regions) { // this.log.debug('Updating entity bboxes'); // debouncedUpdateBboxes(stage, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions, onBboxChanged); } @@ -400,7 +367,6 @@ export class CanvasManager { if ( this._isFirstRender || state.layers.entities !== this._prevState.layers.entities || - state.controlAdapters.entities !== this._prevState.controlAdapters.entities || state.regions.entities !== this._prevState.regions.entities || state.inpaintMask !== this._prevState.inpaintMask || state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id @@ -569,45 +535,43 @@ export class CanvasManager { return konvaNodeToImageData(this.getCompositeLayerStageClone(), rect); }; - getCompositeLayerImageDTO = async (rect?: Rect): Promise => { + getCompositeRasterizedImageCache = (rect: Rect): ImageCache | null => { + const layerState = this.stateApi.getLayersState(); + const imageCache = layerState.compositeRasterizationCache.find((cache) => isEqual(cache.rect, rect)); + return imageCache ?? null; + }; + + getCompositeLayerImageDTO = async (rect: Rect): Promise => { + let imageDTO: ImageDTO | null = null; + const compositeRasterizedImageCache = this.getCompositeRasterizedImageCache(rect); + + if (compositeRasterizedImageCache) { + imageDTO = await getImageDTO(compositeRasterizedImageCache.imageName); + if (imageDTO) { + this.log.trace({ rect, compositeRasterizedImageCache, imageDTO }, 'Using cached composite rasterized image'); + return imageDTO; + } + } + + this.log.trace({ rect }, 'Rasterizing composite layer'); + const blob = await this.getCompositeLayerBlob(rect); - const imageDTO = await uploadImage(blob, 'composite-layer.png', 'general', true); - this.stateApi.setLayerImageCache(imageDTO); + + if (this._isDebugging) { + previewBlob(blob, 'Rasterized entity'); + } + + imageDTO = await uploadImage(blob, 'composite-layer.png', 'general', true); + this.stateApi.compositeLayerRasterized({ imageName: imageDTO.image_name, rect }); return imageDTO; }; getInpaintMaskBlob = (rect?: Rect): Promise => { - return this.inpaintMask.renderer.getBlob({ rect }); + return this.inpaintMask.renderer.getBlob(rect); }; getInpaintMaskImageData = (rect?: Rect): ImageData => { - return this.inpaintMask.renderer.getImageData({ rect }); - }; - - getInpaintMaskImageDTO = async (rect?: Rect): Promise => { - const blob = await this.inpaintMask.renderer.getBlob({ rect }); - const imageDTO = await uploadImage(blob, 'inpaint-mask.png', 'mask', true); - this.stateApi.setInpaintMaskImageCache(imageDTO); - return imageDTO; - }; - - getRegionMaskImageDTO = async (id: string, rect?: Rect): Promise => { - const region = this.stateApi.getEntity({ id, type: 'regional_guidance' }); - assert(region?.type === 'regional_guidance'); - if (region.state.imageCache) { - const imageDTO = await getImageDTO(region.state.imageCache); - if (imageDTO) { - return imageDTO; - } - } - return region.adapter.renderer.getImageDTO({ - rect, - category: 'other', - is_intermediate: true, - onUploaded: (imageDTO) => { - this.stateApi.setRegionMaskImageCache(region.state.id, imageDTO); - }, - }); + return this.inpaintMask.renderer.getImageData(rect); }; getGenerationMode(): GenerationMode { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index 4f6b16cda30..726ec21cf5f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -14,14 +14,16 @@ import type { CanvasEraserLineState, CanvasImageState, CanvasRectState, + ImageCache, Rect, RgbColor, } from 'features/controlLayers/store/types'; import { imageDTOToImageObject } from 'features/controlLayers/store/types'; import Konva from 'konva'; +import { isEqual } from 'lodash-es'; import type { Logger } from 'roarr'; import { getImageDTO, uploadImage } from 'services/api/endpoints/images'; -import type { ImageCategory, ImageDTO } from 'services/api/types'; +import type { ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; /** @@ -62,12 +64,6 @@ export class CanvasObjectRenderer { */ renderers: Map = new Map(); - /** - * A cache of the rasterized image data URL. If the cache is null, the parent has not been rasterized since its last - * change. - */ - rasterizedImageCache: string | null = null; - /** * A object containing singleton Konva nodes. */ @@ -168,19 +164,6 @@ export class CanvasObjectRenderer { didRender = (await this.renderObject(this.buffer)) || didRender; } - if (didRender && this.rasterizedImageCache) { - const hasOneObject = this.renderers.size === 1; - const firstObject = Array.from(this.renderers.values())[0]; - if ( - hasOneObject && - firstObject && - firstObject.state.type === 'image' && - firstObject.state.image.image_name !== this.rasterizedImageCache - ) { - this.rasterizedImageCache = null; - } - } - return didRender; }; @@ -376,29 +359,37 @@ export class CanvasObjectRenderer { return this.renderers.size > 0 || this.buffer !== null; }; + getRasterizedImageCache = (rect: Rect): ImageCache | null => { + const imageCache = this.parent.state.rasterizationCache.find((cache) => isEqual(cache.rect, rect)); + return imageCache ?? null; + }; + /** - * Rasterizes the parent entity. If the entity has a rasterization cache, the cached image is returned after - * validating that it exists on the server. + * Rasterizes the parent entity. If the entity has a rasterization cache for the given rect, the cached image is + * returned. Otherwise, the entity is rasterized and the image is uploaded to the server. * - * The rasterization cache is reset when the entity's objects change. The buffer object is not considered part of the - * entity's objects for this purpose. + * The rasterization cache is reset when the entity's state changes. The buffer object is not considered part of the + * entity state for this purpose as it is a temporary object. * + * @param rect The rect to rasterize. If omitted, the entity's full rect will be used. * @returns A promise that resolves to the rasterized image DTO. */ - rasterize = async (): Promise => { - this.log.debug('Rasterizing entity'); - + rasterize = async (rect?: Rect): Promise => { + rect = rect ?? this.parent.transformer.getRelativeRect(); let imageDTO: ImageDTO | null = null; - if (this.rasterizedImageCache) { - imageDTO = await getImageDTO(this.rasterizedImageCache); - } + const rasterizedImageCache = this.getRasterizedImageCache(rect); - if (imageDTO) { - return imageDTO; + if (rasterizedImageCache) { + imageDTO = await getImageDTO(rasterizedImageCache.imageName); + if (imageDTO) { + this.log.trace({ rect, rasterizedImageCache, imageDTO }, 'Using cached rasterized image'); + return imageDTO; + } } - const rect = this.parent.transformer.getRelativeRect(); - const blob = await this.getBlob({ rect }); + this.log.trace({ rect }, 'Rasterizing entity'); + + const blob = await this.getBlob(rect); if (this.manager._isDebugging) { previewBlob(blob, 'Rasterized entity'); } @@ -408,41 +399,20 @@ export class CanvasObjectRenderer { this.manager.stateApi.rasterizeEntity({ entityIdentifier: this.parent.getEntityIdentifier(), imageObject, - position: { x: Math.round(rect.x), y: Math.round(rect.y) }, + rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: imageDTO.width, height: imageDTO.height }, }); - this.rasterizedImageCache = imageDTO.image_name; - return imageDTO; }; - getBlob = ({ rect }: { rect?: Rect }): Promise => { + getBlob = (rect?: Rect): Promise => { return konvaNodeToBlob(this.konva.objectGroup.clone(), rect); }; - getImageData = ({ rect }: { rect?: Rect }): ImageData => { + getImageData = (rect?: Rect): ImageData => { return konvaNodeToImageData(this.konva.objectGroup.clone(), rect); }; - getImageDTO = async ({ - rect, - category, - is_intermediate, - onUploaded, - }: { - rect?: Rect; - category: ImageCategory; - is_intermediate: boolean; - onUploaded?: (imageDTO: ImageDTO) => void; - }): Promise => { - const blob = await this.getBlob({ rect }); - const imageDTO = await uploadImage(blob, `${this.id}.png`, category, is_intermediate); - if (onUploaded) { - onUploaded(imageDTO); - } - return imageDTO; - }; - /** * Destroys this renderer and all of its object renderers. */ diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index c880f60f59d..4e20aebbe13 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -26,9 +26,7 @@ import { entityReset, entitySelected, eraserWidthChanged, - imImageCacheChanged, - layerImageCacheChanged, - rgImageCacheChanged, + layerCompositeRasterized, toolBufferChanged, toolChanged, } from 'features/controlLayers/store/canvasV2Slice'; @@ -51,7 +49,6 @@ import type { import { RGBA_RED } from 'features/controlLayers/store/types'; import type { WritableAtom } from 'nanostores'; import { atom } from 'nanostores'; -import type { ImageDTO } from 'services/api/types'; type EntityStateAndAdapter = | { @@ -118,6 +115,10 @@ export class CanvasStateApi { log.trace(arg, 'Rasterizing entity'); this._store.dispatch(entityRasterized(arg)); }; + compositeLayerRasterized = (arg: { imageName: string; rect: Rect }) => { + log.trace(arg, 'Composite layer rasterized'); + this._store.dispatch(layerCompositeRasterized(arg)); + }; setSelectedEntity = (arg: EntityIdentifierPayload) => { log.trace({ arg }, 'Setting selected entity'); this._store.dispatch(entitySelected(arg)); @@ -134,18 +135,6 @@ export class CanvasStateApi { log.trace({ width }, 'Setting eraser width'); this._store.dispatch(eraserWidthChanged(width)); }; - setRegionMaskImageCache = (id: string, imageDTO: ImageDTO) => { - log.trace({ id, imageDTO }, 'Setting region mask image cache'); - this._store.dispatch(rgImageCacheChanged({ id, imageDTO })); - }; - setInpaintMaskImageCache = (imageDTO: ImageDTO) => { - log.trace({ imageDTO }, 'Setting inpaint mask image cache'); - this._store.dispatch(imImageCacheChanged({ imageDTO })); - }; - setLayerImageCache = (imageDTO: ImageDTO) => { - log.trace({ imageDTO }, 'Setting layer image cache'); - this._store.dispatch(layerImageCacheChanged({ imageDTO })); - }; setTool = (tool: Tool) => { log.trace({ tool }, 'Setting tool'); this._store.dispatch(toolChanged(tool)); @@ -171,9 +160,6 @@ export class CanvasStateApi { getLayersState = () => { return this.getState().layers; }; - getControlAdaptersState = () => { - return this.getState().controlAdapters; - }; getInpaintMaskState = () => { return this.getState().inpaintMask; }; @@ -202,9 +188,6 @@ export class CanvasStateApi { if (identifier.type === 'layer') { entityState = state.layers.entities.find((i) => i.id === identifier.id) ?? null; entityAdapter = this.manager.layers.get(identifier.id) ?? null; - } else if (identifier.type === 'control_adapter') { - entityState = state.controlAdapters.entities.find((i) => i.id === identifier.id) ?? null; - entityAdapter = this.manager.controlAdapters.get(identifier.id) ?? null; } else if (identifier.type === 'regional_guidance') { entityState = state.regions.entities.find((i) => i.id === identifier.id) ?? null; entityAdapter = this.manager.regions.get(identifier.id) ?? null; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 0470b362816..1c69a67a97e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -5,7 +5,6 @@ import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/uti import { deepClone } from 'common/util/deepClone'; import { bboxReducers } from 'features/controlLayers/store/bboxReducers'; import { compositingReducers } from 'features/controlLayers/store/compositingReducers'; -import { controlAdaptersReducers } from 'features/controlLayers/store/controlAdaptersReducers'; import { inpaintMaskReducers } from 'features/controlLayers/store/inpaintMaskReducers'; import { ipAdaptersReducers } from 'features/controlLayers/store/ipAdaptersReducers'; import { layersReducers } from 'features/controlLayers/store/layersReducers'; @@ -18,13 +17,16 @@ import { toolReducers } from 'features/controlLayers/store/toolReducers'; import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; import { initialAspectRatioState } from 'features/parameters/components/DocumentSize/constants'; import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; -import { pick } from 'lodash-es'; +import { isEqual, pick } from 'lodash-es'; import { atom } from 'nanostores'; import type { InvocationDenoiseProgressEvent } from 'services/events/types'; import { assert } from 'tsafe'; import type { CanvasEntityIdentifier, + CanvasInpaintMaskState, + CanvasLayerState, + CanvasRegionalGuidanceState, CanvasV2State, Coordinate, EntityBrushLineAddedPayload, @@ -41,8 +43,7 @@ import { IMAGE_FILTERS, RGBA_RED } from './types'; const initialState: CanvasV2State = { _version: 3, selectedEntityIdentifier: null, - layers: { entities: [], imageCache: null }, - controlAdapters: { entities: [] }, + layers: { entities: [], compositeRasterizationCache: [] }, ipAdapters: { entities: [] }, regions: { entities: [] }, loras: [], @@ -50,7 +51,7 @@ const initialState: CanvasV2State = { id: 'inpaint_mask', type: 'inpaint_mask', fill: RGBA_RED, - imageCache: null, + rasterizationCache: [], isEnabled: true, objects: [], position: { @@ -144,8 +145,6 @@ export function selectEntity(state: CanvasV2State, { id, type }: CanvasEntityIde switch (type) { case 'layer': return state.layers.entities.find((layer) => layer.id === id); - case 'control_adapter': - return state.controlAdapters.entities.find((ca) => ca.id === id); case 'inpaint_mask': return state.inpaintMask; case 'regional_guidance': @@ -157,13 +156,37 @@ export function selectEntity(state: CanvasV2State, { id, type }: CanvasEntityIde } } +const invalidateCompositeRasterizationCache = (entity: CanvasLayerState, state: CanvasV2State) => { + if (entity.controlAdapter === null) { + state.layers.compositeRasterizationCache = []; + } +}; + +const invalidateRasterizationCaches = ( + entity: CanvasLayerState | CanvasInpaintMaskState | CanvasRegionalGuidanceState, + state: CanvasV2State +) => { + // TODO(psyche): We can be more efficient and only invalidate caches when the entity's changes intersect with the + // cached rect. + + // Reset the entity's rasterization cache + entity.rasterizationCache = []; + + // When an individual layer has its cache reset, we must also reset the composite rasterization cache because the + // layer's image data will contribute to the composite layer's image data. + // If the layer is used as a control layer, it will not contribute to the composite layer, so we do not need to reset + // its cache. + if (entity.type === 'layer') { + invalidateCompositeRasterizationCache(entity, state); + } +}; + export const canvasV2Slice = createSlice({ name: 'canvasV2', initialState, reducers: { ...layersReducers, ...ipAdaptersReducers, - ...controlAdaptersReducers, ...regionsReducers, ...lorasReducers, ...paramsReducers, @@ -182,16 +205,11 @@ export const canvasV2Slice = createSlice({ const entity = selectEntity(state, entityIdentifier); if (!entity) { return; - } else if (entity.type === 'layer') { - entity.isEnabled = true; - entity.objects = []; - entity.position = { x: 0, y: 0 }; - state.layers.imageCache = null; - } else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { + } else if (entity.type === 'layer' || entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { entity.isEnabled = true; entity.objects = []; entity.position = { x: 0, y: 0 }; - entity.imageCache = null; + invalidateRasterizationCaches(entity, state); } else { assert(false, 'Not implemented'); } @@ -209,32 +227,28 @@ export const canvasV2Slice = createSlice({ const entity = selectEntity(state, entityIdentifier); if (!entity) { return; - } else if (entity.type === 'layer') { - entity.position = position; - state.layers.imageCache = null; - } else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { + } + + if (entity.type === 'layer' || entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { entity.position = position; - entity.imageCache = null; - } else { - assert(false, 'Not implemented'); + // When an entity is moved, we need to invalidate the rasterization caches. + invalidateRasterizationCaches(entity, state); } }, entityRasterized: (state, action: PayloadAction) => { - const { entityIdentifier, imageObject, position } = action.payload; + const { entityIdentifier, imageObject, rect } = action.payload; const entity = selectEntity(state, entityIdentifier); if (!entity) { return; - } else if (entity.type === 'layer') { - entity.objects = [imageObject]; - entity.position = position; - entity.imageCache = imageObject.image.image_name; - state.layers.imageCache = null; - } else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { + } + + if (entity.type === 'layer' || entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { entity.objects = [imageObject]; - entity.position = position; - entity.imageCache = imageObject.image.image_name; - } else { - assert(false, 'Not implemented'); + entity.position = { x: rect.x, y: rect.y }; + // Remove the cache for the given rect. This should never happen, because we should never rasterize the same + // rect twice. Just in case, we remove the old cache. + entity.rasterizationCache = entity.rasterizationCache.filter((cache) => !isEqual(cache.rect, rect)); + entity.rasterizationCache.push({ imageName: imageObject.image.image_name, rect }); } }, entityBrushLineAdded: (state, action: PayloadAction) => { @@ -242,14 +256,12 @@ export const canvasV2Slice = createSlice({ const entity = selectEntity(state, entityIdentifier); if (!entity) { return; - } else if (entity.type === 'layer') { - entity.objects.push(brushLine); - state.layers.imageCache = null; - } else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { + } + + if (entity.type === 'layer' || entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { entity.objects.push(brushLine); - entity.imageCache = null; - } else { - assert(false, 'Not implemented'); + // When adding a brush line, we need to invalidate the rasterization caches. + invalidateRasterizationCaches(entity, state); } }, entityEraserLineAdded: (state, action: PayloadAction) => { @@ -257,12 +269,10 @@ export const canvasV2Slice = createSlice({ const entity = selectEntity(state, entityIdentifier); if (!entity) { return; - } else if (entity.type === 'layer') { + } else if (entity.type === 'layer' || entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { entity.objects.push(eraserLine); - state.layers.imageCache = null; - } else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { - entity.objects.push(eraserLine); - entity.imageCache = null; + // When adding an eraser line, we need to invalidate the rasterization caches. + invalidateRasterizationCaches(entity, state); } else { assert(false, 'Not implemented'); } @@ -274,19 +284,21 @@ export const canvasV2Slice = createSlice({ return; } else if (entity.type === 'layer') { entity.objects.push(rect); - state.layers.imageCache = null; - } else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { - entity.objects.push(rect); - entity.imageCache = null; + // When adding an eraser line, we need to invalidate the rasterization caches. + invalidateRasterizationCaches(entity, state); } else { assert(false, 'Not implemented'); } }, entityDeleted: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (entity?.type === 'layer') { + // When a layer is deleted, we may need to invalidate the composite rasterization cache. + invalidateCompositeRasterizationCache(entity, state); + } if (entityIdentifier.type === 'layer') { state.layers.entities = state.layers.entities.filter((layer) => layer.id !== entityIdentifier.id); - state.layers.imageCache = null; } else if (entityIdentifier.type === 'regional_guidance') { state.regions.entities = state.regions.entities.filter((rg) => rg.id !== entityIdentifier.id); } else { @@ -301,11 +313,10 @@ export const canvasV2Slice = createSlice({ } if (entity.type === 'layer') { moveOneToEnd(state.layers.entities, entity); - state.layers.imageCache = null; + // When arranging an entity, we may need to invalidate the composite rasterization cache. + invalidateCompositeRasterizationCache(entity, state); } else if (entity.type === 'regional_guidance') { moveOneToEnd(state.regions.entities, entity); - } else if (entity.type === 'control_adapter') { - moveOneToEnd(state.controlAdapters.entities, entity); } }, entityArrangedToFront: (state, action: PayloadAction) => { @@ -316,11 +327,10 @@ export const canvasV2Slice = createSlice({ } if (entity.type === 'layer') { moveToEnd(state.layers.entities, entity); - state.layers.imageCache = null; + // When arranging an entity, we may need to invalidate the composite rasterization cache. + invalidateCompositeRasterizationCache(entity, state); } else if (entity.type === 'regional_guidance') { moveToEnd(state.regions.entities, entity); - } else if (entity.type === 'control_adapter') { - moveToEnd(state.controlAdapters.entities, entity); } }, entityArrangedBackwardOne: (state, action: PayloadAction) => { @@ -331,11 +341,10 @@ export const canvasV2Slice = createSlice({ } if (entity.type === 'layer') { moveOneToStart(state.layers.entities, entity); - state.layers.imageCache = null; + // When arranging an entity, we may need to invalidate the composite rasterization cache. + invalidateCompositeRasterizationCache(entity, state); } else if (entity.type === 'regional_guidance') { moveOneToStart(state.regions.entities, entity); - } else if (entity.type === 'control_adapter') { - moveOneToStart(state.controlAdapters.entities, entity); } }, entityArrangedToBack: (state, action: PayloadAction) => { @@ -346,19 +355,17 @@ export const canvasV2Slice = createSlice({ } if (entity.type === 'layer') { moveToStart(state.layers.entities, entity); - state.layers.imageCache = null; + // When arranging an entity, we may need to invalidate the composite rasterization cache. + invalidateCompositeRasterizationCache(entity, state); } else if (entity.type === 'regional_guidance') { moveToStart(state.regions.entities, entity); - } else if (entity.type === 'control_adapter') { - moveToStart(state.controlAdapters.entities, entity); } }, allEntitiesDeleted: (state) => { state.regions.entities = []; state.layers.entities = []; - state.layers.imageCache = null; + state.layers.compositeRasterizationCache = []; state.ipAdapters.entities = []; - state.controlAdapters.entities = []; }, filterSelected: (state, action: PayloadAction<{ type: FilterConfig['type'] }>) => { state.filter.config = IMAGE_FILTERS[action.payload.type].buildDefaults(); @@ -366,6 +373,23 @@ export const canvasV2Slice = createSlice({ filterConfigChanged: (state, action: PayloadAction<{ config: FilterConfig }>) => { state.filter.config = action.payload.config; }, + rasterizationCachesInvalidated: (state) => { + // Invalidate the rasterization caches for all entities. + + // Layers & composite layer + state.layers.compositeRasterizationCache = []; + for (const layer of state.layers.entities) { + layer.rasterizationCache = []; + } + + // Regions + for (const region of state.regions.entities) { + region.rasterizationCache = []; + } + + // Inpaint mask + state.inpaintMask.rasterizationCache = []; + }, canvasReset: (state) => { state.bbox = deepClone(initialState.bbox); const optimalDimension = getOptimalDimension(state.params.model); @@ -374,7 +398,6 @@ export const canvasV2Slice = createSlice({ const size = pick(state.bbox.rect, 'width', 'height'); state.bbox.scaledSize = getScaledBoundingBoxDimensions(size, optimalDimension); - state.controlAdapters = deepClone(initialState.controlAdapters); state.ipAdapters = deepClone(initialState.ipAdapters); state.layers = deepClone(initialState.layers); state.regions = deepClone(initialState.regions); @@ -397,6 +420,7 @@ export const { allEntitiesDeleted, clipToBboxChanged, canvasReset, + rasterizationCachesInvalidated, // All entities entitySelected, entityReset, @@ -424,14 +448,13 @@ export const { // layers layerAdded, layerRecalled, - layerOpacityChanged, layerAllDeleted, - layerImageCacheChanged, layerUsedAsControlChanged, layerControlAdapterModelChanged, layerControlAdapterControlModeChanged, layerControlAdapterWeightChanged, layerControlAdapterBeginEndStepPctChanged, + layerCompositeRasterized, // IP Adapters ipaAdded, ipaRecalled, @@ -444,20 +467,6 @@ export const { ipaCLIPVisionModelChanged, ipaWeightChanged, ipaBeginEndStepPctChanged, - // Control Adapters - caAdded, - caAllDeleted, - caOpacityChanged, - caRecalled, - caImageChanged, - caProcessedImageChanged, - caModelChanged, - caControlModeChanged, - caProcessorConfigChanged, - caFilterChanged, - caProcessorPendingBatchIdChanged, - caWeightChanged, - caBeginEndStepPctChanged, // Regions rgAdded, rgRecalled, @@ -465,7 +474,6 @@ export const { rgPositivePromptChanged, rgNegativePromptChanged, rgFillChanged, - rgImageCacheChanged, rgAutoNegativeChanged, rgIPAdapterAdded, rgIPAdapterDeleted, @@ -522,7 +530,6 @@ export const { // Inpaint mask imRecalled, imFillChanged, - imImageCacheChanged, // Staging sessionStartedStaging, sessionImageStaged, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts deleted file mode 100644 index a71b2fcedbc..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts +++ /dev/null @@ -1,197 +0,0 @@ -import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import { zModelIdentifierField } from 'features/nodes/types/common'; -import { isEqual } from 'lodash-es'; -import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types'; -import { assert } from 'tsafe'; -import { v4 as uuidv4 } from 'uuid'; - -import type { - CanvasControlAdapterState, - CanvasControlNetState, - CanvasT2IAdapterState, - CanvasV2State, - ControlModeV2, - ControlNetConfig, - Filter, - FilterConfig, - T2IAdapterConfig, -} from './types'; -import { buildControlAdapterProcessorV2, imageDTOToImageObject } from './types'; - -export const selectCA = (state: CanvasV2State, id: string) => state.controlAdapters.entities.find((ca) => ca.id === id); -export const selectCAOrThrow = (state: CanvasV2State, id: string) => { - const ca = selectCA(state, id); - assert(ca, `Control Adapter with id ${id} not found`); - return ca; -}; - -export const controlAdaptersReducers = { - caAdded: { - reducer: (state, action: PayloadAction<{ id: string; config: ControlNetConfig | T2IAdapterConfig }>) => { - const { id, config } = action.payload; - state.controlAdapters.entities.push({ - id, - type: 'control_adapter', - position: { x: 0, y: 0 }, - bbox: null, - bboxNeedsUpdate: false, - isEnabled: true, - opacity: 1, - filters: ['LightnessToAlphaFilter'], - processorPendingBatchId: null, - ...config, - }); - state.selectedEntityIdentifier = { type: 'control_adapter', id }; - }, - prepare: (payload: { config: ControlNetConfig | T2IAdapterConfig }) => ({ - payload: { id: uuidv4(), ...payload }, - }), - }, - caRecalled: (state, action: PayloadAction<{ data: CanvasControlAdapterState }>) => { - const { data } = action.payload; - state.controlAdapters.entities.push(data); - state.selectedEntityIdentifier = { type: 'control_adapter', id: data.id }; - }, - caAllDeleted: (state) => { - state.controlAdapters.entities = []; - }, - caOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => { - const { id, opacity } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - ca.opacity = opacity; - }, - caImageChanged: { - reducer: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null; objectId: string }>) => { - const { id, imageDTO, objectId } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - ca.bbox = null; - ca.bboxNeedsUpdate = true; - ca.isEnabled = true; - if (imageDTO) { - const newImageObject = imageDTOToImageObject(imageDTO, { filters: ca.filters }); - if (isEqual(newImageObject, ca.imageObject)) { - return; - } - ca.imageObject = newImageObject; - ca.processedImageObject = null; - } else { - ca.imageObject = null; - ca.processedImageObject = null; - } - }, - prepare: (payload: { id: string; imageDTO: ImageDTO | null }) => ({ payload: { ...payload, objectId: uuidv4() } }), - }, - caProcessedImageChanged: { - reducer: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null; objectId: string }>) => { - const { id, imageDTO, objectId } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - ca.bbox = null; - ca.bboxNeedsUpdate = true; - ca.isEnabled = true; - ca.processedImageObject = imageDTO ? imageDTOToImageObject(imageDTO, { filters: ca.filters }) : null; - }, - prepare: (payload: { id: string; imageDTO: ImageDTO | null }) => ({ payload: { ...payload, objectId: uuidv4() } }), - }, - caModelChanged: ( - state, - action: PayloadAction<{ - id: string; - modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null; - }> - ) => { - const { id, modelConfig } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - if (!modelConfig) { - ca.model = null; - return; - } - ca.model = zModelIdentifierField.parse(modelConfig); - - const candidateProcessorConfig = buildControlAdapterProcessorV2(modelConfig); - if (candidateProcessorConfig?.type !== ca.processorConfig?.type) { - // The processor has changed. For example, the previous model was a Canny model and the new model is a Depth - // model. We need to use the new processor. - ca.processedImageObject = null; - ca.processorConfig = candidateProcessorConfig; - } - - // We may need to convert the CA to match the model - if (ca.adapterType === 't2i_adapter' && ca.model.type === 'controlnet') { - const convertedCA: CanvasControlNetState = { ...ca, adapterType: 'controlnet', controlMode: 'balanced' }; - state.controlAdapters.entities.splice(state.controlAdapters.entities.indexOf(ca), 1, convertedCA); - } else if (ca.adapterType === 'controlnet' && ca.model.type === 't2i_adapter') { - const { controlMode: _, ...rest } = ca; - const convertedCA: CanvasT2IAdapterState = { ...rest, adapterType: 't2i_adapter' }; - state.controlAdapters.entities.splice(state.controlAdapters.entities.indexOf(ca), 1, convertedCA); - } - }, - caControlModeChanged: (state, action: PayloadAction<{ id: string; controlMode: ControlModeV2 }>) => { - const { id, controlMode } = action.payload; - const ca = selectCA(state, id); - if (!ca || ca.adapterType !== 'controlnet') { - return; - } - ca.controlMode = controlMode; - }, - caProcessorConfigChanged: (state, action: PayloadAction<{ id: string; processorConfig: FilterConfig | null }>) => { - const { id, processorConfig } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - ca.processorConfig = processorConfig; - if (!processorConfig) { - ca.processedImageObject = null; - } - }, - caFilterChanged: (state, action: PayloadAction<{ id: string; filters: Filter[] }>) => { - const { id, filters } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - ca.filters = filters; - if (ca.imageObject) { - ca.imageObject.filters = filters; - } - if (ca.processedImageObject) { - ca.processedImageObject.filters = filters; - } - }, - caProcessorPendingBatchIdChanged: (state, action: PayloadAction<{ id: string; batchId: string | null }>) => { - const { id, batchId } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - ca.processorPendingBatchId = batchId; - }, - caWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { - const { id, weight } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - ca.weight = weight; - }, - caBeginEndStepPctChanged: (state, action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }>) => { - const { id, beginEndStepPct } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - ca.beginEndStepPct = beginEndStepPct; - }, -} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts index ae7e52805d1..2950dc60b79 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts @@ -1,7 +1,5 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import type { CanvasInpaintMaskState, CanvasV2State } from 'features/controlLayers/store/types'; -import { imageDTOToImageWithDims } from 'features/controlLayers/store/types'; -import type { ImageDTO } from 'services/api/types'; import type { RgbColor } from './types'; @@ -15,8 +13,4 @@ export const inpaintMaskReducers = { const { fill } = action.payload; state.inpaintMask.fill = fill; }, - imImageCacheChanged: (state, action: PayloadAction<{ imageDTO: ImageDTO | null }>) => { - const { imageDTO } = action.payload; - state.inpaintMask.imageCache = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; - }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index 1d0407ec943..8348848168d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -1,12 +1,11 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { zModelIdentifierField } from 'features/nodes/types/common'; -import { merge } from 'lodash-es'; -import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types'; +import { isEqual, merge } from 'lodash-es'; +import type { ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; -import type { CanvasLayerState, CanvasV2State, ControlModeV2, ControlNetConfig, T2IAdapterConfig } from './types'; -import { imageDTOToImageWithDims } from './types'; +import type { CanvasLayerState, CanvasV2State, ControlModeV2, ControlNetConfig, Rect, T2IAdapterConfig } from './types'; export const selectLayer = (state: CanvasV2State, id: string) => state.layers.entities.find((layer) => layer.id === id); export const selectLayerOrThrow = (state: CanvasV2State, id: string) => { @@ -29,7 +28,7 @@ export const layersReducers = { objects: [], opacity: 1, position: { x: 0, y: 0 }, - imageCache: null, + rasterizationCache: [], controlAdapter: null, }; merge(layer, overrides); @@ -37,7 +36,11 @@ export const layersReducers = { if (isSelected) { state.selectedEntityIdentifier = { type: 'layer', id }; } - state.layers.imageCache = null; + + if (layer.objects.length > 0) { + // This new layer will change the composite layer's image data. Invalidate the cache. + state.layers.compositeRasterizationCache = []; + } }, prepare: (payload: { overrides?: Partial; isSelected?: boolean }) => ({ payload: { ...payload, id: getPrefixedId('layer') }, @@ -47,24 +50,20 @@ export const layersReducers = { const { data } = action.payload; state.layers.entities.push(data); state.selectedEntityIdentifier = { type: 'layer', id: data.id }; - state.layers.imageCache = null; + if (data.objects.length > 0) { + // This new layer will change the composite layer's image data. Invalidate the cache. + state.layers.compositeRasterizationCache = []; + } }, layerAllDeleted: (state) => { state.layers.entities = []; - state.layers.imageCache = null; - }, - layerOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => { - const { id, opacity } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - layer.opacity = opacity; - state.layers.imageCache = null; + state.layers.compositeRasterizationCache = []; }, - layerImageCacheChanged: (state, action: PayloadAction<{ imageDTO: ImageDTO | null }>) => { - const { imageDTO } = action.payload; - state.layers.imageCache = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; + layerCompositeRasterized: (state, action: PayloadAction<{ imageName: string; rect: Rect }>) => { + state.layers.compositeRasterizationCache = state.layers.compositeRasterizationCache.filter( + (cache) => !isEqual(cache.rect, action.payload.rect) + ); + state.layers.compositeRasterizationCache.push(action.payload); }, layerUsedAsControlChanged: ( state, @@ -76,6 +75,8 @@ export const layersReducers = { return; } layer.controlAdapter = controlAdapter; + // The composite layer's image data will change when the layer is used as control (or not). Invalidate the cache. + state.layers.compositeRasterizationCache = []; }, layerControlAdapterModelChanged: ( state, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index 372d570e535..fa7a84735ef 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -54,7 +54,7 @@ export const regionsReducers = { positivePrompt: '', negativePrompt: null, ipAdapters: [], - imageCache: null, + rasterizationCache: [], }; state.regions.entities.push(rg); state.selectedEntityIdentifier = { type: 'regional_guidance', id }; @@ -93,14 +93,6 @@ export const regionsReducers = { } rg.fill = fill; }, - rgImageCacheChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO }>) => { - const { id, imageDTO } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - rg.imageCache = imageDTO.image_name; - }, rgAutoNegativeChanged: (state, action: PayloadAction<{ id: string; autoNegative: ParameterAutoNegative }>) => { const { id, autoNegative } = action.payload; const rg = selectRG(state, id); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts index 4a3fd7812d7..b52aca28ae1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts @@ -5,7 +5,7 @@ import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; export const selectEntityCount = createSelector(selectCanvasV2Slice, (canvasV2) => { return ( canvasV2.regions.entities.length + - canvasV2.controlAdapters.entities.length + + // canvasV2.controlAdapters.entities.length + canvasV2.ipAdapters.entities.length + canvasV2.layers.entities.length ); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 1705d5a3c68..a19b694670f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -639,6 +639,12 @@ const zMaskObject = z }) .pipe(z.discriminatedUnion('type', [zCanvasBrushLineState, zCanvasEraserLineState, zCanvasRectState])); +const zImageCache = z.object({ + imageName: z.string(), + rect: zRect, +}); +export type ImageCache = z.infer; + export const zCanvasRegionalGuidanceState = z.object({ id: zId, type: z.literal('regional_guidance'), @@ -650,7 +656,7 @@ export const zCanvasRegionalGuidanceState = z.object({ negativePrompt: zParameterNegativePrompt.nullable(), ipAdapters: z.array(zCanvasIPAdapterState), autoNegative: zAutoNegative, - imageCache: z.string().min(1).nullable(), + rasterizationCache: z.array(zImageCache), }); export type CanvasRegionalGuidanceState = z.infer; @@ -670,7 +676,7 @@ const zCanvasInpaintMaskState = z.object({ position: zCoordinate, fill: zRgbColor, objects: z.array(zCanvasObjectState), - imageCache: z.string().min(1).nullable(), + rasterizationCache: z.array(zImageCache), }); export type CanvasInpaintMaskState = z.infer; @@ -729,7 +735,7 @@ export const zCanvasLayerState = z.object({ position: zCoordinate, opacity: zOpacity, objects: z.array(zCanvasObjectState), - imageCache: z.string().min(1).nullable(), + rasterizationCache: z.array(zImageCache), controlAdapter: z.discriminatedUnion('type', [zControlNetConfig, zT2IAdapterConfig]).nullable(), }); export type CanvasLayerState = z.infer; @@ -826,11 +832,7 @@ export type CanvasV2State = { _version: 3; selectedEntityIdentifier: CanvasEntityIdentifier | null; inpaintMask: CanvasInpaintMaskState; - layers: { - imageCache: ImageWithDims | null; - entities: CanvasLayerState[]; - }; - controlAdapters: { entities: CanvasControlAdapterState[] }; + layers: { entities: CanvasLayerState[]; compositeRasterizationCache: ImageCache[] }; ipAdapters: { entities: CanvasIPAdapterState[] }; regions: { entities: CanvasRegionalGuidanceState[] }; loras: LoRA[]; @@ -938,7 +940,7 @@ export type EntityRectAddedPayload = { entityIdentifier: CanvasEntityIdentifier; export type EntityRasterizedPayload = { entityIdentifier: CanvasEntityIdentifier; imageObject: CanvasImageState; - position: Coordinate; + rect: Rect; }; export type ImageObjectAddedArg = { id: string; imageDTO: ImageDTO; position?: Coordinate }; diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index 5ea7fb0ad0e..54196820d4e 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -13,7 +13,7 @@ import { import { bboxHeightChanged, bboxWidthChanged, - caRecalled, + // caRecalled, ipaRecalled, layerAllDeleted, layerRecalled, @@ -43,8 +43,8 @@ import type { CanvasControlAdapterState, CanvasIPAdapterState, CanvasLayerState, - LoRA, CanvasRegionalGuidanceState, + LoRA, } from 'features/controlLayers/store/types'; import { setHrfEnabled, setHrfMethod, setHrfStrength } from 'features/hrf/store/hrfSlice'; import type { @@ -271,7 +271,7 @@ const recallCA: MetadataRecallFunc = async (ca) => { } // No clobber clone.id = getCAId(uuidv4()); - dispatch(caRecalled({ data: clone })); + // dispatch(caRecalled({ data: clone })); return; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts index b4a8b0b8d08..8b8023cca3f 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts @@ -26,10 +26,11 @@ export const addControlAdapters = async ( const layersWithValidControlAdapters = layers .filter((layer) => layer.isEnabled) .filter((layer) => doesLayerHaveValidControlAdapter(layer, base)); + for (const layer of layersWithValidControlAdapters) { const adapter = manager.layers.get(layer.id); assert(adapter, 'Adapter not found'); - const imageDTO = await adapter.renderer.getImageDTO({ rect: bbox, is_intermediate: true, category: 'control' }); + const imageDTO = await adapter.renderer.rasterize(bbox); if (layer.controlAdapter.type === 'controlnet') { await addControlNetToGraph(g, layer, imageDTO, denoise); } else { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts index 75cf71dcdfb..5f85274ac5c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts @@ -22,7 +22,7 @@ export const addInpaint = async ( denoise.denoising_start = denoising_start; const initialImage = await manager.getCompositeLayerImageDTO(bbox.rect); - const maskImage = await manager.getInpaintMaskImageDTO(bbox.rect); + const maskImage = await manager.inpaintMask.renderer.rasterize(bbox.rect); if (!isEqual(scaledSize, originalSize)) { // Scale before processing requires some resizing diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index fcf5b77393e..7e4296cb779 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts @@ -23,7 +23,7 @@ export const addOutpaint = async ( denoise.denoising_start = denoising_start; const initialImage = await manager.getCompositeLayerImageDTO(bbox.rect); - const maskImage = await manager.getInpaintMaskImageDTO(bbox.rect); + const maskImage = await manager.inpaintMask.renderer.rasterize(bbox.rect); const infill = getInfill(g, compositing); if (!isEqual(scaledSize, originalSize)) { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts index 5e65a3a6693..847348cfb9e 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts @@ -43,15 +43,16 @@ export const addRegions = async ( const validRegions = regions.filter((rg) => isValidRegion(rg, base)); for (const region of validRegions) { - // Upload the mask image, or get the cached image if it exists - const { image_name } = await manager.getRegionMaskImageDTO(region.id, bbox); + const adapter = manager.regions.get(region.id); + assert(adapter, 'Adapter not found'); + const imageDTO = await adapter.renderer.rasterize(bbox); // The main mask-to-tensor node const maskToTensor = g.addNode({ id: `${PROMPT_REGION_MASK_TO_TENSOR_PREFIX}_${region.id}`, type: 'alpha_mask_to_tensor', image: { - image_name, + image_name: imageDTO.image_name, }, }); From af726d1f15aaefe1038c247d8e040d00faf00a8b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 15 Aug 2024 10:35:07 +1000 Subject: [PATCH 354/678] feat(ui): split control layers from raster layers for UI and internal state, same rendering as raster layers --- invokeai/frontend/web/public/locales/en.json | 4 +- .../addCommitStagingAreaImageListener.ts | 8 +- .../listeners/boardAndImagesDeleted.ts | 4 +- .../listeners/imageDeletionListeners.ts | 4 +- .../listeners/imageDropped.ts | 8 +- .../src/common/hooks/useIsReadyToEnqueue.ts | 81 +++++----- .../components/AddLayerButton.tsx | 4 +- .../components/CanvasEntityList.tsx | 6 +- .../components/ControlLayer/ControlLayer.tsx | 39 +++++ .../ControlLayer/ControlLayerActionsMenu.tsx | 17 ++ .../ControlLayerControlAdapter.tsx} | 38 +++-- .../ControlLayer/ControlLayerEntityList.tsx | 38 +++++ .../ControlLayersSettingsPopover.tsx | 21 +++ .../components/ControlLayersToolbar.tsx | 27 +--- .../components/DeleteAllLayersButton.tsx | 2 +- .../components/Filters/Filter.tsx | 6 +- .../components/Filters/FilterWrapper.tsx | 17 -- .../components/IPAdapter/IPAdapter.tsx | 7 +- .../IPAdapter/IPAdapterSettings.tsx | 6 +- .../components/InpaintMask/InpaintMask.tsx | 36 +++-- .../InpaintMask/InpaintMaskSettings.tsx | 8 - .../components/Layer/LayerSettings.tsx | 22 --- .../Layer.tsx => RasterLayer/RasterLayer.tsx} | 12 +- .../RasterLayerActionsMenu.tsx} | 4 +- .../RasterLayerEntityList.tsx} | 14 +- .../RegionalGuidance/RegionalGuidance.tsx | 7 +- .../RegionalGuidanceSettings.tsx | 6 +- .../common/CanvasEntityActionMenuItems.tsx | 59 +++++-- .../common/CanvasEntityGroupTitle.tsx | 2 +- ...gs.tsx => CanvasEntitySettingsWrapper.tsx} | 4 +- .../hooks/useCanvasResetLayerHotkey.ts | 19 +-- .../hooks/useEntityObjectCount.ts | 8 +- .../controlLayers/hooks/useEntityTitle.ts | 8 +- .../hooks/useLayerControlAdapter.ts | 43 ++--- .../controlLayers/konva/CanvasLayerAdapter.ts | 11 +- .../controlLayers/konva/CanvasManager.ts | 131 ++++++++------- .../controlLayers/konva/CanvasStateApi.ts | 43 ++--- .../controlLayers/konva/CanvasTool.ts | 10 +- .../features/controlLayers/konva/events.ts | 9 +- .../controlLayers/store/bboxReducers.ts | 6 +- .../controlLayers/store/canvasV2Slice.ts | 153 +++++++++++------- .../store/controlLayersReducers.ts | 153 ++++++++++++++++++ .../controlLayers/store/layersReducers.ts | 142 ---------------- .../store/rasterLayersReducers.ts | 108 +++++++++++++ .../features/controlLayers/store/selectors.ts | 2 +- .../src/features/controlLayers/store/types.ts | 45 +++--- .../deleteImageModal/store/selectors.ts | 2 +- .../metadata/components/MetadataLayers.tsx | 8 +- .../src/features/metadata/util/handlers.ts | 6 +- .../web/src/features/metadata/util/parsers.ts | 18 +-- .../src/features/metadata/util/recallers.ts | 14 +- .../src/features/metadata/util/validators.ts | 8 +- .../graph/generation/addControlAdapters.ts | 67 ++------ .../nodes/util/graph/generation/addInpaint.ts | 2 +- .../nodes/util/graph/generation/addLayers.ts | 4 +- .../util/graph/generation/addOutpaint.ts | 2 +- .../nodes/util/graph/generation/addRegions.ts | 2 +- .../util/graph/generation/buildSD1Graph.ts | 2 +- .../util/graph/generation/buildSDXLGraph.ts | 2 +- 59 files changed, 867 insertions(+), 672 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerActionsMenu.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{Layer/LayerControlAdapter.tsx => ControlLayer/ControlLayerControlAdapter.tsx} (62%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterWrapper.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskSettings.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerSettings.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{Layer/Layer.tsx => RasterLayer/RasterLayer.tsx} (78%) rename invokeai/frontend/web/src/features/controlLayers/components/{Layer/LayerActionsMenu.tsx => RasterLayer/RasterLayerActionsMenu.tsx} (80%) rename invokeai/frontend/web/src/features/controlLayers/components/{Layer/LayerEntityList.tsx => RasterLayer/RasterLayerEntityList.tsx} (68%) rename invokeai/frontend/web/src/features/controlLayers/components/common/{CanvasEntitySettings.tsx => CanvasEntitySettingsWrapper.tsx} (58%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts delete mode 100644 invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 03702112fda..2f62a6c7963 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1679,7 +1679,9 @@ "opacity": "Opacity", "regionalGuidance_withCount": "Regional Guidance ({{count}})", "controlAdapters_withCount": "Control Adapters ({{count}})", - "layers_withCount": "Raster Layers ({{count}})", + "controlLayer": "Control Layer", + "controlLayers_withCount": "Control Layers ({{count}})", + "rasterLayers_withCount": "Raster Layers ({{count}})", "ipAdapters_withCount": "IP Adapters ({{count}})", "globalControlAdapter": "Global $t(controlnet.controlAdapter_one)", "globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)", diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts index 98ed8071cd3..d6cb10ff43c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts @@ -2,11 +2,11 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { $lastProgressEvent, - layerAdded, + rasterLayerAdded, sessionStagingAreaImageAccepted, sessionStagingAreaReset, } from 'features/controlLayers/store/canvasV2Slice'; -import type { CanvasLayerState } from 'features/controlLayers/store/types'; +import type { CanvasRasterLayerState } from 'features/controlLayers/store/types'; import { imageDTOToImageObject } from 'features/controlLayers/store/types'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; @@ -62,12 +62,12 @@ export const addStagingListeners = (startAppListening: AppStartListening) => { const { imageDTO, offsetX, offsetY } = stagingAreaImage; const imageObject = imageDTOToImageObject(imageDTO); - const overrides: Partial = { + const overrides: Partial = { position: { x: x + offsetX, y: y + offsetY }, objects: [imageObject], }; - api.dispatch(layerAdded({ overrides })); + api.dispatch(rasterLayerAdded({ overrides })); api.dispatch(sessionStagingAreaReset()); }, }); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts index f5c3a95537c..82681c6d79a 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts @@ -1,5 +1,5 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { ipaAllDeleted, layerAllDeleted } from 'features/controlLayers/store/canvasV2Slice'; +import { ipaAllDeleted, rasterLayerAllDeleted } from 'features/controlLayers/store/canvasV2Slice'; import { getImageUsage } from 'features/deleteImageModal/store/selectors'; import { nodeEditorReset } from 'features/nodes/store/nodesSlice'; import { imagesApi } from 'services/api/endpoints/images'; @@ -22,7 +22,7 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS const imageUsage = getImageUsage(nodes.present, canvasV2, image_name); if (imageUsage.isLayerImage && !wereLayersReset) { - dispatch(layerAllDeleted()); + dispatch(rasterLayerAllDeleted()); wereLayersReset = true; } diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts index 17e65967013..2459f2db4f0 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts @@ -55,7 +55,7 @@ const deleteIPAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO }; const deleteLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { - state.canvasV2.layers.entities.forEach(({ id, objects }) => { + state.canvasV2.rasterLayers.entities.forEach(({ id, objects }) => { let shouldDelete = false; for (const obj of objects) { if (obj.type === 'image' && obj.image.image_name === imageDTO.image_name) { @@ -64,7 +64,7 @@ const deleteLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: Im } } if (shouldDelete) { - dispatch(entityDeleted({ entityIdentifier: { id, type: 'layer' } })); + dispatch(entityDeleted({ entityIdentifier: { id, type: 'raster_layer' } })); } }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index 39bb31ce634..9adb0da5bc1 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -4,10 +4,10 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware' import { parseify } from 'common/util/serialize'; import { ipaImageChanged, - layerAdded, + rasterLayerAdded, rgIPAdapterImageChanged, } from 'features/controlLayers/store/canvasV2Slice'; -import type { CanvasLayerState } from 'features/controlLayers/store/types'; +import type { CanvasRasterLayerState } from 'features/controlLayers/store/types'; import { imageDTOToImageObject } from 'features/controlLayers/store/types'; import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import { isValidDrop } from 'features/dnd/util/isValidDrop'; @@ -108,11 +108,11 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => ) { const imageObject = imageDTOToImageObject(activeData.payload.imageDTO); const { x, y } = getState().canvasV2.bbox.rect; - const overrides: Partial = { + const overrides: Partial = { objects: [imageObject], position: { x, y }, }; - dispatch(layerAdded({ overrides, isSelected: true })); + dispatch(rasterLayerAdded({ overrides, isSelected: true })); return; } diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index 48df41fc484..5b18c45d83e 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -2,7 +2,6 @@ import { useStore } from '@nanostores/react'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; -import type { CanvasEntityState } from 'features/controlLayers/store/types'; import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; @@ -18,14 +17,13 @@ import { forEach, upperFirst } from 'lodash-es'; import { useMemo } from 'react'; import { getConnectedEdges } from 'reactflow'; -const LAYER_TYPE_TO_TKEY: Record = { - control_adapter: 'controlLayers.globalControlAdapter', - ip_adapter: 'controlLayers.globalIPAdapter', - regional_guidance: 'controlLayers.regionalGuidance', - layer: 'controlLayers.raster', +const LAYER_TYPE_TO_TKEY = { + ip_adapter: 'controlLayers.ipAdapter', inpaint_mask: 'controlLayers.inpaintMask', - initial_image: 'controlLayers.initialImage', -}; + regional_guidance: 'controlLayers.regionalGuidance', + raster_layer: 'controlLayers.raster', + control_layer: 'controlLayers.globalControlAdapter', +} as const; const createSelector = (templates: Templates) => createMemoizedSelector( @@ -125,41 +123,35 @@ const createSelector = (templates: Templates) => reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') }); } - // canvasV2.controlAdapters.entities - // .filter((ca) => ca.isEnabled) - // .forEach((ca, i) => { - // const layerLiteral = i18n.t('controlLayers.layers_one'); - // const layerNumber = i + 1; - // const layerType = i18n.t(LAYER_TYPE_TO_TKEY[ca.type]); - // const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; - // const problems: string[] = []; - // // Must have model - // if (!ca.model) { - // problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoModelSelected')); - // } - // // Model base must match - // if (ca.model?.base !== model?.base) { - // problems.push(i18n.t('parameters.invoke.layer.controlAdapterIncompatibleBaseModel')); - // } - // // Must have a control image OR, if it has a processor, it must have a processed image - // if (!ca.imageObject) { - // problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoImageSelected')); - // } else if (ca.processorConfig && !ca.processedImageObject) { - // problems.push(i18n.t('parameters.invoke.layer.controlAdapterImageNotProcessed')); - // } - // // T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL) - // if (ca.adapterType === 't2i_adapter') { - // const multiple = model?.base === 'sdxl' ? 32 : 64; - // if (bbox.rect.width % multiple !== 0 || bbox.rect.height % multiple !== 0) { - // problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions', { multiple })); - // } - // } - - // if (problems.length) { - // const content = upperFirst(problems.join(', ')); - // reasons.push({ prefix, content }); - // } - // }); + canvasV2.controlLayers.entities + .filter((controlLayer) => controlLayer.isEnabled) + .forEach((controlLayer, i) => { + const layerLiteral = i18n.t('controlLayers.layers_one'); + const layerNumber = i + 1; + const layerType = i18n.t(LAYER_TYPE_TO_TKEY['control_layer']); + const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; + const problems: string[] = []; + // Must have model + if (!controlLayer.controlAdapter.model) { + problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoModelSelected')); + } + // Model base must match + if (controlLayer.controlAdapter.model?.base !== model?.base) { + problems.push(i18n.t('parameters.invoke.layer.controlAdapterIncompatibleBaseModel')); + } + // T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL) + if (controlLayer.controlAdapter.type === 't2i_adapter') { + const multiple = model?.base === 'sdxl' ? 32 : 64; + if (bbox.rect.width % multiple !== 0 || bbox.rect.height % multiple !== 0) { + problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions', { multiple })); + } + } + + if (problems.length) { + const content = upperFirst(problems.join(', ')); + reasons.push({ prefix, content }); + } + }); canvasV2.ipAdapters.entities .filter((ipa) => ipa.isEnabled) @@ -226,8 +218,9 @@ const createSelector = (templates: Templates) => } }); - canvasV2.layers.entities + canvasV2.rasterLayers.entities .filter((l) => l.isEnabled) + .filter((l) => l.type === 'raster_layer') .forEach((l, i) => { const layerLiteral = i18n.t('controlLayers.layers_one'); const layerNumber = i + 1; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx index 2c4ba2932b6..0234f49ab3a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx @@ -1,7 +1,7 @@ import { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { useAddCALayer, useAddIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; -import { layerAdded, rgAdded } from 'features/controlLayers/store/canvasV2Slice'; +import { rasterLayerAdded, rgAdded } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPlusBold } from 'react-icons/pi'; @@ -15,7 +15,7 @@ export const AddLayerButton = memo(() => { dispatch(rgAdded()); }, [dispatch]); const addRasterLayer = useCallback(() => { - dispatch(layerAdded({ isSelected: true })); + dispatch(rasterLayerAdded({ isSelected: true })); }, [dispatch]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx index edb1708aae6..86d76a4578b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx @@ -1,8 +1,9 @@ import { Flex } from '@invoke-ai/ui-library'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; +import { ControlLayerEntityList } from 'features/controlLayers/components/ControlLayer/ControlLayerEntityList'; import { InpaintMask } from 'features/controlLayers/components/InpaintMask/InpaintMask'; import { IPAdapterList } from 'features/controlLayers/components/IPAdapter/IPAdapterList'; -import { LayerEntityList } from 'features/controlLayers/components/Layer/LayerEntityList'; +import { RasterLayerEntityList } from 'features/controlLayers/components/RasterLayer/RasterLayerEntityList'; import { RegionalGuidanceEntityList } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList'; import { memo } from 'react'; @@ -13,7 +14,8 @@ export const CanvasEntityList = memo(() => { - + + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx new file mode 100644 index 00000000000..046d43f46e1 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx @@ -0,0 +1,39 @@ +import { Spacer } from '@invoke-ai/ui-library'; +import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; +import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; +import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; +import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; +import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper'; +import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; +import { ControlLayerActionsMenu } from 'features/controlLayers/components/ControlLayer/ControlLayerActionsMenu'; +import { ControlLayerControlAdapter } from 'features/controlLayers/components/ControlLayer/ControlLayerControlAdapter'; +import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { memo, useMemo } from 'react'; + +type Props = { + id: string; +}; + +export const ControlLayer = memo(({ id }: Props) => { + const entityIdentifier = useMemo(() => ({ id, type: 'control_layer' }), [id]); + + return ( + + + + + + + + + + + + + + + ); +}); + +ControlLayer.displayName = 'ControlLayer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerActionsMenu.tsx new file mode 100644 index 00000000000..3f2fa45e068 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerActionsMenu.tsx @@ -0,0 +1,17 @@ +import { Menu, MenuList } from '@invoke-ai/ui-library'; +import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/common/CanvasEntityActionMenuItems'; +import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; +import { memo } from 'react'; + +export const ControlLayerActionsMenu = memo(() => { + return ( + + + + + + + ); +}); + +ControlLayerActionsMenu.displayName = 'ControlLayerActionsMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerControlAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx similarity index 62% rename from invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerControlAdapter.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx index 378a411fb18..3e5d1d7fee8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerControlAdapter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx @@ -5,50 +5,48 @@ import { Weight } from 'features/controlLayers/components/common/Weight'; import { ControlAdapterControlModeSelect } from 'features/controlLayers/components/ControlAdapter/ControlAdapterControlModeSelect'; import { ControlAdapterModel } from 'features/controlLayers/components/ControlAdapter/ControlAdapterModel'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useControlLayerControlAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter'; import { - layerControlAdapterBeginEndStepPctChanged, - layerControlAdapterControlModeChanged, - layerControlAdapterModelChanged, - layerControlAdapterWeightChanged, + controlLayerBeginEndStepPctChanged, + controlLayerControlModeChanged, + controlLayerModelChanged, + controlLayerWeightChanged, } from 'features/controlLayers/store/canvasV2Slice'; -import type { ControlModeV2, ControlNetConfig, T2IAdapterConfig } from 'features/controlLayers/store/types'; +import type { ControlModeV2 } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import type { ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types'; -type Props = { - controlAdapter: ControlNetConfig | T2IAdapterConfig; -}; - -export const LayerControlAdapter = memo(({ controlAdapter }: Props) => { +export const ControlLayerControlAdapter = memo(() => { const dispatch = useAppDispatch(); - const { id } = useEntityIdentifierContext(); + const entityIdentifier = useEntityIdentifierContext(); + const controlAdapter = useControlLayerControlAdapter(entityIdentifier); const onChangeBeginEndStepPct = useCallback( (beginEndStepPct: [number, number]) => { - dispatch(layerControlAdapterBeginEndStepPctChanged({ id, beginEndStepPct })); + dispatch(controlLayerBeginEndStepPctChanged({ id: entityIdentifier.id, beginEndStepPct })); }, - [dispatch, id] + [dispatch, entityIdentifier.id] ); const onChangeControlMode = useCallback( (controlMode: ControlModeV2) => { - dispatch(layerControlAdapterControlModeChanged({ id, controlMode })); + dispatch(controlLayerControlModeChanged({ id: entityIdentifier.id, controlMode })); }, - [dispatch, id] + [dispatch, entityIdentifier.id] ); const onChangeWeight = useCallback( (weight: number) => { - dispatch(layerControlAdapterWeightChanged({ id, weight })); + dispatch(controlLayerWeightChanged({ id: entityIdentifier.id, weight })); }, - [dispatch, id] + [dispatch, entityIdentifier.id] ); const onChangeModel = useCallback( (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => { - dispatch(layerControlAdapterModelChanged({ id, modelConfig })); + dispatch(controlLayerModelChanged({ id: entityIdentifier.id, modelConfig })); }, - [dispatch, id] + [dispatch, entityIdentifier.id] ); return ( @@ -63,4 +61,4 @@ export const LayerControlAdapter = memo(({ controlAdapter }: Props) => { ); }); -LayerControlAdapter.displayName = 'LayerControlAdapter'; +ControlLayerControlAdapter.displayName = 'ControlLayerControlAdapter'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx new file mode 100644 index 00000000000..640c7dbf385 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx @@ -0,0 +1,38 @@ +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppSelector } from 'app/store/storeHooks'; +import { CanvasEntityGroupTitle } from 'features/controlLayers/components/common/CanvasEntityGroupTitle'; +import { ControlLayer } from 'features/controlLayers/components/ControlLayer/ControlLayer'; +import { mapId } from 'features/controlLayers/konva/util'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { + return canvasV2.controlLayers.entities.map(mapId).reverse(); +}); + +export const ControlLayerEntityList = memo(() => { + const { t } = useTranslation(); + const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'control_layer')); + const layerIds = useAppSelector(selectEntityIds); + + if (layerIds.length === 0) { + return null; + } + + if (layerIds.length > 0) { + return ( + <> + + {layerIds.map((id) => ( + + ))} + + ); + } +}); + +ControlLayerEntityList.displayName = 'ControlLayerEntityList'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx index 0527377d6fd..11da129a8eb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx @@ -10,8 +10,10 @@ import { PopoverContent, PopoverTrigger, } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { MaskOpacity } from 'features/controlLayers/components/MaskOpacity'; +import { $canvasManager } from 'features/controlLayers/konva/CanvasManager'; import { clipToBboxChanged, invertScrollChanged, @@ -25,6 +27,7 @@ import { RiSettings4Fill } from 'react-icons/ri'; const ControlLayersSettingsPopover = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); + const canvasManager = useStore($canvasManager); const clipToBbox = useAppSelector((s) => s.canvasV2.settings.clipToBbox); const invertScroll = useAppSelector((s) => s.canvasV2.tool.invertScroll); const onChangeInvertScroll = useCallback( @@ -38,6 +41,21 @@ const ControlLayersSettingsPopover = () => { const invalidateRasterizationCaches = useCallback(() => { dispatch(rasterizationCachesInvalidated()); }, [dispatch]); + const calculateBboxes = useCallback(() => { + if (!canvasManager) { + return; + } + for (const adapter of canvasManager.rasterLayerAdapters.values()) { + adapter.transformer.requestRectCalculation(); + } + for (const adapter of canvasManager.controlLayerAdapters.values()) { + adapter.transformer.requestRectCalculation(); + } + for (const adapter of canvasManager.regionalGuidanceAdapters.values()) { + adapter.transformer.requestRectCalculation(); + } + canvasManager.inpaintMaskAdapter.transformer.requestRectCalculation(); + }, [canvasManager]); return ( @@ -58,6 +76,9 @@ const ControlLayersSettingsPopover = () => { + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index c22d15ed82b..c354fd9a19d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -1,5 +1,5 @@ /* eslint-disable i18next/no-literal-string */ -import { Button, Flex, Switch } from '@invoke-ai/ui-library'; +import { Flex, Switch } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; import { BrushWidth } from 'features/controlLayers/components/BrushWidth'; @@ -12,36 +12,15 @@ import { ResetCanvasButton } from 'features/controlLayers/components/ResetCanvas import { ToolChooser } from 'features/controlLayers/components/ToolChooser'; import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup'; import { $canvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { nanoid } from 'features/controlLayers/konva/util'; import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton'; import { ViewerToggleMenu } from 'features/gallery/components/ImageViewer/ViewerToggleMenu'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; -const filter = () => { - const entity = $canvasManager.get()?.stateApi.getSelectedEntity(); - if (!entity || entity.type !== 'layer') { - return; - } - entity.adapter.filter.previewFilter({ - type: 'canny_image_processor', - id: nanoid(), - low_threshold: 50, - high_threshold: 50, - }); -}; - export const ControlLayersToolbar = memo(() => { const tool = useAppSelector((s) => s.canvasV2.tool.selected); const canvasManager = useStore($canvasManager); - const bbox = useCallback(() => { - if (!canvasManager) { - return; - } - for (const l of canvasManager.layers.values()) { - l.transformer.requestRectCalculation(); - } - }, [canvasManager]); + const onChangeDebugging = useCallback( (e: ChangeEvent) => { if (!canvasManager) { @@ -61,7 +40,6 @@ export const ControlLayersToolbar = memo(() => { - @@ -70,7 +48,6 @@ export const ControlLayersToolbar = memo(() => { - debug diff --git a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx index 765608e6d77..b6dc6b4df0f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx @@ -13,7 +13,7 @@ export const DeleteAllLayersButton = memo(() => { s.canvasV2.regions.entities.length + // s.canvasV2.controlAdapters.entities.length + s.canvasV2.ipAdapters.entities.length + - s.canvasV2.layers.entities.length + s.canvasV2.rasterLayers.entities.length ); }); const onClick = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx index 98536318a5d..e2052026ab6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx @@ -18,7 +18,7 @@ export const Filter = memo(() => { return; } const entity = canvasManager.stateApi.getEntity(filteringEntity); - if (!entity || entity.type !== 'layer') { + if (!entity || (entity.type !== 'raster_layer' && entity.type !== 'control_layer')) { return; } entity.adapter.filter.previewFilter(); @@ -33,7 +33,7 @@ export const Filter = memo(() => { return; } const entity = canvasManager.stateApi.getEntity(filteringEntity); - if (!entity || entity.type !== 'layer') { + if (!entity || (entity.type !== 'raster_layer' && entity.type !== 'control_layer')) { return; } entity.adapter.filter.applyFilter(); @@ -48,7 +48,7 @@ export const Filter = memo(() => { return; } const entity = canvasManager.stateApi.getEntity(filteringEntity); - if (!entity || entity.type !== 'layer') { + if (!entity || (entity.type !== 'raster_layer' && entity.type !== 'control_layer')) { return; } entity.adapter.filter.cancelFilter(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterWrapper.tsx deleted file mode 100644 index 6f20a641cc2..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterWrapper.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Flex } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; -import { useFilter } from 'features/controlLayers/components/Filters/Filter'; -import type { PropsWithChildren } from 'react'; -import { memo } from 'react'; - -const FilterWrapper = (props: PropsWithChildren) => { - const isPreviewDisabled = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.type !== 'layer'); - const filter = useFilter(); - return ( - - {props.children} - - ); -}; - -export default memo(FilterWrapper); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapter.tsx index 4b475361794..f71d31dbf0f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapter.tsx @@ -1,4 +1,4 @@ -import { Spacer, useDisclosure } from '@invoke-ai/ui-library'; +import { Spacer } from '@invoke-ai/ui-library'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; @@ -15,18 +15,17 @@ type Props = { export const IPAdapter = memo(({ id }: Props) => { const entityIdentifier = useMemo(() => ({ id, type: 'ip_adapter' }), [id]); - const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); return ( - + - {isOpen && } + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx index a8d331bde94..30a7799cd1d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx @@ -1,7 +1,7 @@ import { Box, Flex } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct'; -import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; +import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper'; import { Weight } from 'features/controlLayers/components/common/Weight'; import { IPAdapterMethod } from 'features/controlLayers/components/IPAdapter/IPAdapterMethod'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; @@ -73,7 +73,7 @@ export const IPAdapterSettings = memo(() => { const postUploadAction = useMemo(() => ({ type: 'SET_IPA_IMAGE', id }), [id]); return ( - + @@ -102,7 +102,7 @@ export const IPAdapterSettings = memo(() => { - + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx index be9ebbfd67a..04b4f88ee60 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx @@ -1,32 +1,38 @@ -import { Spacer, useDisclosure } from '@invoke-ai/ui-library'; +import { Spacer } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; +import { CanvasEntityGroupTitle } from 'features/controlLayers/components/common/CanvasEntityGroupTitle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; import { InpaintMaskActionsMenu } from 'features/controlLayers/components/InpaintMask/InpaintMaskActionsMenu'; -import { InpaintMaskSettings } from 'features/controlLayers/components/InpaintMask/InpaintMaskSettings'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import { InpaintMaskMaskFillColorPicker } from './InpaintMaskMaskFillColorPicker'; export const InpaintMask = memo(() => { + const { t } = useTranslation(); const entityIdentifier = useMemo(() => ({ id: 'inpaint_mask', type: 'inpaint_mask' }), []); - const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: false }); + const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'inpaint_mask')); + return ( - - - - - - - - - - {isOpen && } - - + <> + + + + + + + + + + + + + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskSettings.tsx deleted file mode 100644 index d7719694a8a..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskSettings.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; -import { memo } from 'react'; - -export const InpaintMaskSettings = memo(() => { - return PLACEHOLDER; -}); - -InpaintMaskSettings.displayName = 'InpaintMaskSettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerSettings.tsx deleted file mode 100644 index d29278460be..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerSettings.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; -import { LayerControlAdapter } from 'features/controlLayers/components/Layer/LayerControlAdapter'; -import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { useLayerControlAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter'; -import { memo } from 'react'; - -export const LayerSettings = memo(() => { - const entityIdentifier = useEntityIdentifierContext(); - const controlAdapter = useLayerControlAdapter(entityIdentifier); - - if (!controlAdapter) { - return null; - } - - return ( - - - - ); -}); - -LayerSettings.displayName = 'LayerSettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx similarity index 78% rename from invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx index a91f1044207..e12ce65b4a6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx @@ -4,8 +4,7 @@ import { CanvasEntityDeleteButton } from 'features/controlLayers/components/comm import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; -import { LayerActionsMenu } from 'features/controlLayers/components/Layer/LayerActionsMenu'; -import { LayerSettings } from 'features/controlLayers/components/Layer/LayerSettings'; +import { RasterLayerActionsMenu } from 'features/controlLayers/components/RasterLayer/RasterLayerActionsMenu'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; @@ -14,8 +13,8 @@ type Props = { id: string; }; -export const Layer = memo(({ id }: Props) => { - const entityIdentifier = useMemo(() => ({ id, type: 'layer' }), [id]); +export const RasterLayer = memo(({ id }: Props) => { + const entityIdentifier = useMemo(() => ({ id, type: 'raster_layer' }), [id]); return ( @@ -24,13 +23,12 @@ export const Layer = memo(({ id }: Props) => { - + - ); }); -Layer.displayName = 'Layer'; +RasterLayer.displayName = 'RasterLayer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerActionsMenu.tsx similarity index 80% rename from invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerActionsMenu.tsx index 53a30acc374..576c939ad29 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerActionsMenu.tsx @@ -3,7 +3,7 @@ import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/c import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; import { memo } from 'react'; -export const LayerActionsMenu = memo(() => { +export const RasterLayerActionsMenu = memo(() => { return ( @@ -14,4 +14,4 @@ export const LayerActionsMenu = memo(() => { ); }); -LayerActionsMenu.displayName = 'LayerActionsMenu'; +RasterLayerActionsMenu.displayName = 'RasterLayerActionsMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerEntityList.tsx similarity index 68% rename from invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerEntityList.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerEntityList.tsx index 1dae90ba0b6..1c2c7448f12 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerEntityList.tsx @@ -1,19 +1,19 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityGroupTitle } from 'features/controlLayers/components/common/CanvasEntityGroupTitle'; -import { Layer } from 'features/controlLayers/components/Layer/Layer'; +import { RasterLayer } from 'features/controlLayers/components/RasterLayer/RasterLayer'; import { mapId } from 'features/controlLayers/konva/util'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { - return canvasV2.layers.entities.map(mapId).reverse(); + return canvasV2.rasterLayers.entities.map(mapId).reverse(); }); -export const LayerEntityList = memo(() => { +export const RasterLayerEntityList = memo(() => { const { t } = useTranslation(); - const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'layer')); + const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'raster_layer')); const layerIds = useAppSelector(selectEntityIds); if (layerIds.length === 0) { @@ -24,15 +24,15 @@ export const LayerEntityList = memo(() => { return ( <> {layerIds.map((id) => ( - + ))} ); } }); -LayerEntityList.displayName = 'LayerEntityList'; +RasterLayerEntityList.displayName = 'RasterLayerEntityList'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx index 68f0a9784b9..eeed5126a96 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx @@ -1,4 +1,4 @@ -import { Spacer, useDisclosure } from '@invoke-ai/ui-library'; +import { Spacer } from '@invoke-ai/ui-library'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; @@ -20,11 +20,10 @@ type Props = { export const RegionalGuidance = memo(({ id }: Props) => { const entityIdentifier = useMemo(() => ({ id, type: 'regional_guidance' }), [id]); - const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); return ( - + @@ -34,7 +33,7 @@ export const RegionalGuidance = memo(({ id }: Props) => { - {isOpen && } + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx index f7f96fd79ff..49c10f120c6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx @@ -1,6 +1,6 @@ import { useAppSelector } from 'app/store/storeHooks'; import { AddPromptButtons } from 'features/controlLayers/components/AddPromptButtons'; -import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; +import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import { memo } from 'react'; @@ -16,12 +16,12 @@ export const RegionalGuidanceSettings = memo(() => { const hasIPAdapters = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).ipAdapters.length > 0); return ( - + {!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && } {hasPositivePrompt && } {hasNegativePrompt && } {hasIPAdapters && } - + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx index 0a812ce0c2d..1c8b61c01e4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx @@ -3,16 +3,18 @@ import { useStore } from '@nanostores/react'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { useLayerUseAsControl } from 'features/controlLayers/hooks/useLayerControlAdapter'; +import { useDefaultControlAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter'; import { $canvasManager } from 'features/controlLayers/konva/CanvasManager'; import { $filteringEntity, + controlLayerConvertedToRasterLayer, entityArrangedBackwardOne, entityArrangedForwardOne, entityArrangedToBack, entityArrangedToFront, entityDeleted, entityReset, + rasterLayerConvertedToControlLayer, selectCanvasV2Slice, } from 'features/controlLayers/store/canvasV2Slice'; import type { CanvasEntityIdentifier, CanvasV2State } from 'features/controlLayers/store/types'; @@ -28,17 +30,21 @@ import { PiQuestionMarkBold, PiStarHalfBold, PiTrashSimpleBold, - PiXBold, } from 'react-icons/pi'; const getIndexAndCount = ( canvasV2: CanvasV2State, { id, type }: CanvasEntityIdentifier ): { index: number; count: number } => { - if (type === 'layer') { + if (type === 'raster_layer') { return { - index: canvasV2.layers.entities.findIndex((entity) => entity.id === id), - count: canvasV2.layers.entities.length, + index: canvasV2.rasterLayers.entities.findIndex((entity) => entity.id === id), + count: canvasV2.rasterLayers.entities.length, + }; + } else if (type === 'control_layer') { + return { + index: canvasV2.controlLayers.entities.findIndex((entity) => entity.id === id), + count: canvasV2.controlLayers.entities.length, }; } else if (type === 'regional_guidance') { return { @@ -58,7 +64,6 @@ export const CanvasEntityActionMenuItems = memo(() => { const canvasManager = useStore($canvasManager); const dispatch = useAppDispatch(); const entityIdentifier = useEntityIdentifierContext(); - const useAsControl = useLayerUseAsControl(entityIdentifier); const selectValidActions = useMemo( () => createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { @@ -76,16 +81,39 @@ export const CanvasEntityActionMenuItems = memo(() => { const validActions = useAppSelector(selectValidActions); const isArrangeable = useMemo( - () => entityIdentifier.type === 'layer' || entityIdentifier.type === 'regional_guidance', + () => + entityIdentifier.type === 'raster_layer' || + entityIdentifier.type === 'control_layer' || + entityIdentifier.type === 'regional_guidance', [entityIdentifier.type] ); const isDeleteable = useMemo( - () => entityIdentifier.type === 'layer' || entityIdentifier.type === 'regional_guidance', + () => + entityIdentifier.type === 'raster_layer' || + entityIdentifier.type === 'control_layer' || + entityIdentifier.type === 'regional_guidance', + [entityIdentifier.type] + ); + + const isFilterable = useMemo( + () => entityIdentifier.type === 'raster_layer' || entityIdentifier.type === 'control_layer', [entityIdentifier.type] ); - const isFilterable = useMemo(() => entityIdentifier.type === 'layer', [entityIdentifier.type]); - const isUseAsControlable = useMemo(() => entityIdentifier.type === 'layer', [entityIdentifier.type]); + + const isRasterLayer = useMemo(() => entityIdentifier.type === 'raster_layer', [entityIdentifier.type]); + + const isControlLayer = useMemo(() => entityIdentifier.type === 'control_layer', [entityIdentifier.type]); + + const defaultControlAdapter = useDefaultControlAdapter(); + + const convertRasterLayerToControlLayer = useCallback(() => { + dispatch(rasterLayerConvertedToControlLayer({ id: entityIdentifier.id, controlAdapter: defaultControlAdapter })); + }, [dispatch, defaultControlAdapter, entityIdentifier.id]); + + const convertControlLayerToRasterLayer = useCallback(() => { + dispatch(controlLayerConvertedToRasterLayer({ id: entityIdentifier.id })); + }, [dispatch, entityIdentifier.id]); const deleteEntity = useCallback(() => { dispatch(entityDeleted({ entityIdentifier })); @@ -142,9 +170,14 @@ export const CanvasEntityActionMenuItems = memo(() => { {t('common.filter')} )} - {isUseAsControlable && ( - : }> - {useAsControl.hasControlAdapter ? t('common.removeControl') : t('common.useAsControl')} + {isRasterLayer && ( + }> + {t('common.convertToControlLayer')} + + )} + {isControlLayer && ( + }> + {t('common.convertToRasterLayer')} )} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupTitle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupTitle.tsx index 69fcefe8c3d..9c88b28ed5f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupTitle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupTitle.tsx @@ -8,7 +8,7 @@ type Props = { export const CanvasEntityGroupTitle = memo(({ title, isSelected }: Props) => { return ( - + {title} ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntitySettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntitySettingsWrapper.tsx similarity index 58% rename from invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntitySettings.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntitySettingsWrapper.tsx index d9665c9f0a2..b4ddd01703a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntitySettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntitySettingsWrapper.tsx @@ -2,7 +2,7 @@ import { Flex } from '@invoke-ai/ui-library'; import type { PropsWithChildren } from 'react'; import { memo } from 'react'; -export const CanvasEntitySettings = memo(({ children }: PropsWithChildren) => { +export const CanvasEntitySettingsWrapper = memo(({ children }: PropsWithChildren) => { return ( {children} @@ -10,4 +10,4 @@ export const CanvasEntitySettings = memo(({ children }: PropsWithChildren) => { ); }); -CanvasEntitySettings.displayName = 'CanvasEntitySettings'; +CanvasEntitySettingsWrapper.displayName = 'CanvasEntitySettingsWrapper'; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts index 1a8301f2046..f15d087b0fb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts @@ -1,10 +1,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; -import { - entityReset, - selectCanvasV2Slice, -} from 'features/controlLayers/store/canvasV2Slice'; +import { entityReset, selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { useCallback, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; @@ -17,7 +14,6 @@ export function useCanvasResetLayerHotkey() { useAssertSingleton(useCanvasResetLayerHotkey.name); const dispatch = useAppDispatch(); const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); - const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); const resetSelectedLayer = useCallback(() => { if (selectedEntityIdentifier === null) { @@ -27,16 +23,9 @@ export function useCanvasResetLayerHotkey() { }, [dispatch, selectedEntityIdentifier]); const isResetEnabled = useMemo( - () => - (!isStaging && selectedEntityIdentifier?.type === 'layer') || - selectedEntityIdentifier?.type === 'regional_guidance' || - selectedEntityIdentifier?.type === 'inpaint_mask', - [isStaging, selectedEntityIdentifier?.type] + () => selectedEntityIdentifier?.type === 'inpaint_mask', + [selectedEntityIdentifier?.type] ); - useHotkeys('shift+c', resetSelectedLayer, { enabled: isResetEnabled }, [ - isResetEnabled, - isStaging, - resetSelectedLayer, - ]); + useHotkeys('shift+c', resetSelectedLayer, { enabled: isResetEnabled }, [isResetEnabled, resetSelectedLayer]); } diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityObjectCount.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityObjectCount.ts index 4a9f383b4c5..53ccc5405cb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityObjectCount.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityObjectCount.ts @@ -1,7 +1,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/canvasV2Slice'; -import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { type CanvasEntityIdentifier,isDrawableEntity } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; export const useEntityObjectCount = (entityIdentifier: CanvasEntityIdentifier) => { @@ -11,11 +11,7 @@ export const useEntityObjectCount = (entityIdentifier: CanvasEntityIdentifier) = const entity = selectEntity(canvasV2, entityIdentifier); if (!entity) { return 0; - } else if (entity.type === 'layer') { - return entity.objects.length; - } else if (entity.type === 'inpaint_mask') { - return entity.objects.length; - } else if (entity.type === 'regional_guidance') { + } else if (isDrawableEntity(entity)) { return entity.objects.length; } else { return 0; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts index 82b9a457d6f..a56c82d14b5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts @@ -13,10 +13,10 @@ export const useEntityTitle = (entityIdentifier: CanvasEntityIdentifier) => { const parts: string[] = []; if (entityIdentifier.type === 'inpaint_mask') { parts.push(t('controlLayers.inpaintMask')); - } else if (entityIdentifier.type === 'control_adapter') { - parts.push(t('controlLayers.globalControlAdapter')); - } else if (entityIdentifier.type === 'layer') { - parts.push(t('controlLayers.layer')); + } else if (entityIdentifier.type === 'control_layer') { + parts.push(t('controlLayers.controlLayer')); + } else if (entityIdentifier.type === 'raster_layer') { + parts.push(t('controlLayers.rasterLayer')); } else if (entityIdentifier.type === 'ip_adapter') { parts.push(t('controlLayers.ipAdapter')); } else if (entityIdentifier.type === 'regional_guidance') { diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts index 71bb3470226..9c91bca8472 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts @@ -1,23 +1,19 @@ import { createMemoizedAppSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { deepClone } from 'common/util/deepClone'; -import { layerUsedAsControlChanged, selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; -import { selectLayer } from 'features/controlLayers/store/layersReducers'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { selectControlLayerOrThrow } from 'features/controlLayers/store/controlLayersReducers'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { initialControlNetV2, initialT2IAdapterV2 } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; -import { useCallback, useMemo } from 'react'; +import { useMemo } from 'react'; import { useControlNetAndT2IAdapterModels } from 'services/api/hooks/modelsByType'; -import type { ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types'; -export const useLayerControlAdapter = (entityIdentifier: CanvasEntityIdentifier) => { +export const useControlLayerControlAdapter = (entityIdentifier: CanvasEntityIdentifier) => { const selectControlAdapter = useMemo( () => createMemoizedAppSelector(selectCanvasV2Slice, (canvasV2) => { - const layer = selectLayer(canvasV2, entityIdentifier.id); - if (!layer) { - return null; - } + const layer = selectControlLayerOrThrow(canvasV2, entityIdentifier.id); return layer.controlAdapter; }), [entityIdentifier] @@ -26,32 +22,23 @@ export const useLayerControlAdapter = (entityIdentifier: CanvasEntityIdentifier) return controlAdapter; }; -export const useLayerUseAsControl = (entityIdentifier: CanvasEntityIdentifier) => { - const dispatch = useAppDispatch(); +export const useDefaultControlAdapter = () => { const [modelConfigs] = useControlNetAndT2IAdapterModels(); const baseModel = useAppSelector((s) => s.canvasV2.params.model?.base); - const controlAdapter = useLayerControlAdapter(entityIdentifier); - const model: ControlNetModelConfig | T2IAdapterModelConfig | null = useMemo(() => { - // prefer to use a model that matches the base model + const defaultControlAdapter = useMemo(() => { const compatibleModels = modelConfigs.filter((m) => (baseModel ? m.base === baseModel : true)); - return compatibleModels[0] ?? modelConfigs[0] ?? null; - }, [baseModel, modelConfigs]); - - const toggle = useCallback(() => { - if (controlAdapter) { - dispatch(layerUsedAsControlChanged({ id: entityIdentifier.id, controlAdapter: null })); - return; - } - const newControlAdapter = deepClone(model?.type === 't2i_adapter' ? initialT2IAdapterV2 : initialControlNetV2); + const model = compatibleModels[0] ?? modelConfigs[0] ?? null; + const controlAdapter = + model?.type === 't2i_adapter' ? deepClone(initialT2IAdapterV2) : deepClone(initialControlNetV2); if (model) { - newControlAdapter.model = zModelIdentifierField.parse(model); + controlAdapter.model = zModelIdentifierField.parse(model); } - dispatch(layerUsedAsControlChanged({ id: entityIdentifier.id, controlAdapter: newControlAdapter })); - }, [controlAdapter, dispatch, entityIdentifier.id, model]); + return controlAdapter; + }, [baseModel, modelConfigs]); - return { hasControlAdapter: Boolean(controlAdapter), toggle }; + return defaultControlAdapter; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts index c2b0d7c51a0..82b2317d9ef 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts @@ -4,7 +4,12 @@ import { CanvasFilter } from 'features/controlLayers/konva/CanvasFilter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; -import type { CanvasEntityIdentifier, CanvasLayerState, CanvasV2State } from 'features/controlLayers/store/types'; +import type { + CanvasControlLayerState, + CanvasEntityIdentifier, + CanvasRasterLayerState, + CanvasV2State, +} from 'features/controlLayers/store/types'; import Konva from 'konva'; import { get } from 'lodash-es'; import type { Logger } from 'roarr'; @@ -17,7 +22,7 @@ export class CanvasLayerAdapter { manager: CanvasManager; log: Logger; - state: CanvasLayerState; + state: CanvasRasterLayerState | CanvasControlLayerState; konva: { layer: Konva.Layer; @@ -110,7 +115,7 @@ export class CanvasLayerAdapter { this.konva.layer.visible(isEnabled); }; - updateObjects = async (arg?: { objects: CanvasLayerState['objects'] }) => { + updateObjects = async (arg?: { objects: CanvasRasterLayerState['objects'] }) => { this.log.trace('Updating objects'); const objects = get(arg, 'objects', this.state.objects); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 9d1f76ff4dd..eb8f62cee8c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -29,7 +29,6 @@ import { getImageDTO, uploadImage } from 'services/api/endpoints/images'; import type { ImageDTO } from 'services/api/types'; import { CanvasBackground } from './CanvasBackground'; -import type { CanvasControlAdapter } from './CanvasControlAdapter'; import { CanvasLayerAdapter } from './CanvasLayerAdapter'; import { CanvasMaskAdapter } from './CanvasMaskAdapter'; import { CanvasPreview } from './CanvasPreview'; @@ -46,10 +45,10 @@ export class CanvasManager { path: string[]; stage: Konva.Stage; container: HTMLDivElement; - controlAdapters: Map; - layers: Map; - regions: Map; - inpaintMask: CanvasMaskAdapter; + rasterLayerAdapters: Map = new Map(); + controlLayerAdapters: Map = new Map(); + regionalGuidanceAdapters: Map = new Map(); + inpaintMaskAdapter: CanvasMaskAdapter; stateApi: CanvasStateApi; preview: CanvasPreview; background: CanvasBackground; @@ -94,10 +93,6 @@ export class CanvasManager { this.background = new CanvasBackground(this); this.stage.add(this.background.konva.layer); - this.layers = new Map(); - this.regions = new Map(); - this.controlAdapters = new Map(); - this._worker.onmessage = (event: MessageEvent) => { const { type, data } = event.data; if (type === 'log') { @@ -128,8 +123,8 @@ export class CanvasManager { this.stateApi.$currentFill.set(this.stateApi.getCurrentFill()); this.stateApi.$selectedEntity.set(this.stateApi.getSelectedEntity()); - this.inpaintMask = new CanvasMaskAdapter(this.stateApi.getInpaintMaskState(), this); - this.stage.add(this.inpaintMask.konva.layer); + this.inpaintMaskAdapter = new CanvasMaskAdapter(this.stateApi.getInpaintMaskState(), this); + this.stage.add(this.inpaintMaskAdapter.konva.layer); } enableDebugging() { @@ -152,18 +147,24 @@ export class CanvasManager { } arrangeEntities() { - const { getLayersState, getRegionsState } = this.stateApi; - const layers = getLayersState().entities; - const regions = getRegionsState().entities; let zIndex = 0; + this.background.konva.layer.zIndex(++zIndex); - for (const layer of layers) { - this.layers.get(layer.id)?.konva.layer.zIndex(++zIndex); + + for (const layer of this.stateApi.getRasterLayersState().entities) { + this.rasterLayerAdapters.get(layer.id)?.konva.layer.zIndex(++zIndex); } - for (const rg of regions) { - this.regions.get(rg.id)?.konva.layer.zIndex(++zIndex); + + for (const layer of this.stateApi.getControlLayersState().entities) { + this.controlLayerAdapters.get(layer.id)?.konva.layer.zIndex(++zIndex); } - this.inpaintMask.konva.layer.zIndex(++zIndex); + + for (const rg of this.stateApi.getRegionsState().entities) { + this.regionalGuidanceAdapters.get(rg.id)?.konva.layer.zIndex(++zIndex); + } + + this.inpaintMaskAdapter.konva.layer.zIndex(++zIndex); + this.preview.getLayer().zIndex(++zIndex); } @@ -215,12 +216,14 @@ export class CanvasManager { const { id, type } = transformingEntity; - if (type === 'layer') { - return this.layers.get(id) ?? null; + if (type === 'raster_layer') { + return this.rasterLayerAdapters.get(id) ?? null; + } else if (type === 'control_layer') { + return this.controlLayerAdapters.get(id) ?? null; } else if (type === 'inpaint_mask') { - return this.inpaintMask; + return this.inpaintMaskAdapter; } else if (type === 'regional_guidance') { - return this.regions.get(id) ?? null; + return this.regionalGuidanceAdapters.get(id) ?? null; } return null; @@ -268,21 +271,46 @@ export class CanvasManager { return; } - if (this._isFirstRender || state.layers.entities !== this._prevState.layers.entities) { - this.log.debug('Rendering layers'); + if (this._isFirstRender || state.rasterLayers.entities !== this._prevState.rasterLayers.entities) { + this.log.debug('Rendering raster layers'); - for (const canvasLayer of this.layers.values()) { - if (!state.layers.entities.find((l) => l.id === canvasLayer.id)) { + for (const canvasLayer of this.rasterLayerAdapters.values()) { + if (!state.rasterLayers.entities.find((l) => l.id === canvasLayer.id)) { await canvasLayer.destroy(); - this.layers.delete(canvasLayer.id); + this.rasterLayerAdapters.delete(canvasLayer.id); } } - for (const entityState of state.layers.entities) { - let adapter = this.layers.get(entityState.id); + for (const entityState of state.rasterLayers.entities) { + let adapter = this.rasterLayerAdapters.get(entityState.id); if (!adapter) { adapter = new CanvasLayerAdapter(entityState, this); - this.layers.set(adapter.id, adapter); + this.rasterLayerAdapters.set(adapter.id, adapter); + this.stage.add(adapter.konva.layer); + } + await adapter.update({ + state: entityState, + toolState: state.tool, + isSelected: state.selectedEntityIdentifier?.id === entityState.id, + }); + } + } + + if (this._isFirstRender || state.controlLayers.entities !== this._prevState.controlLayers.entities) { + this.log.debug('Rendering control layers'); + + for (const canvasLayer of this.controlLayerAdapters.values()) { + if (!state.controlLayers.entities.find((l) => l.id === canvasLayer.id)) { + await canvasLayer.destroy(); + this.controlLayerAdapters.delete(canvasLayer.id); + } + } + + for (const entityState of state.controlLayers.entities) { + let adapter = this.controlLayerAdapters.get(entityState.id); + if (!adapter) { + adapter = new CanvasLayerAdapter(entityState, this); + this.controlLayerAdapters.set(adapter.id, adapter); this.stage.add(adapter.konva.layer); } await adapter.update({ @@ -303,18 +331,18 @@ export class CanvasManager { this.log.debug('Rendering regions'); // Destroy the konva nodes for nonexistent entities - for (const canvasRegion of this.regions.values()) { + for (const canvasRegion of this.regionalGuidanceAdapters.values()) { if (!state.regions.entities.find((rg) => rg.id === canvasRegion.id)) { canvasRegion.destroy(); - this.regions.delete(canvasRegion.id); + this.regionalGuidanceAdapters.delete(canvasRegion.id); } } for (const entityState of state.regions.entities) { - let adapter = this.regions.get(entityState.id); + let adapter = this.regionalGuidanceAdapters.get(entityState.id); if (!adapter) { adapter = new CanvasMaskAdapter(entityState, this); - this.regions.set(adapter.id, adapter); + this.regionalGuidanceAdapters.set(adapter.id, adapter); this.stage.add(adapter.konva.layer); } await adapter.update({ @@ -333,7 +361,7 @@ export class CanvasManager { state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id ) { this.log.debug('Rendering inpaint mask'); - await this.inpaintMask.update({ + await this.inpaintMaskAdapter.update({ state: state.inpaintMask, toolState: state.tool, isSelected: state.selectedEntityIdentifier?.id === state.inpaintMask.id, @@ -354,11 +382,6 @@ export class CanvasManager { await this.preview.bbox.render(); } - if (this._isFirstRender || state.layers !== this._prevState.layers || state.regions !== this._prevState.regions) { - // this.log.debug('Updating entity bboxes'); - // debouncedUpdateBboxes(stage, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions, onBboxChanged); - } - if (this._isFirstRender || state.session !== this._prevState.session) { this.log.debug('Rendering staging area'); await this.preview.stagingArea.render(); @@ -366,7 +389,7 @@ export class CanvasManager { if ( this._isFirstRender || - state.layers.entities !== this._prevState.layers.entities || + state.rasterLayers.entities !== this._prevState.rasterLayers.entities || state.regions.entities !== this._prevState.regions.entities || state.inpaintMask !== this._prevState.inpaintMask || state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id @@ -402,15 +425,15 @@ export class CanvasManager { return () => { this.log.debug('Cleaning up konva renderer'); - this.inpaintMask.destroy(); - for (const region of this.regions.values()) { - region.destroy(); + this.inpaintMaskAdapter.destroy(); + for (const adapter of this.regionalGuidanceAdapters.values()) { + adapter.destroy(); } - for (const layer of this.layers.values()) { - layer.destroy(); + for (const adapter of this.rasterLayerAdapters.values()) { + adapter.destroy(); } - for (const controlAdapter of this.controlAdapters.values()) { - controlAdapter.destroy(); + for (const adapter of this.controlLayerAdapters.values()) { + adapter.destroy(); } this.background.destroy(); this.preview.destroy(); @@ -507,7 +530,7 @@ export class CanvasManager { } getCompositeLayerStageClone = (): Konva.Stage => { - const layersState = this.stateApi.getLayersState(); + const layersState = this.stateApi.getRasterLayersState(); const stageClone = this.stage.clone(); stageClone.scaleX(1); @@ -536,7 +559,7 @@ export class CanvasManager { }; getCompositeRasterizedImageCache = (rect: Rect): ImageCache | null => { - const layerState = this.stateApi.getLayersState(); + const layerState = this.stateApi.getRasterLayersState(); const imageCache = layerState.compositeRasterizationCache.find((cache) => isEqual(cache.rect, rect)); return imageCache ?? null; }; @@ -567,11 +590,11 @@ export class CanvasManager { }; getInpaintMaskBlob = (rect?: Rect): Promise => { - return this.inpaintMask.renderer.getBlob(rect); + return this.inpaintMaskAdapter.renderer.getBlob(rect); }; getInpaintMaskImageData = (rect?: Rect): ImageData => { - return this.inpaintMask.renderer.getImageData(rect); + return this.inpaintMaskAdapter.renderer.getImageData(rect); }; getGenerationMode(): GenerationMode { @@ -617,7 +640,7 @@ export class CanvasManager { logDebugInfo() { // eslint-disable-next-line no-console console.log(this); - for (const layer of this.layers.values()) { + for (const layer of this.rasterLayerAdapters.values()) { // eslint-disable-next-line no-console console.log(layer); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index 4e20aebbe13..b6bb0e8fe18 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -26,14 +26,15 @@ import { entityReset, entitySelected, eraserWidthChanged, - layerCompositeRasterized, + rasterLayerCompositeRasterized, toolBufferChanged, toolChanged, } from 'features/controlLayers/store/canvasV2Slice'; import type { + CanvasControlLayerState, CanvasEntityIdentifier, CanvasInpaintMaskState, - CanvasLayerState, + CanvasRasterLayerState, CanvasRegionalGuidanceState, CanvasV2State, EntityBrushLineAddedPayload, @@ -53,8 +54,14 @@ import { atom } from 'nanostores'; type EntityStateAndAdapter = | { id: string; - type: CanvasLayerState['type']; - state: CanvasLayerState; + type: CanvasRasterLayerState['type']; + state: CanvasRasterLayerState; + adapter: CanvasLayerAdapter; + } + | { + id: string; + type: CanvasControlLayerState['type']; + state: CanvasControlLayerState; adapter: CanvasLayerAdapter; } | { @@ -63,12 +70,6 @@ type EntityStateAndAdapter = state: CanvasInpaintMaskState; adapter: CanvasMaskAdapter; } - // | { - // id: string; - // type: CanvasControlAdapterState['type']; - // state: CanvasControlAdapterState; - // adapter: CanvasControlAdapter; - // } | { id: string; type: CanvasRegionalGuidanceState['type']; @@ -117,7 +118,7 @@ export class CanvasStateApi { }; compositeLayerRasterized = (arg: { imageName: string; rect: Rect }) => { log.trace(arg, 'Composite layer rasterized'); - this._store.dispatch(layerCompositeRasterized(arg)); + this._store.dispatch(rasterLayerCompositeRasterized(arg)); }; setSelectedEntity = (arg: EntityIdentifierPayload) => { log.trace({ arg }, 'Setting selected entity'); @@ -157,8 +158,11 @@ export class CanvasStateApi { getRegionsState = () => { return this.getState().regions; }; - getLayersState = () => { - return this.getState().layers; + getRasterLayersState = () => { + return this.getState().rasterLayers; + }; + getControlLayersState = () => { + return this.getState().controlLayers; }; getInpaintMaskState = () => { return this.getState().inpaintMask; @@ -185,15 +189,18 @@ export class CanvasStateApi { let entityState: EntityStateAndAdapter['state'] | null = null; let entityAdapter: EntityStateAndAdapter['adapter'] | null = null; - if (identifier.type === 'layer') { - entityState = state.layers.entities.find((i) => i.id === identifier.id) ?? null; - entityAdapter = this.manager.layers.get(identifier.id) ?? null; + if (identifier.type === 'raster_layer') { + entityState = state.rasterLayers.entities.find((i) => i.id === identifier.id) ?? null; + entityAdapter = this.manager.rasterLayerAdapters.get(identifier.id) ?? null; + } else if (identifier.type === 'control_layer') { + entityState = state.controlLayers.entities.find((i) => i.id === identifier.id) ?? null; + entityAdapter = this.manager.controlLayerAdapters.get(identifier.id) ?? null; } else if (identifier.type === 'regional_guidance') { entityState = state.regions.entities.find((i) => i.id === identifier.id) ?? null; - entityAdapter = this.manager.regions.get(identifier.id) ?? null; + entityAdapter = this.manager.regionalGuidanceAdapters.get(identifier.id) ?? null; } else if (identifier.type === 'inpaint_mask') { entityState = state.inpaintMask; - entityAdapter = this.manager.inpaintMask; + entityAdapter = this.manager.inpaintMaskAdapter; } if (entityState && entityAdapter) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts index 32307fd015d..9e4183aa2a1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts @@ -8,6 +8,7 @@ import { BRUSH_ERASER_BORDER_WIDTH, } from 'features/controlLayers/konva/constants'; import { alignCoordForTool, getPrefixedId } from 'features/controlLayers/konva/util'; +import { isDrawableEntity } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; @@ -156,10 +157,7 @@ export class CanvasTool { const tool = toolState.selected; - const isDrawableEntity = - selectedEntity?.state.type === 'regional_guidance' || - selectedEntity?.state.type === 'layer' || - selectedEntity?.state.type === 'inpaint_mask'; + const isDrawable = selectedEntity && isDrawableEntity(selectedEntity.state); // Update the stage's pointer style if (tool === 'view') { @@ -168,7 +166,7 @@ export class CanvasTool { } else if (renderedEntityCount === 0) { // We have no layers, so we should not render any tool stage.container().style.cursor = 'default'; - } else if (!isDrawableEntity) { + } else if (!isDrawable) { // Non-drawable layers don't have tools stage.container().style.cursor = 'not-allowed'; } else if (tool === 'move' || Boolean(this.manager.stateApi.$transformingEntity.get())) { @@ -186,7 +184,7 @@ export class CanvasTool { stage.draggable(tool === 'view'); - if (!cursorPos || renderedEntityCount === 0 || !isDrawableEntity) { + if (!cursorPos || renderedEntityCount === 0 || !isDrawable) { // We can bail early if the mouse isn't over the stage or there are no layers this.konva.group.visible(false); } else { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 5be9f5ae873..96465480c12 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -6,8 +6,9 @@ import { offsetCoord, } from 'features/controlLayers/konva/util'; import type { + CanvasControlLayerState, CanvasInpaintMaskState, - CanvasLayerState, + CanvasRasterLayerState, CanvasRegionalGuidanceState, CanvasV2State, Coordinate, @@ -84,7 +85,7 @@ const getLastPointOfLine = (points: number[]): Coordinate | null => { }; const getLastPointOfLastLineOfEntity = ( - entity: CanvasLayerState | CanvasRegionalGuidanceState | CanvasInpaintMaskState, + entity: CanvasRasterLayerState | CanvasControlLayerState | CanvasRegionalGuidanceState | CanvasInpaintMaskState, tool: Tool ): Coordinate | null => { const lastObject = entity.objects[entity.objects.length - 1]; @@ -138,7 +139,9 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { return e.evt.buttons === 1; } - function getClip(entity: CanvasRegionalGuidanceState | CanvasLayerState | CanvasInpaintMaskState) { + function getClip( + entity: CanvasRegionalGuidanceState | CanvasControlLayerState | CanvasRasterLayerState | CanvasInpaintMaskState + ) { const settings = getSettings(); const bboxRect = getBbox().rect; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts index 81f9d2fd901..8ac6d4ea4fb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts @@ -12,12 +12,12 @@ import { pick } from 'lodash-es'; export const bboxReducers = { bboxScaledSizeChanged: (state, action: PayloadAction>) => { - state.layers.imageCache = null; + state.rasterLayers.imageCache = null; state.bbox.scaledSize = { ...state.bbox.scaledSize, ...action.payload }; }, bboxScaleMethodChanged: (state, action: PayloadAction) => { state.bbox.scaleMethod = action.payload; - state.layers.imageCache = null; + state.rasterLayers.imageCache = null; if (action.payload === 'auto') { const optimalDimension = getOptimalDimension(state.params.model); @@ -27,7 +27,7 @@ export const bboxReducers = { }, bboxChanged: (state, action: PayloadAction) => { state.bbox.rect = action.payload; - state.layers.imageCache = null; + state.rasterLayers.imageCache = null; if (state.bbox.scaleMethod === 'auto') { const optimalDimension = getOptimalDimension(state.params.model); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 1c69a67a97e..cc70237b851 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -5,11 +5,12 @@ import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/uti import { deepClone } from 'common/util/deepClone'; import { bboxReducers } from 'features/controlLayers/store/bboxReducers'; import { compositingReducers } from 'features/controlLayers/store/compositingReducers'; +import { controlLayersReducers } from 'features/controlLayers/store/controlLayersReducers'; import { inpaintMaskReducers } from 'features/controlLayers/store/inpaintMaskReducers'; import { ipAdaptersReducers } from 'features/controlLayers/store/ipAdaptersReducers'; -import { layersReducers } from 'features/controlLayers/store/layersReducers'; import { lorasReducers } from 'features/controlLayers/store/lorasReducers'; import { paramsReducers } from 'features/controlLayers/store/paramsReducers'; +import { rasterLayersReducers } from 'features/controlLayers/store/rasterLayersReducers'; import { regionsReducers } from 'features/controlLayers/store/regionsReducers'; import { sessionReducers } from 'features/controlLayers/store/sessionReducers'; import { settingsReducers } from 'features/controlLayers/store/settingsReducers'; @@ -23,9 +24,10 @@ import type { InvocationDenoiseProgressEvent } from 'services/events/types'; import { assert } from 'tsafe'; import type { + CanvasControlLayerState, CanvasEntityIdentifier, CanvasInpaintMaskState, - CanvasLayerState, + CanvasRasterLayerState, CanvasRegionalGuidanceState, CanvasV2State, Coordinate, @@ -38,12 +40,13 @@ import type { FilterConfig, StageAttrs, } from './types'; -import { IMAGE_FILTERS, RGBA_RED } from './types'; +import { IMAGE_FILTERS, isDrawableEntity, RGBA_RED } from './types'; const initialState: CanvasV2State = { _version: 3, selectedEntityIdentifier: null, - layers: { entities: [], compositeRasterizationCache: [] }, + rasterLayers: { entities: [], compositeRasterizationCache: [] }, + controlLayers: { entities: [] }, ipAdapters: { entities: [] }, regions: { entities: [] }, loras: [], @@ -143,27 +146,21 @@ const initialState: CanvasV2State = { export function selectEntity(state: CanvasV2State, { id, type }: CanvasEntityIdentifier) { switch (type) { - case 'layer': - return state.layers.entities.find((layer) => layer.id === id); + case 'raster_layer': + return state.rasterLayers.entities.find((layer) => layer.id === id); + case 'control_layer': + return state.controlLayers.entities.find((layer) => layer.id === id); case 'inpaint_mask': return state.inpaintMask; case 'regional_guidance': return state.regions.entities.find((rg) => rg.id === id); - case 'ip_adapter': - return state.ipAdapters.entities.find((ip) => ip.id === id); default: return; } } -const invalidateCompositeRasterizationCache = (entity: CanvasLayerState, state: CanvasV2State) => { - if (entity.controlAdapter === null) { - state.layers.compositeRasterizationCache = []; - } -}; - const invalidateRasterizationCaches = ( - entity: CanvasLayerState | CanvasInpaintMaskState | CanvasRegionalGuidanceState, + entity: CanvasRasterLayerState | CanvasControlLayerState | CanvasInpaintMaskState | CanvasRegionalGuidanceState, state: CanvasV2State ) => { // TODO(psyche): We can be more efficient and only invalidate caches when the entity's changes intersect with the @@ -176,8 +173,8 @@ const invalidateRasterizationCaches = ( // layer's image data will contribute to the composite layer's image data. // If the layer is used as a control layer, it will not contribute to the composite layer, so we do not need to reset // its cache. - if (entity.type === 'layer') { - invalidateCompositeRasterizationCache(entity, state); + if (entity.type === 'raster_layer') { + state.rasterLayers.compositeRasterizationCache = []; } }; @@ -185,7 +182,8 @@ export const canvasV2Slice = createSlice({ name: 'canvasV2', initialState, reducers: { - ...layersReducers, + ...rasterLayersReducers, + ...controlLayersReducers, ...ipAdaptersReducers, ...regionsReducers, ...lorasReducers, @@ -205,7 +203,7 @@ export const canvasV2Slice = createSlice({ const entity = selectEntity(state, entityIdentifier); if (!entity) { return; - } else if (entity.type === 'layer' || entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { + } else if (isDrawableEntity(entity)) { entity.isEnabled = true; entity.objects = []; entity.position = { x: 0, y: 0 }; @@ -229,7 +227,7 @@ export const canvasV2Slice = createSlice({ return; } - if (entity.type === 'layer' || entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { + if (isDrawableEntity(entity)) { entity.position = position; // When an entity is moved, we need to invalidate the rasterization caches. invalidateRasterizationCaches(entity, state); @@ -242,7 +240,7 @@ export const canvasV2Slice = createSlice({ return; } - if (entity.type === 'layer' || entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { + if (isDrawableEntity(entity)) { entity.objects = [imageObject]; entity.position = { x: rect.x, y: rect.y }; // Remove the cache for the given rect. This should never happen, because we should never rasterize the same @@ -258,7 +256,7 @@ export const canvasV2Slice = createSlice({ return; } - if (entity.type === 'layer' || entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { + if (isDrawableEntity(entity)) { entity.objects.push(brushLine); // When adding a brush line, we need to invalidate the rasterization caches. invalidateRasterizationCaches(entity, state); @@ -269,7 +267,7 @@ export const canvasV2Slice = createSlice({ const entity = selectEntity(state, entityIdentifier); if (!entity) { return; - } else if (entity.type === 'layer' || entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { + } else if (isDrawableEntity(entity)) { entity.objects.push(eraserLine); // When adding an eraser line, we need to invalidate the rasterization caches. invalidateRasterizationCaches(entity, state); @@ -282,7 +280,7 @@ export const canvasV2Slice = createSlice({ const entity = selectEntity(state, entityIdentifier); if (!entity) { return; - } else if (entity.type === 'layer') { + } else if (isDrawableEntity(entity)) { entity.objects.push(rect); // When adding an eraser line, we need to invalidate the rasterization caches. invalidateRasterizationCaches(entity, state); @@ -292,18 +290,37 @@ export const canvasV2Slice = createSlice({ }, entityDeleted: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; - const entity = selectEntity(state, entityIdentifier); - if (entity?.type === 'layer') { - // When a layer is deleted, we may need to invalidate the composite rasterization cache. - invalidateCompositeRasterizationCache(entity, state); - } - if (entityIdentifier.type === 'layer') { - state.layers.entities = state.layers.entities.filter((layer) => layer.id !== entityIdentifier.id); + + let selectedEntityIdentifier: CanvasEntityIdentifier = { type: state.inpaintMask.type, id: state.inpaintMask.id }; + + if (entityIdentifier.type === 'raster_layer') { + // When deleting a raster layer, we need to invalidate the composite rasterization cache. + const index = state.rasterLayers.entities.findIndex((layer) => layer.id === entityIdentifier.id); + state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => layer.id !== entityIdentifier.id); + state.rasterLayers.compositeRasterizationCache = []; + const nextRasterLayer = state.rasterLayers.entities[index]; + if (nextRasterLayer) { + selectedEntityIdentifier = { type: nextRasterLayer.type, id: nextRasterLayer.id }; + } + } else if (entityIdentifier.type === 'control_layer') { + const index = state.controlLayers.entities.findIndex((layer) => layer.id === entityIdentifier.id); + state.controlLayers.entities = state.controlLayers.entities.filter((rg) => rg.id !== entityIdentifier.id); + const nextControlLayer = state.controlLayers.entities[index]; + if (nextControlLayer) { + selectedEntityIdentifier = { type: nextControlLayer.type, id: nextControlLayer.id }; + } } else if (entityIdentifier.type === 'regional_guidance') { + const index = state.regions.entities.findIndex((layer) => layer.id === entityIdentifier.id); state.regions.entities = state.regions.entities.filter((rg) => rg.id !== entityIdentifier.id); + const region = state.regions.entities[index]; + if (region) { + selectedEntityIdentifier = { type: region.type, id: region.id }; + } } else { assert(false, 'Not implemented'); } + + state.selectedEntityIdentifier = selectedEntityIdentifier; }, entityArrangedForwardOne: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; @@ -311,10 +328,12 @@ export const canvasV2Slice = createSlice({ if (!entity) { return; } - if (entity.type === 'layer') { - moveOneToEnd(state.layers.entities, entity); - // When arranging an entity, we may need to invalidate the composite rasterization cache. - invalidateCompositeRasterizationCache(entity, state); + if (entity.type === 'raster_layer') { + moveOneToEnd(state.rasterLayers.entities, entity); + // When arranging a raster layer, we need to invalidate the composite rasterization cache. + state.rasterLayers.compositeRasterizationCache = []; + } else if (entity.type === 'control_layer') { + moveOneToEnd(state.controlLayers.entities, entity); } else if (entity.type === 'regional_guidance') { moveOneToEnd(state.regions.entities, entity); } @@ -325,10 +344,12 @@ export const canvasV2Slice = createSlice({ if (!entity) { return; } - if (entity.type === 'layer') { - moveToEnd(state.layers.entities, entity); - // When arranging an entity, we may need to invalidate the composite rasterization cache. - invalidateCompositeRasterizationCache(entity, state); + if (entity.type === 'raster_layer') { + moveToEnd(state.rasterLayers.entities, entity); + // When arranging a raster layer, we need to invalidate the composite rasterization cache. + state.rasterLayers.compositeRasterizationCache = []; + } else if (entity.type === 'control_layer') { + moveToEnd(state.controlLayers.entities, entity); } else if (entity.type === 'regional_guidance') { moveToEnd(state.regions.entities, entity); } @@ -339,10 +360,11 @@ export const canvasV2Slice = createSlice({ if (!entity) { return; } - if (entity.type === 'layer') { - moveOneToStart(state.layers.entities, entity); - // When arranging an entity, we may need to invalidate the composite rasterization cache. - invalidateCompositeRasterizationCache(entity, state); + if (entity.type === 'raster_layer') { + moveOneToStart(state.rasterLayers.entities, entity); + // When arranging a raster layer, we need to invalidate the composite rasterization cache. + } else if (entity.type === 'control_layer') { + moveOneToStart(state.controlLayers.entities, entity); } else if (entity.type === 'regional_guidance') { moveOneToStart(state.regions.entities, entity); } @@ -353,18 +375,19 @@ export const canvasV2Slice = createSlice({ if (!entity) { return; } - if (entity.type === 'layer') { - moveToStart(state.layers.entities, entity); - // When arranging an entity, we may need to invalidate the composite rasterization cache. - invalidateCompositeRasterizationCache(entity, state); + if (entity.type === 'raster_layer') { + moveToStart(state.rasterLayers.entities, entity); + state.rasterLayers.compositeRasterizationCache = []; + } else if (entity.type === 'control_layer') { + moveToStart(state.controlLayers.entities, entity); } else if (entity.type === 'regional_guidance') { moveToStart(state.regions.entities, entity); } }, allEntitiesDeleted: (state) => { state.regions.entities = []; - state.layers.entities = []; - state.layers.compositeRasterizationCache = []; + state.rasterLayers.entities = []; + state.rasterLayers.compositeRasterizationCache = []; state.ipAdapters.entities = []; }, filterSelected: (state, action: PayloadAction<{ type: FilterConfig['type'] }>) => { @@ -377,8 +400,8 @@ export const canvasV2Slice = createSlice({ // Invalidate the rasterization caches for all entities. // Layers & composite layer - state.layers.compositeRasterizationCache = []; - for (const layer of state.layers.entities) { + state.rasterLayers.compositeRasterizationCache = []; + for (const layer of state.rasterLayers.entities) { layer.rasterizationCache = []; } @@ -399,7 +422,8 @@ export const canvasV2Slice = createSlice({ state.bbox.scaledSize = getScaledBoundingBoxDimensions(size, optimalDimension); state.ipAdapters = deepClone(initialState.ipAdapters); - state.layers = deepClone(initialState.layers); + state.rasterLayers = deepClone(initialState.rasterLayers); + state.controlLayers = deepClone(initialState.controlLayers); state.regions = deepClone(initialState.regions); state.selectedEntityIdentifier = deepClone(initialState.selectedEntityIdentifier); state.session = deepClone(initialState.session); @@ -445,16 +469,21 @@ export const { bboxAspectRatioIdChanged, bboxDimensionsSwapped, bboxSizeOptimized, - // layers - layerAdded, - layerRecalled, - layerAllDeleted, - layerUsedAsControlChanged, - layerControlAdapterModelChanged, - layerControlAdapterControlModeChanged, - layerControlAdapterWeightChanged, - layerControlAdapterBeginEndStepPctChanged, - layerCompositeRasterized, + // Raster layers + rasterLayerAdded, + rasterLayerRecalled, + rasterLayerAllDeleted, + rasterLayerConvertedToControlLayer, + rasterLayerCompositeRasterized, + // Control layers + controlLayerAdded, + controlLayerRecalled, + controlLayerAllDeleted, + controlLayerConvertedToRasterLayer, + controlLayerModelChanged, + controlLayerControlModeChanged, + controlLayerWeightChanged, + controlLayerBeginEndStepPctChanged, // IP Adapters ipaAdded, ipaRecalled, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts new file mode 100644 index 00000000000..5e4ef6f251f --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts @@ -0,0 +1,153 @@ +import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import { deepClone } from 'common/util/deepClone'; +import { getPrefixedId } from 'features/controlLayers/konva/util'; +import { zModelIdentifierField } from 'features/nodes/types/common'; +import { merge, omit } from 'lodash-es'; +import type { ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types'; +import { assert } from 'tsafe'; + +import type { + CanvasControlLayerState, + CanvasRasterLayerState, + CanvasV2State, + ControlModeV2, + ControlNetConfig, + T2IAdapterConfig, +} from './types'; +import { initialControlNetV2 } from './types'; + +export const selectControlLayer = (state: CanvasV2State, id: string) => + state.controlLayers.entities.find((layer) => layer.id === id); +export const selectControlLayerOrThrow = (state: CanvasV2State, id: string) => { + const layer = selectControlLayer(state, id); + assert(layer, `Layer with id ${id} not found`); + return layer; +}; + +export const controlLayersReducers = { + controlLayerAdded: { + reducer: ( + state, + action: PayloadAction<{ id: string; overrides?: Partial; isSelected?: boolean }> + ) => { + const { id, overrides, isSelected } = action.payload; + const layer: CanvasControlLayerState = { + id, + type: 'control_layer', + isEnabled: true, + objects: [], + opacity: 1, + position: { x: 0, y: 0 }, + rasterizationCache: [], + controlAdapter: deepClone(initialControlNetV2), + }; + merge(layer, overrides); + state.controlLayers.entities.push(layer); + if (isSelected) { + state.selectedEntityIdentifier = { type: 'control_layer', id }; + } + }, + prepare: (payload: { overrides?: Partial; isSelected?: boolean }) => ({ + payload: { ...payload, id: getPrefixedId('control_layer') }, + }), + }, + controlLayerRecalled: (state, action: PayloadAction<{ data: CanvasControlLayerState }>) => { + const { data } = action.payload; + state.controlLayers.entities.push(data); + state.selectedEntityIdentifier = { type: 'control_layer', id: data.id }; + }, + controlLayerAllDeleted: (state) => { + state.controlLayers.entities = []; + }, + controlLayerConvertedToRasterLayer: { + reducer: (state, action: PayloadAction<{ id: string; newId: string }>) => { + const { id, newId } = action.payload; + const layer = selectControlLayer(state, id); + if (!layer) { + return; + } + + // Convert the raster layer to control layer + const rasterLayerState: CanvasRasterLayerState = { + ...omit(deepClone(layer), ['type', 'controlAdapter']), + id: newId, + type: 'raster_layer', + }; + + // Remove the control layer + state.controlLayers.entities = state.controlLayers.entities.filter((layer) => layer.id !== id); + + // Add the new raster layer + state.rasterLayers.entities.push(rasterLayerState); + + // The composite layer's image data will change when the control layer is converted to raster layer. + state.rasterLayers.compositeRasterizationCache = []; + + state.selectedEntityIdentifier = { type: rasterLayerState.type, id: rasterLayerState.id }; + }, + prepare: (payload: { id: string }) => ({ + payload: { ...payload, newId: getPrefixedId('raster_layer') }, + }), + }, + controlLayerModelChanged: ( + state, + action: PayloadAction<{ + id: string; + modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null; + }> + ) => { + const { id, modelConfig } = action.payload; + const layer = selectControlLayer(state, id); + if (!layer || !layer.controlAdapter) { + return; + } + if (!modelConfig) { + layer.controlAdapter.model = null; + return; + } + layer.controlAdapter.model = zModelIdentifierField.parse(modelConfig); + + // We may need to convert the CA to match the model + if (layer.controlAdapter.type === 't2i_adapter' && layer.controlAdapter.model.type === 'controlnet') { + // Converting from T2I Adapter to ControlNet - add `controlMode` + const controlNetConfig: ControlNetConfig = { + ...layer.controlAdapter, + type: 'controlnet', + controlMode: 'balanced', + }; + layer.controlAdapter = controlNetConfig; + } else if (layer.controlAdapter.type === 'controlnet' && layer.controlAdapter.model.type === 't2i_adapter') { + // Converting from ControlNet to T2I Adapter - remove `controlMode` + const { controlMode: _, ...rest } = layer.controlAdapter; + const t2iAdapterConfig: T2IAdapterConfig = { ...rest, type: 't2i_adapter' }; + layer.controlAdapter = t2iAdapterConfig; + } + }, + controlLayerControlModeChanged: (state, action: PayloadAction<{ id: string; controlMode: ControlModeV2 }>) => { + const { id, controlMode } = action.payload; + const layer = selectControlLayer(state, id); + if (!layer || !layer.controlAdapter || layer.controlAdapter.type !== 'controlnet') { + return; + } + layer.controlAdapter.controlMode = controlMode; + }, + controlLayerWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { + const { id, weight } = action.payload; + const layer = selectControlLayer(state, id); + if (!layer || !layer.controlAdapter) { + return; + } + layer.controlAdapter.weight = weight; + }, + controlLayerBeginEndStepPctChanged: ( + state, + action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }> + ) => { + const { id, beginEndStepPct } = action.payload; + const layer = selectControlLayer(state, id); + if (!layer || !layer.controlAdapter) { + return; + } + layer.controlAdapter.beginEndStepPct = beginEndStepPct; + }, +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts deleted file mode 100644 index 8348848168d..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ /dev/null @@ -1,142 +0,0 @@ -import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import { getPrefixedId } from 'features/controlLayers/konva/util'; -import { zModelIdentifierField } from 'features/nodes/types/common'; -import { isEqual, merge } from 'lodash-es'; -import type { ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types'; -import { assert } from 'tsafe'; - -import type { CanvasLayerState, CanvasV2State, ControlModeV2, ControlNetConfig, Rect, T2IAdapterConfig } from './types'; - -export const selectLayer = (state: CanvasV2State, id: string) => state.layers.entities.find((layer) => layer.id === id); -export const selectLayerOrThrow = (state: CanvasV2State, id: string) => { - const layer = selectLayer(state, id); - assert(layer, `Layer with id ${id} not found`); - return layer; -}; - -export const layersReducers = { - layerAdded: { - reducer: ( - state, - action: PayloadAction<{ id: string; overrides?: Partial; isSelected?: boolean }> - ) => { - const { id, overrides, isSelected } = action.payload; - const layer: CanvasLayerState = { - id, - type: 'layer', - isEnabled: true, - objects: [], - opacity: 1, - position: { x: 0, y: 0 }, - rasterizationCache: [], - controlAdapter: null, - }; - merge(layer, overrides); - state.layers.entities.push(layer); - if (isSelected) { - state.selectedEntityIdentifier = { type: 'layer', id }; - } - - if (layer.objects.length > 0) { - // This new layer will change the composite layer's image data. Invalidate the cache. - state.layers.compositeRasterizationCache = []; - } - }, - prepare: (payload: { overrides?: Partial; isSelected?: boolean }) => ({ - payload: { ...payload, id: getPrefixedId('layer') }, - }), - }, - layerRecalled: (state, action: PayloadAction<{ data: CanvasLayerState }>) => { - const { data } = action.payload; - state.layers.entities.push(data); - state.selectedEntityIdentifier = { type: 'layer', id: data.id }; - if (data.objects.length > 0) { - // This new layer will change the composite layer's image data. Invalidate the cache. - state.layers.compositeRasterizationCache = []; - } - }, - layerAllDeleted: (state) => { - state.layers.entities = []; - state.layers.compositeRasterizationCache = []; - }, - layerCompositeRasterized: (state, action: PayloadAction<{ imageName: string; rect: Rect }>) => { - state.layers.compositeRasterizationCache = state.layers.compositeRasterizationCache.filter( - (cache) => !isEqual(cache.rect, action.payload.rect) - ); - state.layers.compositeRasterizationCache.push(action.payload); - }, - layerUsedAsControlChanged: ( - state, - action: PayloadAction<{ id: string; controlAdapter: ControlNetConfig | T2IAdapterConfig | null }> - ) => { - const { id, controlAdapter } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - layer.controlAdapter = controlAdapter; - // The composite layer's image data will change when the layer is used as control (or not). Invalidate the cache. - state.layers.compositeRasterizationCache = []; - }, - layerControlAdapterModelChanged: ( - state, - action: PayloadAction<{ - id: string; - modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null; - }> - ) => { - const { id, modelConfig } = action.payload; - const layer = selectLayer(state, id); - if (!layer || !layer.controlAdapter) { - return; - } - if (!modelConfig) { - layer.controlAdapter.model = null; - return; - } - layer.controlAdapter.model = zModelIdentifierField.parse(modelConfig); - - // We may need to convert the CA to match the model - if (layer.controlAdapter.type === 't2i_adapter' && layer.controlAdapter.model.type === 'controlnet') { - // Converting from T2I Adapter to ControlNet - add `controlMode` - const controlNetConfig: ControlNetConfig = { - ...layer.controlAdapter, - type: 'controlnet', - controlMode: 'balanced', - }; - layer.controlAdapter = controlNetConfig; - } else if (layer.controlAdapter.type === 'controlnet' && layer.controlAdapter.model.type === 't2i_adapter') { - // Converting from ControlNet to T2I Adapter - remove `controlMode` - const { controlMode: _, ...rest } = layer.controlAdapter; - const t2iAdapterConfig: T2IAdapterConfig = { ...rest, type: 't2i_adapter' }; - layer.controlAdapter = t2iAdapterConfig; - } - }, - layerControlAdapterControlModeChanged: (state, action: PayloadAction<{ id: string; controlMode: ControlModeV2 }>) => { - const { id, controlMode } = action.payload; - const layer = selectLayer(state, id); - if (!layer || !layer.controlAdapter || layer.controlAdapter.type !== 'controlnet') { - return; - } - layer.controlAdapter.controlMode = controlMode; - }, - layerControlAdapterWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { - const { id, weight } = action.payload; - const layer = selectLayer(state, id); - if (!layer || !layer.controlAdapter) { - return; - } - layer.controlAdapter.weight = weight; - }, - layerControlAdapterBeginEndStepPctChanged: ( - state, - action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }> - ) => { - const { id, beginEndStepPct } = action.payload; - const layer = selectLayer(state, id); - if (!layer || !layer.controlAdapter) { - return; - } - layer.controlAdapter.beginEndStepPct = beginEndStepPct; - }, -} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts new file mode 100644 index 00000000000..728cfe24fb3 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts @@ -0,0 +1,108 @@ +import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import { deepClone } from 'common/util/deepClone'; +import { getPrefixedId } from 'features/controlLayers/konva/util'; +import { isEqual, merge } from 'lodash-es'; +import { assert } from 'tsafe'; + +import type { + CanvasControlLayerState, + CanvasRasterLayerState, + CanvasV2State, + ControlNetConfig, + Rect, + T2IAdapterConfig, +} from './types'; + +export const selectRasterLayer = (state: CanvasV2State, id: string) => + state.rasterLayers.entities.find((layer) => layer.id === id); +export const selectLayerOrThrow = (state: CanvasV2State, id: string) => { + const layer = selectRasterLayer(state, id); + assert(layer, `Layer with id ${id} not found`); + return layer; +}; + +export const rasterLayersReducers = { + rasterLayerAdded: { + reducer: ( + state, + action: PayloadAction<{ id: string; overrides?: Partial; isSelected?: boolean }> + ) => { + const { id, overrides, isSelected } = action.payload; + const layer: CanvasRasterLayerState = { + id, + type: 'raster_layer', + isEnabled: true, + objects: [], + opacity: 1, + position: { x: 0, y: 0 }, + rasterizationCache: [], + }; + merge(layer, overrides); + state.rasterLayers.entities.push(layer); + if (isSelected) { + state.selectedEntityIdentifier = { type: 'raster_layer', id }; + } + + if (layer.objects.length > 0) { + // This new layer will change the composite layer's image data. Invalidate the cache. + state.rasterLayers.compositeRasterizationCache = []; + } + }, + prepare: (payload: { overrides?: Partial; isSelected?: boolean }) => ({ + payload: { ...payload, id: getPrefixedId('raster_layer') }, + }), + }, + rasterLayerRecalled: (state, action: PayloadAction<{ data: CanvasRasterLayerState }>) => { + const { data } = action.payload; + state.rasterLayers.entities.push(data); + state.selectedEntityIdentifier = { type: 'raster_layer', id: data.id }; + if (data.objects.length > 0) { + // This new layer will change the composite layer's image data. Invalidate the cache. + state.rasterLayers.compositeRasterizationCache = []; + } + }, + rasterLayerAllDeleted: (state) => { + state.rasterLayers.entities = []; + state.rasterLayers.compositeRasterizationCache = []; + }, + rasterLayerCompositeRasterized: (state, action: PayloadAction<{ imageName: string; rect: Rect }>) => { + state.rasterLayers.compositeRasterizationCache = state.rasterLayers.compositeRasterizationCache.filter( + (cache) => !isEqual(cache.rect, action.payload.rect) + ); + state.rasterLayers.compositeRasterizationCache.push(action.payload); + }, + rasterLayerConvertedToControlLayer: { + reducer: ( + state, + action: PayloadAction<{ id: string; newId: string; controlAdapter: ControlNetConfig | T2IAdapterConfig }> + ) => { + const { id, newId, controlAdapter } = action.payload; + const layer = selectRasterLayer(state, id); + if (!layer) { + return; + } + + // Convert the raster layer to control layer + const controlLayerState: CanvasControlLayerState = { + ...deepClone(layer), + id: newId, + type: 'control_layer', + controlAdapter, + }; + + // Remove the raster layer + state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => layer.id !== id); + + // Add the converted control layer + state.controlLayers.entities.push(controlLayerState); + + // The composite layer's image data will change when the raster layer is converted to control layer. + state.rasterLayers.compositeRasterizationCache = []; + + state.selectedEntityIdentifier = { type: controlLayerState.type, id: controlLayerState.id }; + }, + prepare: (payload: { id: string; controlAdapter: ControlNetConfig | T2IAdapterConfig }) => ({ + payload: { ...payload, newId: getPrefixedId('control_layer') }, + }), + }, +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts index b52aca28ae1..c81856b5dfe 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts @@ -7,7 +7,7 @@ export const selectEntityCount = createSelector(selectCanvasV2Slice, (canvasV2) canvasV2.regions.entities.length + // canvasV2.controlAdapters.entities.length + canvasV2.ipAdapters.entities.length + - canvasV2.layers.entities.length + canvasV2.rasterLayers.entities.length ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index a19b694670f..9d7bd6fa71b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -728,23 +728,22 @@ const zT2IAdapterConfig = z.object({ }); export type T2IAdapterConfig = z.infer; -export const zCanvasLayerState = z.object({ +export const zCanvasRasterLayerState = z.object({ id: zId, - type: z.literal('layer'), + type: z.literal('raster_layer'), isEnabled: z.boolean(), position: zCoordinate, opacity: zOpacity, objects: z.array(zCanvasObjectState), rasterizationCache: z.array(zImageCache), - controlAdapter: z.discriminatedUnion('type', [zControlNetConfig, zT2IAdapterConfig]).nullable(), }); -export type CanvasLayerState = z.infer; -export type CanvasLayerStateWithValidControlNet = Omit & { - controlAdapter: Omit & { model: ControlNetModelConfig }; -}; -export type CanvasLayerStateWithValidT2IAdapter = Omit & { - controlAdapter: Omit & { model: T2IAdapterModelConfig }; -}; +export type CanvasRasterLayerState = z.infer; + +export const zCanvasControlLayerState = zCanvasRasterLayerState.extend({ + type: z.literal('control_layer'), + controlAdapter: z.discriminatedUnion('type', [zControlNetConfig, zT2IAdapterConfig]), +}); +export type CanvasControlLayerState = z.infer; export const initialControlNetV2: ControlNetConfig = { type: 'controlnet', @@ -808,8 +807,8 @@ export const isBoundingBoxScaleMethod = (v: unknown): v is BoundingBoxScaleMetho zBoundingBoxScaleMethod.safeParse(v).success; export type CanvasEntityState = - | CanvasLayerState - | CanvasControlAdapterState + | CanvasRasterLayerState + | CanvasControlLayerState | CanvasRegionalGuidanceState | CanvasInpaintMaskState | CanvasIPAdapterState; @@ -832,7 +831,8 @@ export type CanvasV2State = { _version: 3; selectedEntityIdentifier: CanvasEntityIdentifier | null; inpaintMask: CanvasInpaintMaskState; - layers: { entities: CanvasLayerState[]; compositeRasterizationCache: ImageCache[] }; + rasterLayers: { entities: CanvasRasterLayerState[]; compositeRasterizationCache: ImageCache[] }; + controlLayers: { entities: CanvasControlLayerState[] }; ipAdapters: { entities: CanvasIPAdapterState[] }; regions: { entities: CanvasRegionalGuidanceState[] }; loras: LoRA[]; @@ -962,10 +962,19 @@ export type RemoveIndexString = { export type GenerationMode = 'txt2img' | 'img2img' | 'inpaint' | 'outpaint'; +export function isDrawableEntityType(entityType: CanvasEntityState['type']) { + return ( + entityType === 'raster_layer' || + entityType === 'control_layer' || + entityType === 'regional_guidance' || + entityType === 'inpaint_mask' + ); +} + export function isDrawableEntity( entity: CanvasEntityState -): entity is CanvasLayerState | CanvasRegionalGuidanceState | CanvasInpaintMaskState { - return entity.type === 'layer' || entity.type === 'regional_guidance' || entity.type === 'inpaint_mask'; +): entity is CanvasRasterLayerState | CanvasControlLayerState | CanvasRegionalGuidanceState | CanvasInpaintMaskState { + return isDrawableEntityType(entity.type); } export function isDrawableEntityAdapter( @@ -973,9 +982,3 @@ export function isDrawableEntityAdapter( ): adapter is CanvasLayerAdapter | CanvasMaskAdapter { return adapter instanceof CanvasLayerAdapter || adapter instanceof CanvasMaskAdapter; } - -export function isDrawableEntityType( - entityType: CanvasEntityState['type'] -): entityType is 'layer' | 'regional_guidance' | 'inpaint_mask' { - return entityType === 'layer' || entityType === 'regional_guidance' || entityType === 'inpaint_mask'; -} diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts index 23eec846906..a9423747fe2 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts @@ -11,7 +11,7 @@ import { some } from 'lodash-es'; import type { ImageUsage } from './types'; export const getImageUsage = (nodes: NodesState, canvasV2: CanvasV2State, image_name: string) => { - const isLayerImage = canvasV2.layers.entities.some((layer) => + const isLayerImage = canvasV2.rasterLayers.entities.some((layer) => layer.objects.some((obj) => obj.type === 'image' && obj.image.image_name === image_name) ); diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx index 740a1a2200b..c1b2f85d8a4 100644 --- a/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx +++ b/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx @@ -1,4 +1,4 @@ -import type { CanvasLayerState } from 'features/controlLayers/store/types'; +import type { CanvasRasterLayerState } from 'features/controlLayers/store/types'; import { MetadataItemView } from 'features/metadata/components/MetadataItemView'; import type { MetadataHandlers } from 'features/metadata/types'; import { handlers } from 'features/metadata/util/handlers'; @@ -9,7 +9,7 @@ type Props = { }; export const MetadataLayers = ({ metadata }: Props) => { - const [layers, setLayers] = useState([]); + const [layers, setLayers] = useState([]); useEffect(() => { const parse = async () => { @@ -40,8 +40,8 @@ const MetadataViewLayer = ({ handlers, }: { label: string; - layer: CanvasLayerState; - handlers: MetadataHandlers; + layer: CanvasRasterLayerState; + handlers: MetadataHandlers; }) => { const onRecall = useCallback(() => { if (!handlers.recallItem) { diff --git a/invokeai/frontend/web/src/features/metadata/util/handlers.ts b/invokeai/frontend/web/src/features/metadata/util/handlers.ts index 887b1fbd2c9..6bea34438be 100644 --- a/invokeai/frontend/web/src/features/metadata/util/handlers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/handlers.ts @@ -2,7 +2,7 @@ import { getStore } from 'app/store/nanostores/store'; import { deepClone } from 'common/util/deepClone'; import { objectKeys } from 'common/util/objectKeys'; import { shouldConcatPromptsChanged } from 'features/controlLayers/store/canvasV2Slice'; -import type { CanvasLayerState, LoRA } from 'features/controlLayers/store/types'; +import type { CanvasRasterLayerState, LoRA } from 'features/controlLayers/store/types'; import type { AnyControlAdapterConfigMetadata, BuildMetadataHandlers, @@ -48,7 +48,7 @@ const renderControlAdapterValue: MetadataRenderValueFunc = async (layer) => { +const renderLayerValue: MetadataRenderValueFunc = async (layer) => { if (layer.type === 'initial_image_layer') { let rendered = t('controlLayers.globalInitialImageLayer'); if (layer.image) { @@ -88,7 +88,7 @@ const renderLayerValue: MetadataRenderValueFunc = async (layer } assert(false, 'Unknown layer type'); }; -const renderLayersValue: MetadataRenderValueFunc = async (layers) => { +const renderLayersValue: MetadataRenderValueFunc = async (layers) => { return `${layers.length} ${t('controlLayers.layers', { count: layers.length })}`; }; diff --git a/invokeai/frontend/web/src/features/metadata/util/parsers.ts b/invokeai/frontend/web/src/features/metadata/util/parsers.ts index 5ee43d344aa..e28399809c8 100644 --- a/invokeai/frontend/web/src/features/metadata/util/parsers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/parsers.ts @@ -1,6 +1,6 @@ import { getCAId, getImageObjectId, getIPAId, getLayerId } from 'features/controlLayers/konva/naming'; import { defaultLoRAConfig } from 'features/controlLayers/store/lorasReducers'; -import type { CanvasControlAdapterState, CanvasIPAdapterState, CanvasLayerState, LoRA } from 'features/controlLayers/store/types'; +import type { CanvasControlAdapterState, CanvasIPAdapterState, CanvasRasterLayerState, LoRA } from 'features/controlLayers/store/types'; import { IMAGE_FILTERS, imageDTOToImageWithDims, @@ -8,7 +8,7 @@ import { initialIPAdapterV2, initialT2IAdapterV2, isFilterType, - zCanvasLayerState, + zCanvasRasterLayerState, } from 'features/controlLayers/store/types'; import type { ControlNetConfigMetadata, @@ -424,22 +424,22 @@ const parseAllIPAdapters: MetadataParseFunc = async ( }; //#region Control Layers -const parseLayer: MetadataParseFunc = async (metadataItem) => zCanvasLayerState.parseAsync(metadataItem); +const parseLayer: MetadataParseFunc = async (metadataItem) => zCanvasRasterLayerState.parseAsync(metadataItem); -const parseLayers: MetadataParseFunc = async (metadata) => { +const parseLayers: MetadataParseFunc = async (metadata) => { // We need to support recalling pre-Control Layers metadata into Control Layers. A separate set of parsers handles // taking pre-CL metadata and parsing it into layers. It doesn't always map 1-to-1, so this is best-effort. For // example, CL Control Adapters don't support resize mode, so we simply omit that property. try { - const layers: CanvasLayerState[] = []; + const layers: CanvasRasterLayerState[] = []; try { const control_layers = await getProperty(metadata, 'control_layers'); const controlLayersRaw = await getProperty(control_layers, 'layers', isArray); const controlLayersParseResults = await Promise.allSettled(controlLayersRaw.map(parseLayer)); const controlLayers = controlLayersParseResults - .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') .map((result) => result.value); layers.push(...controlLayers); } catch { @@ -498,16 +498,16 @@ const parseLayers: MetadataParseFunc = async (metadata) => { } }; -const parseInitialImageToInitialImageLayer: MetadataParseFunc = async (metadata) => { +const parseInitialImageToInitialImageLayer: MetadataParseFunc = async (metadata) => { // TODO(psyche): recall denoise strength // const denoisingStrength = await getProperty(metadata, 'strength', isParameterStrength); const imageName = await getProperty(metadata, 'init_image', isString); const imageDTO = await getImageDTO(imageName); assert(imageDTO, 'ImageDTO is null'); const id = getLayerId(uuidv4()); - const layer: CanvasLayerState = { + const layer: CanvasRasterLayerState = { id, - type: 'layer', + type: 'raster_layer', bbox: null, bboxNeedsUpdate: true, x: 0, diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index 54196820d4e..c1bf41d1f45 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -15,8 +15,8 @@ import { bboxWidthChanged, // caRecalled, ipaRecalled, - layerAllDeleted, - layerRecalled, + rasterLayerAllDeleted, + rasterLayerRecalled, loraAllDeleted, loraRecalled, negativePrompt2Changed, @@ -42,7 +42,7 @@ import { import type { CanvasControlAdapterState, CanvasIPAdapterState, - CanvasLayerState, + CanvasRasterLayerState, CanvasRegionalGuidanceState, LoRA, } from 'features/controlLayers/store/types'; @@ -328,7 +328,7 @@ const recallRG: MetadataRecallFunc = async (rg) => }; //#region Control Layers -const recallLayer: MetadataRecallFunc = async (layer) => { +const recallLayer: MetadataRecallFunc = async (layer) => { const { dispatch } = getStore(); const clone = deepClone(layer); const invalidObjects: string[] = []; @@ -355,13 +355,13 @@ const recallLayer: MetadataRecallFunc = async (layer) => { } } clone.id = getRGId(uuidv4()); - dispatch(layerRecalled({ data: clone })); + dispatch(rasterLayerRecalled({ data: clone })); return; }; -const recallLayers: MetadataRecallFunc = (layers) => { +const recallLayers: MetadataRecallFunc = (layers) => { const { dispatch } = getStore(); - dispatch(layerAllDeleted()); + dispatch(rasterLayerAllDeleted()); for (const l of layers) { recallLayer(l); } diff --git a/invokeai/frontend/web/src/features/metadata/util/validators.ts b/invokeai/frontend/web/src/features/metadata/util/validators.ts index 5423a7e3595..9d667141a02 100644 --- a/invokeai/frontend/web/src/features/metadata/util/validators.ts +++ b/invokeai/frontend/web/src/features/metadata/util/validators.ts @@ -1,5 +1,5 @@ import { getStore } from 'app/store/nanostores/store'; -import type { CanvasLayerState, LoRA } from 'features/controlLayers/store/types'; +import type { CanvasRasterLayerState, LoRA } from 'features/controlLayers/store/types'; import type { ControlNetConfigMetadata, IPAdapterConfigMetadata, @@ -109,7 +109,7 @@ const validateIPAdapters: MetadataValidateFunc = (ipA return new Promise((resolve) => resolve(validatedIPAdapters)); }; -const validateLayer: MetadataValidateFunc = async (layer) => { +const validateLayer: MetadataValidateFunc = async (layer) => { if (layer.type === 'control_adapter_layer') { const model = layer.controlAdapter.model; assert(model, 'Control Adapter layer missing model'); @@ -131,8 +131,8 @@ const validateLayer: MetadataValidateFunc = async (layer) => { return layer; }; -const validateLayers: MetadataValidateFunc = async (layers) => { - const validatedLayers: CanvasLayerState[] = []; +const validateLayers: MetadataValidateFunc = async (layers) => { + const validatedLayers: CanvasRasterLayerState[] = []; for (const l of layers) { try { const validated = await validateLayer(l); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts index 8b8023cca3f..7f180b858b9 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts @@ -1,15 +1,10 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { - CanvasLayerState, - CanvasLayerStateWithValidControlNet, - CanvasLayerStateWithValidT2IAdapter, + CanvasControlLayerState, ControlNetConfig, - FilterConfig, - ImageWithDims, Rect, T2IAdapterConfig, } from 'features/controlLayers/store/types'; -import type { ImageField } from 'features/nodes/types/common'; import { CONTROL_NET_COLLECT, T2I_ADAPTER_COLLECT } from 'features/nodes/util/graph/constants'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import type { BaseModelType, ImageDTO, Invocation } from 'services/api/types'; @@ -17,18 +12,18 @@ import { assert } from 'tsafe'; export const addControlAdapters = async ( manager: CanvasManager, - layers: CanvasLayerState[], + layers: CanvasControlLayerState[], g: Graph, bbox: Rect, denoise: Invocation<'denoise_latents'>, base: BaseModelType -): Promise<(CanvasLayerStateWithValidControlNet | CanvasLayerStateWithValidT2IAdapter)[]> => { - const layersWithValidControlAdapters = layers +): Promise => { + const validControlLayers = layers .filter((layer) => layer.isEnabled) - .filter((layer) => doesLayerHaveValidControlAdapter(layer, base)); + .filter((layer) => isValidControlAdapter(layer.controlAdapter, base)); - for (const layer of layersWithValidControlAdapters) { - const adapter = manager.layers.get(layer.id); + for (const layer of validControlLayers) { + const adapter = manager.controlLayerAdapters.get(layer.id); assert(adapter, 'Adapter not found'); const imageDTO = await adapter.renderer.rasterize(bbox); if (layer.controlAdapter.type === 'controlnet') { @@ -37,7 +32,7 @@ export const addControlAdapters = async ( await addT2IAdapterToGraph(g, layer, imageDTO, denoise); } } - return layersWithValidControlAdapters; + return validControlLayers; }; const addControlNetCollectorSafe = (g: Graph, denoise: Invocation<'denoise_latents'>): Invocation<'collect'> => { @@ -59,12 +54,14 @@ const addControlNetCollectorSafe = (g: Graph, denoise: Invocation<'denoise_laten const addControlNetToGraph = ( g: Graph, - layer: CanvasLayerStateWithValidControlNet, + layer: CanvasControlLayerState, imageDTO: ImageDTO, denoise: Invocation<'denoise_latents'> ) => { const { id, controlAdapter } = layer; + assert(controlAdapter.type === 'controlnet'); const { beginEndStepPct, model, weight, controlMode } = controlAdapter; + assert(model !== null); const { image_name } = imageDTO; const controlNetCollect = addControlNetCollectorSafe(g, denoise); @@ -103,12 +100,14 @@ const addT2IAdapterCollectorSafe = (g: Graph, denoise: Invocation<'denoise_laten const addT2IAdapterToGraph = ( g: Graph, - layer: CanvasLayerStateWithValidT2IAdapter, + layer: CanvasControlLayerState, imageDTO: ImageDTO, denoise: Invocation<'denoise_latents'> ) => { const { id, controlAdapter } = layer; + assert(controlAdapter.type === 't2i_adapter'); const { beginEndStepPct, model, weight } = controlAdapter; + assert(model !== null); const { image_name } = imageDTO; const t2iAdapterCollect = addT2IAdapterCollectorSafe(g, denoise); @@ -127,25 +126,6 @@ const addT2IAdapterToGraph = ( g.addEdge(t2iAdapter, 't2i_adapter', t2iAdapterCollect, 'item'); }; -const buildControlImage = ( - image: ImageWithDims | null, - processedImage: ImageWithDims | null, - processorConfig: FilterConfig | null -): ImageField => { - if (processedImage && processorConfig) { - // We've processed the image in the app - use it for the control image. - return { - image_name: processedImage.image_name, - }; - } else if (image) { - // No processor selected, and we have an image - the user provided a processed image, use it for the control image. - return { - image_name: image.image_name, - }; - } - assert(false, 'Attempted to add unprocessed control image'); -}; - const isValidControlAdapter = (controlAdapter: ControlNetConfig | T2IAdapterConfig, base: BaseModelType): boolean => { // Must be have a model const hasModel = Boolean(controlAdapter.model); @@ -153,22 +133,3 @@ const isValidControlAdapter = (controlAdapter: ControlNetConfig | T2IAdapterConf const modelMatchesBase = controlAdapter.model?.base === base; return hasModel && modelMatchesBase; }; - -const doesLayerHaveValidControlAdapter = ( - layer: CanvasLayerState, - base: BaseModelType -): layer is CanvasLayerStateWithValidControlNet | CanvasLayerStateWithValidT2IAdapter => { - if (!layer.controlAdapter) { - // Must have a control adapter - return false; - } - if (!layer.controlAdapter.model) { - // Control adapter must have a model selected - return false; - } - if (layer.controlAdapter.model.base !== base) { - // Selected model must match current base model - return false; - } - return true; -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts index 5f85274ac5c..d36a2b8b170 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts @@ -22,7 +22,7 @@ export const addInpaint = async ( denoise.denoising_start = denoising_start; const initialImage = await manager.getCompositeLayerImageDTO(bbox.rect); - const maskImage = await manager.inpaintMask.renderer.rasterize(bbox.rect); + const maskImage = await manager.inpaintMaskAdapter.renderer.rasterize(bbox.rect); if (!isEqual(scaledSize, originalSize)) { // Scale before processing requires some resizing diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts index fad839efad4..d647eb556fb 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts @@ -1,6 +1,6 @@ -import type { CanvasLayerState } from 'features/controlLayers/store/types'; +import type { CanvasRasterLayerState } from 'features/controlLayers/store/types'; -export const isValidLayerWithoutControlAdapter = (layer: CanvasLayerState) => { +export const isValidLayerWithoutControlAdapter = (layer: CanvasRasterLayerState) => { return ( layer.isEnabled && // Boolean(entity.bbox) && TODO(psyche): Re-enable this check when we have a way to calculate bbox for all layers diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index 7e4296cb779..118fb779a24 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts @@ -23,7 +23,7 @@ export const addOutpaint = async ( denoise.denoising_start = denoising_start; const initialImage = await manager.getCompositeLayerImageDTO(bbox.rect); - const maskImage = await manager.inpaintMask.renderer.rasterize(bbox.rect); + const maskImage = await manager.inpaintMaskAdapter.renderer.rasterize(bbox.rect); const infill = getInfill(g, compositing); if (!isEqual(scaledSize, originalSize)) { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts index 847348cfb9e..f4a8f429e6c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts @@ -43,7 +43,7 @@ export const addRegions = async ( const validRegions = regions.filter((rg) => isValidRegion(rg, base)); for (const region of validRegions) { - const adapter = manager.regions.get(region.id); + const adapter = manager.regionalGuidanceAdapters.get(region.id); assert(adapter, 'Adapter not found'); const imageDTO = await adapter.renderer.rasterize(bbox); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index 8c9e198d6d3..ef403a5ae76 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -215,7 +215,7 @@ export const buildSD1Graph = async (state: RootState, manager: CanvasManager): P const _addedCAs = await addControlAdapters( manager, - state.canvasV2.layers.entities, + state.canvasV2.rasterLayers.entities, g, state.canvasV2.bbox.rect, denoise, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index 56b4292c1e5..f80d47b5c61 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -219,7 +219,7 @@ export const buildSDXLGraph = async (state: RootState, manager: CanvasManager): const _addedCAs = await addControlAdapters( manager, - state.canvasV2.layers.entities, + state.canvasV2.rasterLayers.entities, g, state.canvasV2.bbox.rect, denoise, From ba14fe3600e914bc751bf259d04d9767799e7d38 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:29:19 +1000 Subject: [PATCH 355/678] feat(ui): revise entity menus --- invokeai/frontend/web/public/locales/en.json | 7 +- .../components/AddLayerButton.tsx | 30 ++- .../components/AddPromptButtons.tsx | 19 +- .../components/ControlLayer/ControlLayer.tsx | 2 - .../ControlLayer/ControlLayerActionsMenu.tsx | 17 -- .../ControlLayerControlAdapter.tsx | 8 +- ...ControlLayerControlAdapterControlMode.tsx} | 4 +- .../ControlLayerControlAdapterModel.tsx} | 4 +- .../ControlLayer/ControlLayerMenuItems.tsx | 23 ++ .../ControlLayerMenuItemsControlToRaster.tsx | 25 +++ .../components/ControlLayersPanelContent.tsx | 4 +- .../components/InpaintMask/InpaintMask.tsx | 2 - .../InpaintMask/InpaintMaskActionsMenu.tsx | 17 -- .../InpaintMask/InpaintMaskMenuItems.tsx | 12 ++ .../components/RasterLayer/RasterLayer.tsx | 2 - .../RasterLayer/RasterLayerActionsMenu.tsx | 17 -- .../RasterLayer/RasterLayerMenuItems.tsx | 23 ++ .../RasterLayerMenuItemsRasterToControl.tsx | 28 +++ .../RegionalGuidance/RegionalGuidance.tsx | 2 - .../RegionalGuidanceActionsMenu.tsx | 62 ------ .../RegionalGuidanceMenuItems.tsx | 21 ++ ...uidanceMenuItemsAddPromptsAndIPAdapter.tsx | 58 +++++ ...sButton.tsx => ResetAllEntitiesButton.tsx} | 17 +- .../common/CanvasEntityActionMenuItems.tsx | 200 ------------------ .../components/common/CanvasEntityHeader.tsx | 52 ++++- .../common/CanvasEntityMenuItemsArrange.tsx | 95 +++++++++ .../common/CanvasEntityMenuItemsDelete.tsx | 25 +++ .../common/CanvasEntityMenuItemsFilter.tsx | 22 ++ .../common/CanvasEntityMenuItemsReset.tsx | 25 +++ .../controlLayers/hooks/addLayerHooks.ts | 89 -------- .../hooks/useLayerControlAdapter.ts | 24 ++- .../controlLayers/store/canvasV2Slice.ts | 12 +- 32 files changed, 475 insertions(+), 473 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerActionsMenu.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAdapter/ControlAdapterControlModeSelect.tsx => ControlLayer/ControlLayerControlAdapterControlMode.tsx} (90%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAdapter/ControlAdapterModel.tsx => ControlLayer/ControlLayerControlAdapterModel.tsx} (91%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItems.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsControlToRaster.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskActionsMenu.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItems.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerActionsMenu.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItems.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceActionsMenu.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItems.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{DeleteAllLayersButton.tsx => ResetAllEntitiesButton.tsx} (56%) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDelete.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsFilter.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsReset.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 2f62a6c7963..0867c084ec9 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1651,7 +1651,7 @@ "storeNotInitialized": "Store is not initialized" }, "controlLayers": { - "deleteAll": "Delete All", + "resetAll": "Reset All", "addLayer": "Add Layer", "moveToFront": "Move to Front", "moveToBack": "Move to Back", @@ -1699,7 +1699,10 @@ "layers_other": "Layers", "objects_zero": "empty", "objects_one": "{{count}} object", - "objects_other": "{{count}} objects" + "objects_other": "{{count}} objects", + "filter": "Filter", + "convertToControlLayer": "Convert to Control Layer", + "convertToRasterLayer": "Convert to Raster Layer" }, "upscaling": { "upscale": "Upscale", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx index 0234f49ab3a..433faf4c69d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx @@ -1,7 +1,7 @@ import { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; -import { useAddCALayer, useAddIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; -import { rasterLayerAdded, rgAdded } from 'features/controlLayers/store/canvasV2Slice'; +import { useDefaultControlAdapter, useDefaultIPAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter'; +import { controlLayerAdded, ipaAdded, rasterLayerAdded, rgAdded } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPlusBold } from 'react-icons/pi'; @@ -9,14 +9,20 @@ import { PiPlusBold } from 'react-icons/pi'; export const AddLayerButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const [addCALayer, isAddCALayerDisabled] = useAddCALayer(); - const [addIPALayer, isAddIPALayerDisabled] = useAddIPALayer(); + const defaultControlAdapter = useDefaultControlAdapter(); + const defaultIPAdapter = useDefaultIPAdapter(); const addRGLayer = useCallback(() => { dispatch(rgAdded()); }, [dispatch]); const addRasterLayer = useCallback(() => { dispatch(rasterLayerAdded({ isSelected: true })); }, [dispatch]); + const addControlLayer = useCallback(() => { + dispatch(controlLayerAdded({ isSelected: true, overrides: { controlAdapter: defaultControlAdapter } })); + }, [defaultControlAdapter, dispatch]); + const addIPAdapter = useCallback(() => { + dispatch(ipaAdded({ config: defaultIPAdapter })); + }, [defaultIPAdapter, dispatch]); return ( @@ -29,18 +35,10 @@ export const AddLayerButton = memo(() => { {t('controlLayers.addLayer')} - } onClick={addRGLayer}> - {t('controlLayers.regionalGuidanceLayer')} - - } onClick={addRasterLayer}> - {t('controlLayers.rasterLayer')} - - } onClick={addCALayer} isDisabled={isAddCALayerDisabled}> - {t('controlLayers.globalControlAdapterLayer')} - - } onClick={addIPALayer} isDisabled={isAddIPALayerDisabled}> - {t('controlLayers.globalIPAdapterLayer')} - + {t('controlLayers.regionalGuidanceLayer')} + {t('controlLayers.rasterLayer')} + {t('controlLayers.controlLayer')} + {t('controlLayers.globalIPAdapterLayer')} ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx index 7256d6d9e02..4b0804c9827 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx @@ -1,8 +1,10 @@ import { Button, Flex } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useAddIPAdapterToRGLayer } from 'features/controlLayers/hooks/addLayerHooks'; +import { useDefaultIPAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter'; +import { nanoid } from 'features/controlLayers/konva/util'; import { + rgIPAdapterAdded, rgNegativePromptChanged, rgPositivePromptChanged, selectCanvasV2Slice, @@ -18,7 +20,7 @@ type AddPromptButtonProps = { export const AddPromptButtons = ({ id }: AddPromptButtonProps) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToRGLayer(id); + const defaultIPAdapter = useDefaultIPAdapter(); const selectValidActions = useMemo( () => createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { @@ -37,6 +39,11 @@ export const AddPromptButtons = ({ id }: AddPromptButtonProps) => { const addNegativePrompt = useCallback(() => { dispatch(rgNegativePromptChanged({ id, prompt: '' })); }, [dispatch, id]); + const addIPAdapter = useCallback(() => { + dispatch( + rgIPAdapterAdded({ id, ipAdapter: { ...defaultIPAdapter, id: nanoid(), type: 'ip_adapter', isEnabled: true } }) + ); + }, [defaultIPAdapter, dispatch, id]); return ( @@ -58,13 +65,7 @@ export const AddPromptButtons = ({ id }: AddPromptButtonProps) => { > {t('common.negativePrompt')} - diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx index 046d43f46e1..af47649a20b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx @@ -5,7 +5,6 @@ import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/com import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; -import { ControlLayerActionsMenu } from 'features/controlLayers/components/ControlLayer/ControlLayerActionsMenu'; import { ControlLayerControlAdapter } from 'features/controlLayers/components/ControlLayer/ControlLayerControlAdapter'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; @@ -25,7 +24,6 @@ export const ControlLayer = memo(({ id }: Props) => { - diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerActionsMenu.tsx deleted file mode 100644 index 3f2fa45e068..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerActionsMenu.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Menu, MenuList } from '@invoke-ai/ui-library'; -import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/common/CanvasEntityActionMenuItems'; -import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; -import { memo } from 'react'; - -export const ControlLayerActionsMenu = memo(() => { - return ( - - - - - - - ); -}); - -ControlLayerActionsMenu.displayName = 'ControlLayerActionsMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx index 3e5d1d7fee8..435e90cdd4f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx @@ -2,8 +2,8 @@ import { Flex } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct'; import { Weight } from 'features/controlLayers/components/common/Weight'; -import { ControlAdapterControlModeSelect } from 'features/controlLayers/components/ControlAdapter/ControlAdapterControlModeSelect'; -import { ControlAdapterModel } from 'features/controlLayers/components/ControlAdapter/ControlAdapterModel'; +import { ControlLayerControlAdapterControlMode } from 'features/controlLayers/components/ControlLayer/ControlLayerControlAdapterControlMode'; +import { ControlLayerControlAdapterModel } from 'features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { useControlLayerControlAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter'; import { @@ -51,11 +51,11 @@ export const ControlLayerControlAdapter = memo(() => { return ( - + {controlAdapter.type === 'controlnet' && ( - + )} ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterControlModeSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterControlMode.tsx similarity index 90% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterControlModeSelect.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterControlMode.tsx index e3f03b6d48f..e0f57e6df87 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterControlModeSelect.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterControlMode.tsx @@ -12,7 +12,7 @@ type Props = { onChange: (controlMode: ControlModeV2) => void; }; -export const ControlAdapterControlModeSelect = memo(({ controlMode, onChange }: Props) => { +export const ControlLayerControlAdapterControlMode = memo(({ controlMode, onChange }: Props) => { const { t } = useTranslation(); const CONTROL_MODE_DATA = useMemo( () => [ @@ -57,4 +57,4 @@ export const ControlAdapterControlModeSelect = memo(({ controlMode, onChange }: ); }); -ControlAdapterControlModeSelect.displayName = 'ControlAdapterControlModeSelect'; +ControlLayerControlAdapterControlMode.displayName = 'ControlLayerControlAdapterControlMode'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterModel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx similarity index 91% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterModel.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx index 13e4b17e907..f50218e4aad 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterModel.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx @@ -11,7 +11,7 @@ type Props = { onChange: (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => void; }; -export const ControlAdapterModel = memo(({ modelKey, onChange: onChangeModel }: Props) => { +export const ControlLayerControlAdapterModel = memo(({ modelKey, onChange: onChangeModel }: Props) => { const { t } = useTranslation(); const currentBaseModel = useAppSelector((s) => s.canvasV2.params.model?.base); const [modelConfigs, { isLoading }] = useControlNetAndT2IAdapterModels(); @@ -60,4 +60,4 @@ export const ControlAdapterModel = memo(({ modelKey, onChange: onChangeModel }: ); }); -ControlAdapterModel.displayName = 'ControlAdapterModel'; +ControlLayerControlAdapterModel.displayName = 'ControlLayerControlAdapterModel'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItems.tsx new file mode 100644 index 00000000000..49f25589069 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItems.tsx @@ -0,0 +1,23 @@ +import { MenuDivider } from '@invoke-ai/ui-library'; +import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange'; +import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete'; +import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter'; +import { CanvasEntityMenuItemsReset } from 'features/controlLayers/components/common/CanvasEntityMenuItemsReset'; +import { ControlLayerMenuItemsControlToRaster } from 'features/controlLayers/components/ControlLayer/ControlLayerMenuItemsControlToRaster'; +import { memo } from 'react'; + +export const ControlLayerMenuItems = memo(() => { + return ( + <> + + + + + + + + + ); +}); + +ControlLayerMenuItems.displayName = 'ControlLayerMenuItems'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsControlToRaster.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsControlToRaster.tsx new file mode 100644 index 00000000000..e64df4e9e5c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsControlToRaster.tsx @@ -0,0 +1,25 @@ +import { MenuItem } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { controlLayerConvertedToRasterLayer } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiLightningBold } from 'react-icons/pi'; + +export const ControlLayerMenuItemsControlToRaster = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const entityIdentifier = useEntityIdentifierContext(); + + const convertControlLayerToRasterLayer = useCallback(() => { + dispatch(controlLayerConvertedToRasterLayer({ id: entityIdentifier.id })); + }, [dispatch, entityIdentifier.id]); + + return ( + }> + {t('controlLayers.convertToRasterLayer')} + + ); +}); + +ControlLayerMenuItemsControlToRaster.displayName = 'ControlLayerMenuItemsControlToRaster'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index ddaa0090555..195d7747072 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -3,8 +3,8 @@ import { Flex } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { AddLayerButton } from 'features/controlLayers/components/AddLayerButton'; import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList'; -import { DeleteAllLayersButton } from 'features/controlLayers/components/DeleteAllLayersButton'; import { Filter } from 'features/controlLayers/components/Filters/Filter'; +import { ResetAllEntitiesButton } from 'features/controlLayers/components/ResetAllEntitiesButton'; import { $filteringEntity } from 'features/controlLayers/store/canvasV2Slice'; import ResizeHandle from 'features/ui/components/tabs/ResizeHandle'; import { memo } from 'react'; @@ -18,7 +18,7 @@ export const ControlLayersPanelContent = memo(() => { - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx index 04b4f88ee60..ba3c3dee82e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx @@ -5,7 +5,6 @@ import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/com import { CanvasEntityGroupTitle } from 'features/controlLayers/components/common/CanvasEntityGroupTitle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; -import { InpaintMaskActionsMenu } from 'features/controlLayers/components/InpaintMask/InpaintMaskActionsMenu'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; @@ -28,7 +27,6 @@ export const InpaintMask = memo(() => { - diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskActionsMenu.tsx deleted file mode 100644 index 5ce40241955..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskActionsMenu.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Menu, MenuList } from '@invoke-ai/ui-library'; -import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/common/CanvasEntityActionMenuItems'; -import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; -import { memo } from 'react'; - -export const InpaintMaskActionsMenu = memo(() => { - return ( - - - - - - - ); -}); - -InpaintMaskActionsMenu.displayName = 'InpaintMaskActionsMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItems.tsx new file mode 100644 index 00000000000..d0d1a7dc105 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItems.tsx @@ -0,0 +1,12 @@ +import { CanvasEntityMenuItemsReset } from 'features/controlLayers/components/common/CanvasEntityMenuItemsReset'; +import { memo } from 'react'; + +export const InpaintMaskMenuItems = memo(() => { + return ( + <> + + + ); +}); + +InpaintMaskMenuItems.displayName = 'InpaintMaskMenuItems'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx index e12ce65b4a6..303d4191ed9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx @@ -4,7 +4,6 @@ import { CanvasEntityDeleteButton } from 'features/controlLayers/components/comm import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; -import { RasterLayerActionsMenu } from 'features/controlLayers/components/RasterLayer/RasterLayerActionsMenu'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; @@ -23,7 +22,6 @@ export const RasterLayer = memo(({ id }: Props) => { - diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerActionsMenu.tsx deleted file mode 100644 index 576c939ad29..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerActionsMenu.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Menu, MenuList } from '@invoke-ai/ui-library'; -import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/common/CanvasEntityActionMenuItems'; -import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; -import { memo } from 'react'; - -export const RasterLayerActionsMenu = memo(() => { - return ( - - - - - - - ); -}); - -RasterLayerActionsMenu.displayName = 'RasterLayerActionsMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItems.tsx new file mode 100644 index 00000000000..2ad6a48f107 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItems.tsx @@ -0,0 +1,23 @@ +import { MenuDivider } from '@invoke-ai/ui-library'; +import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange'; +import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete'; +import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter'; +import { CanvasEntityMenuItemsReset } from 'features/controlLayers/components/common/CanvasEntityMenuItemsReset'; +import { RasterLayerMenuItemsRasterToControl } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl'; +import { memo } from 'react'; + +export const RasterLayerMenuItems = memo(() => { + return ( + <> + + + + + + + + + ); +}); + +RasterLayerMenuItems.displayName = 'RasterLayerMenuItems'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl.tsx new file mode 100644 index 00000000000..9513f0409c8 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl.tsx @@ -0,0 +1,28 @@ +import { MenuItem } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useDefaultControlAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter'; +import { rasterLayerConvertedToControlLayer } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiLightningBold } from 'react-icons/pi'; + +export const RasterLayerMenuItemsRasterToControl = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const entityIdentifier = useEntityIdentifierContext(); + + const defaultControlAdapter = useDefaultControlAdapter(); + + const convertRasterLayerToControlLayer = useCallback(() => { + dispatch(rasterLayerConvertedToControlLayer({ id: entityIdentifier.id, controlAdapter: defaultControlAdapter })); + }, [dispatch, defaultControlAdapter, entityIdentifier.id]); + + return ( + }> + {t('controlLayers.convertToControlLayer')} + + ); +}); + +RasterLayerMenuItemsRasterToControl.displayName = 'RasterLayerMenuItemsRasterToControl'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx index eeed5126a96..fd368e53e1e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx @@ -4,7 +4,6 @@ import { CanvasEntityDeleteButton } from 'features/controlLayers/components/comm import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; -import { RegionalGuidanceActionsMenu } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceActionsMenu'; import { RegionalGuidanceBadges } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges'; import { RegionalGuidanceSettings } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; @@ -30,7 +29,6 @@ export const RegionalGuidance = memo(({ id }: Props) => { - diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceActionsMenu.tsx deleted file mode 100644 index e8d9db88857..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceActionsMenu.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Menu, MenuDivider, MenuItem, MenuList } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/common/CanvasEntityActionMenuItems'; -import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; -import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { useAddIPAdapterToRGLayer } from 'features/controlLayers/hooks/addLayerHooks'; -import { - rgNegativePromptChanged, - rgPositivePromptChanged, - selectCanvasV2Slice, -} from 'features/controlLayers/store/canvasV2Slice'; -import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiPlusBold } from 'react-icons/pi'; - -export const RegionalGuidanceActionsMenu = memo(() => { - const { id } = useEntityIdentifierContext(); - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const [onAddIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToRGLayer(id); - const selectActionsValidity = useMemo( - () => - createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { - const rg = selectRGOrThrow(canvasV2, id); - return { - isAddPositivePromptDisabled: rg.positivePrompt === null, - isAddNegativePromptDisabled: rg.negativePrompt === null, - }; - }), - [id] - ); - const actions = useAppSelector(selectActionsValidity); - const onAddPositivePrompt = useCallback(() => { - dispatch(rgPositivePromptChanged({ id: id, prompt: '' })); - }, [dispatch, id]); - const onAddNegativePrompt = useCallback(() => { - dispatch(rgNegativePromptChanged({ id: id, prompt: '' })); - }, [dispatch, id]); - - return ( - - - - }> - {t('controlLayers.addPositivePrompt')} - - }> - {t('controlLayers.addNegativePrompt')} - - } isDisabled={isAddIPAdapterDisabled}> - {t('controlLayers.addIPAdapter')} - - - - - - ); -}); - -RegionalGuidanceActionsMenu.displayName = 'RegionalGuidanceActionsMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItems.tsx new file mode 100644 index 00000000000..9c495bb6dbc --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItems.tsx @@ -0,0 +1,21 @@ +import { MenuDivider } from '@invoke-ai/ui-library'; +import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange'; +import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete'; +import { CanvasEntityMenuItemsReset } from 'features/controlLayers/components/common/CanvasEntityMenuItemsReset'; +import { RegionalGuidanceMenuItemsAddPromptsAndIPAdapter } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter'; +import { memo } from 'react'; + +export const RegionalGuidanceMenuItems = memo(() => { + return ( + <> + + + + + + + + ); +}); + +RegionalGuidanceMenuItems.displayName = 'RegionalGuidanceMenuItems'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx new file mode 100644 index 00000000000..66bcee04e91 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx @@ -0,0 +1,58 @@ +import { MenuItem } from '@invoke-ai/ui-library'; +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useDefaultIPAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter'; +import { nanoid } from 'features/controlLayers/konva/util'; +import { + rgIPAdapterAdded, + rgNegativePromptChanged, + rgPositivePromptChanged, + selectCanvasV2Slice, +} from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const RegionalGuidanceMenuItemsAddPromptsAndIPAdapter = memo(() => { + const { id } = useEntityIdentifierContext(); + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const defaultIPAdapter = useDefaultIPAdapter(); + const selectValidActions = useMemo( + () => + createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { + const rg = canvasV2.regions.entities.find((rg) => rg.id === id); + return { + canAddPositivePrompt: rg?.positivePrompt === null, + canAddNegativePrompt: rg?.negativePrompt === null, + }; + }), + [id] + ); + const validActions = useAppSelector(selectValidActions); + const addPositivePrompt = useCallback(() => { + dispatch(rgPositivePromptChanged({ id: id, prompt: '' })); + }, [dispatch, id]); + const addNegativePrompt = useCallback(() => { + dispatch(rgNegativePromptChanged({ id: id, prompt: '' })); + }, [dispatch, id]); + const addIPAdapter = useCallback(() => { + dispatch( + rgIPAdapterAdded({ id, ipAdapter: { ...defaultIPAdapter, id: nanoid(), type: 'ip_adapter', isEnabled: true } }) + ); + }, [defaultIPAdapter, dispatch, id]); + + return ( + <> + + {t('controlLayers.addPositivePrompt')} + + + {t('controlLayers.addNegativePrompt')} + + {t('controlLayers.addIPAdapter')} + + ); +}); + +RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.displayName = 'RegionalGuidanceMenuItemsExtra'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ResetAllEntitiesButton.tsx similarity index 56% rename from invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ResetAllEntitiesButton.tsx index b6dc6b4df0f..6851f7c5ab8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ResetAllEntitiesButton.tsx @@ -1,21 +1,13 @@ import { Button } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch } from 'app/store/storeHooks'; import { allEntitiesDeleted } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiTrashSimpleBold } from 'react-icons/pi'; -export const DeleteAllLayersButton = memo(() => { +export const ResetAllEntitiesButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const entityCount = useAppSelector((s) => { - return ( - s.canvasV2.regions.entities.length + - // s.canvasV2.controlAdapters.entities.length + - s.canvasV2.ipAdapters.entities.length + - s.canvasV2.rasterLayers.entities.length - ); - }); const onClick = useCallback(() => { dispatch(allEntitiesDeleted()); }, [dispatch]); @@ -26,12 +18,11 @@ export const DeleteAllLayersButton = memo(() => { leftIcon={} variant="ghost" colorScheme="error" - isDisabled={entityCount === 0} data-testid="control-layers-delete-all-layers-button" > - {t('controlLayers.deleteAll')} + {t('controlLayers.resetAll')} ); }); -DeleteAllLayersButton.displayName = 'DeleteAllLayersButton'; +ResetAllEntitiesButton.displayName = 'ResetAllEntitiesButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx deleted file mode 100644 index 1c8b61c01e4..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import { MenuDivider, MenuItem } from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { useDefaultControlAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter'; -import { $canvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { - $filteringEntity, - controlLayerConvertedToRasterLayer, - entityArrangedBackwardOne, - entityArrangedForwardOne, - entityArrangedToBack, - entityArrangedToFront, - entityDeleted, - entityReset, - rasterLayerConvertedToControlLayer, - selectCanvasV2Slice, -} from 'features/controlLayers/store/canvasV2Slice'; -import type { CanvasEntityIdentifier, CanvasV2State } from 'features/controlLayers/store/types'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { - PiArrowCounterClockwiseBold, - PiArrowDownBold, - PiArrowLineDownBold, - PiArrowLineUpBold, - PiArrowUpBold, - PiCheckBold, - PiQuestionMarkBold, - PiStarHalfBold, - PiTrashSimpleBold, -} from 'react-icons/pi'; - -const getIndexAndCount = ( - canvasV2: CanvasV2State, - { id, type }: CanvasEntityIdentifier -): { index: number; count: number } => { - if (type === 'raster_layer') { - return { - index: canvasV2.rasterLayers.entities.findIndex((entity) => entity.id === id), - count: canvasV2.rasterLayers.entities.length, - }; - } else if (type === 'control_layer') { - return { - index: canvasV2.controlLayers.entities.findIndex((entity) => entity.id === id), - count: canvasV2.controlLayers.entities.length, - }; - } else if (type === 'regional_guidance') { - return { - index: canvasV2.regions.entities.findIndex((entity) => entity.id === id), - count: canvasV2.regions.entities.length, - }; - } else { - return { - index: -1, - count: 0, - }; - } -}; - -export const CanvasEntityActionMenuItems = memo(() => { - const { t } = useTranslation(); - const canvasManager = useStore($canvasManager); - const dispatch = useAppDispatch(); - const entityIdentifier = useEntityIdentifierContext(); - const selectValidActions = useMemo( - () => - createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { - const { index, count } = getIndexAndCount(canvasV2, entityIdentifier); - return { - canMoveForwardOne: index < count - 1, - canMoveBackwardOne: index > 0, - canMoveToFront: index < count - 1, - canMoveToBack: index > 0, - }; - }), - [entityIdentifier] - ); - - const validActions = useAppSelector(selectValidActions); - - const isArrangeable = useMemo( - () => - entityIdentifier.type === 'raster_layer' || - entityIdentifier.type === 'control_layer' || - entityIdentifier.type === 'regional_guidance', - [entityIdentifier.type] - ); - - const isDeleteable = useMemo( - () => - entityIdentifier.type === 'raster_layer' || - entityIdentifier.type === 'control_layer' || - entityIdentifier.type === 'regional_guidance', - [entityIdentifier.type] - ); - - const isFilterable = useMemo( - () => entityIdentifier.type === 'raster_layer' || entityIdentifier.type === 'control_layer', - [entityIdentifier.type] - ); - - const isRasterLayer = useMemo(() => entityIdentifier.type === 'raster_layer', [entityIdentifier.type]); - - const isControlLayer = useMemo(() => entityIdentifier.type === 'control_layer', [entityIdentifier.type]); - - const defaultControlAdapter = useDefaultControlAdapter(); - - const convertRasterLayerToControlLayer = useCallback(() => { - dispatch(rasterLayerConvertedToControlLayer({ id: entityIdentifier.id, controlAdapter: defaultControlAdapter })); - }, [dispatch, defaultControlAdapter, entityIdentifier.id]); - - const convertControlLayerToRasterLayer = useCallback(() => { - dispatch(controlLayerConvertedToRasterLayer({ id: entityIdentifier.id })); - }, [dispatch, entityIdentifier.id]); - - const deleteEntity = useCallback(() => { - dispatch(entityDeleted({ entityIdentifier })); - }, [dispatch, entityIdentifier]); - const resetEntity = useCallback(() => { - dispatch(entityReset({ entityIdentifier })); - }, [dispatch, entityIdentifier]); - const moveForwardOne = useCallback(() => { - dispatch(entityArrangedForwardOne({ entityIdentifier })); - }, [dispatch, entityIdentifier]); - const moveToFront = useCallback(() => { - dispatch(entityArrangedToFront({ entityIdentifier })); - }, [dispatch, entityIdentifier]); - const moveBackwardOne = useCallback(() => { - dispatch(entityArrangedBackwardOne({ entityIdentifier })); - }, [dispatch, entityIdentifier]); - const moveToBack = useCallback(() => { - dispatch(entityArrangedToBack({ entityIdentifier })); - }, [dispatch, entityIdentifier]); - const filter = useCallback(() => { - $filteringEntity.set(entityIdentifier); - }, [entityIdentifier]); - const debug = useCallback(() => { - if (!canvasManager) { - return; - } - const entity = canvasManager.stateApi.getEntity(entityIdentifier); - if (!entity) { - return; - } - console.debug(entity); - }, [canvasManager, entityIdentifier]); - - return ( - <> - {isArrangeable && ( - <> - }> - {t('controlLayers.moveToFront')} - - }> - {t('controlLayers.moveForward')} - - }> - {t('controlLayers.moveBackward')} - - }> - {t('controlLayers.moveToBack')} - - - )} - {isFilterable && ( - }> - {t('common.filter')} - - )} - {isRasterLayer && ( - }> - {t('common.convertToControlLayer')} - - )} - {isControlLayer && ( - }> - {t('common.convertToRasterLayer')} - - )} - - }> - {t('accessibility.reset')} - - {isDeleteable && ( - } color="error.300"> - {t('common.delete')} - - )} - - } color="warn.300"> - {t('common.debug')} - - - ); -}); - -CanvasEntityActionMenuItems.displayName = 'CanvasEntityActionMenuItems'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx index 5fe02038614..0521ab803db 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx @@ -1,16 +1,54 @@ import type { FlexProps } from '@invoke-ai/ui-library'; import { ContextMenu, Flex, MenuList } from '@invoke-ai/ui-library'; -import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/common/CanvasEntityActionMenuItems'; +import { ControlLayerMenuItems } from 'features/controlLayers/components/ControlLayer/ControlLayerMenuItems'; +import { InpaintMaskMenuItems } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItems'; +import { RasterLayerMenuItems } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItems'; +import { RegionalGuidanceMenuItems } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItems'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { memo, useCallback } from 'react'; +import { assert } from 'tsafe'; export const CanvasEntityHeader = memo(({ children, ...rest }: FlexProps) => { + const entityIdentifier = useEntityIdentifierContext(); const renderMenu = useCallback(() => { - return ( - - - - ); - }, []); + if (entityIdentifier.type === 'regional_guidance') { + return ( + + + + ); + } + + if (entityIdentifier.type === 'inpaint_mask') { + return ( + + + + ); + } + + if (entityIdentifier.type === 'raster_layer') { + return ( + + + + ); + } + + if (entityIdentifier.type === 'control_layer') { + return ( + + + + ); + } + + if (entityIdentifier.type === 'ip_adapter') { + return {/* */}; + } + + assert(false, 'Unhandled entity type'); + }, [entityIdentifier]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx new file mode 100644 index 00000000000..8bcb88f6231 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx @@ -0,0 +1,95 @@ +import { MenuItem } from '@invoke-ai/ui-library'; +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { + entityArrangedBackwardOne, + entityArrangedForwardOne, + entityArrangedToBack, + entityArrangedToFront, + selectCanvasV2Slice, +} from 'features/controlLayers/store/canvasV2Slice'; +import type { CanvasEntityIdentifier, CanvasV2State } from 'features/controlLayers/store/types'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiArrowDownBold, PiArrowLineDownBold, PiArrowLineUpBold, PiArrowUpBold } from 'react-icons/pi'; + +const getIndexAndCount = ( + canvasV2: CanvasV2State, + { id, type }: CanvasEntityIdentifier +): { index: number; count: number } => { + if (type === 'raster_layer') { + return { + index: canvasV2.rasterLayers.entities.findIndex((entity) => entity.id === id), + count: canvasV2.rasterLayers.entities.length, + }; + } else if (type === 'control_layer') { + return { + index: canvasV2.controlLayers.entities.findIndex((entity) => entity.id === id), + count: canvasV2.controlLayers.entities.length, + }; + } else if (type === 'regional_guidance') { + return { + index: canvasV2.regions.entities.findIndex((entity) => entity.id === id), + count: canvasV2.regions.entities.length, + }; + } else { + return { + index: -1, + count: 0, + }; + } +}; + +export const CanvasEntityMenuItemsArrange = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const entityIdentifier = useEntityIdentifierContext(); + const selectValidActions = useMemo( + () => + createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { + const { index, count } = getIndexAndCount(canvasV2, entityIdentifier); + return { + canMoveForwardOne: index < count - 1, + canMoveBackwardOne: index > 0, + canMoveToFront: index < count - 1, + canMoveToBack: index > 0, + }; + }), + [entityIdentifier] + ); + + const validActions = useAppSelector(selectValidActions); + + const moveForwardOne = useCallback(() => { + dispatch(entityArrangedForwardOne({ entityIdentifier })); + }, [dispatch, entityIdentifier]); + const moveToFront = useCallback(() => { + dispatch(entityArrangedToFront({ entityIdentifier })); + }, [dispatch, entityIdentifier]); + const moveBackwardOne = useCallback(() => { + dispatch(entityArrangedBackwardOne({ entityIdentifier })); + }, [dispatch, entityIdentifier]); + const moveToBack = useCallback(() => { + dispatch(entityArrangedToBack({ entityIdentifier })); + }, [dispatch, entityIdentifier]); + + return ( + <> + }> + {t('controlLayers.moveToFront')} + + }> + {t('controlLayers.moveForward')} + + }> + {t('controlLayers.moveBackward')} + + }> + {t('controlLayers.moveToBack')} + + + ); +}); + +CanvasEntityMenuItemsArrange.displayName = 'CanvasEntityArrangeMenuItems'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDelete.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDelete.tsx new file mode 100644 index 00000000000..24d9a326829 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDelete.tsx @@ -0,0 +1,25 @@ +import { MenuItem } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { entityDeleted } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiTrashSimpleBold } from 'react-icons/pi'; + +export const CanvasEntityMenuItemsDelete = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const entityIdentifier = useEntityIdentifierContext(); + + const deleteEntity = useCallback(() => { + dispatch(entityDeleted({ entityIdentifier })); + }, [dispatch, entityIdentifier]); + + return ( + } color="error.300"> + {t('common.delete')} + + ); +}); + +CanvasEntityMenuItemsDelete.displayName = 'CanvasEntityMenuItemsDelete'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsFilter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsFilter.tsx new file mode 100644 index 00000000000..5e15543e5cd --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsFilter.tsx @@ -0,0 +1,22 @@ +import { MenuItem } from '@invoke-ai/ui-library'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { $filteringEntity } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiShootingStarBold } from 'react-icons/pi'; + +export const CanvasEntityMenuItemsFilter = memo(() => { + const { t } = useTranslation(); + const entityIdentifier = useEntityIdentifierContext(); + const filter = useCallback(() => { + $filteringEntity.set(entityIdentifier); + }, [entityIdentifier]); + + return ( + }> + {t('controlLayers.filter')} + + ); +}); + +CanvasEntityMenuItemsFilter.displayName = 'CanvasEntityMenuItemsFilter'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsReset.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsReset.tsx new file mode 100644 index 00000000000..3a2387bc86e --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsReset.tsx @@ -0,0 +1,25 @@ +import { MenuItem } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { entityReset } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiArrowCounterClockwiseBold } from 'react-icons/pi'; + +export const CanvasEntityMenuItemsReset = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const entityIdentifier = useEntityIdentifierContext(); + + const resetEntity = useCallback(() => { + dispatch(entityReset({ entityIdentifier })); + }, [dispatch, entityIdentifier]); + + return ( + }> + {t('accessibility.reset')} + + ); +}); + +CanvasEntityMenuItemsReset.displayName = 'CanvasEntityMenuItemsReset'; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts deleted file mode 100644 index c7fd177d755..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { deepClone } from 'common/util/deepClone'; -import { ipaAdded, rgIPAdapterAdded } from 'features/controlLayers/store/canvasV2Slice'; -import { - IMAGE_FILTERS, - initialControlNetV2, - initialIPAdapterV2, - initialT2IAdapterV2, - isFilterType, -} from 'features/controlLayers/store/types'; -import { zModelIdentifierField } from 'features/nodes/types/common'; -import { useCallback, useMemo } from 'react'; -import { useControlNetAndT2IAdapterModels, useIPAdapterModels } from 'services/api/hooks/modelsByType'; -import type { ControlNetModelConfig, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types'; -import { v4 as uuidv4 } from 'uuid'; - -export const useAddCALayer = () => { - const dispatch = useAppDispatch(); - const baseModel = useAppSelector((s) => s.canvasV2.params.model?.base); - const [modelConfigs] = useControlNetAndT2IAdapterModels(); - const model: ControlNetModelConfig | T2IAdapterModelConfig | null = useMemo(() => { - // prefer to use a model that matches the base model - const compatibleModels = modelConfigs.filter((m) => (baseModel ? m.base === baseModel : true)); - return compatibleModels[0] ?? modelConfigs[0] ?? null; - }, [baseModel, modelConfigs]); - const isDisabled = useMemo(() => !model, [model]); - const addCALayer = useCallback(() => { - if (!model) { - return; - } - - const defaultPreprocessor = model.default_settings?.preprocessor; - const processorConfig = isFilterType(defaultPreprocessor) - ? IMAGE_FILTERS[defaultPreprocessor].buildDefaults(baseModel) - : null; - - const initialConfig = deepClone(model.type === 'controlnet' ? initialControlNetV2 : initialT2IAdapterV2); - const config = { ...initialConfig, model: zModelIdentifierField.parse(model), processorConfig }; - - // dispatch(caAdded({ config })); - }, [dispatch, model, baseModel]); - - return [addCALayer, isDisabled] as const; -}; - -export const useAddIPALayer = () => { - const dispatch = useAppDispatch(); - const baseModel = useAppSelector((s) => s.canvasV2.params.model?.base); - const [modelConfigs] = useIPAdapterModels(); - const model: IPAdapterModelConfig | null = useMemo(() => { - // prefer to use a model that matches the base model - const compatibleModels = modelConfigs.filter((m) => (baseModel ? m.base === baseModel : true)); - return compatibleModels[0] ?? modelConfigs[0] ?? null; - }, [baseModel, modelConfigs]); - const isDisabled = useMemo(() => !model, [model]); - const addIPALayer = useCallback(() => { - if (!model) { - return; - } - - const initialConfig = deepClone(initialIPAdapterV2); - const config = { ...initialConfig, model: zModelIdentifierField.parse(model) }; - dispatch(ipaAdded({ config })); - }, [dispatch, model]); - - return [addIPALayer, isDisabled] as const; -}; - -export const useAddIPAdapterToRGLayer = (id: string) => { - const dispatch = useAppDispatch(); - const baseModel = useAppSelector((s) => s.canvasV2.params.model?.base); - const [modelConfigs] = useIPAdapterModels(); - const model: IPAdapterModelConfig | null = useMemo(() => { - // prefer to use a model that matches the base model - const compatibleModels = modelConfigs.filter((m) => (baseModel ? m.base === baseModel : true)); - return compatibleModels[0] ?? modelConfigs[0] ?? null; - }, [baseModel, modelConfigs]); - const isDisabled = useMemo(() => !model, [model]); - const addIPAdapter = useCallback(() => { - if (!model) { - return; - } - const initialConfig = deepClone(initialIPAdapterV2); - const config = { ...initialConfig, model: zModelIdentifierField.parse(model) }; - dispatch(rgIPAdapterAdded({ id, ipAdapter: { ...config, id: uuidv4(), type: 'ip_adapter', isEnabled: true } })); - }, [model, dispatch, id]); - - return [addIPAdapter, isDisabled] as const; -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts index 9c91bca8472..cc174e87713 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts @@ -4,10 +4,10 @@ import { deepClone } from 'common/util/deepClone'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { selectControlLayerOrThrow } from 'features/controlLayers/store/controlLayersReducers'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; -import { initialControlNetV2, initialT2IAdapterV2 } from 'features/controlLayers/store/types'; +import { initialControlNetV2, initialIPAdapterV2, initialT2IAdapterV2 } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { useMemo } from 'react'; -import { useControlNetAndT2IAdapterModels } from 'services/api/hooks/modelsByType'; +import { useControlNetAndT2IAdapterModels, useIPAdapterModels } from 'services/api/hooks/modelsByType'; export const useControlLayerControlAdapter = (entityIdentifier: CanvasEntityIdentifier) => { const selectControlAdapter = useMemo( @@ -42,3 +42,23 @@ export const useDefaultControlAdapter = () => { return defaultControlAdapter; }; + +export const useDefaultIPAdapter = () => { + const [modelConfigs] = useIPAdapterModels(); + + const baseModel = useAppSelector((s) => s.canvasV2.params.model?.base); + + const defaultControlAdapter = useMemo(() => { + const compatibleModels = modelConfigs.filter((m) => (baseModel ? m.base === baseModel : true)); + const model = compatibleModels[0] ?? modelConfigs[0] ?? null; + const ipAdapter = deepClone(initialIPAdapterV2); + + if (model) { + ipAdapter.model = zModelIdentifierField.parse(model); + } + + return ipAdapter; + }, [baseModel, modelConfigs]); + + return defaultControlAdapter; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index cc70237b851..c3818f3703a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -44,7 +44,7 @@ import { IMAGE_FILTERS, isDrawableEntity, RGBA_RED } from './types'; const initialState: CanvasV2State = { _version: 3, - selectedEntityIdentifier: null, + selectedEntityIdentifier: { id: 'inpaint_mask', type: 'inpaint_mask' }, rasterLayers: { entities: [], compositeRasterizationCache: [] }, controlLayers: { entities: [] }, ipAdapters: { entities: [] }, @@ -385,10 +385,13 @@ export const canvasV2Slice = createSlice({ } }, allEntitiesDeleted: (state) => { - state.regions.entities = []; - state.rasterLayers.entities = []; + state.ipAdapters = deepClone(initialState.ipAdapters); + state.rasterLayers = deepClone(initialState.rasterLayers); state.rasterLayers.compositeRasterizationCache = []; - state.ipAdapters.entities = []; + state.controlLayers = deepClone(initialState.controlLayers); + state.regions = deepClone(initialState.regions); + state.inpaintMask = deepClone(initialState.inpaintMask); + state.selectedEntityIdentifier = deepClone(initialState.selectedEntityIdentifier); }, filterSelected: (state, action: PayloadAction<{ type: FilterConfig['type'] }>) => { state.filter.config = IMAGE_FILTERS[action.payload.type].buildDefaults(); @@ -423,6 +426,7 @@ export const canvasV2Slice = createSlice({ state.ipAdapters = deepClone(initialState.ipAdapters); state.rasterLayers = deepClone(initialState.rasterLayers); + state.rasterLayers.compositeRasterizationCache = []; state.controlLayers = deepClone(initialState.controlLayers); state.regions = deepClone(initialState.regions); state.selectedEntityIdentifier = deepClone(initialState.selectedEntityIdentifier); From af7d14cd59ead72a9dba34617b75d87582264063 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:58:44 +1000 Subject: [PATCH 356/678] feat(ui): rename layers --- .../components/ControlLayer/ControlLayer.tsx | 8 ++- .../components/RasterLayer/RasterLayer.tsx | 8 ++- .../RegionalGuidance/RegionalGuidance.tsx | 9 ++- .../common/CanvasEntityTitleEdit.tsx | 56 +++++++++++++++++++ .../controlLayers/hooks/useEntityTitle.ts | 24 +++++++- .../controlLayers/store/canvasV2Slice.ts | 13 +++++ .../store/controlLayersReducers.ts | 1 + .../store/rasterLayersReducers.ts | 1 + .../controlLayers/store/regionsReducers.ts | 1 + .../src/features/controlLayers/store/types.ts | 2 + 10 files changed, 112 insertions(+), 11 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitleEdit.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx index af47649a20b..0781f72d082 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx @@ -1,10 +1,11 @@ -import { Spacer } from '@invoke-ai/ui-library'; +import { Spacer, useDisclosure } from '@invoke-ai/ui-library'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; +import { CanvasEntityTitleEdit } from 'features/controlLayers/components/common/CanvasEntityTitleEdit'; import { ControlLayerControlAdapter } from 'features/controlLayers/components/ControlLayer/ControlLayerControlAdapter'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; @@ -16,13 +17,14 @@ type Props = { export const ControlLayer = memo(({ id }: Props) => { const entityIdentifier = useMemo(() => ({ id, type: 'control_layer' }), [id]); + const editing = useDisclosure({ defaultIsOpen: false }); return ( - + - + {editing.isOpen ? : } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx index 303d4191ed9..11008749dbf 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx @@ -1,9 +1,10 @@ -import { Spacer } from '@invoke-ai/ui-library'; +import { Spacer, useDisclosure } from '@invoke-ai/ui-library'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; +import { CanvasEntityTitleEdit } from 'features/controlLayers/components/common/CanvasEntityTitleEdit'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; @@ -14,13 +15,14 @@ type Props = { export const RasterLayer = memo(({ id }: Props) => { const entityIdentifier = useMemo(() => ({ id, type: 'raster_layer' }), [id]); + const editing = useDisclosure({ defaultIsOpen: false }); return ( - + - + {editing.isOpen ? : } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx index fd368e53e1e..3da03db0493 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx @@ -1,9 +1,10 @@ -import { Spacer } from '@invoke-ai/ui-library'; +import { Spacer, useDisclosure } from '@invoke-ai/ui-library'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; +import { CanvasEntityTitleEdit } from 'features/controlLayers/components/common/CanvasEntityTitleEdit'; import { RegionalGuidanceBadges } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges'; import { RegionalGuidanceSettings } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; @@ -19,12 +20,14 @@ type Props = { export const RegionalGuidance = memo(({ id }: Props) => { const entityIdentifier = useMemo(() => ({ id, type: 'regional_guidance' }), [id]); + const editing = useDisclosure({ defaultIsOpen: false }); + return ( - + - + {editing.isOpen ? : } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitleEdit.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitleEdit.tsx new file mode 100644 index 00000000000..0cc621ef63f --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitleEdit.tsx @@ -0,0 +1,56 @@ +import { Input } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useEntityTitle } from 'features/controlLayers/hooks/useEntityTitle'; +import { entityNameChanged } from 'features/controlLayers/store/canvasV2Slice'; +import type { ChangeEvent, KeyboardEvent } from 'react'; +import { memo, useCallback, useEffect, useRef, useState } from 'react'; + +type Props = { + onStopEditing: () => void; +}; + +export const CanvasEntityTitleEdit = memo(({ onStopEditing }: Props) => { + const dispatch = useAppDispatch(); + const ref = useRef(null); + const entityIdentifier = useEntityIdentifierContext(); + const title = useEntityTitle(entityIdentifier); + const [localTitle, setLocalTitle] = useState(title); + + const onChange = useCallback((e: ChangeEvent) => { + setLocalTitle(e.target.value); + }, []); + + const onBlur = useCallback(() => { + const trimmedTitle = localTitle.trim(); + if (trimmedTitle.length === 0) { + dispatch(entityNameChanged({ entityIdentifier, name: null })); + } else if (trimmedTitle !== title) { + dispatch(entityNameChanged({ entityIdentifier, name: trimmedTitle })); + } + onStopEditing(); + }, [dispatch, entityIdentifier, localTitle, onStopEditing, title]); + + const onKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Enter') { + onBlur(); + } else if (e.key === 'Escape') { + setLocalTitle(title); + onStopEditing(); + } + }, + [onBlur, onStopEditing, title] + ); + + useEffect(() => { + ref.current?.focus(); + ref.current?.select(); + }, []); + + return ( + + ); +}); + +CanvasEntityTitleEdit.displayName = 'CanvasEntityTitleEdit'; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts index a56c82d14b5..e5723d88781 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts @@ -1,15 +1,35 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { useAppSelector } from 'app/store/storeHooks'; import { useEntityObjectCount } from 'features/controlLayers/hooks/useEntityObjectCount'; +import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/canvasV2Slice'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { assert } from 'tsafe'; +const createSelectName = (entityIdentifier: CanvasEntityIdentifier) => + createSelector(selectCanvasV2Slice, (canvasV2) => { + const entity = selectEntity(canvasV2, entityIdentifier); + if (!entity) { + return null; + } + if (entity.type === 'inpaint_mask') { + return null; + } + return entity.name; + }); + export const useEntityTitle = (entityIdentifier: CanvasEntityIdentifier) => { const { t } = useTranslation(); - + const selectName = useMemo(() => createSelectName(entityIdentifier), [entityIdentifier]); + const name = useAppSelector(selectName); const objectCount = useEntityObjectCount(entityIdentifier); const title = useMemo(() => { + if (name) { + return name; + } + const parts: string[] = []; if (entityIdentifier.type === 'inpaint_mask') { parts.push(t('controlLayers.inpaintMask')); @@ -30,7 +50,7 @@ export const useEntityTitle = (entityIdentifier: CanvasEntityIdentifier) => { } return parts.join(' '); - }, [entityIdentifier.type, objectCount, t]); + }, [entityIdentifier.type, name, objectCount, t]); return title; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index c3818f3703a..450b850c4e5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -198,6 +198,18 @@ export const canvasV2Slice = createSlice({ const { entityIdentifier } = action.payload; state.selectedEntityIdentifier = entityIdentifier; }, + entityNameChanged: (state, action: PayloadAction) => { + const { entityIdentifier, name } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } + if (entity.type === 'inpaint_mask') { + // Inpaint mask cannot be renamed + return; + } + entity.name = name; + }, entityReset: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; const entity = selectEntity(state, entityIdentifier); @@ -451,6 +463,7 @@ export const { rasterizationCachesInvalidated, // All entities entitySelected, + entityNameChanged, entityReset, entityIsEnabledToggled, entityMoved, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts index 5e4ef6f251f..6133f5f7908 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts @@ -33,6 +33,7 @@ export const controlLayersReducers = { const { id, overrides, isSelected } = action.payload; const layer: CanvasControlLayerState = { id, + name: null, type: 'control_layer', isEnabled: true, objects: [], diff --git a/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts index 728cfe24fb3..df6326dc9f5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts @@ -30,6 +30,7 @@ export const rasterLayersReducers = { const { id, overrides, isSelected } = action.payload; const layer: CanvasRasterLayerState = { id, + name: null, type: 'raster_layer', isEnabled: true, objects: [], diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index fa7a84735ef..cbe65507ba2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -45,6 +45,7 @@ export const regionsReducers = { const { id } = action.payload; const rg: CanvasRegionalGuidanceState = { id, + name: null, type: 'regional_guidance', isEnabled: true, objects: [], diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 9d7bd6fa71b..cc0fb37f350 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -647,6 +647,7 @@ export type ImageCache = z.infer; export const zCanvasRegionalGuidanceState = z.object({ id: zId, + name: z.string().nullable(), type: z.literal('regional_guidance'), isEnabled: z.boolean(), position: zCoordinate, @@ -730,6 +731,7 @@ export type T2IAdapterConfig = z.infer; export const zCanvasRasterLayerState = z.object({ id: zId, + name: z.string().nullable(), type: z.literal('raster_layer'), isEnabled: z.boolean(), position: zCoordinate, From daede9c9cf3f79567762de29d586660c3b2cfeed Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 15 Aug 2024 19:39:47 +1000 Subject: [PATCH 357/678] fix(ui): ip adapters work --- .../listeners/modelsLoaded.ts | 26 ++-- .../src/common/hooks/useIsReadyToEnqueue.ts | 33 +++-- .../components/AddLayerButton.tsx | 2 +- .../components/ControlLayer/ControlLayer.tsx | 9 +- .../components/IPAdapter/IPAdapter.tsx | 7 +- .../IPAdapter/IPAdapterSettings.tsx | 8 +- .../components/RasterLayer/RasterLayer.tsx | 9 +- .../RegionalGuidance/RegionalGuidance.tsx | 9 +- .../RegionalGuidanceBadges.tsx | 4 +- .../RegionalGuidanceIPAdapterSettings.tsx | 6 +- .../RegionalGuidanceIPAdapters.tsx | 37 +++-- .../RegionalGuidanceMaskFillColorPicker.tsx | 4 +- ...uidanceMenuItemsAddPromptsAndIPAdapter.tsx | 4 +- .../RegionalGuidanceNegativePrompt.tsx | 4 +- .../RegionalGuidancePositivePrompt.tsx | 4 +- .../RegionalGuidanceSettings.tsx | 29 +++- .../RegionalGuidanceSettingsPopover.tsx | 4 +- .../common/CanvasEntityTitleEdit.tsx | 10 +- .../controlLayers/hooks/useEntityTitle.ts | 2 +- .../hooks/useLayerControlAdapter.ts | 11 +- .../controlLayers/konva/CanvasFilter.ts | 5 +- .../controlLayers/konva/CanvasManager.ts | 4 +- .../konva/CanvasObjectRenderer.ts | 4 +- .../controlLayers/konva/CanvasTransformer.ts | 13 +- .../controlLayers/store/canvasV2Slice.ts | 18 ++- .../controlLayers/store/ipAdaptersReducers.ts | 78 +++++------ .../controlLayers/store/regionsReducers.ts | 126 ++++++++---------- .../src/features/controlLayers/store/types.ts | 38 ++++-- .../util/graph/generation/addIPAdapters.ts | 25 ++-- .../nodes/util/graph/generation/addLayers.ts | 9 +- .../nodes/util/graph/generation/addRegions.ts | 16 ++- .../util/graph/generation/buildSD1Graph.ts | 2 +- .../util/graph/generation/buildSDXLGraph.ts | 2 +- 33 files changed, 307 insertions(+), 255 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts index 86daeab55a3..cf3088789cb 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts @@ -5,6 +5,7 @@ import type { JSONObject } from 'common/types'; import { bboxHeightChanged, bboxWidthChanged, + controlLayerModelChanged, ipaModelChanged, loraDeleted, modelChanged, @@ -20,6 +21,7 @@ import type { Logger } from 'roarr'; import { modelConfigsAdapterSelectors, modelsApi } from 'services/api/endpoints/models'; import type { AnyModelConfig } from 'services/api/types'; import { + isControlNetOrT2IAdapterModelConfig, isIPAdapterModelConfig, isLoRAModelConfig, isNonRefinerMainModelConfig, @@ -31,7 +33,7 @@ import { export const addModelsLoadedListener = (startAppListening: AppStartListening) => { startAppListening({ predicate: modelsApi.endpoints.getModelConfigs.matchFulfilled, - effect: async (action, { getState, dispatch }) => { + effect: (action, { getState, dispatch }) => { // models loaded, we need to ensure the selected model is available and if not, select the first one const log = logger('models'); log.info({ models: action.payload.entities }, `Models loaded (${action.payload.ids.length})`); @@ -169,24 +171,24 @@ const handleLoRAModels: ModelHandler = (models, state, dispatch, _log) => { }; const handleControlAdapterModels: ModelHandler = (models, state, dispatch, _log) => { - // const caModels = models.filter(isControlNetOrT2IAdapterModelConfig); - // state.canvasV2.controlAdapters.entities.forEach((ca) => { - // const isModelAvailable = caModels.some((m) => m.key === ca.model?.key); - // if (isModelAvailable) { - // return; - // } - // dispatch(caModelChanged({ id: ca.id, modelConfig: null })); - // }); + const caModels = models.filter(isControlNetOrT2IAdapterModelConfig); + state.canvasV2.controlLayers.entities.forEach((entity) => { + const isModelAvailable = caModels.some((m) => m.key === entity.controlAdapter.model?.key); + if (isModelAvailable) { + return; + } + dispatch(controlLayerModelChanged({ id: entity.id, modelConfig: null })); + }); }; const handleIPAdapterModels: ModelHandler = (models, state, dispatch, _log) => { const ipaModels = models.filter(isIPAdapterModelConfig); - state.canvasV2.ipAdapters.entities.forEach(({ id, model }) => { - const isModelAvailable = ipaModels.some((m) => m.key === model?.key); + state.canvasV2.ipAdapters.entities.forEach((entity) => { + const isModelAvailable = ipaModels.some((m) => m.key === entity.ipAdapter.model?.key); if (isModelAvailable) { return; } - dispatch(ipaModelChanged({ id, modelConfig: null })); + dispatch(ipaModelChanged({ id: entity.id, modelConfig: null })); }); state.canvasV2.regions.entities.forEach(({ id, ipAdapters }) => { diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index 5b18c45d83e..eb2ff5bc279 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -154,24 +154,24 @@ const createSelector = (templates: Templates) => }); canvasV2.ipAdapters.entities - .filter((ipa) => ipa.isEnabled) - .forEach((ipa, i) => { + .filter((entity) => entity.isEnabled) + .forEach((entity, i) => { const layerLiteral = i18n.t('controlLayers.layers_one'); const layerNumber = i + 1; - const layerType = i18n.t(LAYER_TYPE_TO_TKEY[ipa.type]); + const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]); const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; const problems: string[] = []; // Must have model - if (!ipa.model) { + if (!entity.ipAdapter.model) { problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected')); } // Model base must match - if (ipa.model?.base !== model?.base) { + if (entity.ipAdapter.model?.base !== model?.base) { problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel')); } // Must have an image - if (!ipa.imageObject) { + if (!entity.ipAdapter.image) { problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected')); } @@ -182,22 +182,22 @@ const createSelector = (templates: Templates) => }); canvasV2.regions.entities - .filter((rg) => rg.isEnabled) - .forEach((rg, i) => { + .filter((entity) => entity.isEnabled) + .forEach((entity, i) => { const layerLiteral = i18n.t('controlLayers.layers_one'); const layerNumber = i + 1; - const layerType = i18n.t(LAYER_TYPE_TO_TKEY[rg.type]); + const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]); const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; const problems: string[] = []; // Must have a region - if (rg.objects.length === 0) { + if (entity.objects.length === 0) { problems.push(i18n.t('parameters.invoke.layer.rgNoRegion')); } // Must have at least 1 prompt or IP Adapter - if (rg.positivePrompt === null && rg.negativePrompt === null && rg.ipAdapters.length === 0) { + if (entity.positivePrompt === null && entity.negativePrompt === null && entity.ipAdapters.length === 0) { problems.push(i18n.t('parameters.invoke.layer.rgNoPromptsOrIPAdapters')); } - rg.ipAdapters.forEach((ipAdapter) => { + entity.ipAdapters.forEach((ipAdapter) => { // Must have model if (!ipAdapter.model) { problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected')); @@ -207,7 +207,7 @@ const createSelector = (templates: Templates) => problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel')); } // Must have an image - if (!ipAdapter.imageObject) { + if (!ipAdapter.image) { problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected')); } }); @@ -219,12 +219,11 @@ const createSelector = (templates: Templates) => }); canvasV2.rasterLayers.entities - .filter((l) => l.isEnabled) - .filter((l) => l.type === 'raster_layer') - .forEach((l, i) => { + .filter((entity) => entity.isEnabled) + .forEach((entity, i) => { const layerLiteral = i18n.t('controlLayers.layers_one'); const layerNumber = i + 1; - const layerType = i18n.t(LAYER_TYPE_TO_TKEY[l.type]); + const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]); const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; const problems: string[] = []; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx index 433faf4c69d..b33f5304fa3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx @@ -21,7 +21,7 @@ export const AddLayerButton = memo(() => { dispatch(controlLayerAdded({ isSelected: true, overrides: { controlAdapter: defaultControlAdapter } })); }, [defaultControlAdapter, dispatch]); const addIPAdapter = useCallback(() => { - dispatch(ipaAdded({ config: defaultIPAdapter })); + dispatch(ipaAdded({ ipAdapter: defaultIPAdapter })); }, [defaultIPAdapter, dispatch]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx index 0781f72d082..60b5832063d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx @@ -1,4 +1,5 @@ -import { Spacer, useDisclosure } from '@invoke-ai/ui-library'; +import { Spacer } from '@invoke-ai/ui-library'; +import { useBoolean } from 'common/hooks/useBoolean'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; @@ -17,14 +18,14 @@ type Props = { export const ControlLayer = memo(({ id }: Props) => { const entityIdentifier = useMemo(() => ({ id, type: 'control_layer' }), [id]); - const editing = useDisclosure({ defaultIsOpen: false }); + const editing = useBoolean(false); return ( - + - {editing.isOpen ? : } + {editing.isTrue ? : } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapter.tsx index f71d31dbf0f..f775bf99535 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapter.tsx @@ -1,9 +1,11 @@ import { Spacer } from '@invoke-ai/ui-library'; +import { useBoolean } from 'common/hooks/useBoolean'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; +import { CanvasEntityTitleEdit } from 'features/controlLayers/components/common/CanvasEntityTitleEdit'; import { IPAdapterSettings } from 'features/controlLayers/components/IPAdapter/IPAdapterSettings'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; @@ -15,13 +17,14 @@ type Props = { export const IPAdapter = memo(({ id }: Props) => { const entityIdentifier = useMemo(() => ({ id, type: 'ip_adapter' }), [id]); + const editing = useBoolean(false); return ( - + - + {editing.isTrue ? : } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx index 30a7799cd1d..d0d91646e96 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx @@ -13,7 +13,7 @@ import { ipaModelChanged, ipaWeightChanged, } from 'features/controlLayers/store/canvasV2Slice'; -import { selectIPAOrThrow } from 'features/controlLayers/store/ipAdaptersReducers'; +import { selectIPAdapterEntityOrThrow } from 'features/controlLayers/store/ipAdaptersReducers'; import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types'; import type { IPAImageDropData } from 'features/dnd/types'; import { memo, useCallback, useMemo } from 'react'; @@ -25,7 +25,7 @@ import { IPAdapterModel } from './IPAdapterModel'; export const IPAdapterSettings = memo(() => { const dispatch = useAppDispatch(); const { id } = useEntityIdentifierContext(); - const ipAdapter = useAppSelector((s) => selectIPAOrThrow(s.canvasV2, id)); + const ipAdapter = useAppSelector((s) => selectIPAdapterEntityOrThrow(s.canvasV2, id).ipAdapter); const onChangeBeginEndStepPct = useCallback( (beginEndStepPct: [number, number]) => { @@ -93,9 +93,9 @@ export const IPAdapterSettings = memo(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx index 11008749dbf..50be5258e27 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx @@ -1,4 +1,5 @@ -import { Spacer, useDisclosure } from '@invoke-ai/ui-library'; +import { Spacer } from '@invoke-ai/ui-library'; +import { useBoolean } from 'common/hooks/useBoolean'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; @@ -15,14 +16,14 @@ type Props = { export const RasterLayer = memo(({ id }: Props) => { const entityIdentifier = useMemo(() => ({ id, type: 'raster_layer' }), [id]); - const editing = useDisclosure({ defaultIsOpen: false }); + const editing = useBoolean(false); return ( - + - {editing.isOpen ? : } + {editing.isTrue ? : } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx index 3da03db0493..40a9ac2c8d4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx @@ -1,4 +1,5 @@ -import { Spacer, useDisclosure } from '@invoke-ai/ui-library'; +import { Spacer } from '@invoke-ai/ui-library'; +import { useBoolean } from 'common/hooks/useBoolean'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; @@ -20,14 +21,14 @@ type Props = { export const RegionalGuidance = memo(({ id }: Props) => { const entityIdentifier = useMemo(() => ({ id, type: 'regional_guidance' }), [id]); - const editing = useDisclosure({ defaultIsOpen: false }); + const editing = useBoolean(false); return ( - + - {editing.isOpen ? : } + {editing.isTrue ? : } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges.tsx index 541a2a25e33..e8edc655313 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges.tsx @@ -1,14 +1,14 @@ import { Badge } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; +import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; export const RegionalGuidanceBadges = memo(() => { const { id } = useEntityIdentifierContext(); const { t } = useTranslation(); - const autoNegative = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).autoNegative); + const autoNegative = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).autoNegative); return ( <> diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx index ed8f4b58047..47497848bf6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx @@ -14,7 +14,7 @@ import { rgIPAdapterModelChanged, rgIPAdapterWeightChanged, } from 'features/controlLayers/store/canvasV2Slice'; -import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; +import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers'; import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types'; import type { RGIPAdapterImageDropData } from 'features/dnd/types'; import { memo, useCallback, useMemo } from 'react'; @@ -34,7 +34,7 @@ export const RegionalGuidanceIPAdapterSettings = memo(({ id, ipAdapterId, ipAdap dispatch(rgIPAdapterDeleted({ id, ipAdapterId })); }, [dispatch, ipAdapterId, id]); const ipAdapter = useAppSelector((s) => { - const ipa = selectRGOrThrow(s.canvasV2, id).ipAdapters.find((ipa) => ipa.id === ipAdapterId); + const ipa = selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).ipAdapters.find((ipa) => ipa.id === ipAdapterId); assert(ipa, `Regional GuidanceIP Adapter with id ${ipAdapterId} not found`); return ipa; }); @@ -123,7 +123,7 @@ export const RegionalGuidanceIPAdapterSettings = memo(({ id, ipAdapterId, ipAdap { - const ipAdapterIds = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).ipAdapters.map(({ id }) => id)); + const selectIPAdapterIds = useMemo( + () => + createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { + const ipAdapterIds = selectRegionalGuidanceEntityOrThrow(canvasV2, id).ipAdapters.map(({ id }) => id); + if (ipAdapterIds.length === 0) { + return EMPTY_ARRAY; + } + return ipAdapterIds; + }), + [id] + ); + + const ipAdapterIds = useAppSelector(selectIPAdapterIds); if (ipAdapterIds.length === 0) { return null; @@ -17,15 +32,11 @@ export const RegionalGuidanceIPAdapters = memo(({ id }: Props) => { return ( <> - {ipAdapterIds.map((id, index) => ( - - {index > 0 && ( - - - - )} - - + {ipAdapterIds.map((ipAdapterId, index) => ( + + {index > 0 && } + + ))} ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx index da26f8940c6..66e19c9b35d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx @@ -5,7 +5,7 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { stopPropagation } from 'common/util/stopPropagation'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { rgFillChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; +import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers'; import { memo, useCallback } from 'react'; import type { RgbColor } from 'react-colorful'; import { useTranslation } from 'react-i18next'; @@ -14,7 +14,7 @@ export const RegionalGuidanceMaskFillColorPicker = memo(() => { const entityIdentifier = useEntityIdentifierContext(); const { t } = useTranslation(); const dispatch = useAppDispatch(); - const fill = useAppSelector((s) => selectRGOrThrow(s.canvasV2, entityIdentifier.id).fill); + const fill = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, entityIdentifier.id).fill); const onChange = useCallback( (fill: RgbColor) => { dispatch(rgFillChanged({ id: entityIdentifier.id, fill })); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx index 66bcee04e91..bda94d91dee 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx @@ -37,9 +37,7 @@ export const RegionalGuidanceMenuItemsAddPromptsAndIPAdapter = memo(() => { dispatch(rgNegativePromptChanged({ id: id, prompt: '' })); }, [dispatch, id]); const addIPAdapter = useCallback(() => { - dispatch( - rgIPAdapterAdded({ id, ipAdapter: { ...defaultIPAdapter, id: nanoid(), type: 'ip_adapter', isEnabled: true } }) - ); + dispatch(rgIPAdapterAdded({ id, ipAdapter: { ...defaultIPAdapter, id: nanoid() } })); }, [defaultIPAdapter, dispatch, id]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceNegativePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceNegativePrompt.tsx index b35f2ece443..f24dedce035 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceNegativePrompt.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceNegativePrompt.tsx @@ -2,7 +2,7 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { RegionalGuidanceDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceDeletePromptButton'; import { rgNegativePromptChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; +import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; @@ -15,7 +15,7 @@ type Props = { }; export const RegionalGuidanceNegativePrompt = memo(({ id }: Props) => { - const prompt = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).negativePrompt ?? ''); + const prompt = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).negativePrompt ?? ''); const dispatch = useAppDispatch(); const textareaRef = useRef(null); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidancePositivePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidancePositivePrompt.tsx index cd04eee31db..44a371873e4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidancePositivePrompt.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidancePositivePrompt.tsx @@ -2,7 +2,7 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { RegionalGuidanceDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceDeletePromptButton'; import { rgPositivePromptChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; +import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; @@ -15,7 +15,7 @@ type Props = { }; export const RegionalGuidancePositivePrompt = memo(({ id }: Props) => { - const prompt = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).positivePrompt ?? ''); + const prompt = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).positivePrompt ?? ''); const dispatch = useAppDispatch(); const textareaRef = useRef(null); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx index 49c10f120c6..fb359e06c5b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx @@ -1,8 +1,9 @@ +import { Divider } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { AddPromptButtons } from 'features/controlLayers/components/AddPromptButtons'; import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; +import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers'; import { memo } from 'react'; import { RegionalGuidanceIPAdapters } from './RegionalGuidanceIPAdapters'; @@ -11,15 +12,31 @@ import { RegionalGuidancePositivePrompt } from './RegionalGuidancePositivePrompt export const RegionalGuidanceSettings = memo(() => { const { id } = useEntityIdentifierContext(); - const hasPositivePrompt = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).positivePrompt !== null); - const hasNegativePrompt = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).negativePrompt !== null); - const hasIPAdapters = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).ipAdapters.length > 0); + const hasPositivePrompt = useAppSelector( + (s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).positivePrompt !== null + ); + const hasNegativePrompt = useAppSelector( + (s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).negativePrompt !== null + ); + const hasIPAdapters = useAppSelector( + (s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).ipAdapters.length > 0 + ); return ( {!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && } - {hasPositivePrompt && } - {hasNegativePrompt && } + {hasPositivePrompt && ( + <> + + {(hasNegativePrompt || hasIPAdapters) && } + + )} + {hasNegativePrompt && ( + <> + + {hasIPAdapters && } + + )} {hasIPAdapters && } ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettingsPopover.tsx index 0565026cfd1..c67fdbe1757 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettingsPopover.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettingsPopover.tsx @@ -14,7 +14,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { rgAutoNegativeChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; +import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -24,7 +24,7 @@ export const RegionalGuidanceSettingsPopover = memo(() => { const entityIdentifier = useEntityIdentifierContext(); const { t } = useTranslation(); const dispatch = useAppDispatch(); - const autoNegative = useAppSelector((s) => selectRGOrThrow(s.canvasV2, entityIdentifier.id).autoNegative); + const autoNegative = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, entityIdentifier.id).autoNegative); const onChange = useCallback( (e: ChangeEvent) => { dispatch(rgAutoNegativeChanged({ id: entityIdentifier.id, autoNegative: e.target.checked ? 'invert' : 'off' })); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitleEdit.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitleEdit.tsx index 0cc621ef63f..8021356875c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitleEdit.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitleEdit.tsx @@ -49,7 +49,15 @@ export const CanvasEntityTitleEdit = memo(({ onStopEditing }: Props) => { }, []); return ( - + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts index e5723d88781..38058f8215c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts @@ -38,7 +38,7 @@ export const useEntityTitle = (entityIdentifier: CanvasEntityIdentifier) => { } else if (entityIdentifier.type === 'raster_layer') { parts.push(t('controlLayers.rasterLayer')); } else if (entityIdentifier.type === 'ip_adapter') { - parts.push(t('controlLayers.ipAdapter')); + parts.push(t('common.ipAdapter')); } else if (entityIdentifier.type === 'regional_guidance') { parts.push(t('controlLayers.regionalGuidance')); } else { diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts index cc174e87713..5f7aca0237d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts @@ -3,7 +3,12 @@ import { useAppSelector } from 'app/store/storeHooks'; import { deepClone } from 'common/util/deepClone'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { selectControlLayerOrThrow } from 'features/controlLayers/store/controlLayersReducers'; -import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import type { + CanvasEntityIdentifier, + ControlNetConfig, + IPAdapterConfig, + T2IAdapterConfig, +} from 'features/controlLayers/store/types'; import { initialControlNetV2, initialIPAdapterV2, initialT2IAdapterV2 } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { useMemo } from 'react'; @@ -22,7 +27,7 @@ export const useControlLayerControlAdapter = (entityIdentifier: CanvasEntityIden return controlAdapter; }; -export const useDefaultControlAdapter = () => { +export const useDefaultControlAdapter = (): ControlNetConfig | T2IAdapterConfig => { const [modelConfigs] = useControlNetAndT2IAdapterModels(); const baseModel = useAppSelector((s) => s.canvasV2.params.model?.base); @@ -43,7 +48,7 @@ export const useDefaultControlAdapter = () => { return defaultControlAdapter; }; -export const useDefaultIPAdapter = () => { +export const useDefaultIPAdapter = (): IPAdapterConfig => { const [modelConfigs] = useIPAdapterModels(); const baseModel = useAppSelector((s) => s.canvasV2.params.model?.base); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilter.ts index f718b78a61d..e2821b74f7d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilter.ts @@ -38,8 +38,8 @@ export class CanvasFilter { const { config } = this.manager.stateApi.getFilterState(); this.log.trace({ config }, 'Previewing filter'); const dispatch = this.manager.stateApi._store.dispatch; - - const imageDTO = await this.parent.renderer.rasterize(); + const rect = this.parent.transformer.getRelativeRect() + const imageDTO = await this.parent.renderer.rasterize(rect, false); // TODO(psyche): I can't get TS to be happy, it thinkgs `config` is `never` but it should be inferred from the generic... I'll just cast it for now const filterNode = IMAGE_FILTERS[config.type].buildNode(imageDTO, config as never); const enqueueBatchArg: BatchConfig = { @@ -106,6 +106,7 @@ export class CanvasFilter { width: this.imageState.image.height, height: this.imageState.image.width, }, + replaceObjects: true, }); this.parent.renderer.showObjects(); this.manager.stateApi.$filteringEntity.set(null); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index eb8f62cee8c..3df53ee23ce 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -20,7 +20,6 @@ import type { ImageCache, Rect, } from 'features/controlLayers/store/types'; -import { isValidLayerWithoutControlAdapter } from 'features/nodes/util/graph/generation/addLayers'; import type Konva from 'konva'; import { clamp, isEqual } from 'lodash-es'; import { atom } from 'nanostores'; @@ -538,7 +537,8 @@ export class CanvasManager { stageClone.x(0); stageClone.y(0); - const validLayers = layersState.entities.filter(isValidLayerWithoutControlAdapter); + const validLayers = layersState.entities.filter((entity) => entity.isEnabled && entity.objects.length > 0); + // getLayers() returns the internal `children` array of the stage directly - calling destroy on a layer will // mutate that array. We need to clone the array to avoid mutating the original. for (const konvaLayer of stageClone.getLayers().slice()) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index 726ec21cf5f..e73a83aef73 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -374,8 +374,7 @@ export class CanvasObjectRenderer { * @param rect The rect to rasterize. If omitted, the entity's full rect will be used. * @returns A promise that resolves to the rasterized image DTO. */ - rasterize = async (rect?: Rect): Promise => { - rect = rect ?? this.parent.transformer.getRelativeRect(); + rasterize = async (rect: Rect, replaceObjects: boolean = false): Promise => { let imageDTO: ImageDTO | null = null; const rasterizedImageCache = this.getRasterizedImageCache(rect); @@ -400,6 +399,7 @@ export class CanvasObjectRenderer { entityIdentifier: this.parent.getEntityIdentifier(), imageObject, rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: imageDTO.width, height: imageDTO.height }, + replaceObjects, }); return imageDTO; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index 7b71014c35f..aec02d3e7f1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -58,7 +58,7 @@ export class CanvasTransformer { /** * Whether the transformer is currently calculating the rect of the parent. */ - isPendingRectCalculation: boolean = false; + isPendingRectCalculation: boolean = true; /** * A set of subscriptions that should be cleaned up when the transformer is destroyed. @@ -506,7 +506,8 @@ export class CanvasTransformer { */ applyTransform = async () => { this.log.debug('Applying transform'); - await this.parent.renderer.rasterize(); + const rect = this.getRelativeRect(); + await this.parent.renderer.rasterize(rect, true); this.requestRectCalculation(); this.stopTransform(); }; @@ -589,7 +590,7 @@ export class CanvasTransformer { }; updateBbox = () => { - this.log.trace('Updating bbox'); + this.log.trace({ nodeRect: this.nodeRect, pixelRect: this.pixelRect }, 'Updating bbox'); if (this.isPendingRectCalculation) { this.syncInteractionState(); @@ -600,10 +601,8 @@ export class CanvasTransformer { // eraser lines, fully clipped brush lines or if it has been fully erased. if (this.pixelRect.width === 0 || this.pixelRect.height === 0) { // We shouldn't reset on the first render - the bbox will be calculated on the next render - if (!this.parent.renderer.hasObjects()) { - // The layer is fully transparent but has objects - reset it - this.manager.stateApi.resetEntity({ entityIdentifier: this.parent.getEntityIdentifier() }); - } + // The layer is fully transparent but has objects - reset it + this.manager.stateApi.resetEntity({ entityIdentifier: this.parent.getEntityIdentifier() }); this.syncInteractionState(); return; } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 450b850c4e5..8624e467416 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -154,6 +154,8 @@ export function selectEntity(state: CanvasV2State, { id, type }: CanvasEntityIde return state.inpaintMask; case 'regional_guidance': return state.regions.entities.find((rg) => rg.id === id); + case 'ip_adapter': + return state.ipAdapters.entities.find((ipa) => ipa.id === id); default: return; } @@ -246,19 +248,22 @@ export const canvasV2Slice = createSlice({ } }, entityRasterized: (state, action: PayloadAction) => { - const { entityIdentifier, imageObject, rect } = action.payload; + const { entityIdentifier, imageObject, rect, replaceObjects } = action.payload; const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } if (isDrawableEntity(entity)) { - entity.objects = [imageObject]; - entity.position = { x: rect.x, y: rect.y }; // Remove the cache for the given rect. This should never happen, because we should never rasterize the same // rect twice. Just in case, we remove the old cache. entity.rasterizationCache = entity.rasterizationCache.filter((cache) => !isEqual(cache.rect, rect)); entity.rasterizationCache.push({ imageName: imageObject.image.image_name, rect }); + + if (replaceObjects) { + entity.objects = [imageObject]; + entity.position = { x: rect.x, y: rect.y }; + } } }, entityBrushLineAdded: (state, action: PayloadAction) => { @@ -328,6 +333,13 @@ export const canvasV2Slice = createSlice({ if (region) { selectedEntityIdentifier = { type: region.type, id: region.id }; } + } else if (entityIdentifier.type === 'ip_adapter') { + const index = state.ipAdapters.entities.findIndex((layer) => layer.id === entityIdentifier.id); + state.ipAdapters.entities = state.ipAdapters.entities.filter((rg) => rg.id !== entityIdentifier.id); + const entity = state.ipAdapters.entities[index]; + if (entity) { + selectedEntityIdentifier = { type: entity.type, id: entity.id }; + } } else { assert(false, 'Not implemented'); } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts index 561a769880c..cf0081a17a8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts @@ -4,30 +4,32 @@ import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; -import type { CanvasV2State, CLIPVisionModelV2, IPAdapterConfig, CanvasIPAdapterState, IPMethodV2 } from './types'; -import { imageDTOToImageObject } from './types'; +import type { CanvasIPAdapterState, CanvasV2State, CLIPVisionModelV2, IPAdapterConfig, IPMethodV2 } from './types'; +import { imageDTOToImageWithDims } from './types'; -export const selectIPA = (state: CanvasV2State, id: string) => state.ipAdapters.entities.find((ipa) => ipa.id === id); -export const selectIPAOrThrow = (state: CanvasV2State, id: string) => { - const ipa = selectIPA(state, id); - assert(ipa, `IP Adapter with id ${id} not found`); - return ipa; +export const selectIPAdapterEntity = (state: CanvasV2State, id: string) => + state.ipAdapters.entities.find((ipa) => ipa.id === id); +export const selectIPAdapterEntityOrThrow = (state: CanvasV2State, id: string) => { + const entity = selectIPAdapterEntity(state, id); + assert(entity, `IP Adapter with id ${id} not found`); + return entity; }; export const ipAdaptersReducers = { ipaAdded: { - reducer: (state, action: PayloadAction<{ id: string; config: IPAdapterConfig }>) => { - const { id, config } = action.payload; + reducer: (state, action: PayloadAction<{ id: string; ipAdapter: IPAdapterConfig }>) => { + const { id, ipAdapter } = action.payload; const layer: CanvasIPAdapterState = { id, type: 'ip_adapter', + name: null, isEnabled: true, - ...config, + ipAdapter, }; state.ipAdapters.entities.push(layer); state.selectedEntityIdentifier = { type: 'ip_adapter', id }; }, - prepare: (payload: { config: IPAdapterConfig }) => ({ payload: { id: uuidv4(), ...payload } }), + prepare: (payload: { ipAdapter: IPAdapterConfig }) => ({ payload: { id: uuidv4(), ...payload } }), }, ipaRecalled: (state, action: PayloadAction<{ data: CanvasIPAdapterState }>) => { const { data } = action.payload; @@ -36,7 +38,7 @@ export const ipAdaptersReducers = { }, ipaIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; - const ipa = selectIPA(state, id); + const ipa = selectIPAdapterEntity(state, id); if (ipa) { ipa.isEnabled = !ipa.isEnabled; } @@ -49,64 +51,54 @@ export const ipAdaptersReducers = { state.ipAdapters.entities = []; }, ipaImageChanged: { - reducer: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null; objectId: string }>) => { - const { id, imageDTO, objectId } = action.payload; - const ipa = selectIPA(state, id); - if (!ipa) { + reducer: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => { + const { id, imageDTO } = action.payload; + const entity = selectIPAdapterEntity(state, id); + if (!entity) { return; } - ipa.imageObject = imageDTO ? imageDTOToImageObject(imageDTO) : null; + entity.ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; }, prepare: (payload: { id: string; imageDTO: ImageDTO | null }) => ({ payload: { ...payload, objectId: uuidv4() } }), }, ipaMethodChanged: (state, action: PayloadAction<{ id: string; method: IPMethodV2 }>) => { const { id, method } = action.payload; - const ipa = selectIPA(state, id); - if (!ipa) { + const entity = selectIPAdapterEntity(state, id); + if (!entity) { return; } - ipa.method = method; + entity.ipAdapter.method = method; }, - ipaModelChanged: ( - state, - action: PayloadAction<{ - id: string; - modelConfig: IPAdapterModelConfig | null; - }> - ) => { + ipaModelChanged: (state, action: PayloadAction<{ id: string; modelConfig: IPAdapterModelConfig | null }>) => { const { id, modelConfig } = action.payload; - const ipa = selectIPA(state, id); - if (!ipa) { + const entity = selectIPAdapterEntity(state, id); + if (!entity) { return; } - if (modelConfig) { - ipa.model = zModelIdentifierField.parse(modelConfig); - } else { - ipa.model = null; - } + entity.ipAdapter.model = modelConfig ? zModelIdentifierField.parse(modelConfig) : null; }, ipaCLIPVisionModelChanged: (state, action: PayloadAction<{ id: string; clipVisionModel: CLIPVisionModelV2 }>) => { const { id, clipVisionModel } = action.payload; - const ipa = selectIPA(state, id); - if (!ipa) { + const entity = selectIPAdapterEntity(state, id); + if (!entity) { return; } - ipa.clipVisionModel = clipVisionModel; + entity.ipAdapter.clipVisionModel = clipVisionModel; }, ipaWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { const { id, weight } = action.payload; - const ipa = selectIPA(state, id); - if (!ipa) { + const entity = selectIPAdapterEntity(state, id); + if (!entity) { return; } - ipa.weight = weight; + entity.ipAdapter.weight = weight; }, ipaBeginEndStepPctChanged: (state, action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }>) => { const { id, beginEndStepPct } = action.payload; - const ipa = selectIPA(state, id); - if (!ipa) { + const entity = selectIPAdapterEntity(state, id); + if (!entity) { return; } - ipa.beginEndStepPct = beginEndStepPct; + entity.ipAdapter.beginEndStepPct = beginEndStepPct; }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index cbe65507ba2..4aca5553812 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -1,18 +1,32 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import type { CanvasV2State, CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types'; -import { imageDTOToImageObject } from 'features/controlLayers/store/types'; +import type { + CanvasV2State, + CLIPVisionModelV2, + IPMethodV2, + RegionalGuidanceIPAdapterConfig, +} from 'features/controlLayers/store/types'; +import { imageDTOToImageWithDims } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas'; import { isEqual } from 'lodash-es'; import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; -import type { CanvasIPAdapterState, CanvasRegionalGuidanceState, RgbColor } from './types'; +import type { CanvasRegionalGuidanceState, RgbColor } from './types'; -export const selectRG = (state: CanvasV2State, id: string) => state.regions.entities.find((rg) => rg.id === id); -export const selectRGOrThrow = (state: CanvasV2State, id: string) => { - const rg = selectRG(state, id); +export const selectRegionalGuidanceEntity = (state: CanvasV2State, id: string) => { + return state.regions.entities.find((rg) => rg.id === id); +}; +export const selectRegionalGuidanceIPAdapter = (state: CanvasV2State, id: string, ipAdapterId: string) => { + const entity = state.regions.entities.find((rg) => rg.id === id); + if (!entity) { + return; + } + return entity.ipAdapters.find((ipa) => ipa.id === ipAdapterId); +}; +export const selectRegionalGuidanceEntityOrThrow = (state: CanvasV2State, id: string) => { + const rg = selectRegionalGuidanceEntity(state, id); assert(rg, `Region with id ${id} not found`); return rg; }; @@ -72,105 +86,89 @@ export const regionsReducers = { }, rgPositivePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => { const { id, prompt } = action.payload; - const rg = selectRG(state, id); - if (!rg) { + const entity = selectRegionalGuidanceEntity(state, id); + if (!entity) { return; } - rg.positivePrompt = prompt; + entity.positivePrompt = prompt; }, rgNegativePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => { const { id, prompt } = action.payload; - const rg = selectRG(state, id); - if (!rg) { + const entity = selectRegionalGuidanceEntity(state, id); + if (!entity) { return; } - rg.negativePrompt = prompt; + entity.negativePrompt = prompt; }, rgFillChanged: (state, action: PayloadAction<{ id: string; fill: RgbColor }>) => { const { id, fill } = action.payload; - const rg = selectRG(state, id); - if (!rg) { + const entity = selectRegionalGuidanceEntity(state, id); + if (!entity) { return; } - rg.fill = fill; + entity.fill = fill; }, rgAutoNegativeChanged: (state, action: PayloadAction<{ id: string; autoNegative: ParameterAutoNegative }>) => { const { id, autoNegative } = action.payload; - const rg = selectRG(state, id); + const rg = selectRegionalGuidanceEntity(state, id); if (!rg) { return; } rg.autoNegative = autoNegative; }, - rgIPAdapterAdded: (state, action: PayloadAction<{ id: string; ipAdapter: CanvasIPAdapterState }>) => { + rgIPAdapterAdded: (state, action: PayloadAction<{ id: string; ipAdapter: RegionalGuidanceIPAdapterConfig }>) => { const { id, ipAdapter } = action.payload; - const rg = selectRG(state, id); - if (!rg) { + const entity = selectRegionalGuidanceEntity(state, id); + if (!entity) { return; } - rg.ipAdapters.push(ipAdapter); + entity.ipAdapters.push(ipAdapter); }, rgIPAdapterDeleted: (state, action: PayloadAction<{ id: string; ipAdapterId: string }>) => { const { id, ipAdapterId } = action.payload; - const rg = selectRG(state, id); - if (!rg) { + const entity = selectRegionalGuidanceEntity(state, id); + if (!entity) { return; } - rg.ipAdapters = rg.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId); + entity.ipAdapters = entity.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId); }, rgIPAdapterImageChanged: ( state, - action: PayloadAction<{ id: string; ipAdapterId: string; imageDTO: ImageDTO | null; objectId: string }> + action: PayloadAction<{ id: string; ipAdapterId: string; imageDTO: ImageDTO | null }> ) => { const { id, ipAdapterId, imageDTO } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); - if (!ipa) { + const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId); + if (!ipAdapter) { return; } - ipa.imageObject = imageDTO ? imageDTOToImageObject(imageDTO) : null; + ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; }, rgIPAdapterWeightChanged: (state, action: PayloadAction<{ id: string; ipAdapterId: string; weight: number }>) => { const { id, ipAdapterId, weight } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); - if (!ipa) { + const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId); + if (!ipAdapter) { return; } - ipa.weight = weight; + ipAdapter.weight = weight; }, rgIPAdapterBeginEndStepPctChanged: ( state, action: PayloadAction<{ id: string; ipAdapterId: string; beginEndStepPct: [number, number] }> ) => { const { id, ipAdapterId, beginEndStepPct } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); - if (!ipa) { + const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId); + if (!ipAdapter) { return; } - ipa.beginEndStepPct = beginEndStepPct; + ipAdapter.beginEndStepPct = beginEndStepPct; }, rgIPAdapterMethodChanged: (state, action: PayloadAction<{ id: string; ipAdapterId: string; method: IPMethodV2 }>) => { const { id, ipAdapterId, method } = action.payload; - const rg = selectRG(state, id); - if (!rg) { + const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId); + if (!ipAdapter) { return; } - const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); - if (!ipa) { - return; - } - ipa.method = method; + ipAdapter.method = method; }, rgIPAdapterModelChanged: ( state, @@ -181,33 +179,21 @@ export const regionsReducers = { }> ) => { const { id, ipAdapterId, modelConfig } = action.payload; - const rg = selectRG(state, id); - if (!rg) { + const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId); + if (!ipAdapter) { return; } - const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); - if (!ipa) { - return; - } - if (modelConfig) { - ipa.model = zModelIdentifierField.parse(modelConfig); - } else { - ipa.model = null; - } + ipAdapter.model = modelConfig ? zModelIdentifierField.parse(modelConfig) : null; }, rgIPAdapterCLIPVisionModelChanged: ( state, action: PayloadAction<{ id: string; ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }> ) => { const { id, ipAdapterId, clipVisionModel } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); - if (!ipa) { + const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId); + if (!ipAdapter) { return; } - ipa.clipVisionModel = clipVisionModel; + ipAdapter.clipVisionModel = clipVisionModel; }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index cc0fb37f350..c7e54b2a836 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -581,22 +581,24 @@ export function isCanvasBrushLineState(obj: CanvasObjectState): obj is CanvasBru return obj.type === 'brush_line'; } +const zIPAdapterConfig = z.object({ + image: zImageWithDims.nullable(), + model: zModelIdentifierField.nullable(), + weight: z.number().gte(-1).lte(2), + beginEndStepPct: zBeginEndStepPct, + method: zIPMethodV2, + clipVisionModel: zCLIPVisionModelV2, +}); +export type IPAdapterConfig = z.infer; + export const zCanvasIPAdapterState = z.object({ id: zId, + name: z.string().nullable(), type: z.literal('ip_adapter'), isEnabled: z.boolean(), - weight: z.number().gte(-1).lte(2), - method: zIPMethodV2, - imageObject: zCanvasImageState.nullable(), - model: zModelIdentifierField.nullable(), - clipVisionModel: zCLIPVisionModelV2, - beginEndStepPct: zBeginEndStepPct, + ipAdapter: zIPAdapterConfig, }); export type CanvasIPAdapterState = z.infer; -export type IPAdapterConfig = Pick< - CanvasIPAdapterState, - 'weight' | 'imageObject' | 'beginEndStepPct' | 'model' | 'clipVisionModel' | 'method' ->; const zMaskObject = z .discriminatedUnion('type', [ @@ -645,6 +647,17 @@ const zImageCache = z.object({ }); export type ImageCache = z.infer; +const zRegionalGuidanceIPAdapterConfig = z.object({ + id: zId, + image: zImageWithDims.nullable(), + model: zModelIdentifierField.nullable(), + weight: z.number().gte(-1).lte(2), + beginEndStepPct: zBeginEndStepPct, + method: zIPMethodV2, + clipVisionModel: zCLIPVisionModelV2, +}); +export type RegionalGuidanceIPAdapterConfig = z.infer; + export const zCanvasRegionalGuidanceState = z.object({ id: zId, name: z.string().nullable(), @@ -655,7 +668,7 @@ export const zCanvasRegionalGuidanceState = z.object({ fill: zRgbColor, positivePrompt: zParameterPositivePrompt.nullable(), negativePrompt: zParameterNegativePrompt.nullable(), - ipAdapters: z.array(zCanvasIPAdapterState), + ipAdapters: z.array(zRegionalGuidanceIPAdapterConfig), autoNegative: zAutoNegative, rasterizationCache: z.array(zImageCache), }); @@ -763,7 +776,7 @@ export const initialT2IAdapterV2: T2IAdapterConfig = { }; export const initialIPAdapterV2: IPAdapterConfig = { - imageObject: null, + image: null, model: null, beginEndStepPct: [0, 1], method: 'full', @@ -943,6 +956,7 @@ export type EntityRasterizedPayload = { entityIdentifier: CanvasEntityIdentifier; imageObject: CanvasImageState; rect: Rect; + replaceObjects: boolean; }; export type ImageObjectAddedArg = { id: string; imageDTO: ImageDTO; position?: Coordinate }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts index 569b57310d8..99d5009220d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addIPAdapters.ts @@ -1,4 +1,4 @@ -import type { CanvasIPAdapterState } from 'features/controlLayers/store/types'; +import type { CanvasIPAdapterState, IPAdapterConfig } from 'features/controlLayers/store/types'; import { IP_ADAPTER_COLLECT } from 'features/nodes/util/graph/constants'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import type { BaseModelType, Invocation } from 'services/api/types'; @@ -10,7 +10,7 @@ export const addIPAdapters = ( denoise: Invocation<'denoise_latents'>, base: BaseModelType ): CanvasIPAdapterState[] => { - const validIPAdapters = ipAdapters.filter((ipa) => isValidIPAdapter(ipa, base)); + const validIPAdapters = ipAdapters.filter((entity) => isValidIPAdapter(entity.ipAdapter, base)); for (const ipa of validIPAdapters) { addIPAdapter(ipa, g, denoise); } @@ -33,13 +33,14 @@ export const addIPAdapterCollectorSafe = (g: Graph, denoise: Invocation<'denoise } }; -const addIPAdapter = (ipa: CanvasIPAdapterState, g: Graph, denoise: Invocation<'denoise_latents'>) => { - const { id, weight, model, clipVisionModel, method, beginEndStepPct, imageObject } = ipa; - assert(imageObject, 'IP Adapter image is required'); +const addIPAdapter = (entity: CanvasIPAdapterState, g: Graph, denoise: Invocation<'denoise_latents'>) => { + const { id, ipAdapter } = entity; + const { weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapter; + assert(image, 'IP Adapter image is required'); assert(model, 'IP Adapter model is required'); const ipAdapterCollect = addIPAdapterCollectorSafe(g, denoise); - const ipAdapter = g.addNode({ + const ipAdapterNode = g.addNode({ id: `ip_adapter_${id}`, type: 'ip_adapter', weight, @@ -49,16 +50,16 @@ const addIPAdapter = (ipa: CanvasIPAdapterState, g: Graph, denoise: Invocation<' begin_step_percent: beginEndStepPct[0], end_step_percent: beginEndStepPct[1], image: { - image_name: imageObject.image.image_name, + image_name: image.image_name, }, }); - g.addEdge(ipAdapter, 'ip_adapter', ipAdapterCollect, 'item'); + g.addEdge(ipAdapterNode, 'ip_adapter', ipAdapterCollect, 'item'); }; -export const isValidIPAdapter = (ipa: CanvasIPAdapterState, base: BaseModelType): boolean => { +export const isValidIPAdapter = (ipAdapter: IPAdapterConfig, base: BaseModelType): boolean => { // Must be have a model that matches the current base and must have a control image - const hasModel = Boolean(ipa.model); - const modelMatchesBase = ipa.model?.base === base; - const hasImage = Boolean(ipa.imageObject); + const hasModel = Boolean(ipAdapter.model); + const modelMatchesBase = ipAdapter.model?.base === base; + const hasImage = Boolean(ipAdapter.image); return hasModel && modelMatchesBase && hasImage; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts index d647eb556fb..4b40c941729 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLayers.ts @@ -1,10 +1,5 @@ import type { CanvasRasterLayerState } from 'features/controlLayers/store/types'; -export const isValidLayerWithoutControlAdapter = (layer: CanvasRasterLayerState) => { - return ( - layer.isEnabled && - // Boolean(entity.bbox) && TODO(psyche): Re-enable this check when we have a way to calculate bbox for all layers - layer.objects.length > 0 && - layer.controlAdapter === null - ); +export const isValidLayer = (layer: CanvasRasterLayerState) => { + return layer.isEnabled && layer.objects.length > 0; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts index f4a8f429e6c..a0a2b098292 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts @@ -1,6 +1,10 @@ import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { CanvasIPAdapterState, CanvasRegionalGuidanceState, Rect } from 'features/controlLayers/store/types'; +import type { + CanvasRegionalGuidanceState, + Rect, + RegionalGuidanceIPAdapterConfig, +} from 'features/controlLayers/store/types'; import { PROMPT_REGION_INVERT_TENSOR_MASK_PREFIX, PROMPT_REGION_MASK_TO_TENSOR_PREFIX, @@ -174,13 +178,15 @@ export const addRegions = async ( } } - const validRGIPAdapters: CanvasIPAdapterState[] = region.ipAdapters.filter((ipa) => isValidIPAdapter(ipa, base)); + const validRGIPAdapters: RegionalGuidanceIPAdapterConfig[] = region.ipAdapters.filter((ipAdapter) => + isValidIPAdapter(ipAdapter, base) + ); for (const ipa of validRGIPAdapters) { const ipAdapterCollect = addIPAdapterCollectorSafe(g, denoise); - const { id, weight, model, clipVisionModel, method, beginEndStepPct, imageObject } = ipa; + const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipa; assert(model, 'IP Adapter model is required'); - assert(imageObject, 'IP Adapter image is required'); + assert(image, 'IP Adapter image is required'); const ipAdapter = g.addNode({ id: `ip_adapter_${id}`, @@ -192,7 +198,7 @@ export const addRegions = async ( begin_step_percent: beginEndStepPct[0], end_step_percent: beginEndStepPct[1], image: { - image_name: imageObject.image.image_name, + image_name: image.image_name, }, }); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index ef403a5ae76..4ac130d15ef 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -215,7 +215,7 @@ export const buildSD1Graph = async (state: RootState, manager: CanvasManager): P const _addedCAs = await addControlAdapters( manager, - state.canvasV2.rasterLayers.entities, + state.canvasV2.controlLayers.entities, g, state.canvasV2.bbox.rect, denoise, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index f80d47b5c61..98fadc83e2e 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -219,7 +219,7 @@ export const buildSDXLGraph = async (state: RootState, manager: CanvasManager): const _addedCAs = await addControlAdapters( manager, - state.canvasV2.rasterLayers.entities, + state.canvasV2.controlLayers.entities, g, state.canvasV2.bbox.rect, denoise, From 006a5723da87279bf91d84da0235f0dd3d8dc5b9 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 15 Aug 2024 20:41:57 +1000 Subject: [PATCH 358/678] feat(ui): rough out eyedropper tool It's a bit slow bc we are converting the stage to canvas on every mouse move. Also need to improve the visual but it works. --- .../components/ControlLayersToolbar.tsx | 1 - .../controlLayers/components/ToolChooser.tsx | 2 + .../components/ToolEyeDropperButton.tsx | 34 +++++++++++ .../controlLayers/konva/CanvasStateApi.ts | 5 ++ .../controlLayers/konva/CanvasTool.ts | 60 ++++++++++++++++--- .../features/controlLayers/konva/events.ts | 31 ++++++++++ .../src/features/controlLayers/store/types.ts | 2 +- 7 files changed, 125 insertions(+), 10 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ToolEyeDropperButton.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index c354fd9a19d..dd98df8dab5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -20,7 +20,6 @@ import { memo, useCallback } from 'react'; export const ControlLayersToolbar = memo(() => { const tool = useAppSelector((s) => s.canvasV2.tool.selected); const canvasManager = useStore($canvasManager); - const onChangeDebugging = useCallback( (e: ChangeEvent) => { if (!canvasManager) { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx index 860bf3c601c..0a0c2f98df5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx @@ -5,6 +5,7 @@ import { BrushToolButton } from 'features/controlLayers/components/BrushToolButt import { EraserToolButton } from 'features/controlLayers/components/EraserToolButton'; import { MoveToolButton } from 'features/controlLayers/components/MoveToolButton'; import { RectToolButton } from 'features/controlLayers/components/RectToolButton'; +import { ToolEyeDropperButton } from 'features/controlLayers/components/ToolEyeDropperButton'; import { TransformToolButton } from 'features/controlLayers/components/TransformToolButton'; import { ViewToolButton } from 'features/controlLayers/components/ViewToolButton'; import { useCanvasDeleteLayerHotkey } from 'features/controlLayers/hooks/useCanvasDeleteLayerHotkey'; @@ -24,6 +25,7 @@ export const ToolChooser: React.FC = () => { + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolEyeDropperButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolEyeDropperButton.tsx new file mode 100644 index 00000000000..7bf746ca609 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolEyeDropperButton.tsx @@ -0,0 +1,34 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; +import { PiEyedropperBold } from 'react-icons/pi'; + +export const ToolEyeDropperButton = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isDisabled = useAppSelector((s) => s.canvasV2.session.isStaging || s.canvasV2.tool.isTransforming); + const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'eyeDropper'); + + const onClick = useCallback(() => { + dispatch(toolChanged('eyeDropper')); + }, [dispatch]); + + useHotkeys('i', onClick, { enabled: !isDisabled }, [onClick, isDisabled]); + + return ( + } + colorScheme={isSelected ? 'invokeBlue' : 'base'} + variant="outline" + onClick={onClick} + isDisabled={isDisabled} + /> + ); +}); + +ToolEyeDropperButton.displayName = 'ToolEyeDropperButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index b6bb0e8fe18..ed67ebda222 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -26,6 +26,7 @@ import { entityReset, entitySelected, eraserWidthChanged, + fillChanged, rasterLayerCompositeRasterized, toolBufferChanged, toolChanged, @@ -144,6 +145,9 @@ export class CanvasStateApi { log.trace({ toolBuffer }, 'Setting tool buffer'); this._store.dispatch(toolBufferChanged(toolBuffer)); }; + setFill = (fill: RgbaColor) => { + return this._store.dispatch(fillChanged(fill)); + }; getBbox = () => { return this.getState().bbox; @@ -257,6 +261,7 @@ export class CanvasStateApi { $currentFill: WritableAtom = atom(); $selectedEntity: WritableAtom = atom(); $selectedEntityIdentifier: WritableAtom = atom(); + $colorUnderCursor: WritableAtom = atom(); // Read-write state, ephemeral interaction state $isDrawing = $isDrawing; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts index 9e4183aa2a1..f112fec3a76 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts @@ -35,6 +35,11 @@ export class CanvasTool { innerBorderCircle: Konva.Circle; outerBorderCircle: Konva.Circle; }; + eyeDropper: { + group: Konva.Group; + fillCircle: Konva.Circle; + transparentCenterCircle: Konva.Circle; + }; }; /** @@ -96,6 +101,26 @@ export class CanvasTool { strokeEnabled: true, }), }, + eyeDropper: { + group: new Konva.Group({ name: `${this.type}:eyeDropper_group` }), + fillCircle: new Konva.Circle({ + name: `${this.type}:eyeDropper_fill_circle`, + listening: false, + fill: '', + radius: 20, + strokeWidth: 1, + stroke: 'black', + strokeScaleEnabled: false, + }), + transparentCenterCircle: new Konva.Circle({ + name: `${this.type}:eyeDropper_fill_circle`, + listening: false, + strokeEnabled: false, + fill: 'white', + radius: 5, + globalCompositeOperation: 'destination-out', + }), + }, }; this.konva.brush.group.add(this.konva.brush.fillCircle); this.konva.brush.group.add(this.konva.brush.innerBorderCircle); @@ -107,6 +132,10 @@ export class CanvasTool { this.konva.eraser.group.add(this.konva.eraser.outerBorderCircle); this.konva.group.add(this.konva.eraser.group); + this.konva.eyeDropper.group.add(this.konva.eyeDropper.fillCircle); + this.konva.eyeDropper.group.add(this.konva.eyeDropper.transparentCenterCircle); + this.konva.group.add(this.konva.eyeDropper.group); + this.subscriptions.add( this.manager.stateApi.$stageAttrs.listen(() => { this.render(); @@ -146,6 +175,12 @@ export class CanvasTool { }); }; + setToolVisibility = (tool: 'brush' | 'eraser' | 'eyeDropper' | 'none') => { + this.konva.brush.group.visible(tool === 'brush'); + this.konva.eraser.group.visible(tool === 'eraser'); + this.konva.eyeDropper.group.visible(tool === 'eyeDropper'); + }; + render() { const stage = this.manager.stage; const renderedEntityCount: number = 1; // TODO(psyche): this.manager should be renderable entity count @@ -154,6 +189,7 @@ export class CanvasTool { const cursorPos = this.manager.stateApi.$lastCursorPos.get(); const isDrawing = this.manager.stateApi.$isDrawing.get(); const isMouseDown = this.manager.stateApi.$isMouseDown.get(); + const colorUnderCursor = this.manager.stateApi.$colorUnderCursor.get(); const tool = toolState.selected; @@ -180,6 +216,8 @@ export class CanvasTool { stage.container().style.cursor = 'none'; } else if (tool === 'bbox') { stage.container().style.cursor = 'default'; + } else if (tool === 'eyeDropper') { + stage.container().style.cursor = 'none'; } stage.draggable(tool === 'view'); @@ -216,9 +254,7 @@ export class CanvasTool { }); this.scaleTool(); - - this.konva.brush.group.visible(true); - this.konva.eraser.group.visible(false); + this.setToolVisibility('brush'); } else if (cursorPos && tool === 'eraser') { const alignedCursorPos = alignCoordForTool(cursorPos, toolState.eraser.width); @@ -243,12 +279,20 @@ export class CanvasTool { }); this.scaleTool(); - - this.konva.brush.group.visible(false); - this.konva.eraser.group.visible(true); + this.setToolVisibility('eraser'); + } else if (cursorPos && colorUnderCursor) { + this.konva.eyeDropper.fillCircle.setAttrs({ + x: cursorPos.x, + y: cursorPos.y, + fill: rgbaColorToString(colorUnderCursor), + }); + this.konva.eyeDropper.transparentCenterCircle.setAttrs({ + x: cursorPos.x, + y: cursorPos.y, + }); + this.setToolVisibility('eyeDropper'); } else { - this.konva.brush.group.visible(false); - this.konva.eraser.group.visible(false); + this.setToolVisibility('none'); } } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 96465480c12..96f5ece261d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -12,6 +12,7 @@ import type { CanvasRegionalGuidanceState, CanvasV2State, Coordinate, + RgbaColor, Tool, } from 'features/controlLayers/store/types'; import { isDrawableEntity, isDrawableEntityAdapter } from 'features/controlLayers/store/types'; @@ -115,6 +116,23 @@ const getLastPointOfLastLineOfEntity = ( return { x, y }; }; +const getColorUnderCursor = (stage: Konva.Stage): RgbaColor | null => { + const pos = stage.getPointerPosition(); + if (!pos) { + return null; + } + const ctx = stage.toCanvas({ x: pos.x, y: pos.y, width: 1, height: 1 }).getContext('2d'); + if (!ctx) { + return null; + } + const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data; + if (r === undefined || g === undefined || b === undefined || a === undefined) { + return null; + } + + return { r, g, b, a }; +}; + export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { const { stage, stateApi } = manager; const { @@ -174,6 +192,14 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { const pos = updateLastCursorPos(stage, $lastCursorPos.set); const selectedEntity = getSelectedEntity(); + if (toolState.selected === 'eyeDropper') { + const color = getColorUnderCursor(stage); + manager.stateApi.$colorUnderCursor.set(color); + if (color) { + manager.stateApi.setFill(color); + } + } + if ( pos && selectedEntity && @@ -322,6 +348,11 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { const pos = updateLastCursorPos(stage, $lastCursorPos.set); const selectedEntity = getSelectedEntity(); + if (toolState.selected === 'eyeDropper') { + const color = getColorUnderCursor(stage); + manager.stateApi.$colorUnderCursor.set(color); + } + if ( pos && selectedEntity && diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index c7e54b2a836..a6d9115386e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -469,7 +469,7 @@ export const IMAGE_FILTERS: { [key in FilterConfig['type']]: ImageFilterData; export function isDrawingTool(tool: Tool): tool is 'brush' | 'eraser' | 'rect' { return tool === 'brush' || tool === 'eraser' || tool === 'rect'; From b41f1a897d05fe9ff682b656c8e586eccf070632 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 15 Aug 2024 20:52:51 +1000 Subject: [PATCH 359/678] tidy(ui): tool components & translations --- invokeai/frontend/web/public/locales/en.json | 12 ++++++- .../components/ControlLayersToolbar.tsx | 14 ++++---- .../ToolBboxButton.tsx} | 8 ++--- .../ToolBrushButton.tsx} | 8 ++--- .../ToolBrushWidth.tsx} | 4 +-- .../components/Tool/ToolChooser.tsx | 34 +++++++++++++++++++ .../ToolEraserButton.tsx} | 8 ++--- .../ToolEraserWidth.tsx} | 4 +-- .../{ => Tool}/ToolEyeDropperButton.tsx | 4 +-- .../ToolFillColorPicker.tsx} | 4 +-- .../ToolMoveButton.tsx} | 8 ++--- .../ToolRectButton.tsx} | 8 ++--- .../ToolTransformButton.tsx} | 8 ++--- .../ToolViewButton.tsx} | 8 ++--- .../controlLayers/components/ToolChooser.tsx | 33 ------------------ .../controlLayers/konva/CanvasTool.ts | 15 +++----- 16 files changed, 93 insertions(+), 87 deletions(-) rename invokeai/frontend/web/src/features/controlLayers/components/{BboxToolButton.tsx => Tool/ToolBboxButton.tsx} (83%) rename invokeai/frontend/web/src/features/controlLayers/components/{BrushToolButton.tsx => Tool/ToolBrushButton.tsx} (86%) rename invokeai/frontend/web/src/features/controlLayers/components/{BrushWidth.tsx => Tool/ToolBrushWidth.tsx} (94%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolChooser.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{EraserToolButton.tsx => Tool/ToolEraserButton.tsx} (86%) rename invokeai/frontend/web/src/features/controlLayers/components/{EraserWidth.tsx => Tool/ToolEraserWidth.tsx} (94%) rename invokeai/frontend/web/src/features/controlLayers/components/{ => Tool}/ToolEyeDropperButton.tsx (90%) rename invokeai/frontend/web/src/features/controlLayers/components/{FillColorPicker.tsx => Tool/ToolFillColorPicker.tsx} (92%) rename invokeai/frontend/web/src/features/controlLayers/components/{MoveToolButton.tsx => Tool/ToolMoveButton.tsx} (84%) rename invokeai/frontend/web/src/features/controlLayers/components/{RectToolButton.tsx => Tool/ToolRectButton.tsx} (86%) rename invokeai/frontend/web/src/features/controlLayers/components/{TransformToolButton.tsx => Tool/ToolTransformButton.tsx} (88%) rename invokeai/frontend/web/src/features/controlLayers/components/{ViewToolButton.tsx => Tool/ToolViewButton.tsx} (83%) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 0867c084ec9..f7f93a7a1aa 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1702,7 +1702,17 @@ "objects_other": "{{count}} objects", "filter": "Filter", "convertToControlLayer": "Convert to Control Layer", - "convertToRasterLayer": "Convert to Raster Layer" + "convertToRasterLayer": "Convert to Raster Layer", + "tool": { + "brush": "Brush", + "eraser": "Eraser", + "rectangle": "Rectangle", + "bbox": "Bbox", + "move": "Move", + "view": "View", + "transform": "Transform", + "eyeDropper": "Eye Dropper" + } }, "upscaling": { "upscale": "Upscale", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index dd98df8dab5..69ec88c5cfa 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -2,14 +2,14 @@ import { Flex, Switch } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; -import { BrushWidth } from 'features/controlLayers/components/BrushWidth'; import { CanvasResetViewButton } from 'features/controlLayers/components/CanvasResetViewButton'; import { CanvasScale } from 'features/controlLayers/components/CanvasScale'; import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover'; -import { EraserWidth } from 'features/controlLayers/components/EraserWidth'; -import { FillColorPicker } from 'features/controlLayers/components/FillColorPicker'; import { ResetCanvasButton } from 'features/controlLayers/components/ResetCanvasButton'; -import { ToolChooser } from 'features/controlLayers/components/ToolChooser'; +import { ToolBrushWidth } from 'features/controlLayers/components/Tool/ToolBrushWidth'; +import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser'; +import { ToolEraserWidth } from 'features/controlLayers/components/Tool/ToolEraserWidth'; +import { ToolFillColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker'; import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup'; import { $canvasManager } from 'features/controlLayers/konva/CanvasManager'; import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton'; @@ -42,15 +42,15 @@ export const ControlLayersToolbar = memo(() => { - {tool === 'brush' && } - {tool === 'eraser' && } + {tool === 'brush' && } + {tool === 'eraser' && } debug - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/BboxToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBboxButton.tsx similarity index 83% rename from invokeai/frontend/web/src/features/controlLayers/components/BboxToolButton.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBboxButton.tsx index 6eecd06860e..407efcb9f9d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/BboxToolButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBboxButton.tsx @@ -6,7 +6,7 @@ import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiBoundingBoxBold } from 'react-icons/pi'; -export const BboxToolButton = memo(() => { +export const ToolBboxButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const isDisabled = useAppSelector((s) => s.canvasV2.session.isStaging || s.canvasV2.tool.isTransforming); @@ -20,8 +20,8 @@ export const BboxToolButton = memo(() => { return ( } colorScheme={isSelected ? 'invokeBlue' : 'base'} variant="outline" @@ -31,4 +31,4 @@ export const BboxToolButton = memo(() => { ); }); -BboxToolButton.displayName = 'BboxToolButton'; +ToolBboxButton.displayName = 'ToolBboxButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/BrushToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushButton.tsx similarity index 86% rename from invokeai/frontend/web/src/features/controlLayers/components/BrushToolButton.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushButton.tsx index 551568e7cf3..f170e3f2de8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/BrushToolButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushButton.tsx @@ -7,7 +7,7 @@ import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiPaintBrushBold } from 'react-icons/pi'; -export const BrushToolButton = memo(() => { +export const ToolBrushButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'brush'); @@ -26,8 +26,8 @@ export const BrushToolButton = memo(() => { return ( } colorScheme={isSelected ? 'invokeBlue' : 'base'} variant="outline" @@ -37,4 +37,4 @@ export const BrushToolButton = memo(() => { ); }); -BrushToolButton.displayName = 'BrushToolButton'; +ToolBrushButton.displayName = 'ToolBrushButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/BrushWidth.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushWidth.tsx similarity index 94% rename from invokeai/frontend/web/src/features/controlLayers/components/BrushWidth.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushWidth.tsx index fa72e542596..20a51155315 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/BrushWidth.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushWidth.tsx @@ -17,7 +17,7 @@ import { useTranslation } from 'react-i18next'; const marks = [0, 100, 200, 300]; const formatPx = (v: number | string) => `${v} px`; -export const BrushWidth = memo(() => { +export const ToolBrushWidth = memo(() => { const dispatch = useAppDispatch(); const { t } = useTranslation(); const width = useAppSelector((s) => s.canvasV2.tool.brush.width); @@ -53,4 +53,4 @@ export const BrushWidth = memo(() => { ); }); -BrushWidth.displayName = 'BrushSize'; +ToolBrushWidth.displayName = 'ToolBrushWidth'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolChooser.tsx new file mode 100644 index 00000000000..c9349a069fd --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolChooser.tsx @@ -0,0 +1,34 @@ +import { ButtonGroup } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { ToolBboxButton } from 'features/controlLayers/components/Tool/ToolBboxButton'; +import { ToolBrushButton } from 'features/controlLayers/components/Tool/ToolBrushButton'; +import { ToolEyeDropperButton } from 'features/controlLayers/components/Tool/ToolEyeDropperButton'; +import { ToolMoveButton } from 'features/controlLayers/components/Tool/ToolMoveButton'; +import { ToolRectButton } from 'features/controlLayers/components/Tool/ToolRectButton'; +import { useCanvasDeleteLayerHotkey } from 'features/controlLayers/hooks/useCanvasDeleteLayerHotkey'; +import { useCanvasResetLayerHotkey } from 'features/controlLayers/hooks/useCanvasResetLayerHotkey'; + +import { ToolEraserButton } from './ToolEraserButton'; +import { ToolTransformButton } from './ToolTransformButton'; +import { ToolViewButton } from './ToolViewButton'; + +export const ToolChooser: React.FC = () => { + useCanvasResetLayerHotkey(); + useCanvasDeleteLayerHotkey(); + const isTransforming = useAppSelector((s) => s.canvasV2.tool.isTransforming); + + return ( + <> + + + + + + + + + + + + ); +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/EraserToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserButton.tsx similarity index 86% rename from invokeai/frontend/web/src/features/controlLayers/components/EraserToolButton.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserButton.tsx index f73d38a7695..924c7098b6c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/EraserToolButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserButton.tsx @@ -7,7 +7,7 @@ import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiEraserBold } from 'react-icons/pi'; -export const EraserToolButton = memo(() => { +export const ToolEraserButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'eraser'); @@ -26,8 +26,8 @@ export const EraserToolButton = memo(() => { return ( } colorScheme={isSelected ? 'invokeBlue' : 'base'} variant="outline" @@ -37,4 +37,4 @@ export const EraserToolButton = memo(() => { ); }); -EraserToolButton.displayName = 'EraserToolButton'; +ToolEraserButton.displayName = 'ToolEraserButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/EraserWidth.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserWidth.tsx similarity index 94% rename from invokeai/frontend/web/src/features/controlLayers/components/EraserWidth.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserWidth.tsx index deff803bb1c..e227e0bedd1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/EraserWidth.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserWidth.tsx @@ -17,7 +17,7 @@ import { useTranslation } from 'react-i18next'; const marks = [0, 100, 200, 300]; const formatPx = (v: number | string) => `${v} px`; -export const EraserWidth = memo(() => { +export const ToolEraserWidth = memo(() => { const dispatch = useAppDispatch(); const { t } = useTranslation(); const width = useAppSelector((s) => s.canvasV2.tool.eraser.width); @@ -53,4 +53,4 @@ export const EraserWidth = memo(() => { ); }); -EraserWidth.displayName = 'EraserWidth'; +ToolEraserWidth.displayName = 'ToolEraserWidth'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolEyeDropperButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEyeDropperButton.tsx similarity index 90% rename from invokeai/frontend/web/src/features/controlLayers/components/ToolEyeDropperButton.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEyeDropperButton.tsx index 7bf746ca609..df6ede28cbb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ToolEyeDropperButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEyeDropperButton.tsx @@ -20,8 +20,8 @@ export const ToolEyeDropperButton = memo(() => { return ( } colorScheme={isSelected ? 'invokeBlue' : 'base'} variant="outline" diff --git a/invokeai/frontend/web/src/features/controlLayers/components/FillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolFillColorPicker.tsx similarity index 92% rename from invokeai/frontend/web/src/features/controlLayers/components/FillColorPicker.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolFillColorPicker.tsx index 3d71523c628..477e23bc594 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/FillColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolFillColorPicker.tsx @@ -7,7 +7,7 @@ import type { RgbaColor } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -export const FillColorPicker = memo(() => { +export const ToolFillColorPicker = memo(() => { const { t } = useTranslation(); const fill = useAppSelector((s) => s.canvasV2.tool.fill); const dispatch = useAppDispatch(); @@ -41,4 +41,4 @@ export const FillColorPicker = memo(() => { ); }); -FillColorPicker.displayName = 'BrushColorPicker'; +ToolFillColorPicker.displayName = 'ToolFillColorPicker'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/MoveToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolMoveButton.tsx similarity index 84% rename from invokeai/frontend/web/src/features/controlLayers/components/MoveToolButton.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolMoveButton.tsx index 5d97542369c..4fb1984a03f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/MoveToolButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolMoveButton.tsx @@ -6,7 +6,7 @@ import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiCursorBold } from 'react-icons/pi'; -export const MoveToolButton = memo(() => { +export const ToolMoveButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'move'); @@ -22,8 +22,8 @@ export const MoveToolButton = memo(() => { return ( } colorScheme={isSelected ? 'invokeBlue' : 'base'} variant="outline" @@ -33,4 +33,4 @@ export const MoveToolButton = memo(() => { ); }); -MoveToolButton.displayName = 'MoveToolButton'; +ToolMoveButton.displayName = 'ToolMoveButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RectToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolRectButton.tsx similarity index 86% rename from invokeai/frontend/web/src/features/controlLayers/components/RectToolButton.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolRectButton.tsx index 3c8acd4ae8e..530343b271f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RectToolButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolRectButton.tsx @@ -7,7 +7,7 @@ import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiRectangleBold } from 'react-icons/pi'; -export const RectToolButton = memo(() => { +export const ToolRectButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'rect'); @@ -26,8 +26,8 @@ export const RectToolButton = memo(() => { return ( } colorScheme={isSelected ? 'invokeBlue' : 'base'} variant="outline" @@ -37,4 +37,4 @@ export const RectToolButton = memo(() => { ); }); -RectToolButton.displayName = 'RectToolButton'; +ToolRectButton.displayName = 'ToolRectButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolTransformButton.tsx similarity index 88% rename from invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolTransformButton.tsx index e26635f1ad1..354da79c55c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/TransformToolButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolTransformButton.tsx @@ -8,7 +8,7 @@ import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiResizeBold } from 'react-icons/pi'; -export const TransformToolButton = memo(() => { +export const ToolTransformButton = memo(() => { const { t } = useTranslation(); const canvasManager = useStore($canvasManager); const transformingEntity = useStore($transformingEntity); @@ -50,8 +50,8 @@ export const TransformToolButton = memo(() => { return ( } variant="solid" onClick={onTransform} @@ -60,4 +60,4 @@ export const TransformToolButton = memo(() => { ); }); -TransformToolButton.displayName = 'TransformToolButton'; +ToolTransformButton.displayName = 'ToolTransformButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ViewToolButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolViewButton.tsx similarity index 83% rename from invokeai/frontend/web/src/features/controlLayers/components/ViewToolButton.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolViewButton.tsx index 184e38d7ad9..22156a3401a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ViewToolButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolViewButton.tsx @@ -6,7 +6,7 @@ import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiHandBold } from 'react-icons/pi'; -export const ViewToolButton = memo(() => { +export const ToolViewButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'view'); @@ -19,8 +19,8 @@ export const ViewToolButton = memo(() => { return ( } colorScheme={isSelected ? 'invokeBlue' : 'base'} variant="outline" @@ -30,4 +30,4 @@ export const ViewToolButton = memo(() => { ); }); -ViewToolButton.displayName = 'ViewToolButton'; +ToolViewButton.displayName = 'ToolViewButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx deleted file mode 100644 index 0a0c2f98df5..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { ButtonGroup } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; -import { BboxToolButton } from 'features/controlLayers/components/BboxToolButton'; -import { BrushToolButton } from 'features/controlLayers/components/BrushToolButton'; -import { EraserToolButton } from 'features/controlLayers/components/EraserToolButton'; -import { MoveToolButton } from 'features/controlLayers/components/MoveToolButton'; -import { RectToolButton } from 'features/controlLayers/components/RectToolButton'; -import { ToolEyeDropperButton } from 'features/controlLayers/components/ToolEyeDropperButton'; -import { TransformToolButton } from 'features/controlLayers/components/TransformToolButton'; -import { ViewToolButton } from 'features/controlLayers/components/ViewToolButton'; -import { useCanvasDeleteLayerHotkey } from 'features/controlLayers/hooks/useCanvasDeleteLayerHotkey'; -import { useCanvasResetLayerHotkey } from 'features/controlLayers/hooks/useCanvasResetLayerHotkey'; - -export const ToolChooser: React.FC = () => { - useCanvasResetLayerHotkey(); - useCanvasDeleteLayerHotkey(); - const isTransforming = useAppSelector((s) => s.canvasV2.tool.isTransforming); - - return ( - <> - - - - - - - - - - - - ); -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts index f112fec3a76..37020570f8c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts @@ -8,6 +8,7 @@ import { BRUSH_ERASER_BORDER_WIDTH, } from 'features/controlLayers/konva/constants'; import { alignCoordForTool, getPrefixedId } from 'features/controlLayers/konva/util'; +import type { Tool } from 'features/controlLayers/store/types'; import { isDrawableEntity } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; @@ -175,7 +176,7 @@ export class CanvasTool { }); }; - setToolVisibility = (tool: 'brush' | 'eraser' | 'eyeDropper' | 'none') => { + setToolVisibility = (tool: Tool) => { this.konva.brush.group.visible(tool === 'brush'); this.konva.eraser.group.visible(tool === 'eraser'); this.konva.eyeDropper.group.visible(tool === 'eyeDropper'); @@ -252,9 +253,6 @@ export class CanvasTool { y: cursorPos.y, radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, }); - - this.scaleTool(); - this.setToolVisibility('brush'); } else if (cursorPos && tool === 'eraser') { const alignedCursorPos = alignCoordForTool(cursorPos, toolState.eraser.width); @@ -277,9 +275,6 @@ export class CanvasTool { y: cursorPos.y, radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, }); - - this.scaleTool(); - this.setToolVisibility('eraser'); } else if (cursorPos && colorUnderCursor) { this.konva.eyeDropper.fillCircle.setAttrs({ x: cursorPos.x, @@ -290,10 +285,10 @@ export class CanvasTool { x: cursorPos.x, y: cursorPos.y, }); - this.setToolVisibility('eyeDropper'); - } else { - this.setToolVisibility('none'); } + + this.scaleTool(); + this.setToolVisibility(tool); } } From 163687aef3fa861d692fe6e0a6237285ba8b1e4a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 15 Aug 2024 20:54:02 +1000 Subject: [PATCH 360/678] fix(ui): do not smooth pixel data when using eyeDropper --- .../frontend/web/src/features/controlLayers/konva/events.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 96f5ece261d..8058f2ba23c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -121,7 +121,9 @@ const getColorUnderCursor = (stage: Konva.Stage): RgbaColor | null => { if (!pos) { return null; } - const ctx = stage.toCanvas({ x: pos.x, y: pos.y, width: 1, height: 1 }).getContext('2d'); + const ctx = stage + .toCanvas({ x: pos.x, y: pos.y, width: 1, height: 1, imageSmoothingEnabled: false }) + .getContext('2d'); if (!ctx) { return null; } From d8a2efc691b98c859d7b4b030bcfa181b703eb28 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 16 Aug 2024 18:56:04 +1000 Subject: [PATCH 361/678] build(ui): add vite types to tsconfig --- invokeai/frontend/web/tsconfig.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/tsconfig.json b/invokeai/frontend/web/tsconfig.json index 67d709940ff..50de71b68e1 100644 --- a/invokeai/frontend/web/tsconfig.json +++ b/invokeai/frontend/web/tsconfig.json @@ -3,6 +3,7 @@ "target": "ESNext", "useDefineForClassFields": true, "lib": ["DOM", "DOM.Iterable", "ESNext"], + "types": ["vite/client"], "allowJs": false, "skipLibCheck": true, "esModuleInterop": true, @@ -29,7 +30,8 @@ "references": [{ "path": "./tsconfig.node.json" }], "ts-node": { "compilerOptions": { - "jsx": "preserve" + "jsx": "preserve", + "types": ["vite/client"] }, "esm": true } From eb644d4e6a71d22daf40f3fa83b58da235e42874 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 16 Aug 2024 18:56:26 +1000 Subject: [PATCH 362/678] feat(ui): mask fill patterns --- invokeai/frontend/web/public/locales/en.json | 9 +++ .../InpaintMaskMaskFillColorPicker.tsx | 26 +++++--- .../RegionalGuidanceMaskFillColorPicker.tsx | 26 +++++--- .../components/common/MaskFillStyle.tsx | 64 ++++++++++++++++++ .../konva/CanvasObjectRenderer.ts | 65 +++++++++++++------ .../controlLayers/konva/CanvasStateApi.ts | 2 +- .../konva/patterns/getPatternSVG.ts | 27 ++++++++ .../konva/patterns/pattern-crosshatch.svg | 13 ++++ .../konva/patterns/pattern-diagonal.svg | 11 ++++ .../konva/patterns/pattern-grid.svg | 13 ++++ .../konva/patterns/pattern-horizontal.svg | 10 +++ .../konva/patterns/pattern-vertical.svg | 10 +++ .../controlLayers/store/canvasV2Slice.ts | 11 +++- .../store/inpaintMaskReducers.ts | 15 +++-- .../controlLayers/store/regionsReducers.ts | 23 +++++-- .../src/features/controlLayers/store/types.ts | 19 +++--- 16 files changed, 283 insertions(+), 61 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/common/MaskFillStyle.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/patterns/getPatternSVG.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-crosshatch.svg create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-diagonal.svg create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-grid.svg create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-horizontal.svg create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-vertical.svg diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index f7f93a7a1aa..1060dc7891a 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1703,6 +1703,15 @@ "filter": "Filter", "convertToControlLayer": "Convert to Control Layer", "convertToRasterLayer": "Convert to Raster Layer", + "fill": { + "fillStyle": "Fill Style", + "solid": "Solid", + "grid": "Grid", + "crosshatch": "Crosshatch", + "vertical": "Vertical", + "horizontal": "Horizontal", + "diagonal": "Diagonal" + }, "tool": { "brush": "Brush", "eraser": "Eraser", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx index 891b38a4184..f7d834bfbc6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx @@ -3,7 +3,9 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import RgbColorPicker from 'common/components/RgbColorPicker'; import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { stopPropagation } from 'common/util/stopPropagation'; -import { imFillChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { MaskFillStyle } from 'features/controlLayers/components/common/MaskFillStyle'; +import { imFillColorChanged, imFillStyleChanged } from 'features/controlLayers/store/canvasV2Slice'; +import type { FillStyle } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import type { RgbColor } from 'react-colorful'; import { useTranslation } from 'react-i18next'; @@ -12,9 +14,15 @@ export const InpaintMaskMaskFillColorPicker = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const fill = useAppSelector((s) => s.canvasV2.inpaintMask.fill); - const onChange = useCallback( - (fill: RgbColor) => { - dispatch(imFillChanged({ fill })); + const onChangeFillColor = useCallback( + (color: RgbColor) => { + dispatch(imFillColorChanged({ color })); + }, + [dispatch] + ); + const onChangeFillStyle = useCallback( + (style: FillStyle) => { + dispatch(imFillStyleChanged({ style })); }, [dispatch] ); @@ -22,21 +30,23 @@ export const InpaintMaskMaskFillColorPicker = memo(() => { - + + + + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx index 66e19c9b35d..85bd4758f63 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx @@ -3,9 +3,11 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import RgbColorPicker from 'common/components/RgbColorPicker'; import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { stopPropagation } from 'common/util/stopPropagation'; +import { MaskFillStyle } from 'features/controlLayers/components/common/MaskFillStyle'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { rgFillChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { rgFillColorChanged, rgFillStyleChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers'; +import type { FillStyle } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import type { RgbColor } from 'react-colorful'; import { useTranslation } from 'react-i18next'; @@ -15,9 +17,15 @@ export const RegionalGuidanceMaskFillColorPicker = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const fill = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, entityIdentifier.id).fill); - const onChange = useCallback( - (fill: RgbColor) => { - dispatch(rgFillChanged({ id: entityIdentifier.id, fill })); + const onChangeFillColor = useCallback( + (color: RgbColor) => { + dispatch(rgFillColorChanged({ id: entityIdentifier.id, color })); + }, + [dispatch, entityIdentifier.id] + ); + const onChangeFillStyle = useCallback( + (style: FillStyle) => { + dispatch(rgFillStyleChanged({ id: entityIdentifier.id, style })); }, [dispatch, entityIdentifier.id] ); @@ -25,21 +33,23 @@ export const RegionalGuidanceMaskFillColorPicker = memo(() => { - + + + + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/MaskFillStyle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/MaskFillStyle.tsx new file mode 100644 index 00000000000..e7c53d9f767 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/MaskFillStyle.tsx @@ -0,0 +1,64 @@ +import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; +import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import type { FillStyle } from 'features/controlLayers/store/types'; +import { isFillStyle } from 'features/controlLayers/store/types'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +type Props = { + style: FillStyle; + onChange: (style: FillStyle) => void; +}; + +export const MaskFillStyle = memo(({ style, onChange }: Props) => { + const { t } = useTranslation(); + const _onChange = useCallback( + (v) => { + if (!isFillStyle(v?.value)) { + return; + } + onChange(v.value); + }, + [onChange] + ); + + const options = useMemo(() => { + return [ + { + value: 'solid', + label: t('controlLayers.fill.solid'), + }, + { + value: 'diagonal', + label: t('controlLayers.fill.diagonal'), + }, + { + value: 'crosshatch', + label: t('controlLayers.fill.crosshatch'), + }, + { + value: 'grid', + label: t('controlLayers.fill.grid'), + }, + { + value: 'horizontal', + label: t('controlLayers.fill.horizontal'), + }, + { + value: 'vertical', + label: t('controlLayers.fill.vertical'), + }, + ]; + }, [t]); + + const value = useMemo(() => options.find((o) => o.value === style), [options, style]); + + return ( + + {t('controlLayers.fill.fillStyle')} + + + ); +}); + +MaskFillStyle.displayName = 'MaskFillStyle'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index e73a83aef73..d0a409ab94c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -8,24 +8,35 @@ import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLaye import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; +import { getPatternSVG } from 'features/controlLayers/konva/patterns/getPatternSVG'; import { getPrefixedId, konvaNodeToBlob, konvaNodeToImageData, previewBlob } from 'features/controlLayers/konva/util'; import type { CanvasBrushLineState, CanvasEraserLineState, CanvasImageState, CanvasRectState, + Fill, ImageCache, Rect, - RgbColor, } from 'features/controlLayers/store/types'; import { imageDTOToImageObject } from 'features/controlLayers/store/types'; import Konva from 'konva'; +import type { RectConfig } from 'konva/lib/shapes/Rect'; import { isEqual } from 'lodash-es'; import type { Logger } from 'roarr'; import { getImageDTO, uploadImage } from 'services/api/endpoints/images'; import type { ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; +function setFillPatternImage(shape: Konva.Shape, ...args: Parameters): HTMLImageElement { + const imageElement = new Image(); + imageElement.onload = () => { + shape.fillPatternImage(imageElement); + }; + imageElement.src = getPatternSVG(...args); + return imageElement; +} + /** * Union of all object renderers. */ @@ -86,7 +97,11 @@ export class CanvasObjectRenderer { * * The compositing rect is not added to the object group. */ - compositingRect: Konva.Rect | null; + compositing: { + group: Konva.Group; + rect: Konva.Rect; + patternImage: HTMLImageElement; + } | null; }; constructor(parent: CanvasLayerAdapter | CanvasMaskAdapter) { @@ -99,20 +114,26 @@ export class CanvasObjectRenderer { this.konva = { objectGroup: new Konva.Group({ name: `${this.type}:object_group`, listening: false }), - compositingRect: null, + compositing: null, }; this.parent.konva.layer.add(this.konva.objectGroup); if (this.parent.state.type === 'inpaint_mask' || this.parent.state.type === 'regional_guidance') { - this.konva.compositingRect = new Konva.Rect({ + const rect = new Konva.Rect({ name: `${this.type}:compositing_rect`, globalCompositeOperation: 'source-in', listening: false, strokeEnabled: false, perfectDrawEnabled: false, }); - this.parent.konva.layer.add(this.konva.compositingRect); + this.konva.compositing = { + group: new Konva.Group({ name: `${this.type}:compositing_group`, listening: false }), + rect, + patternImage: new Image(), // we will set the src on this on the first render + }; + this.konva.compositing.group.add(this.konva.compositing.rect); + this.parent.konva.layer.add(this.konva.compositing.group); } this.subscriptions.add( @@ -126,14 +147,9 @@ export class CanvasObjectRenderer { // The compositing rect must cover the whole stage at all times. When the stage is scaled, moved or resized, we // need to update the compositing rect to match the stage. this.subscriptions.add( - this.manager.stateApi.$stageAttrs.listen(({ x, y, width, height, scale }) => { - if (this.konva.compositingRect) { - this.konva.compositingRect.setAttrs({ - x: -x / scale, - y: -y / scale, - width: width / scale, - height: height / scale, - }); + this.manager.stateApi.$stageAttrs.listen(() => { + if (this.konva.compositing && this.parent.type === 'mask_adapter') { + this.updateCompositingRect(this.parent.state.fill, this.manager.stateApi.getMaskOpacity()); } }) ); @@ -167,20 +183,31 @@ export class CanvasObjectRenderer { return didRender; }; - updateCompositingRect = (fill: RgbColor, opacity: number) => { + updateCompositingRect = (fill: Fill, opacity: number) => { this.log.trace('Updating compositing rect'); - assert(this.konva.compositingRect, 'Missing compositing rect'); + assert(this.konva.compositing, 'Missing compositing rect'); - const rgbColor = rgbColorToString(fill); const { x, y, width, height, scale } = this.manager.stateApi.$stageAttrs.get(); - this.konva.compositingRect.setAttrs({ - fill: rgbColor, + console.log('stageAttrs', this.manager.stateApi.$stageAttrs.get()); + const attrs: RectConfig = { opacity, x: -x / scale, y: -y / scale, width: width / scale, height: height / scale, - }); + }; + + if (fill.style === 'solid') { + attrs.fill = rgbColorToString(fill.color); + attrs.fillPriority = 'color'; + this.konva.compositing.rect.setAttrs(attrs); + } else { + attrs.fillPatternScaleX = 1 / scale; + attrs.fillPatternScaleY = 1 / scale; + attrs.fillPriority = 'pattern'; + this.konva.compositing.rect.setAttrs(attrs); + setFillPatternImage(this.konva.compositing.rect, fill.style, fill.color); + } }; /** diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index ed67ebda222..e96e6c3ea07 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -248,7 +248,7 @@ export class CanvasStateApi { if (selectedEntity) { // The brush should use the mask opacity for these entity types if (selectedEntity.state.type === 'regional_guidance' || selectedEntity.state.type === 'inpaint_mask') { - currentFill = { ...selectedEntity.state.fill, a: this.getSettings().maskOpacity }; + currentFill = { ...selectedEntity.state.fill.color, a: this.getSettings().maskOpacity }; } } return currentFill; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/patterns/getPatternSVG.ts b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/getPatternSVG.ts new file mode 100644 index 00000000000..2836b139794 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/getPatternSVG.ts @@ -0,0 +1,27 @@ +import { rgbColorToString } from 'common/util/colorCodeTransformers'; +import type { FillStyle, RgbColor } from 'features/controlLayers/store/types'; + +import crosshatch from './pattern-crosshatch.svg?raw'; +import diagonal from './pattern-diagonal.svg?raw'; +import grid from './pattern-grid.svg?raw'; +import horizontal from './pattern-horizontal.svg?raw'; +import vertical from './pattern-vertical.svg?raw'; + +export function getPatternSVG(pattern: Exclude, color: RgbColor) { + let content: string = 'data:image/svg+xml;utf8,'; + if (pattern === 'crosshatch') { + content += crosshatch; + } else if (pattern === 'diagonal') { + content += diagonal; + } else if (pattern === 'horizontal') { + content += horizontal; + } else if (pattern === 'vertical') { + content += vertical; + } else if (pattern === 'grid') { + content += grid; + } + + content = content.replaceAll('stroke:black', `stroke:${rgbColorToString(color)}`); + + return content; +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-crosshatch.svg b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-crosshatch.svg new file mode 100644 index 00000000000..4bf6edaad91 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-crosshatch.svg @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-diagonal.svg b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-diagonal.svg new file mode 100644 index 00000000000..642bb7bdee1 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-diagonal.svg @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-grid.svg b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-grid.svg new file mode 100644 index 00000000000..a5f00896176 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-grid.svg @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-horizontal.svg b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-horizontal.svg new file mode 100644 index 00000000000..d710b818955 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-horizontal.svg @@ -0,0 +1,10 @@ + + + + + + + diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-vertical.svg b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-vertical.svg new file mode 100644 index 00000000000..879a8c0445c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-vertical.svg @@ -0,0 +1,10 @@ + + + + + + + diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 8624e467416..3f9d53b6a5d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -53,7 +53,10 @@ const initialState: CanvasV2State = { inpaintMask: { id: 'inpaint_mask', type: 'inpaint_mask', - fill: RGBA_RED, + fill: { + style: 'diagonal', + color: RGBA_RED, + }, rasterizationCache: [], isEnabled: true, objects: [], @@ -531,7 +534,8 @@ export const { rgAllDeleted, rgPositivePromptChanged, rgNegativePromptChanged, - rgFillChanged, + rgFillColorChanged, + rgFillStyleChanged, rgAutoNegativeChanged, rgIPAdapterAdded, rgIPAdapterDeleted, @@ -587,7 +591,8 @@ export const { loraAllDeleted, // Inpaint mask imRecalled, - imFillChanged, + imFillColorChanged, + imFillStyleChanged, // Staging sessionStartedStaging, sessionImageStaged, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts index 2950dc60b79..b1a954ca5cf 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts @@ -1,7 +1,6 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import type { CanvasInpaintMaskState, CanvasV2State } from 'features/controlLayers/store/types'; - -import type { RgbColor } from './types'; +import type { CanvasInpaintMaskState, CanvasV2State, FillStyle } from 'features/controlLayers/store/types'; +import type { RgbColor } from 'react-colorful'; export const inpaintMaskReducers = { imRecalled: (state, action: PayloadAction<{ data: CanvasInpaintMaskState }>) => { @@ -9,8 +8,12 @@ export const inpaintMaskReducers = { state.inpaintMask = data; state.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id }; }, - imFillChanged: (state, action: PayloadAction<{ fill: RgbColor }>) => { - const { fill } = action.payload; - state.inpaintMask.fill = fill; + imFillColorChanged: (state, action: PayloadAction<{ color: RgbColor }>) => { + const { color } = action.payload; + state.inpaintMask.fill.color = color; + }, + imFillStyleChanged: (state, action: PayloadAction<{ style: FillStyle }>) => { + const { style } = action.payload; + state.inpaintMask.fill.style = style; }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index 4aca5553812..6aa4a171c40 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -3,6 +3,7 @@ import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { CanvasV2State, CLIPVisionModelV2, + FillStyle, IPMethodV2, RegionalGuidanceIPAdapterConfig, } from 'features/controlLayers/store/types'; @@ -42,7 +43,7 @@ const DEFAULT_MASK_COLORS: RgbColor[] = [ ]; const getRGMaskFill = (state: CanvasV2State): RgbColor => { - const lastFill = state.regions.entities.slice(-1)[0]?.fill; + const lastFill = state.regions.entities.slice(-1)[0]?.fill.color; let i = DEFAULT_MASK_COLORS.findIndex((c) => isEqual(c, lastFill)); if (i === -1) { i = 0; @@ -63,7 +64,10 @@ export const regionsReducers = { type: 'regional_guidance', isEnabled: true, objects: [], - fill: getRGMaskFill(state), + fill: { + style: 'solid', + color: getRGMaskFill(state), + }, position: { x: 0, y: 0 }, autoNegative: 'invert', positivePrompt: '', @@ -100,14 +104,23 @@ export const regionsReducers = { } entity.negativePrompt = prompt; }, - rgFillChanged: (state, action: PayloadAction<{ id: string; fill: RgbColor }>) => { - const { id, fill } = action.payload; + rgFillColorChanged: (state, action: PayloadAction<{ id: string; color: RgbColor }>) => { + const { id, color } = action.payload; const entity = selectRegionalGuidanceEntity(state, id); if (!entity) { return; } - entity.fill = fill; + entity.fill.color = color; }, + rgFillStyleChanged: (state, action: PayloadAction<{ id: string; style: FillStyle }>) => { + const { id, style } = action.payload; + const entity = selectRegionalGuidanceEntity(state, id); + if (!entity) { + return; + } + entity.fill.style = style; + }, + rgAutoNegativeChanged: (state, action: PayloadAction<{ id: string; autoNegative: ParameterAutoNegative }>) => { const { id, autoNegative } = action.payload; const rg = selectRegionalGuidanceEntity(state, id); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index a6d9115386e..3598d4a412a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -641,6 +641,12 @@ const zMaskObject = z }) .pipe(z.discriminatedUnion('type', [zCanvasBrushLineState, zCanvasEraserLineState, zCanvasRectState])); +const zFillStyle = z.enum(['solid', 'grid', 'crosshatch', 'diagonal', 'horizontal', 'vertical']); +export type FillStyle = z.infer; +export const isFillStyle = (v: unknown): v is FillStyle => zFillStyle.safeParse(v).success; +const zFill = z.object({ style: zFillStyle, color: zRgbColor }); +export type Fill = z.infer; + const zImageCache = z.object({ imageName: z.string(), rect: zRect, @@ -665,7 +671,7 @@ export const zCanvasRegionalGuidanceState = z.object({ isEnabled: z.boolean(), position: zCoordinate, objects: z.array(zCanvasObjectState), - fill: zRgbColor, + fill: zFill, positivePrompt: zParameterPositivePrompt.nullable(), negativePrompt: zParameterNegativePrompt.nullable(), ipAdapters: z.array(zRegionalGuidanceIPAdapterConfig), @@ -674,21 +680,12 @@ export const zCanvasRegionalGuidanceState = z.object({ }); export type CanvasRegionalGuidanceState = z.infer; -const zColorFill = z.object({ - type: z.literal('color_fill'), - color: zRgbaColor, -}); -const zImageFill = z.object({ - type: z.literal('image_fill'), - src: z.string(), -}); -const zFill = z.discriminatedUnion('type', [zColorFill, zImageFill]); const zCanvasInpaintMaskState = z.object({ id: z.literal('inpaint_mask'), type: z.literal('inpaint_mask'), isEnabled: z.boolean(), position: zCoordinate, - fill: zRgbColor, + fill: zFill, objects: z.array(zCanvasObjectState), rasterizationCache: z.array(zImageCache), }); From d2b5d6342c2f241bd7a0dc5389cd212d49a0aa8e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 16 Aug 2024 19:56:01 +1000 Subject: [PATCH 363/678] feat(ui): mask layers choose own opacity --- .../ControlLayersSettingsPopover.tsx | 2 - .../InpaintMaskMaskFillColorPicker.tsx | 9 ++-- .../controlLayers/components/MaskOpacity.tsx | 49 ------------------- .../RegionalGuidanceMaskFillColorPicker.tsx | 9 ++-- .../controlLayers/konva/CanvasManager.ts | 2 - .../controlLayers/konva/CanvasMaskAdapter.ts | 17 +------ .../konva/CanvasObjectRenderer.ts | 11 ++--- .../controlLayers/konva/CanvasStateApi.ts | 20 +++----- .../konva/patterns/getPatternSVG.ts | 8 +-- .../controlLayers/store/canvasV2Slice.ts | 8 ++- .../store/inpaintMaskReducers.ts | 5 +- .../controlLayers/store/regionsReducers.ts | 23 ++++----- .../controlLayers/store/settingsReducers.ts | 3 -- .../src/features/controlLayers/store/types.ts | 3 +- 14 files changed, 44 insertions(+), 125 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/MaskOpacity.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx index 11da129a8eb..8a7eb0b98e6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx @@ -12,7 +12,6 @@ import { } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { MaskOpacity } from 'features/controlLayers/components/MaskOpacity'; import { $canvasManager } from 'features/controlLayers/konva/CanvasManager'; import { clipToBboxChanged, @@ -64,7 +63,6 @@ const ControlLayersSettingsPopover = () => { - {t('unifiedCanvas.invertBrushSizeScrollDirection')} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx index f7d834bfbc6..42951878b4f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx @@ -1,13 +1,12 @@ import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import RgbColorPicker from 'common/components/RgbColorPicker'; +import IAIColorPicker from 'common/components/IAIColorPicker'; import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { stopPropagation } from 'common/util/stopPropagation'; import { MaskFillStyle } from 'features/controlLayers/components/common/MaskFillStyle'; import { imFillColorChanged, imFillStyleChanged } from 'features/controlLayers/store/canvasV2Slice'; -import type { FillStyle } from 'features/controlLayers/store/types'; +import type { FillStyle, RgbaColor } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; -import type { RgbColor } from 'react-colorful'; import { useTranslation } from 'react-i18next'; export const InpaintMaskMaskFillColorPicker = memo(() => { @@ -15,7 +14,7 @@ export const InpaintMaskMaskFillColorPicker = memo(() => { const dispatch = useAppDispatch(); const fill = useAppSelector((s) => s.canvasV2.inpaintMask.fill); const onChangeFillColor = useCallback( - (color: RgbColor) => { + (color: RgbaColor) => { dispatch(imFillColorChanged({ color })); }, [dispatch] @@ -44,7 +43,7 @@ export const InpaintMaskMaskFillColorPicker = memo(() => { - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/MaskOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/MaskOpacity.tsx deleted file mode 100644 index d3372f4faea..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/MaskOpacity.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { maskOpacityChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -const marks = [0, 25, 50, 75, 100]; -const formatPct = (v: number | string) => `${v} %`; - -export const MaskOpacity = memo(() => { - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - const opacity = useAppSelector((s) => Math.round(s.canvasV2.settings.maskOpacity * 100)); - const onChange = useCallback( - (v: number) => { - dispatch(maskOpacityChanged(Math.max(v / 100, 0.25))); - }, - [dispatch] - ); - return ( - - {t('controlLayers.globalMaskOpacity')} - - - - - - ); -}); - -MaskOpacity.displayName = 'MaskOpacity'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx index 85bd4758f63..4f1780d6f6e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx @@ -1,15 +1,14 @@ import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import RgbColorPicker from 'common/components/RgbColorPicker'; +import IAIColorPicker from 'common/components/IAIColorPicker'; import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { stopPropagation } from 'common/util/stopPropagation'; import { MaskFillStyle } from 'features/controlLayers/components/common/MaskFillStyle'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { rgFillColorChanged, rgFillStyleChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers'; -import type { FillStyle } from 'features/controlLayers/store/types'; +import type { FillStyle, RgbaColor } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; -import type { RgbColor } from 'react-colorful'; import { useTranslation } from 'react-i18next'; export const RegionalGuidanceMaskFillColorPicker = memo(() => { @@ -18,7 +17,7 @@ export const RegionalGuidanceMaskFillColorPicker = memo(() => { const dispatch = useAppDispatch(); const fill = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, entityIdentifier.id).fill); const onChangeFillColor = useCallback( - (color: RgbColor) => { + (color: RgbaColor) => { dispatch(rgFillColorChanged({ id: entityIdentifier.id, color })); }, [dispatch, entityIdentifier.id] @@ -47,7 +46,7 @@ export const RegionalGuidanceMaskFillColorPicker = memo(() => { - + diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 3df53ee23ce..ef0e8fe7813 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -323,7 +323,6 @@ export class CanvasManager { if ( this._isFirstRender || state.regions.entities !== this._prevState.regions.entities || - state.settings.maskOpacity !== this._prevState.settings.maskOpacity || state.tool.selected !== this._prevState.tool.selected || state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id ) { @@ -355,7 +354,6 @@ export class CanvasManager { if ( this._isFirstRender || state.inpaintMask !== this._prevState.inpaintMask || - state.settings.maskOpacity !== this._prevState.settings.maskOpacity || state.tool.selected !== this._prevState.tool.selected || state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id ) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts index 550e15fa002..da70ab1fa68 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts @@ -22,7 +22,6 @@ export class CanvasMaskAdapter { log: Logger; state: CanvasInpaintMaskState | CanvasRegionalGuidanceState; - maskOpacity: number; transformer: CanvasTransformer; renderer: CanvasObjectRenderer; @@ -54,8 +53,6 @@ export class CanvasMaskAdapter { this.renderer = new CanvasObjectRenderer(this); this.transformer = new CanvasTransformer(this); - - this.maskOpacity = this.manager.stateApi.getMaskOpacity(); } /** @@ -79,14 +76,8 @@ export class CanvasMaskAdapter { isSelected: boolean; }) => { const state = get(arg, 'state', this.state); - const maskOpacity = this.manager.stateApi.getMaskOpacity(); - - if ( - !this.isFirstRender && - state === this.state && - state.fill === this.state.fill && - maskOpacity === this.maskOpacity - ) { + + if (!this.isFirstRender && state === this.state && state.fill === this.state.fill) { this.log.trace('State unchanged, skipping update'); return; } @@ -107,10 +98,6 @@ export class CanvasMaskAdapter { this.updateVisibility({ isEnabled }); } - if (this.isFirstRender || state.fill !== this.state.fill || maskOpacity !== this.maskOpacity) { - this.renderer.updateCompositingRect(state.fill, maskOpacity); - this.maskOpacity = maskOpacity; - } // this.transformer.syncInteractionState(); if (this.isFirstRender) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index d0a409ab94c..9fd475ffa45 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -1,5 +1,5 @@ import type { JSONObject } from 'common/types'; -import { rgbColorToString } from 'common/util/colorCodeTransformers'; +import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; import { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine'; import { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine'; @@ -149,7 +149,7 @@ export class CanvasObjectRenderer { this.subscriptions.add( this.manager.stateApi.$stageAttrs.listen(() => { if (this.konva.compositing && this.parent.type === 'mask_adapter') { - this.updateCompositingRect(this.parent.state.fill, this.manager.stateApi.getMaskOpacity()); + this.updateCompositingRect(this.parent.state.fill); } }) ); @@ -183,14 +183,13 @@ export class CanvasObjectRenderer { return didRender; }; - updateCompositingRect = (fill: Fill, opacity: number) => { + updateCompositingRect = (fill: Fill) => { this.log.trace('Updating compositing rect'); assert(this.konva.compositing, 'Missing compositing rect'); const { x, y, width, height, scale } = this.manager.stateApi.$stageAttrs.get(); - console.log('stageAttrs', this.manager.stateApi.$stageAttrs.get()); + const attrs: RectConfig = { - opacity, x: -x / scale, y: -y / scale, width: width / scale, @@ -198,7 +197,7 @@ export class CanvasObjectRenderer { }; if (fill.style === 'solid') { - attrs.fill = rgbColorToString(fill.color); + attrs.fill = rgbaColorToString(fill.color); attrs.fillPriority = 'color'; this.konva.compositing.rect.setAttrs(attrs); } else { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index e96e6c3ea07..262e28dce52 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -171,9 +171,6 @@ export class CanvasStateApi { getInpaintMaskState = () => { return this.getState().inpaintMask; }; - getMaskOpacity = () => { - return this.getState().settings.maskOpacity; - }; getSession = () => { return this.getState().session; }; @@ -232,26 +229,23 @@ export class CanvasStateApi { let currentFill: RgbaColor = state.tool.fill; const selectedEntity = this.getSelectedEntity(); if (selectedEntity) { - // These two entity types use a compositing rect for opacity. Their fill is always white. + // These two entity types use a compositing rect for opacity. Their fill is always a solid color. if (selectedEntity.state.type === 'regional_guidance' || selectedEntity.state.type === 'inpaint_mask') { currentFill = RGBA_RED; - // currentFill = RGBA_WHITE; } } return currentFill; }; - getBrushPreviewFill = () => { - const state = this.getState(); - let currentFill: RgbaColor = state.tool.fill; + getBrushPreviewFill = (): RgbaColor => { const selectedEntity = this.getSelectedEntity(); - if (selectedEntity) { + if (selectedEntity?.state.type === 'regional_guidance' || selectedEntity?.state.type === 'inpaint_mask') { // The brush should use the mask opacity for these entity types - if (selectedEntity.state.type === 'regional_guidance' || selectedEntity.state.type === 'inpaint_mask') { - currentFill = { ...selectedEntity.state.fill.color, a: this.getSettings().maskOpacity }; - } + return selectedEntity.state.fill.color; + } else { + const state = this.getState(); + return state.tool.fill; } - return currentFill; }; $transformingEntity = $transformingEntity; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/patterns/getPatternSVG.ts b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/getPatternSVG.ts index 2836b139794..aaf4a2c5775 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/patterns/getPatternSVG.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/getPatternSVG.ts @@ -1,5 +1,5 @@ -import { rgbColorToString } from 'common/util/colorCodeTransformers'; -import type { FillStyle, RgbColor } from 'features/controlLayers/store/types'; +import { rgbaColorToString } from 'common/util/colorCodeTransformers'; +import type { FillStyle, RgbaColor } from 'features/controlLayers/store/types'; import crosshatch from './pattern-crosshatch.svg?raw'; import diagonal from './pattern-diagonal.svg?raw'; @@ -7,7 +7,7 @@ import grid from './pattern-grid.svg?raw'; import horizontal from './pattern-horizontal.svg?raw'; import vertical from './pattern-vertical.svg?raw'; -export function getPatternSVG(pattern: Exclude, color: RgbColor) { +export function getPatternSVG(pattern: Exclude, color: RgbaColor) { let content: string = 'data:image/svg+xml;utf8,'; if (pattern === 'crosshatch') { content += crosshatch; @@ -21,7 +21,7 @@ export function getPatternSVG(pattern: Exclude, color: RgbCo content += grid; } - content = content.replaceAll('stroke:black', `stroke:${rgbColorToString(color)}`); + content = content.replaceAll('stroke:black', `stroke:${rgbaColorToString(color)}`); return content; } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 3f9d53b6a5d..55a6fdb2cad 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -40,7 +40,7 @@ import type { FilterConfig, StageAttrs, } from './types'; -import { IMAGE_FILTERS, isDrawableEntity, RGBA_RED } from './types'; +import { IMAGE_FILTERS, isDrawableEntity } from './types'; const initialState: CanvasV2State = { _version: 3, @@ -55,7 +55,7 @@ const initialState: CanvasV2State = { type: 'inpaint_mask', fill: { style: 'diagonal', - color: RGBA_RED, + color: { r: 255, g: 122, b: 0, a: 1 }, // some orange color }, rasterizationCache: [], isEnabled: true, @@ -69,7 +69,7 @@ const initialState: CanvasV2State = { selected: 'view', selectedBuffer: null, invertScroll: false, - fill: RGBA_RED, + fill: { r: 31, g: 160, b: 224, a: 1 }, // invokeBlue.500 brush: { width: 50, }, @@ -87,7 +87,6 @@ const initialState: CanvasV2State = { }, }, settings: { - maskOpacity: 0.3, // TODO(psyche): These are copied from old canvas state, need to be implemented autoSave: false, imageSmoothing: true, @@ -471,7 +470,6 @@ export const { invertScrollChanged, toolChanged, toolBufferChanged, - maskOpacityChanged, allEntitiesDeleted, clipToBboxChanged, canvasReset, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts index b1a954ca5cf..03f9d600618 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts @@ -1,6 +1,5 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import type { CanvasInpaintMaskState, CanvasV2State, FillStyle } from 'features/controlLayers/store/types'; -import type { RgbColor } from 'react-colorful'; +import type { CanvasInpaintMaskState, CanvasV2State, FillStyle, RgbaColor } from 'features/controlLayers/store/types'; export const inpaintMaskReducers = { imRecalled: (state, action: PayloadAction<{ data: CanvasInpaintMaskState }>) => { @@ -8,7 +7,7 @@ export const inpaintMaskReducers = { state.inpaintMask = data; state.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id }; }, - imFillColorChanged: (state, action: PayloadAction<{ color: RgbColor }>) => { + imFillColorChanged: (state, action: PayloadAction<{ color: RgbaColor }>) => { const { color } = action.payload; state.inpaintMask.fill.color = color; }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index 6aa4a171c40..aebd1cb410a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -6,6 +6,7 @@ import type { FillStyle, IPMethodV2, RegionalGuidanceIPAdapterConfig, + RgbaColor, } from 'features/controlLayers/store/types'; import { imageDTOToImageWithDims } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; @@ -14,7 +15,7 @@ import { isEqual } from 'lodash-es'; import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; -import type { CanvasRegionalGuidanceState, RgbColor } from './types'; +import type { CanvasRegionalGuidanceState } from './types'; export const selectRegionalGuidanceEntity = (state: CanvasV2State, id: string) => { return state.regions.entities.find((rg) => rg.id === id); @@ -32,17 +33,17 @@ export const selectRegionalGuidanceEntityOrThrow = (state: CanvasV2State, id: st return rg; }; -const DEFAULT_MASK_COLORS: RgbColor[] = [ - { r: 121, g: 157, b: 219 }, // rgb(121, 157, 219) - { r: 131, g: 214, b: 131 }, // rgb(131, 214, 131) - { r: 250, g: 225, b: 80 }, // rgb(250, 225, 80) - { r: 220, g: 144, b: 101 }, // rgb(220, 144, 101) - { r: 224, g: 117, b: 117 }, // rgb(224, 117, 117) - { r: 213, g: 139, b: 202 }, // rgb(213, 139, 202) - { r: 161, g: 120, b: 214 }, // rgb(161, 120, 214) +const DEFAULT_MASK_COLORS: RgbaColor[] = [ + { r: 121, g: 157, b: 219, a: 0.5 }, // rgb(121, 157, 219) + { r: 131, g: 214, b: 131, a: 0.5 }, // rgb(131, 214, 131) + { r: 250, g: 225, b: 80, a: 0.5 }, // rgb(250, 225, 80) + { r: 220, g: 144, b: 101, a: 0.5 }, // rgb(220, 144, 101) + { r: 224, g: 117, b: 117, a: 0.5 }, // rgb(224, 117, 117) + { r: 213, g: 139, b: 202, a: 0.5 }, // rgb(213, 139, 202) + { r: 161, g: 120, b: 214, a: 0.5 }, // rgb(161, 120, 214) ]; -const getRGMaskFill = (state: CanvasV2State): RgbColor => { +const getRGMaskFill = (state: CanvasV2State): RgbaColor => { const lastFill = state.regions.entities.slice(-1)[0]?.fill.color; let i = DEFAULT_MASK_COLORS.findIndex((c) => isEqual(c, lastFill)); if (i === -1) { @@ -104,7 +105,7 @@ export const regionsReducers = { } entity.negativePrompt = prompt; }, - rgFillColorChanged: (state, action: PayloadAction<{ id: string; color: RgbColor }>) => { + rgFillColorChanged: (state, action: PayloadAction<{ id: string; color: RgbaColor }>) => { const { id, color } = action.payload; const entity = selectRegionalGuidanceEntity(state, id); if (!entity) { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/settingsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/settingsReducers.ts index d9f9a8d3e7c..cecdd381350 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/settingsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/settingsReducers.ts @@ -2,9 +2,6 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import type { CanvasV2State } from 'features/controlLayers/store/types'; export const settingsReducers = { - maskOpacityChanged: (state, action: PayloadAction) => { - state.settings.maskOpacity = action.payload; - }, clipToBboxChanged: (state, action: PayloadAction) => { state.settings.clipToBbox = action.payload; }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 3598d4a412a..3d37342a077 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -644,7 +644,7 @@ const zMaskObject = z const zFillStyle = z.enum(['solid', 'grid', 'crosshatch', 'diagonal', 'horizontal', 'vertical']); export type FillStyle = z.infer; export const isFillStyle = (v: unknown): v is FillStyle => zFillStyle.safeParse(v).success; -const zFill = z.object({ style: zFillStyle, color: zRgbColor }); +const zFill = z.object({ style: zFillStyle, color: zRgbaColor }); export type Fill = z.infer; const zImageCache = z.object({ @@ -858,7 +858,6 @@ export type CanvasV2State = { }; settings: { imageSmoothing: boolean; - maskOpacity: number; showHUD: boolean; autoSave: boolean; preserveMaskedArea: boolean; From 13fc539f8b11c65a249cbd4ed1f635e2676df030 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 16 Aug 2024 20:59:32 +1000 Subject: [PATCH 364/678] feat(ui): add canvas background style --- invokeai/frontend/web/public/locales/en.json | 6 +++ .../CanvasSettingsBackgroundStyle.tsx | 50 +++++++++++++++++++ .../ControlLayersSettingsPopover.tsx | 2 + .../components/StageComponent.tsx | 15 ++++++ .../controlLayers/konva/CanvasBackground.ts | 9 ++++ .../controlLayers/konva/CanvasManager.ts | 7 +++ .../controlLayers/store/canvasV2Slice.ts | 2 + .../controlLayers/store/settingsReducers.ts | 5 +- .../src/features/controlLayers/store/types.ts | 6 +++ 9 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasSettingsBackgroundStyle.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 1060dc7891a..719707faa2e 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1721,6 +1721,12 @@ "view": "View", "transform": "Transform", "eyeDropper": "Eye Dropper" + }, + "background": { + "backgroundStyle": "Background Style", + "solid": "Solid", + "checkerboard": "Checkerboard", + "dynamicGrid": "Dynamic Grid" } }, "upscaling": { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasSettingsBackgroundStyle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasSettingsBackgroundStyle.tsx new file mode 100644 index 00000000000..20ea323680c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasSettingsBackgroundStyle.tsx @@ -0,0 +1,50 @@ +import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; +import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { canvasBackgroundStyleChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { isCanvasBackgroundStyle } from 'features/controlLayers/store/types'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const CanvasSettingsBackgroundStyle = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const canvasBackgroundStyle = useAppSelector((s) => s.canvasV2.settings.canvasBackgroundStyle); + const onChange = useCallback( + (v) => { + if (!isCanvasBackgroundStyle(v?.value)) { + return; + } + dispatch(canvasBackgroundStyleChanged(v.value)); + }, + [dispatch] + ); + + const options = useMemo(() => { + return [ + { + value: 'solid', + label: t('controlLayers.background.solid'), + }, + { + value: 'checkerboard', + label: t('controlLayers.background.checkerboard'), + }, + { + value: 'dynamicGrid', + label: t('controlLayers.background.dynamicGrid'), + }, + ]; + }, [t]); + + const value = useMemo(() => options.find((o) => o.value === canvasBackgroundStyle), [options, canvasBackgroundStyle]); + + return ( + + {t('controlLayers.background.backgroundStyle')} + + + ); +}); + +CanvasSettingsBackgroundStyle.displayName = 'CanvasSettingsBackgroundStyle'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx index 8a7eb0b98e6..a6dca5eb90b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx @@ -12,6 +12,7 @@ import { } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { CanvasSettingsBackgroundStyle } from 'features/controlLayers/components/CanvasSettingsBackgroundStyle'; import { $canvasManager } from 'features/controlLayers/konva/CanvasManager'; import { clipToBboxChanged, @@ -71,6 +72,7 @@ const ControlLayersSettingsPopover = () => { {t('unifiedCanvas.clipToBbox')} + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index ae8e217df81..9870e3c72d9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -3,8 +3,10 @@ import { useStore } from '@nanostores/react'; import { $socket } from 'app/hooks/useSocketIO'; import { logger } from 'app/logging/logger'; import { useAppStore } from 'app/store/nanostores/store'; +import { useAppSelector } from 'app/store/storeHooks'; import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay'; import { $canvasManager, CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants'; import Konva from 'konva'; import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react'; import { useDevicePixelRatio } from 'use-device-pixel-ratio'; @@ -52,6 +54,8 @@ type Props = { }; export const StageComponent = memo(({ asPreview = false }: Props) => { + const canvasBackgroundStyle = useAppSelector((s) => s.canvasV2.settings.canvasBackgroundStyle); + const [stage] = useState( () => new Konva.Stage({ @@ -77,6 +81,17 @@ export const StageComponent = memo(({ asPreview = false }: Props) => { return ( + {canvasBackgroundStyle === 'checkerboard' && ( + + )} ) => { state.settings.clipToBbox = action.payload; }, + canvasBackgroundStyleChanged: (state, action: PayloadAction) => { + state.settings.canvasBackgroundStyle = action.payload; + }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 3d37342a077..ea010ff06b4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -839,6 +839,11 @@ export type StagingAreaImage = { offsetY: number; }; +const zCanvasBackgroundStyle = z.enum(['checkerboard', 'dynamicGrid', 'solid']); +export type CanvasBackgroundStyle = z.infer; +export const isCanvasBackgroundStyle = (v: unknown): v is CanvasBackgroundStyle => + zCanvasBackgroundStyle.safeParse(v).success; + export type CanvasV2State = { _version: 3; selectedEntityIdentifier: CanvasEntityIdentifier | null; @@ -863,6 +868,7 @@ export type CanvasV2State = { preserveMaskedArea: boolean; cropToBboxOnSave: boolean; clipToBbox: boolean; + canvasBackgroundStyle: CanvasBackgroundStyle; }; bbox: { rect: { From d6fb220b2cab3d99d1a50b1e8320773e6546f575 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 16 Aug 2024 21:52:39 +1000 Subject: [PATCH 365/678] fix(ui): update compositing rect when fill changes --- .../web/src/features/controlLayers/konva/CanvasMaskAdapter.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts index da70ab1fa68..b472b9717b9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts @@ -98,6 +98,10 @@ export class CanvasMaskAdapter { this.updateVisibility({ isEnabled }); } + if (this.isFirstRender || state.fill !== this.state.fill) { + this.renderer.updateCompositingRect(state.fill); + } + // this.transformer.syncInteractionState(); if (this.isFirstRender) { From 4e6a9d990c4e748738bb6fbea390f075e185787f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 17 Aug 2024 08:39:10 +1000 Subject: [PATCH 366/678] fix(ui): rebase conflicts --- invokeai/frontend/web/src/app/store/storeHooks.ts | 4 ++-- .../controlLayers/components/AddPromptButtons.tsx | 4 +--- .../controlLayers/hooks/useEntitySelectionColor.ts | 4 ++-- .../web/src/features/controlLayers/store/bboxReducers.ts | 3 --- .../nodes/util/graph/generation/buildSDXLGraph.ts | 4 ++-- .../features/parameters/components/Prompts/Prompts.tsx | 9 ++++----- .../stylePresets/components/ActiveStylePreset.tsx | 2 +- .../stylePresets/hooks/usePresetModifiedPrompts.ts | 3 ++- 8 files changed, 14 insertions(+), 19 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/storeHooks.ts b/invokeai/frontend/web/src/app/store/storeHooks.ts index 632ea76332e..7f93819c3ae 100644 --- a/invokeai/frontend/web/src/app/store/storeHooks.ts +++ b/invokeai/frontend/web/src/app/store/storeHooks.ts @@ -1,8 +1,8 @@ -import type { AppStore, AppThunkDispatch, RootState } from 'app/store/store'; +import type { AppThunkDispatch, RootState } from 'app/store/store'; import type { TypedUseSelectorHook } from 'react-redux'; import {useDispatch, useSelector, useStore } from 'react-redux'; // Use throughout your app instead of plain `useDispatch` and `useSelector` export const useAppDispatch = () => useDispatch(); export const useAppSelector: TypedUseSelectorHook = useSelector; -export const useAppStore = () => useStore(); +export const useAppStore = () => useStore(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx index 4b0804c9827..aa05176e380 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx @@ -40,9 +40,7 @@ export const AddPromptButtons = ({ id }: AddPromptButtonProps) => { dispatch(rgNegativePromptChanged({ id, prompt: '' })); }, [dispatch, id]); const addIPAdapter = useCallback(() => { - dispatch( - rgIPAdapterAdded({ id, ipAdapter: { ...defaultIPAdapter, id: nanoid(), type: 'ip_adapter', isEnabled: true } }) - ); + dispatch(rgIPAdapterAdded({ id, ipAdapter: { ...defaultIPAdapter, id: nanoid() } })); }, [defaultIPAdapter, dispatch, id]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntitySelectionColor.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntitySelectionColor.ts index 48f56985898..2815577bff6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntitySelectionColor.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntitySelectionColor.ts @@ -13,9 +13,9 @@ export const useEntitySelectionColor = (entityIdentifier: CanvasEntityIdentifier if (!entity) { return 'base.400'; } else if (entity.type === 'inpaint_mask') { - return rgbColorToString(entity.fill); + return rgbColorToString(entity.fill.color); } else if (entity.type === 'regional_guidance') { - return rgbColorToString(entity.fill); + return rgbColorToString(entity.fill.color); } else { return 'base.400'; } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts index 8ac6d4ea4fb..d414144a3cf 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts @@ -12,12 +12,10 @@ import { pick } from 'lodash-es'; export const bboxReducers = { bboxScaledSizeChanged: (state, action: PayloadAction>) => { - state.rasterLayers.imageCache = null; state.bbox.scaledSize = { ...state.bbox.scaledSize, ...action.payload }; }, bboxScaleMethodChanged: (state, action: PayloadAction) => { state.bbox.scaleMethod = action.payload; - state.rasterLayers.imageCache = null; if (action.payload === 'auto') { const optimalDimension = getOptimalDimension(state.params.model); @@ -27,7 +25,6 @@ export const bboxReducers = { }, bboxChanged: (state, action: PayloadAction) => { state.bbox.rect = action.payload; - state.rasterLayers.imageCache = null; if (state.bbox.scaleMethod === 'auto') { const optimalDimension = getOptimalDimension(state.params.model); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index 98fadc83e2e..031873c1e91 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -27,7 +27,7 @@ import { addSeamless } from 'features/nodes/util/graph/generation/addSeamless'; import { addTextToImage } from 'features/nodes/util/graph/generation/addTextToImage'; import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermarker'; import { Graph } from 'features/nodes/util/graph/generation/Graph'; -import { getBoardField, getSDXLStylePrompts, getSizes } from 'features/nodes/util/graph/graphBuilderUtils'; +import { getBoardField, getPresetModifiedPrompts, getSizes } from 'features/nodes/util/graph/graphBuilderUtils'; import type { Invocation } from 'services/api/types'; import { isNonRefinerMainModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; @@ -61,7 +61,7 @@ export const buildSDXLGraph = async (state: RootState, manager: CanvasManager): const { originalSize, scaledSize } = getSizes(bbox); - const { positiveStylePrompt, negativeStylePrompt } = getSDXLStylePrompts(state); + const { positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state); const g = new Graph(SDXL_CONTROL_LAYERS_GRAPH); const modelLoader = g.addNode({ diff --git a/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx b/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx index c41f929ae9f..44286d517b2 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx @@ -1,18 +1,17 @@ import { Flex } from '@invoke-ai/ui-library'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { ParamNegativePrompt } from 'features/parameters/components/Core/ParamNegativePrompt'; import { ParamPositivePrompt } from 'features/parameters/components/Core/ParamPositivePrompt'; -import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; import { ParamSDXLNegativeStylePrompt } from 'features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt'; import { ParamSDXLPositiveStylePrompt } from 'features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt'; import { memo } from 'react'; const concatPromptsSelector = createSelector( - [selectGenerationSlice, selectControlLayersSlice], - (generation, controlLayers) => { - return generation.model?.base !== 'sdxl' || controlLayers.present.shouldConcatPrompts; + [selectCanvasV2Slice], + (canvasV2) => { + return canvasV2.params.model?.base !== 'sdxl' || canvasV2.params.shouldConcatPrompts; } ); diff --git a/invokeai/frontend/web/src/features/stylePresets/components/ActiveStylePreset.tsx b/invokeai/frontend/web/src/features/stylePresets/components/ActiveStylePreset.tsx index 8a68ce63960..39f1dd0d3f2 100644 --- a/invokeai/frontend/web/src/features/stylePresets/components/ActiveStylePreset.tsx +++ b/invokeai/frontend/web/src/features/stylePresets/components/ActiveStylePreset.tsx @@ -1,6 +1,6 @@ import { Badge, Flex, IconButton, Spacer, Text, Tooltip } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { negativePromptChanged, positivePromptChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { negativePromptChanged, positivePromptChanged } from 'features/controlLayers/store/canvasV2Slice'; import { usePresetModifiedPrompts } from 'features/stylePresets/hooks/usePresetModifiedPrompts'; import { activeStylePresetIdChanged, viewModeChanged } from 'features/stylePresets/store/stylePresetSlice'; import type { MouseEventHandler } from 'react'; diff --git a/invokeai/frontend/web/src/features/stylePresets/hooks/usePresetModifiedPrompts.ts b/invokeai/frontend/web/src/features/stylePresets/hooks/usePresetModifiedPrompts.ts index 4a136d1d714..661a6972315 100644 --- a/invokeai/frontend/web/src/features/stylePresets/hooks/usePresetModifiedPrompts.ts +++ b/invokeai/frontend/web/src/features/stylePresets/hooks/usePresetModifiedPrompts.ts @@ -10,7 +10,8 @@ export const buildPresetModifiedPrompt = (presetPrompt: string, currentPrompt: s }; export const usePresetModifiedPrompts = () => { - const { positivePrompt, negativePrompt } = useAppSelector((s) => s.canvasV2.prompts); + const positivePrompt = useAppSelector((s) => s.canvasV2.params.positivePrompt); + const negativePrompt = useAppSelector((s) => s.canvasV2.params.negativePrompt); const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId); From b152937f30dee033e7631529bb04ebbfd7e3bf1a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 17 Aug 2024 15:30:55 +1000 Subject: [PATCH 367/678] feat(ui): move socket event handling out of redux Download events and invocation status events (including progress images) are very frequent. There's no real need for these to pass through redux. Handling them outside redux is a significant performance win - far fewer store subscription calls, far fewer trips through middleware. All event handling is moved outside middleware. Cleanup of unused actions and listeners to follow. --- .../frontend/web/src/app/hooks/useSocketIO.ts | 10 +- .../addCommitStagingAreaImageListener.ts | 4 +- .../socketio/socketGeneratorProgress.ts | 4 +- .../listeners/socketio/socketQueueEvents.tsx | 10 +- .../src/common/hooks/useIsReadyToEnqueue.ts | 8 +- invokeai/frontend/web/src/common/types.ts | 5 + .../IPAdapter/IPAdapterImagePreview.tsx | 142 ++-- .../konva/CanvasProgressImage.ts | 2 +- .../controlLayers/konva/CanvasStagingArea.ts | 2 +- .../controlLayers/konva/CanvasStateApi.ts | 4 +- .../controlLayers/store/canvasV2Slice.ts | 2 - .../components/DeleteImageButton.tsx | 4 +- .../ImageViewer/CurrentImageButtons.tsx | 24 +- .../ImageViewer/CurrentImagePreview.tsx | 4 +- .../components/ImageViewer/ProgressImage.tsx | 12 +- .../nodes/CurrentImage/CurrentImageNode.tsx | 27 +- .../inputs/ImageFieldInputComponent.tsx | 6 +- .../features/queue/hooks/useCancelBatch.ts | 5 +- .../queue/hooks/useCancelCurrentQueueItem.ts | 5 +- .../queue/hooks/useCancelQueueItem.ts | 5 +- .../queue/hooks/useClearInvocationCache.ts | 5 +- .../src/features/queue/hooks/useClearQueue.ts | 6 +- .../queue/hooks/useDisableInvocationCache.ts | 5 +- .../queue/hooks/useEnableInvocationCache.ts | 5 +- .../features/queue/hooks/usePauseProcessor.ts | 5 +- .../src/features/queue/hooks/usePruneQueue.ts | 6 +- .../queue/hooks/useResumeProcessor.ts | 5 +- .../system/components/ProgressBar.tsx | 26 +- .../system/components/StatusIndicator.tsx | 5 +- .../src/services/events/setEventListeners.ts | 136 ---- .../src/services/events/setEventListeners.tsx | 621 ++++++++++++++++++ 31 files changed, 809 insertions(+), 301 deletions(-) delete mode 100644 invokeai/frontend/web/src/services/events/setEventListeners.ts create mode 100644 invokeai/frontend/web/src/services/events/setEventListeners.tsx diff --git a/invokeai/frontend/web/src/app/hooks/useSocketIO.ts b/invokeai/frontend/web/src/app/hooks/useSocketIO.ts index 8a530b8229b..89cb1ae1721 100644 --- a/invokeai/frontend/web/src/app/hooks/useSocketIO.ts +++ b/invokeai/frontend/web/src/app/hooks/useSocketIO.ts @@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react'; import { $authToken } from 'app/store/nanostores/authToken'; import { $baseUrl } from 'app/store/nanostores/baseUrl'; import { $isDebugging } from 'app/store/nanostores/isDebugging'; -import { useAppDispatch } from 'app/store/storeHooks'; +import { useAppStore } from 'app/store/nanostores/store'; import type { MapStore } from 'nanostores'; import { atom, map } from 'nanostores'; import { useEffect, useMemo } from 'react'; @@ -28,13 +28,15 @@ export const getSocket = () => { return socket; }; export const $socketOptions = map>({}); + const $isSocketInitialized = atom(false); +export const $isConnected = atom(false); /** * Initializes the socket.io connection and sets up event listeners. */ export const useSocketIO = () => { - const dispatch = useAppDispatch(); + const { dispatch, getState } = useAppStore(); const baseUrl = useStore($baseUrl); const authToken = useStore($authToken); const addlSocketOptions = useStore($socketOptions); @@ -72,7 +74,7 @@ export const useSocketIO = () => { const socket: AppSocket = io(socketUrl, socketOptions); $socket.set(socket); - setEventListeners({ dispatch, socket }); + setEventListeners({ socket, dispatch, getState, setIsConnected: $isConnected.set }); socket.connect(); if ($isDebugging.get() || import.meta.env.MODE === 'development') { @@ -94,5 +96,5 @@ export const useSocketIO = () => { socket.disconnect(); $isSocketInitialized.set(false); }; - }, [dispatch, socketOptions, socketUrl]); + }, [dispatch, getState, socketOptions, socketUrl]); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts index d6cb10ff43c..6b8d9782ca3 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts @@ -1,7 +1,6 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { - $lastProgressEvent, rasterLayerAdded, sessionStagingAreaImageAccepted, sessionStagingAreaReset, @@ -11,6 +10,7 @@ import { imageDTOToImageObject } from 'features/controlLayers/store/types'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { queueApi } from 'services/api/endpoints/queue'; +import { $lastCanvasProgressEvent } from 'services/events/setEventListeners'; import { assert } from 'tsafe'; export const addStagingListeners = (startAppListening: AppStartListening) => { @@ -29,7 +29,7 @@ export const addStagingListeners = (startAppListening: AppStartListening) => { const { canceled } = await req.unwrap(); req.reset(); - $lastProgressEvent.set(null); + $lastCanvasProgressEvent.set(null); if (canceled > 0) { log.debug(`Canceled ${canceled} canvas batches`); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts index e28235da594..1aff46d0a3b 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts @@ -2,10 +2,10 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { deepClone } from 'common/util/deepClone'; import { parseify } from 'common/util/serialize'; -import { $lastProgressEvent } from 'features/controlLayers/store/canvasV2Slice'; import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; import { zNodeStatus } from 'features/nodes/types/invocation'; import { socketGeneratorProgress } from 'services/events/actions'; +import { $lastCanvasProgressEvent } from 'services/events/setEventListeners'; const log = logger('socketio'); @@ -27,7 +27,7 @@ export const addGeneratorProgressEventListener = (startAppListening: AppStartLis } if (origin === 'canvas') { - $lastProgressEvent.set(action.payload.data); + $lastCanvasProgressEvent.set(action.payload.data); } }, }); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueEvents.tsx b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueEvents.tsx index 5ba1013bb78..0b37104ca6d 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueEvents.tsx +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueEvents.tsx @@ -1,7 +1,6 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { deepClone } from 'common/util/deepClone'; -import { $lastProgressEvent } from 'features/controlLayers/store/canvasV2Slice'; import { $nodeExecutionStates } from 'features/nodes/hooks/useExecutionState'; import { zNodeStatus } from 'features/nodes/types/invocation'; import ErrorToastDescription, { getTitleFromErrorType } from 'features/toast/ErrorToastDescription'; @@ -9,6 +8,7 @@ import { toast } from 'features/toast/toast'; import { forEach } from 'lodash-es'; import { queueApi, queueItemsAdapter } from 'services/api/endpoints/queue'; import { socketQueueItemStatusChanged } from 'services/events/actions'; +import { $lastCanvasProgressEvent } from 'services/events/setEventListeners'; const log = logger('socketio'); @@ -17,13 +17,13 @@ export const addSocketQueueEventsListeners = (startAppListening: AppStartListeni startAppListening({ matcher: queueApi.endpoints.clearQueue.matchFulfilled, effect: () => { - $lastProgressEvent.set(null); + $lastCanvasProgressEvent.set(null); }, }); startAppListening({ actionCreator: socketQueueItemStatusChanged, - effect: async (action, { dispatch, getState }) => { + effect: (action, { dispatch, getState }) => { // we've got new status for the queue item, batch and queue const { item_id, @@ -103,7 +103,7 @@ export const addSocketQueueEventsListeners = (startAppListening: AppStartListeni const isLocal = getState().config.isLocal ?? true; const sessionId = session_id; if (origin === 'canvas') { - $lastProgressEvent.set(null); + $lastCanvasProgressEvent.set(null); } toast({ @@ -122,7 +122,7 @@ export const addSocketQueueEventsListeners = (startAppListening: AppStartListeni ), }); } else if (status === 'canceled' && origin === 'canvas') { - $lastProgressEvent.set(null); + $lastCanvasProgressEvent.set(null); } }, }); diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index eb2ff5bc279..d30ee3f9645 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -1,4 +1,5 @@ import { useStore } from '@nanostores/react'; +import { $isConnected } from 'app/hooks/useSocketIO'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; @@ -25,7 +26,7 @@ const LAYER_TYPE_TO_TKEY = { control_layer: 'controlLayers.globalControlAdapter', } as const; -const createSelector = (templates: Templates) => +const createSelector = (templates: Templates, isConnected: boolean) => createMemoizedSelector( [ selectSystemSlice, @@ -41,8 +42,6 @@ const createSelector = (templates: Templates) => const { bbox } = canvasV2; const { model, positivePrompt } = canvasV2.params; - const { isConnected } = system; - const reasons: { prefix?: string; content: string }[] = []; // Cannot generate if not connected @@ -240,7 +239,8 @@ const createSelector = (templates: Templates) => export const useIsReadyToEnqueue = () => { const templates = useStore($templates); - const selector = useMemo(() => createSelector(templates), [templates]); + const isConnected = useStore($isConnected) + const selector = useMemo(() => createSelector(templates, isConnected), [templates, isConnected]); const value = useAppSelector(selector); return value; }; diff --git a/invokeai/frontend/web/src/common/types.ts b/invokeai/frontend/web/src/common/types.ts index f3037dcc2be..dd23638b8f9 100644 --- a/invokeai/frontend/web/src/common/types.ts +++ b/invokeai/frontend/web/src/common/types.ts @@ -3,3 +3,8 @@ type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string export interface JSONObject { [k: string]: JSONValue; } + +type SerializableValue = string | number | boolean | null | undefined | SerializableValue[] | SerializableObject; +export type SerializableObject = { + [k: string | number]: SerializableValue; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterImagePreview.tsx index 9e76aa1b917..e1f6b078570 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterImagePreview.tsx @@ -1,5 +1,7 @@ import { Flex, useShiftModifier } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { skipToken } from '@reduxjs/toolkit/query'; +import { $isConnected } from 'app/hooks/useSocketIO'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; @@ -22,79 +24,85 @@ type Props = { postUploadAction: PostUploadAction; }; -export const IPAdapterImagePreview = memo(({ image, onChangeImage, ipAdapterId, droppableData, postUploadAction }: Props) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const isConnected = useAppSelector((s) => s.system.isConnected); - const optimalDimension = useAppSelector(selectOptimalDimension); - const shift = useShiftModifier(); +export const IPAdapterImagePreview = memo( + ({ image, onChangeImage, ipAdapterId, droppableData, postUploadAction }: Props) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isConnected = useStore($isConnected); + const optimalDimension = useAppSelector(selectOptimalDimension); + const shift = useShiftModifier(); - const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(image?.image_name ?? skipToken); - const handleResetControlImage = useCallback(() => { - onChangeImage(null); - }, [onChangeImage]); + const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery( + image?.image_name ?? skipToken + ); + const handleResetControlImage = useCallback(() => { + onChangeImage(null); + }, [onChangeImage]); - const handleSetControlImageToDimensions = useCallback(() => { - if (!controlImage) { - return; - } + const handleSetControlImageToDimensions = useCallback(() => { + if (!controlImage) { + return; + } - const options = { updateAspectRatio: true, clamp: true }; - if (shift) { - const { width, height } = controlImage; - dispatch(bboxWidthChanged({ width, ...options })); - dispatch(bboxHeightChanged({ height, ...options })); - } else { - const { width, height } = calculateNewSize( - controlImage.width / controlImage.height, - optimalDimension * optimalDimension - ); - dispatch(bboxWidthChanged({ width, ...options })); - dispatch(bboxHeightChanged({ height, ...options })); - } - }, [controlImage, dispatch, optimalDimension, shift]); + const options = { updateAspectRatio: true, clamp: true }; + if (shift) { + const { width, height } = controlImage; + dispatch(bboxWidthChanged({ width, ...options })); + dispatch(bboxHeightChanged({ height, ...options })); + } else { + const { width, height } = calculateNewSize( + controlImage.width / controlImage.height, + optimalDimension * optimalDimension + ); + dispatch(bboxWidthChanged({ width, ...options })); + dispatch(bboxHeightChanged({ height, ...options })); + } + }, [controlImage, dispatch, optimalDimension, shift]); - const draggableData = useMemo(() => { - if (controlImage) { - return { - id: ipAdapterId, - payloadType: 'IMAGE_DTO', - payload: { imageDTO: controlImage }, - }; - } - }, [controlImage, ipAdapterId]); + const draggableData = useMemo(() => { + if (controlImage) { + return { + id: ipAdapterId, + payloadType: 'IMAGE_DTO', + payload: { imageDTO: controlImage }, + }; + } + }, [controlImage, ipAdapterId]); - useEffect(() => { - if (isConnected && isErrorControlImage) { - handleResetControlImage(); - } - }, [handleResetControlImage, isConnected, isErrorControlImage]); + useEffect(() => { + if (isConnected && isErrorControlImage) { + handleResetControlImage(); + } + }, [handleResetControlImage, isConnected, isErrorControlImage]); - return ( - - + return ( + + - {controlImage && ( - - } - tooltip={t('controlnet.resetControlImage')} - /> - } - tooltip={shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')} - /> - - )} - - ); -}); + {controlImage && ( + + } + tooltip={t('controlnet.resetControlImage')} + /> + } + tooltip={ + shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions') + } + /> + + )} + + ); + } +); IPAdapterImagePreview.displayName = 'IPAdapterImagePreview'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts index 00c796b2c27..0739c267b55 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts @@ -48,7 +48,7 @@ export class CanvasProgressImage { image: null, }; - this.manager.stateApi.$lastProgressEvent.listen((event) => { + this.manager.stateApi.$lastCanvasProgressEvent.listen((event) => { this.lastProgressEvent = event; this.render(); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts index c58186a14dd..2ce21d239e6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts @@ -76,7 +76,7 @@ export class CanvasStagingArea { if (!this.image.isLoading && !this.image.isError) { await this.image.updateImageSource(imageDTO.image_name); - this.manager.stateApi.$lastProgressEvent.set(null); + this.manager.stateApi.$lastCanvasProgressEvent.set(null); } this.image.konva.group.visible(shouldShowStagedImage); } else { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index 262e28dce52..31a2aee8b29 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -11,7 +11,6 @@ import { $lastAddedPoint, $lastCursorPos, $lastMouseDownPos, - $lastProgressEvent, $shouldShowStagedImage, $spaceKey, $stageAttrs, @@ -51,6 +50,7 @@ import type { import { RGBA_RED } from 'features/controlLayers/store/types'; import type { WritableAtom } from 'nanostores'; import { atom } from 'nanostores'; +import { $lastCanvasProgressEvent } from 'services/events/setEventListeners'; type EntityStateAndAdapter = | { @@ -263,7 +263,7 @@ export class CanvasStateApi { $lastAddedPoint = $lastAddedPoint; $lastMouseDownPos = $lastMouseDownPos; $lastCursorPos = $lastCursorPos; - $lastProgressEvent = $lastProgressEvent; + $lastCanvasProgressEvent = $lastCanvasProgressEvent; $spaceKey = $spaceKey; $altKey = $alt; $ctrlKey = $ctrl; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index bc939f08945..d0ae06c28a0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -20,7 +20,6 @@ import { initialAspectRatioState } from 'features/parameters/components/Document import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; import { isEqual, pick } from 'lodash-es'; import { atom } from 'nanostores'; -import type { InvocationDenoiseProgressEvent } from 'services/events/types'; import { assert } from 'tsafe'; import type { @@ -622,7 +621,6 @@ export const $stageAttrs = atom({ scale: 0, }); export const $shouldShowStagedImage = atom(true); -export const $lastProgressEvent = atom(null); export const $isDrawing = atom(false); export const $isMouseDown = atom(false); export const $lastAddedPoint = atom(null); diff --git a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageButton.tsx b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageButton.tsx index 6855cb8e550..452d101fa22 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageButton.tsx +++ b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageButton.tsx @@ -1,5 +1,7 @@ import type { IconButtonProps } from '@invoke-ai/ui-library'; import { IconButton } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { $isConnected } from 'app/hooks/useSocketIO'; import { useAppSelector } from 'app/store/storeHooks'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -12,7 +14,7 @@ type DeleteImageButtonProps = Omit & { export const DeleteImageButton = memo((props: DeleteImageButtonProps) => { const { onClick, isDisabled } = props; const { t } = useTranslation(); - const isConnected = useAppSelector((s) => s.system.isConnected); + const isConnected = useStore($isConnected); const imageSelectionLength: number = useAppSelector((s) => s.gallery.selection.length); const labelMessage: string = `${t('gallery.deleteImage', { count: imageSelectionLength })} (Del)`; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx index 1ef91e7e2eb..9ccd69b898a 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx @@ -1,7 +1,7 @@ import { ButtonGroup, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; -import { createSelector } from '@reduxjs/toolkit'; import { skipToken } from '@reduxjs/toolkit/query'; +import { $isConnected } from 'app/hooks/useSocketIO'; import { adHocPostProcessingRequested } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton'; @@ -10,17 +10,15 @@ import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMe import { useImageActions } from 'features/gallery/hooks/useImageActions'; import { sentImageToImg2Img } from 'features/gallery/store/actions'; import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors'; -import { selectGallerySlice } from 'features/gallery/store/gallerySlice'; import { parseAndRecallImageDimensions } from 'features/metadata/util/handlers'; import { $templates } from 'features/nodes/store/nodesSlice'; import { PostProcessingPopover } from 'features/parameters/components/PostProcessing/PostProcessingPopover'; import { useIsQueueMutationInProgress } from 'features/queue/hooks/useIsQueueMutationInProgress'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; -import { selectSystemSlice } from 'features/system/store/systemSlice'; import { setActiveTab } from 'features/ui/store/uiSlice'; import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow'; import { size } from 'lodash-es'; -import { memo, useCallback } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { @@ -33,23 +31,17 @@ import { PiRulerBold, } from 'react-icons/pi'; import { useGetImageDTOQuery } from 'services/api/endpoints/images'; - -const selectShouldDisableToolbarButtons = createSelector( - selectSystemSlice, - selectGallerySlice, - selectLastSelectedImage, - (system, gallery, lastSelectedImage) => { - const hasProgressImage = Boolean(system.denoiseProgress?.progress_image); - return hasProgressImage || !lastSelectedImage; - } -); +import { $progressImage } from 'services/events/setEventListeners'; const CurrentImageButtons = () => { const dispatch = useAppDispatch(); - const isConnected = useAppSelector((s) => s.system.isConnected); + const isConnected = useStore($isConnected); const lastSelectedImage = useAppSelector(selectLastSelectedImage); + const progressImage = useStore($progressImage); const selection = useAppSelector((s) => s.gallery.selection); - const shouldDisableToolbarButtons = useAppSelector(selectShouldDisableToolbarButtons); + const shouldDisableToolbarButtons = useMemo(() => { + return Boolean(progressImage) || !lastSelectedImage; + }, [lastSelectedImage, progressImage]); const templates = useStore($templates); const isUpscalingEnabled = useFeatureStatus('upscaling'); const isQueueMutationInProgress = useIsQueueMutationInProgress(); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx index a812391992d..23e75498ec9 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx @@ -1,4 +1,5 @@ import { Box, Flex } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { skipToken } from '@reduxjs/toolkit/query'; import { useAppSelector } from 'app/store/storeHooks'; @@ -14,6 +15,7 @@ import { memo, useCallback, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { PiImageBold } from 'react-icons/pi'; import { useGetImageDTOQuery } from 'services/api/endpoints/images'; +import { $hasProgress } from 'services/events/setEventListeners'; import ProgressImage from './ProgressImage'; @@ -26,7 +28,7 @@ const CurrentImagePreview = () => { const { t } = useTranslation(); const shouldShowImageDetails = useAppSelector((s) => s.ui.shouldShowImageDetails); const imageName = useAppSelector(selectLastSelectedImageName); - const hasDenoiseProgress = useAppSelector((s) => Boolean(s.system.denoiseProgress)); + const hasDenoiseProgress = useStore($hasProgress); const shouldShowProgressInViewer = useAppSelector((s) => s.ui.shouldShowProgressInViewer); const { currentData: imageDTO } = useGetImageDTOQuery(imageName ?? skipToken); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ProgressImage.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ProgressImage.tsx index 0ee75fbcd4c..46c1bd71c28 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ProgressImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ProgressImage.tsx @@ -1,10 +1,12 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library'; import { Image } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; import { memo, useMemo } from 'react'; +import { $progressImage } from 'services/events/setEventListeners'; const CurrentImagePreview = () => { - const progress_image = useAppSelector((s) => s.system.denoiseProgress?.progress_image); + const progressImage = useStore($progressImage); const shouldAntialiasProgressImage = useAppSelector((s) => s.system.shouldAntialiasProgressImage); const sx = useMemo( @@ -14,15 +16,15 @@ const CurrentImagePreview = () => { [shouldAntialiasProgressImage] ); - if (!progress_image) { + if (!progressImage) { return null; } return ( { - const imageDTO = gallery.selection[gallery.selection.length - 1]; - - return { - imageDTO, - progressImage: system.denoiseProgress?.progress_image, - }; -}); +import { $lastProgressEvent } from 'services/events/setEventListeners'; const CurrentImageNode = (props: NodeProps) => { - const { progressImage, imageDTO } = useAppSelector(selector); + const imageDTO = useAppSelector((s) => s.gallery.selection[s.gallery.selection.length - 1]); + const lastProgressEvent = useStore($lastProgressEvent); - if (progressImage) { + if (lastProgressEvent?.progress_image) { return ( - + ); } diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldInputComponent.tsx index c3224238c5d..1ec0b575f6c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldInputComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldInputComponent.tsx @@ -1,6 +1,8 @@ import { Flex, Text } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { skipToken } from '@reduxjs/toolkit/query'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { $isConnected } from 'app/hooks/useSocketIO'; +import { useAppDispatch } from 'app/store/storeHooks'; import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; @@ -17,7 +19,7 @@ import type { FieldComponentProps } from './types'; const ImageFieldInputComponent = (props: FieldComponentProps) => { const { nodeId, field } = props; const dispatch = useAppDispatch(); - const isConnected = useAppSelector((s) => s.system.isConnected); + const isConnected = useStore($isConnected); const { currentData: imageDTO, isError } = useGetImageDTOQuery(field.value?.image_name ?? skipToken); const handleReset = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/queue/hooks/useCancelBatch.ts b/invokeai/frontend/web/src/features/queue/hooks/useCancelBatch.ts index 9d92eabff88..d9ad1a736f5 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useCancelBatch.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useCancelBatch.ts @@ -1,11 +1,12 @@ -import { useAppSelector } from 'app/store/storeHooks'; +import { useStore } from '@nanostores/react'; +import { $isConnected } from 'app/hooks/useSocketIO'; import { toast } from 'features/toast/toast'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useCancelByBatchIdsMutation, useGetBatchStatusQuery } from 'services/api/endpoints/queue'; export const useCancelBatch = (batch_id: string) => { - const isConnected = useAppSelector((s) => s.system.isConnected); + const isConnected = useStore($isConnected); const { isCanceled } = useGetBatchStatusQuery( { batch_id }, { diff --git a/invokeai/frontend/web/src/features/queue/hooks/useCancelCurrentQueueItem.ts b/invokeai/frontend/web/src/features/queue/hooks/useCancelCurrentQueueItem.ts index 057490ed99c..9ae8e2dd2e7 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useCancelCurrentQueueItem.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useCancelCurrentQueueItem.ts @@ -1,4 +1,5 @@ -import { useAppSelector } from 'app/store/storeHooks'; +import { useStore } from '@nanostores/react'; +import { $isConnected } from 'app/hooks/useSocketIO'; import { toast } from 'features/toast/toast'; import { isNil } from 'lodash-es'; import { useCallback, useMemo } from 'react'; @@ -6,7 +7,7 @@ import { useTranslation } from 'react-i18next'; import { useCancelQueueItemMutation, useGetQueueStatusQuery } from 'services/api/endpoints/queue'; export const useCancelCurrentQueueItem = () => { - const isConnected = useAppSelector((s) => s.system.isConnected); + const isConnected = useStore($isConnected); const { data: queueStatus } = useGetQueueStatusQuery(); const [trigger, { isLoading }] = useCancelQueueItemMutation(); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItem.ts b/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItem.ts index 268eca75ccd..bf0af41605a 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItem.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useCancelQueueItem.ts @@ -1,11 +1,12 @@ -import { useAppSelector } from 'app/store/storeHooks'; +import { useStore } from '@nanostores/react'; +import { $isConnected } from 'app/hooks/useSocketIO'; import { toast } from 'features/toast/toast'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useCancelQueueItemMutation } from 'services/api/endpoints/queue'; export const useCancelQueueItem = (item_id: number) => { - const isConnected = useAppSelector((s) => s.system.isConnected); + const isConnected = useStore($isConnected); const [trigger, { isLoading }] = useCancelQueueItemMutation(); const { t } = useTranslation(); const cancelQueueItem = useCallback(async () => { diff --git a/invokeai/frontend/web/src/features/queue/hooks/useClearInvocationCache.ts b/invokeai/frontend/web/src/features/queue/hooks/useClearInvocationCache.ts index 7ef9d93742b..d177a72f5f4 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useClearInvocationCache.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useClearInvocationCache.ts @@ -1,4 +1,5 @@ -import { useAppSelector } from 'app/store/storeHooks'; +import { useStore } from '@nanostores/react'; +import { $isConnected } from 'app/hooks/useSocketIO'; import { toast } from 'features/toast/toast'; import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -7,7 +8,7 @@ import { useClearInvocationCacheMutation, useGetInvocationCacheStatusQuery } fro export const useClearInvocationCache = () => { const { t } = useTranslation(); const { data: cacheStatus } = useGetInvocationCacheStatusQuery(); - const isConnected = useAppSelector((s) => s.system.isConnected); + const isConnected = useStore($isConnected); const [trigger, { isLoading }] = useClearInvocationCacheMutation({ fixedCacheKey: 'clearInvocationCache', }); diff --git a/invokeai/frontend/web/src/features/queue/hooks/useClearQueue.ts b/invokeai/frontend/web/src/features/queue/hooks/useClearQueue.ts index ca7d1e4894f..bb80f7aa104 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useClearQueue.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useClearQueue.ts @@ -1,4 +1,6 @@ -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useStore } from '@nanostores/react'; +import { $isConnected } from 'app/hooks/useSocketIO'; +import { useAppDispatch } from 'app/store/storeHooks'; import { listCursorChanged, listPriorityChanged } from 'features/queue/store/queueSlice'; import { toast } from 'features/toast/toast'; import { useCallback, useMemo } from 'react'; @@ -9,7 +11,7 @@ export const useClearQueue = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const { data: queueStatus } = useGetQueueStatusQuery(); - const isConnected = useAppSelector((s) => s.system.isConnected); + const isConnected = useStore($isConnected); const [trigger, { isLoading }] = useClearQueueMutation({ fixedCacheKey: 'clearQueue', }); diff --git a/invokeai/frontend/web/src/features/queue/hooks/useDisableInvocationCache.ts b/invokeai/frontend/web/src/features/queue/hooks/useDisableInvocationCache.ts index 371e9198e73..cf71e4bd4ba 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useDisableInvocationCache.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useDisableInvocationCache.ts @@ -1,4 +1,5 @@ -import { useAppSelector } from 'app/store/storeHooks'; +import { useStore } from '@nanostores/react'; +import { $isConnected } from 'app/hooks/useSocketIO'; import { toast } from 'features/toast/toast'; import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -7,7 +8,7 @@ import { useDisableInvocationCacheMutation, useGetInvocationCacheStatusQuery } f export const useDisableInvocationCache = () => { const { t } = useTranslation(); const { data: cacheStatus } = useGetInvocationCacheStatusQuery(); - const isConnected = useAppSelector((s) => s.system.isConnected); + const isConnected = useStore($isConnected); const [trigger, { isLoading }] = useDisableInvocationCacheMutation({ fixedCacheKey: 'disableInvocationCache', }); diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnableInvocationCache.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnableInvocationCache.ts index fb39cf7347a..7f28bddd787 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useEnableInvocationCache.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useEnableInvocationCache.ts @@ -1,4 +1,5 @@ -import { useAppSelector } from 'app/store/storeHooks'; +import { useStore } from '@nanostores/react'; +import { $isConnected } from 'app/hooks/useSocketIO'; import { toast } from 'features/toast/toast'; import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -7,7 +8,7 @@ import { useEnableInvocationCacheMutation, useGetInvocationCacheStatusQuery } fr export const useEnableInvocationCache = () => { const { t } = useTranslation(); const { data: cacheStatus } = useGetInvocationCacheStatusQuery(); - const isConnected = useAppSelector((s) => s.system.isConnected); + const isConnected = useStore($isConnected); const [trigger, { isLoading }] = useEnableInvocationCacheMutation({ fixedCacheKey: 'enableInvocationCache', }); diff --git a/invokeai/frontend/web/src/features/queue/hooks/usePauseProcessor.ts b/invokeai/frontend/web/src/features/queue/hooks/usePauseProcessor.ts index f5424c6b18c..d25c8051e50 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/usePauseProcessor.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/usePauseProcessor.ts @@ -1,4 +1,5 @@ -import { useAppSelector } from 'app/store/storeHooks'; +import { useStore } from '@nanostores/react'; +import { $isConnected } from 'app/hooks/useSocketIO'; import { toast } from 'features/toast/toast'; import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -6,7 +7,7 @@ import { useGetQueueStatusQuery, usePauseProcessorMutation } from 'services/api/ export const usePauseProcessor = () => { const { t } = useTranslation(); - const isConnected = useAppSelector((s) => s.system.isConnected); + const isConnected = useStore($isConnected); const { data: queueStatus } = useGetQueueStatusQuery(); const [trigger, { isLoading }] = usePauseProcessorMutation({ fixedCacheKey: 'pauseProcessor', diff --git a/invokeai/frontend/web/src/features/queue/hooks/usePruneQueue.ts b/invokeai/frontend/web/src/features/queue/hooks/usePruneQueue.ts index eaeabe5423f..f9426291bea 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/usePruneQueue.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/usePruneQueue.ts @@ -1,4 +1,6 @@ -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useStore } from '@nanostores/react'; +import { $isConnected } from 'app/hooks/useSocketIO'; +import { useAppDispatch } from 'app/store/storeHooks'; import { listCursorChanged, listPriorityChanged } from 'features/queue/store/queueSlice'; import { toast } from 'features/toast/toast'; import { useCallback, useMemo } from 'react'; @@ -8,7 +10,7 @@ import { useGetQueueStatusQuery, usePruneQueueMutation } from 'services/api/endp export const usePruneQueue = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const isConnected = useAppSelector((s) => s.system.isConnected); + const isConnected = useStore($isConnected); const [trigger, { isLoading }] = usePruneQueueMutation({ fixedCacheKey: 'pruneQueue', }); diff --git a/invokeai/frontend/web/src/features/queue/hooks/useResumeProcessor.ts b/invokeai/frontend/web/src/features/queue/hooks/useResumeProcessor.ts index 851b268416e..72d787103b3 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useResumeProcessor.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useResumeProcessor.ts @@ -1,11 +1,12 @@ -import { useAppSelector } from 'app/store/storeHooks'; +import { useStore } from '@nanostores/react'; +import { $isConnected } from 'app/hooks/useSocketIO'; import { toast } from 'features/toast/toast'; import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useGetQueueStatusQuery, useResumeProcessorMutation } from 'services/api/endpoints/queue'; export const useResumeProcessor = () => { - const isConnected = useAppSelector((s) => s.system.isConnected); + const isConnected = useStore($isConnected); const { data: queueStatus } = useGetQueueStatusQuery(); const { t } = useTranslation(); const [trigger, { isLoading }] = useResumeProcessorMutation({ diff --git a/invokeai/frontend/web/src/features/system/components/ProgressBar.tsx b/invokeai/frontend/web/src/features/system/components/ProgressBar.tsx index 43894318133..06c7e70c7f5 100644 --- a/invokeai/frontend/web/src/features/system/components/ProgressBar.tsx +++ b/invokeai/frontend/web/src/features/system/components/ProgressBar.tsx @@ -1,28 +1,28 @@ import { Progress } from '@invoke-ai/ui-library'; -import { createSelector } from '@reduxjs/toolkit'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectSystemSlice } from 'features/system/store/systemSlice'; -import { memo } from 'react'; +import { useStore } from '@nanostores/react'; +import { $isConnected } from 'app/hooks/useSocketIO'; +import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useGetQueueStatusQuery } from 'services/api/endpoints/queue'; - -const selectProgressValue = createSelector( - selectSystemSlice, - (system) => (system.denoiseProgress?.percentage ?? 0) * 100 -); +import { $lastProgressEvent } from 'services/events/setEventListeners'; const ProgressBar = () => { const { t } = useTranslation(); const { data: queueStatus } = useGetQueueStatusQuery(); - const isConnected = useAppSelector((s) => s.system.isConnected); - const hasSteps = useAppSelector((s) => Boolean(s.system.denoiseProgress)); - const value = useAppSelector(selectProgressValue); + const isConnected = useStore($isConnected); + const lastProgressEvent = useStore($lastProgressEvent); + const value = useMemo(() => { + if (!lastProgressEvent) { + return 0; + } + return (lastProgressEvent.percentage ?? 0) * 100; + }, [lastProgressEvent]); return ( { - const isConnected = useAppSelector((s) => s.system.isConnected); + const isConnected = useStore($isConnected); const { t } = useTranslation(); if (!isConnected) { diff --git a/invokeai/frontend/web/src/services/events/setEventListeners.ts b/invokeai/frontend/web/src/services/events/setEventListeners.ts deleted file mode 100644 index 8c8c9da2e8c..00000000000 --- a/invokeai/frontend/web/src/services/events/setEventListeners.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { $baseUrl } from 'app/store/nanostores/baseUrl'; -import { $bulkDownloadId } from 'app/store/nanostores/bulkDownloadId'; -import { $queueId } from 'app/store/nanostores/queueId'; -import type { AppDispatch } from 'app/store/store'; -import { toast } from 'features/toast/toast'; -import { - socketBatchEnqueued, - socketBulkDownloadComplete, - socketBulkDownloadError, - socketBulkDownloadStarted, - socketConnected, - socketDisconnected, - socketDownloadCancelled, - socketDownloadComplete, - socketDownloadError, - socketDownloadProgress, - socketDownloadStarted, - socketGeneratorProgress, - socketInvocationComplete, - socketInvocationError, - socketInvocationStarted, - socketModelInstallCancelled, - socketModelInstallComplete, - socketModelInstallDownloadProgress, - socketModelInstallDownloadsComplete, - socketModelInstallError, - socketModelInstallStarted, - socketModelLoadComplete, - socketModelLoadStarted, - socketQueueCleared, - socketQueueItemStatusChanged, -} from 'services/events/actions'; -import type { ClientToServerEvents, ServerToClientEvents } from 'services/events/types'; -import type { Socket } from 'socket.io-client'; - -type SetEventListenersArg = { - socket: Socket; - dispatch: AppDispatch; -}; - -export const setEventListeners = ({ socket, dispatch }: SetEventListenersArg) => { - socket.on('connect', () => { - dispatch(socketConnected()); - const queue_id = $queueId.get(); - socket.emit('subscribe_queue', { queue_id }); - if (!$baseUrl.get()) { - const bulk_download_id = $bulkDownloadId.get(); - socket.emit('subscribe_bulk_download', { bulk_download_id }); - } - }); - socket.on('connect_error', (error) => { - if (error && error.message) { - const data: string | undefined = (error as unknown as { data: string | undefined }).data; - if (data === 'ERR_UNAUTHENTICATED') { - toast({ - id: `connect-error-${error.message}`, - title: error.message, - status: 'error', - duration: 10000, - }); - } - } - }); - socket.on('disconnect', () => { - dispatch(socketDisconnected()); - }); - socket.on('invocation_started', (data) => { - dispatch(socketInvocationStarted({ data })); - }); - socket.on('invocation_denoise_progress', (data) => { - dispatch(socketGeneratorProgress({ data })); - }); - socket.on('invocation_error', (data) => { - dispatch(socketInvocationError({ data })); - }); - socket.on('invocation_complete', (data) => { - dispatch(socketInvocationComplete({ data })); - }); - socket.on('model_load_started', (data) => { - dispatch(socketModelLoadStarted({ data })); - }); - socket.on('model_load_complete', (data) => { - dispatch(socketModelLoadComplete({ data })); - }); - socket.on('download_started', (data) => { - dispatch(socketDownloadStarted({ data })); - }); - socket.on('download_progress', (data) => { - dispatch(socketDownloadProgress({ data })); - }); - socket.on('download_complete', (data) => { - dispatch(socketDownloadComplete({ data })); - }); - socket.on('download_cancelled', (data) => { - dispatch(socketDownloadCancelled({ data })); - }); - socket.on('download_error', (data) => { - dispatch(socketDownloadError({ data })); - }); - socket.on('model_install_started', (data) => { - dispatch(socketModelInstallStarted({ data })); - }); - socket.on('model_install_download_progress', (data) => { - dispatch(socketModelInstallDownloadProgress({ data })); - }); - socket.on('model_install_downloads_complete', (data) => { - dispatch(socketModelInstallDownloadsComplete({ data })); - }); - socket.on('model_install_complete', (data) => { - dispatch(socketModelInstallComplete({ data })); - }); - socket.on('model_install_error', (data) => { - dispatch(socketModelInstallError({ data })); - }); - socket.on('model_install_cancelled', (data) => { - dispatch(socketModelInstallCancelled({ data })); - }); - socket.on('queue_item_status_changed', (data) => { - dispatch(socketQueueItemStatusChanged({ data })); - }); - socket.on('queue_cleared', (data) => { - dispatch(socketQueueCleared({ data })); - }); - socket.on('batch_enqueued', (data) => { - dispatch(socketBatchEnqueued({ data })); - }); - socket.on('bulk_download_started', (data) => { - dispatch(socketBulkDownloadStarted({ data })); - }); - socket.on('bulk_download_complete', (data) => { - dispatch(socketBulkDownloadComplete({ data })); - }); - socket.on('bulk_download_error', (data) => { - dispatch(socketBulkDownloadError({ data })); - }); -}; diff --git a/invokeai/frontend/web/src/services/events/setEventListeners.tsx b/invokeai/frontend/web/src/services/events/setEventListeners.tsx new file mode 100644 index 00000000000..379b2800329 --- /dev/null +++ b/invokeai/frontend/web/src/services/events/setEventListeners.tsx @@ -0,0 +1,621 @@ +import { ExternalLink } from '@invoke-ai/ui-library'; +import { logger } from 'app/logging/logger'; +import { $baseUrl } from 'app/store/nanostores/baseUrl'; +import { $bulkDownloadId } from 'app/store/nanostores/bulkDownloadId'; +import { $queueId } from 'app/store/nanostores/queueId'; +import type { AppDispatch, RootState } from 'app/store/store'; +import type { SerializableObject } from 'common/types'; +import { deepClone } from 'common/util/deepClone'; +import { sessionImageStaged } from 'features/controlLayers/store/canvasV2Slice'; +import { boardIdSelected, galleryViewChanged, imageSelected, offsetChanged } from 'features/gallery/store/gallerySlice'; +import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; +import { zNodeStatus } from 'features/nodes/types/invocation'; +import ErrorToastDescription, { getTitleFromErrorType } from 'features/toast/ErrorToastDescription'; +import { toast } from 'features/toast/toast'; +import { t } from 'i18next'; +import { forEach } from 'lodash-es'; +import { atom, computed } from 'nanostores'; +import { api, LIST_TAG } from 'services/api'; +import { boardsApi } from 'services/api/endpoints/boards'; +import { imagesApi } from 'services/api/endpoints/images'; +import { modelsApi } from 'services/api/endpoints/models'; +import { queueApi, queueItemsAdapter } from 'services/api/endpoints/queue'; +import { getCategories, getListImagesUrl } from 'services/api/util'; +import { socketConnected } from 'services/events/actions'; +import type { ClientToServerEvents, InvocationDenoiseProgressEvent, ServerToClientEvents } from 'services/events/types'; +import type { Socket } from 'socket.io-client'; + +const log = logger('socketio'); + +type SetEventListenersArg = { + socket: Socket; + dispatch: AppDispatch; + getState: () => RootState; + setIsConnected: (isConnected: boolean) => void; +}; + +const selectModelInstalls = modelsApi.endpoints.listModelInstalls.select(); +const nodeTypeDenylist = ['load_image', 'image']; +export const $lastProgressEvent = atom(null); +export const $lastCanvasProgressEvent = atom(null); +export const $hasProgress = computed($lastProgressEvent, (val) => Boolean(val)); +export const $progressImage = computed($lastProgressEvent, (val) => val?.progress_image ?? null); +const cancellations = new Set(); + +export const setEventListeners = ({ socket, dispatch, getState, setIsConnected }: SetEventListenersArg) => { + socket.on('connect', () => { + log.debug('Connected'); + setIsConnected(true); + dispatch(socketConnected()); + const queue_id = $queueId.get(); + socket.emit('subscribe_queue', { queue_id }); + if (!$baseUrl.get()) { + const bulk_download_id = $bulkDownloadId.get(); + socket.emit('subscribe_bulk_download', { bulk_download_id }); + } + $lastProgressEvent.set(null); + $lastCanvasProgressEvent.set(null); + cancellations.clear(); + }); + + socket.on('connect_error', (error) => { + log.debug('Connect error'); + setIsConnected(false); + $lastProgressEvent.set(null); + $lastCanvasProgressEvent.set(null); + if (error && error.message) { + const data: string | undefined = (error as unknown as { data: string | undefined }).data; + if (data === 'ERR_UNAUTHENTICATED') { + toast({ + id: `connect-error-${error.message}`, + title: error.message, + status: 'error', + duration: 10000, + }); + } + } + cancellations.clear(); + }); + + socket.on('disconnect', () => { + log.debug('Disconnected'); + $lastProgressEvent.set(null); + $lastCanvasProgressEvent.set(null); + setIsConnected(false); + cancellations.clear(); + }); + + socket.on('invocation_started', (data) => { + const { invocation_source_id, invocation } = data; + log.debug({ data } as SerializableObject, `Invocation started (${invocation.type}, ${invocation_source_id})`); + const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]); + if (nes) { + nes.status = zNodeStatus.enum.IN_PROGRESS; + upsertExecutionState(nes.nodeId, nes); + } + cancellations.clear(); + }); + + socket.on('invocation_denoise_progress', (data) => { + const { invocation_source_id, invocation, step, total_steps, progress_image, origin, percentage, session_id } = + data; + + if (cancellations.has(session_id)) { + // Do not update the progress if this session has been cancelled. This prevents a race condition where we get a + // progress update after the session has been cancelled. + return; + } + + log.trace( + { data } as SerializableObject, + `Denoise ${Math.round(percentage * 100)}% (${invocation.type}, ${invocation_source_id})` + ); + + $lastProgressEvent.set(data); + + if (origin === 'workflows') { + const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]); + if (nes) { + nes.status = zNodeStatus.enum.IN_PROGRESS; + nes.progress = (step + 1) / total_steps; + nes.progressImage = progress_image ?? null; + upsertExecutionState(nes.nodeId, nes); + } + } + + if (origin === 'canvas') { + $lastCanvasProgressEvent.set(data); + } + }); + + socket.on('invocation_error', (data) => { + const { invocation_source_id, invocation, error_type, error_message, error_traceback } = data; + log.error({ data } as SerializableObject, `Invocation error (${invocation.type}, ${invocation_source_id})`); + const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]); + if (nes) { + nes.status = zNodeStatus.enum.FAILED; + nes.progress = null; + nes.progressImage = null; + nes.error = { + error_type, + error_message, + error_traceback, + }; + upsertExecutionState(nes.nodeId, nes); + } + }); + + socket.on('invocation_complete', async (data) => { + log.debug( + { data } as SerializableObject, + `Invocation complete (${data.invocation.type}, ${data.invocation_source_id})` + ); + + const { result, invocation_source_id } = data; + + if (data.origin === 'workflows') { + const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]); + if (nes) { + nes.status = zNodeStatus.enum.COMPLETED; + if (nes.progress !== null) { + nes.progress = 1; + } + nes.outputs.push(result); + upsertExecutionState(nes.nodeId, nes); + } + } + + // This complete event has an associated image output + if ( + (data.result.type === 'image_output' || data.result.type === 'canvas_v2_mask_and_crop_output') && + !nodeTypeDenylist.includes(data.invocation.type) + ) { + const { image_name } = data.result.image; + const { gallery, canvasV2 } = getState(); + + // This populates the `getImageDTO` cache + const imageDTORequest = dispatch( + imagesApi.endpoints.getImageDTO.initiate(image_name, { + forceRefetch: true, + }) + ); + + const imageDTO = await imageDTORequest.unwrap(); + imageDTORequest.unsubscribe(); + + // handle tab-specific logic + if (data.origin === 'canvas' && data.invocation_source_id === 'canvas_output') { + if (data.result.type === 'canvas_v2_mask_and_crop_output') { + const { offset_x, offset_y } = data.result; + if (canvasV2.session.isStaging) { + dispatch(sessionImageStaged({ stagingAreaImage: { imageDTO, offsetX: offset_x, offsetY: offset_y } })); + } + } else if (data.result.type === 'image_output') { + if (canvasV2.session.isStaging) { + dispatch(sessionImageStaged({ stagingAreaImage: { imageDTO, offsetX: 0, offsetY: 0 } })); + } + } + } + + if (!imageDTO.is_intermediate) { + // update the total images for the board + dispatch( + boardsApi.util.updateQueryData('getBoardImagesTotal', imageDTO.board_id ?? 'none', (draft) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + draft.total += 1; + }) + ); + + dispatch( + imagesApi.util.invalidateTags([ + { type: 'Board', id: imageDTO.board_id ?? 'none' }, + { + type: 'ImageList', + id: getListImagesUrl({ + board_id: imageDTO.board_id ?? 'none', + categories: getCategories(imageDTO), + }), + }, + ]) + ); + + const { shouldAutoSwitch } = gallery; + + // If auto-switch is enabled, select the new image + if (shouldAutoSwitch) { + // if auto-add is enabled, switch the gallery view and board if needed as the image comes in + if (gallery.galleryView !== 'images') { + dispatch(galleryViewChanged('images')); + } + + if (imageDTO.board_id && imageDTO.board_id !== gallery.selectedBoardId) { + dispatch( + boardIdSelected({ + boardId: imageDTO.board_id, + selectedImageName: imageDTO.image_name, + }) + ); + } + + dispatch(offsetChanged({ offset: 0 })); + + if (!imageDTO.board_id && gallery.selectedBoardId !== 'none') { + dispatch( + boardIdSelected({ + boardId: 'none', + selectedImageName: imageDTO.image_name, + }) + ); + } + + dispatch(imageSelected(imageDTO)); + } + } + } + + $lastProgressEvent.set(null); + }); + + socket.on('model_load_started', (data) => { + const { config, submodel_type } = data; + const { name, base, type } = config; + + const extras: string[] = [base, type]; + + if (submodel_type) { + extras.push(submodel_type); + } + + const message = `Model load started: ${name} (${extras.join(', ')})`; + + log.debug({ data }, message); + }); + + socket.on('model_load_complete', (data) => { + const { config, submodel_type } = data; + const { name, base, type } = config; + + const extras: string[] = [base, type]; + if (submodel_type) { + extras.push(submodel_type); + } + + const message = `Model load complete: ${name} (${extras.join(', ')})`; + + log.debug({ data }, message); + }); + + socket.on('download_started', (data) => { + log.debug({ data }, 'Download started'); + }); + + socket.on('download_progress', (data) => { + log.trace({ data }, 'Download progress'); + }); + + socket.on('download_complete', (data) => { + log.debug({ data }, 'Download complete'); + }); + + socket.on('download_cancelled', (data) => { + log.warn({ data }, 'Download cancelled'); + }); + + socket.on('download_error', (data) => { + log.error({ data }, 'Download error'); + }); + + socket.on('model_install_started', (data) => { + log.debug({ data }, 'Model install started'); + + const { id } = data; + const installs = selectModelInstalls(getState()).data; + + if (!installs?.find((install) => install.id === id)) { + dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }])); + } else { + dispatch( + modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => { + const modelImport = draft.find((m) => m.id === id); + if (modelImport) { + modelImport.status = 'running'; + } + return draft; + }) + ); + } + }); + + socket.on('model_install_download_started', (data) => { + log.debug({ data }, 'Model install download started'); + + const { id } = data; + const installs = selectModelInstalls(getState()).data; + + if (!installs?.find((install) => install.id === id)) { + dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }])); + } else { + dispatch( + modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => { + const modelImport = draft.find((m) => m.id === id); + if (modelImport) { + modelImport.status = 'downloading'; + } + return draft; + }) + ); + } + }); + + socket.on('model_install_download_progress', (data) => { + log.trace({ data }, 'Model install download progress'); + + const { bytes, total_bytes, id } = data; + const installs = selectModelInstalls(getState()).data; + + if (!installs?.find((install) => install.id === id)) { + dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }])); + } else { + dispatch( + modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => { + const modelImport = draft.find((m) => m.id === id); + if (modelImport) { + modelImport.bytes = bytes; + modelImport.total_bytes = total_bytes; + modelImport.status = 'downloading'; + } + return draft; + }) + ); + } + }); + + socket.on('model_install_downloads_complete', (data) => { + log.debug({ data }, 'Model install downloads complete'); + + const { id } = data; + const installs = selectModelInstalls(getState()).data; + + if (!installs?.find((install) => install.id === id)) { + dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }])); + } else { + dispatch( + modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => { + const modelImport = draft.find((m) => m.id === id); + if (modelImport) { + modelImport.status = 'downloads_done'; + } + return draft; + }) + ); + } + }); + + socket.on('model_install_complete', (data) => { + log.debug({ data }, 'Model install complete'); + + const { id } = data; + + const installs = selectModelInstalls(getState()).data; + + if (!installs?.find((install) => install.id === id)) { + dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }])); + } else { + dispatch( + modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => { + const modelImport = draft.find((m) => m.id === id); + if (modelImport) { + modelImport.status = 'completed'; + } + return draft; + }) + ); + } + + dispatch(api.util.invalidateTags([{ type: 'ModelConfig', id: LIST_TAG }])); + dispatch(api.util.invalidateTags([{ type: 'ModelScanFolderResults', id: LIST_TAG }])); + }); + + socket.on('model_install_error', (data) => { + log.error({ data }, 'Model install error'); + + const { id, error, error_type } = data; + const installs = selectModelInstalls(getState()).data; + + if (!installs?.find((install) => install.id === id)) { + dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }])); + } else { + dispatch( + modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => { + const modelImport = draft.find((m) => m.id === id); + if (modelImport) { + modelImport.status = 'error'; + modelImport.error_reason = error_type; + modelImport.error = error; + } + return draft; + }) + ); + } + }); + + socket.on('model_install_cancelled', (data) => { + log.warn({ data }, 'Model install cancelled'); + + const { id } = data; + const installs = selectModelInstalls(getState()).data; + + if (!installs?.find((install) => install.id === id)) { + dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }])); + } else { + dispatch( + modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => { + const modelImport = draft.find((m) => m.id === id); + if (modelImport) { + modelImport.status = 'cancelled'; + } + return draft; + }) + ); + } + }); + + socket.on('queue_item_status_changed', (data) => { + // we've got new status for the queue item, batch and queue + const { + item_id, + session_id, + status, + started_at, + updated_at, + completed_at, + batch_status, + queue_status, + error_type, + error_message, + error_traceback, + origin, + } = data; + + log.debug({ data }, `Queue item ${item_id} status updated: ${status}`); + + // Update this specific queue item in the list of queue items (this is the queue item DTO, without the session) + dispatch( + queueApi.util.updateQueryData('listQueueItems', undefined, (draft) => { + queueItemsAdapter.updateOne(draft, { + id: String(item_id), + changes: { + status, + started_at, + updated_at: updated_at ?? undefined, + completed_at: completed_at ?? undefined, + error_type, + error_message, + error_traceback, + }, + }); + }) + ); + + // Update the queue status (we do not get the processor status here) + dispatch( + queueApi.util.updateQueryData('getQueueStatus', undefined, (draft) => { + if (!draft) { + return; + } + Object.assign(draft.queue, queue_status); + }) + ); + + // Update the batch status + dispatch(queueApi.util.updateQueryData('getBatchStatus', { batch_id: batch_status.batch_id }, () => batch_status)); + + // Invalidate caches for things we cannot update + // TODO: technically, we could possibly update the current session queue item, but feels safer to just request it again + dispatch( + queueApi.util.invalidateTags([ + 'CurrentSessionQueueItem', + 'NextSessionQueueItem', + 'InvocationCacheStatus', + { type: 'SessionQueueItem', id: item_id }, + ]) + ); + + if (status === 'in_progress') { + forEach($nodeExecutionStates.get(), (nes) => { + if (!nes) { + return; + } + const clone = deepClone(nes); + clone.status = zNodeStatus.enum.PENDING; + clone.error = null; + clone.progress = null; + clone.progressImage = null; + clone.outputs = []; + $nodeExecutionStates.setKey(clone.nodeId, clone); + }); + } else if (status === 'failed' && error_type) { + const isLocal = getState().config.isLocal ?? true; + const sessionId = session_id; + $lastProgressEvent.set(null); + + if (origin === 'canvas') { + $lastCanvasProgressEvent.set(null); + } + + toast({ + id: `INVOCATION_ERROR_${error_type}`, + title: getTitleFromErrorType(error_type), + status: 'error', + duration: null, + updateDescription: isLocal, + description: ( + + ), + }); + cancellations.add(session_id); + } else if (status === 'canceled') { + $lastProgressEvent.set(null); + if (origin === 'canvas') { + $lastCanvasProgressEvent.set(null); + } + cancellations.add(session_id); + } else if (status === 'completed') { + $lastProgressEvent.set(null); + cancellations.add(session_id); + } + }); + + socket.on('queue_cleared', (data) => { + log.debug({ data }, 'Queue cleared'); + }); + + socket.on('batch_enqueued', (data) => { + log.debug({ data }, 'Batch enqueued'); + }); + + socket.on('bulk_download_started', (data) => { + log.debug({ data }, 'Bulk gallery download preparation started'); + }); + + socket.on('bulk_download_complete', (data) => { + log.debug({ data }, 'Bulk gallery download ready'); + const { bulk_download_item_name } = data; + + // TODO(psyche): This URL may break in in some environments (e.g. Nvidia workbench) but we need to test it first + const url = `/api/v1/images/download/${bulk_download_item_name}`; + + toast({ + id: bulk_download_item_name, + title: t('gallery.bulkDownloadReady', 'Download ready'), + status: 'success', + description: ( + + ), + duration: null, + }); + }); + + socket.on('bulk_download_error', (data) => { + log.error({ data }, 'Bulk gallery download error'); + + const { bulk_download_item_name, error } = data; + + toast({ + id: bulk_download_item_name, + title: t('gallery.bulkDownloadFailed'), + status: 'error', + description: error, + duration: null, + }); + }); +}; From 64f50ab278b96ee49a8713f01c90a2909c3e458c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 17 Aug 2024 15:41:34 +1000 Subject: [PATCH 368/678] tidy(ui): cleanup after events change --- .../middleware/devtools/actionSanitizer.ts | 10 - .../middleware/listenerMiddleware/index.ts | 20 +- .../listeners/bulkDownload.tsx | 61 +----- .../listeners/promptChanged.ts | 2 +- .../{socketio => }/socketConnected.ts | 2 +- .../listeners/socketio/socketDisconnected.ts | 14 -- .../socketio/socketGeneratorProgress.ts | 34 --- .../socketio/socketInvocationComplete.ts | 132 ------------ .../socketio/socketInvocationError.ts | 31 --- .../socketio/socketInvocationStarted.ts | 24 --- .../listeners/socketio/socketModelInstall.ts | 196 ------------------ .../listeners/socketio/socketModelLoad.ts | 42 ---- .../listeners/socketio/socketQueueEvents.tsx | 129 ------------ .../deleteImageModal/store/selectors.ts | 2 +- .../src/features/system/store/systemSlice.ts | 92 +------- .../web/src/features/system/store/types.ts | 16 -- .../web/src/services/events/actions.ts | 66 ------ .../src/services/events/setEventListeners.tsx | 4 +- 18 files changed, 12 insertions(+), 865 deletions(-) rename invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/{socketio => }/socketConnected.ts (97%) delete mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketDisconnected.ts delete mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts delete mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts delete mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationError.ts delete mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationStarted.ts delete mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketModelInstall.ts delete mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketModelLoad.ts delete mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueEvents.tsx delete mode 100644 invokeai/frontend/web/src/services/events/actions.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts b/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts index f0ea175aec7..d130a0895b8 100644 --- a/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts +++ b/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts @@ -1,9 +1,7 @@ import type { UnknownAction } from '@reduxjs/toolkit'; -import { deepClone } from 'common/util/deepClone'; import { isAnyGraphBuilt } from 'features/nodes/store/actions'; import { appInfoApi } from 'services/api/endpoints/appInfo'; import type { Graph } from 'services/api/types'; -import { socketGeneratorProgress } from 'services/events/actions'; export const actionSanitizer = (action: A): A => { if (isAnyGraphBuilt(action)) { @@ -24,13 +22,5 @@ export const actionSanitizer = (action: A): A => { }; } - if (socketGeneratorProgress.match(action)) { - const sanitized = deepClone(action); - if (sanitized.payload.data.progress_image) { - sanitized.payload.data.progress_image.dataURL = ''; - } - return sanitized; - } - return action; }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts index c9dd883c6d8..2e12f195415 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -26,15 +26,7 @@ import { addModelSelectedListener } from 'app/store/middleware/listenerMiddlewar import { addModelsLoadedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelsLoaded'; import { addDynamicPromptsListener } from 'app/store/middleware/listenerMiddleware/listeners/promptChanged'; import { addSetDefaultSettingsListener } from 'app/store/middleware/listenerMiddleware/listeners/setDefaultSettings'; -import { addSocketConnectedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketConnected'; -import { addSocketDisconnectedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketDisconnected'; -import { addGeneratorProgressEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress'; -import { addInvocationCompleteEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete'; -import { addInvocationErrorEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationError'; -import { addInvocationStartedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationStarted'; -import { addModelInstallEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketModelInstall'; -import { addModelLoadEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketModelLoad'; -import { addSocketQueueEventsListeners } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueEvents'; +import { addSocketConnectedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketConnected'; import { addUpdateAllNodesRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested'; import { addWorkflowLoadRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested'; import type { AppDispatch, RootState } from 'app/store/store'; @@ -90,15 +82,9 @@ addBatchEnqueuedListener(startAppListening); addStagingListeners(startAppListening); // Socket.IO -addGeneratorProgressEventListener(startAppListening); -addInvocationCompleteEventListener(startAppListening); -addInvocationErrorEventListener(startAppListening); -addInvocationStartedEventListener(startAppListening); addSocketConnectedEventListener(startAppListening); -addSocketDisconnectedEventListener(startAppListening); -addModelLoadEventListener(startAppListening); -addModelInstallEventListener(startAppListening); -addSocketQueueEventsListeners(startAppListening); + +// Gallery bulk download addBulkDownloadListeners(startAppListening); // Boards diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/bulkDownload.tsx b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/bulkDownload.tsx index 489f218370c..049b28ff844 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/bulkDownload.tsx +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/bulkDownload.tsx @@ -1,21 +1,15 @@ -import { ExternalLink } from '@invoke-ai/ui-library'; import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { imagesApi } from 'services/api/endpoints/images'; -import { - socketBulkDownloadComplete, - socketBulkDownloadError, - socketBulkDownloadStarted, -} from 'services/events/actions'; const log = logger('images'); export const addBulkDownloadListeners = (startAppListening: AppStartListening) => { startAppListening({ matcher: imagesApi.endpoints.bulkDownloadImages.matchFulfilled, - effect: async (action) => { + effect: (action) => { log.debug(action.payload, 'Bulk download requested'); // If we have an item name, we are processing the bulk download locally and should use it as the toast id to @@ -33,7 +27,7 @@ export const addBulkDownloadListeners = (startAppListening: AppStartListening) = startAppListening({ matcher: imagesApi.endpoints.bulkDownloadImages.matchRejected, - effect: async () => { + effect: () => { log.debug('Bulk download request failed'); // There isn't any toast to update if we get this event. @@ -44,55 +38,4 @@ export const addBulkDownloadListeners = (startAppListening: AppStartListening) = }); }, }); - - startAppListening({ - actionCreator: socketBulkDownloadStarted, - effect: async (action) => { - // This should always happen immediately after the bulk download request, so we don't need to show a toast here. - log.debug(action.payload.data, 'Bulk download preparation started'); - }, - }); - - startAppListening({ - actionCreator: socketBulkDownloadComplete, - effect: async (action) => { - log.debug(action.payload.data, 'Bulk download preparation completed'); - - const { bulk_download_item_name } = action.payload.data; - - // TODO(psyche): This URL may break in in some environments (e.g. Nvidia workbench) but we need to test it first - const url = `/api/v1/images/download/${bulk_download_item_name}`; - - toast({ - id: bulk_download_item_name, - title: t('gallery.bulkDownloadReady', 'Download ready'), - status: 'success', - description: ( - - ), - duration: null, - }); - }, - }); - - startAppListening({ - actionCreator: socketBulkDownloadError, - effect: async (action) => { - log.debug(action.payload.data, 'Bulk download preparation failed'); - - const { bulk_download_item_name } = action.payload.data; - - toast({ - id: bulk_download_item_name, - title: t('gallery.bulkDownloadFailed'), - status: 'error', - description: action.payload.data.error, - duration: null, - }); - }, - }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts index aba3e8ecc3d..c5c2caa2277 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts @@ -15,7 +15,7 @@ import { getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilder import { activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice'; import { stylePresetsApi } from 'services/api/endpoints/stylePresets'; import { utilitiesApi } from 'services/api/endpoints/utilities'; -import { socketConnected } from 'services/events/actions'; +import { socketConnected } from 'services/events/setEventListeners'; const matcher = isAnyOf( positivePromptChanged, diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketConnected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketConnected.ts similarity index 97% rename from invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketConnected.ts rename to invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketConnected.ts index 0b2644f1243..babed47d0bc 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketConnected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketConnected.ts @@ -6,7 +6,7 @@ import { atom } from 'nanostores'; import { api } from 'services/api'; import { modelsApi } from 'services/api/endpoints/models'; import { queueApi, selectQueueStatus } from 'services/api/endpoints/queue'; -import { socketConnected } from 'services/events/actions'; +import { socketConnected } from 'services/events/setEventListeners'; const log = logger('socketio'); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketDisconnected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketDisconnected.ts deleted file mode 100644 index be1a7663b38..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketDisconnected.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { socketDisconnected } from 'services/events/actions'; - -const log = logger('socketio'); - -export const addSocketDisconnectedEventListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: socketDisconnected, - effect: () => { - log.debug('Disconnected'); - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts deleted file mode 100644 index 1aff46d0a3b..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { deepClone } from 'common/util/deepClone'; -import { parseify } from 'common/util/serialize'; -import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; -import { zNodeStatus } from 'features/nodes/types/invocation'; -import { socketGeneratorProgress } from 'services/events/actions'; -import { $lastCanvasProgressEvent } from 'services/events/setEventListeners'; - -const log = logger('socketio'); - -export const addGeneratorProgressEventListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: socketGeneratorProgress, - effect: (action) => { - const { invocation_source_id, invocation, step, total_steps, progress_image, origin } = action.payload.data; - log.trace(parseify(action.payload), `Generator progress (${invocation.type}, ${invocation_source_id})`); - - if (origin === 'workflows') { - const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]); - if (nes) { - nes.status = zNodeStatus.enum.IN_PROGRESS; - nes.progress = (step + 1) / total_steps; - nes.progressImage = progress_image ?? null; - upsertExecutionState(nes.nodeId, nes); - } - } - - if (origin === 'canvas') { - $lastCanvasProgressEvent.set(action.payload.data); - } - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts deleted file mode 100644 index 13057f9f506..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { deepClone } from 'common/util/deepClone'; -import { parseify } from 'common/util/serialize'; -import { sessionImageStaged } from 'features/controlLayers/store/canvasV2Slice'; -import { boardIdSelected, galleryViewChanged, imageSelected, offsetChanged } from 'features/gallery/store/gallerySlice'; -import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; -import { zNodeStatus } from 'features/nodes/types/invocation'; -import { boardsApi } from 'services/api/endpoints/boards'; -import { imagesApi } from 'services/api/endpoints/images'; -import { getCategories, getListImagesUrl } from 'services/api/util'; -import { socketInvocationComplete } from 'services/events/actions'; - -// These nodes output an image, but do not actually *save* an image, so we don't want to handle the gallery logic on them -const nodeTypeDenylist = ['load_image', 'image']; - -const log = logger('socketio'); - -export const addInvocationCompleteEventListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: socketInvocationComplete, - effect: async (action, { dispatch, getState }) => { - const { data } = action.payload; - log.debug( - { data: parseify(data) }, - `Invocation complete (${data.invocation.type}, ${data.invocation_source_id})` - ); - - const { result, invocation_source_id } = data; - - if (data.origin === 'workflows') { - const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]); - if (nes) { - nes.status = zNodeStatus.enum.COMPLETED; - if (nes.progress !== null) { - nes.progress = 1; - } - nes.outputs.push(result); - upsertExecutionState(nes.nodeId, nes); - } - } - - // This complete event has an associated image output - if ( - (data.result.type === 'image_output' || data.result.type === 'canvas_v2_mask_and_crop_output') && - !nodeTypeDenylist.includes(data.invocation.type) - ) { - const { image_name } = data.result.image; - const { gallery, canvasV2 } = getState(); - - // This populates the `getImageDTO` cache - const imageDTORequest = dispatch( - imagesApi.endpoints.getImageDTO.initiate(image_name, { - forceRefetch: true, - }) - ); - - const imageDTO = await imageDTORequest.unwrap(); - imageDTORequest.unsubscribe(); - - // handle tab-specific logic - if (data.origin === 'canvas' && data.invocation_source_id === 'canvas_output') { - if (data.result.type === 'canvas_v2_mask_and_crop_output') { - const { offset_x, offset_y } = data.result; - if (canvasV2.session.isStaging) { - dispatch(sessionImageStaged({ stagingAreaImage: { imageDTO, offsetX: offset_x, offsetY: offset_y } })); - } - } else if (data.result.type === 'image_output') { - if (canvasV2.session.isStaging) { - dispatch(sessionImageStaged({ stagingAreaImage: { imageDTO, offsetX: 0, offsetY: 0 } })); - } - } - } - - if (!imageDTO.is_intermediate) { - // update the total images for the board - dispatch( - boardsApi.util.updateQueryData('getBoardImagesTotal', imageDTO.board_id ?? 'none', (draft) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - draft.total += 1; - }) - ); - - dispatch( - imagesApi.util.invalidateTags([ - { type: 'Board', id: imageDTO.board_id ?? 'none' }, - { - type: 'ImageList', - id: getListImagesUrl({ - board_id: imageDTO.board_id ?? 'none', - categories: getCategories(imageDTO), - }), - }, - ]) - ); - - const { shouldAutoSwitch } = gallery; - - // If auto-switch is enabled, select the new image - if (shouldAutoSwitch) { - // if auto-add is enabled, switch the gallery view and board if needed as the image comes in - if (gallery.galleryView !== 'images') { - dispatch(galleryViewChanged('images')); - } - - if (imageDTO.board_id && imageDTO.board_id !== gallery.selectedBoardId) { - dispatch( - boardIdSelected({ - boardId: imageDTO.board_id, - selectedImageName: imageDTO.image_name, - }) - ); - } - - dispatch(offsetChanged({ offset: 0 })); - - if (!imageDTO.board_id && gallery.selectedBoardId !== 'none') { - dispatch( - boardIdSelected({ - boardId: 'none', - selectedImageName: imageDTO.image_name, - }) - ); - } - - dispatch(imageSelected(imageDTO)); - } - } - } - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationError.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationError.ts deleted file mode 100644 index cb3e1d2fa5b..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationError.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { deepClone } from 'common/util/deepClone'; -import { parseify } from 'common/util/serialize'; -import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; -import { zNodeStatus } from 'features/nodes/types/invocation'; -import { socketInvocationError } from 'services/events/actions'; - -const log = logger('socketio'); - -export const addInvocationErrorEventListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: socketInvocationError, - effect: (action) => { - const { invocation_source_id, invocation, error_type, error_message, error_traceback } = action.payload.data; - log.error(parseify(action.payload), `Invocation error (${invocation.type}, ${invocation_source_id})`); - const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]); - if (nes) { - nes.status = zNodeStatus.enum.FAILED; - nes.progress = null; - nes.progressImage = null; - nes.error = { - error_type, - error_message, - error_traceback, - }; - upsertExecutionState(nes.nodeId, nes); - } - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationStarted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationStarted.ts deleted file mode 100644 index d32a43b8f9f..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationStarted.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { deepClone } from 'common/util/deepClone'; -import { parseify } from 'common/util/serialize'; -import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; -import { zNodeStatus } from 'features/nodes/types/invocation'; -import { socketInvocationStarted } from 'services/events/actions'; - -const log = logger('socketio'); - -export const addInvocationStartedEventListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: socketInvocationStarted, - effect: (action) => { - const { invocation_source_id, invocation } = action.payload.data; - log.debug(parseify(action.payload), `Invocation started (${invocation.type}, ${invocation_source_id})`); - const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]); - if (nes) { - nes.status = zNodeStatus.enum.IN_PROGRESS; - upsertExecutionState(nes.nodeId, nes); - } - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketModelInstall.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketModelInstall.ts deleted file mode 100644 index 22ad87fbe94..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketModelInstall.ts +++ /dev/null @@ -1,196 +0,0 @@ -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { api, LIST_TAG } from 'services/api'; -import { modelsApi } from 'services/api/endpoints/models'; -import { - socketModelInstallCancelled, - socketModelInstallComplete, - socketModelInstallDownloadProgress, - socketModelInstallDownloadsComplete, - socketModelInstallDownloadStarted, - socketModelInstallError, - socketModelInstallStarted, -} from 'services/events/actions'; - -/** - * A model install has two main stages - downloading and installing. All these events are namespaced under `model_install_` - * which is a bit misleading. For example, a `model_install_started` event is actually fired _after_ the model has fully - * downloaded and is being "physically" installed. - * - * Note: the download events are only fired for remote model installs, not local. - * - * Here's the expected flow: - * - API receives install request, model manager preps the install - * - `model_install_download_started` fired when the download starts - * - `model_install_download_progress` fired continually until the download is complete - * - `model_install_download_complete` fired when the download is complete - * - `model_install_started` fired when the "physical" installation starts - * - `model_install_complete` fired when the installation is complete - * - `model_install_cancelled` fired if the installation is cancelled - * - `model_install_error` fired if the installation has an error - */ - -const selectModelInstalls = modelsApi.endpoints.listModelInstalls.select(); - -export const addModelInstallEventListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: socketModelInstallDownloadStarted, - effect: async (action, { dispatch, getState }) => { - const { id } = action.payload.data; - const { data } = selectModelInstalls(getState()); - - if (!data || !data.find((m) => m.id === id)) { - dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }])); - } else { - dispatch( - modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => { - const modelImport = draft.find((m) => m.id === id); - if (modelImport) { - modelImport.status = 'downloading'; - } - return draft; - }) - ); - } - }, - }); - - startAppListening({ - actionCreator: socketModelInstallStarted, - effect: async (action, { dispatch, getState }) => { - const { id } = action.payload.data; - const { data } = selectModelInstalls(getState()); - - if (!data || !data.find((m) => m.id === id)) { - dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }])); - } else { - dispatch( - modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => { - const modelImport = draft.find((m) => m.id === id); - if (modelImport) { - modelImport.status = 'running'; - } - return draft; - }) - ); - } - }, - }); - - startAppListening({ - actionCreator: socketModelInstallDownloadProgress, - effect: async (action, { dispatch, getState }) => { - const { bytes, total_bytes, id } = action.payload.data; - const { data } = selectModelInstalls(getState()); - - if (!data || !data.find((m) => m.id === id)) { - dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }])); - } else { - dispatch( - modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => { - const modelImport = draft.find((m) => m.id === id); - if (modelImport) { - modelImport.bytes = bytes; - modelImport.total_bytes = total_bytes; - modelImport.status = 'downloading'; - } - return draft; - }) - ); - } - }, - }); - - startAppListening({ - actionCreator: socketModelInstallComplete, - effect: (action, { dispatch, getState }) => { - const { id } = action.payload.data; - - const { data } = selectModelInstalls(getState()); - - if (!data || !data.find((m) => m.id === id)) { - dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }])); - } else { - dispatch( - modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => { - const modelImport = draft.find((m) => m.id === id); - if (modelImport) { - modelImport.status = 'completed'; - } - return draft; - }) - ); - } - - dispatch(api.util.invalidateTags([{ type: 'ModelConfig', id: LIST_TAG }])); - dispatch(api.util.invalidateTags([{ type: 'ModelScanFolderResults', id: LIST_TAG }])); - }, - }); - - startAppListening({ - actionCreator: socketModelInstallError, - effect: (action, { dispatch, getState }) => { - const { id, error, error_type } = action.payload.data; - const { data } = selectModelInstalls(getState()); - - if (!data || !data.find((m) => m.id === id)) { - dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }])); - } else { - dispatch( - modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => { - const modelImport = draft.find((m) => m.id === id); - if (modelImport) { - modelImport.status = 'error'; - modelImport.error_reason = error_type; - modelImport.error = error; - } - return draft; - }) - ); - } - }, - }); - - startAppListening({ - actionCreator: socketModelInstallCancelled, - effect: (action, { dispatch, getState }) => { - const { id } = action.payload.data; - const { data } = selectModelInstalls(getState()); - - if (!data || !data.find((m) => m.id === id)) { - dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }])); - } else { - dispatch( - modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => { - const modelImport = draft.find((m) => m.id === id); - if (modelImport) { - modelImport.status = 'cancelled'; - } - return draft; - }) - ); - } - }, - }); - - startAppListening({ - actionCreator: socketModelInstallDownloadsComplete, - effect: (action, { dispatch, getState }) => { - const { id } = action.payload.data; - const { data } = selectModelInstalls(getState()); - - if (!data || !data.find((m) => m.id === id)) { - dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }])); - } else { - dispatch( - modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => { - const modelImport = draft.find((m) => m.id === id); - if (modelImport) { - modelImport.status = 'downloads_done'; - } - return draft; - }) - ); - } - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketModelLoad.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketModelLoad.ts deleted file mode 100644 index 0240fe219a1..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketModelLoad.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { socketModelLoadComplete, socketModelLoadStarted } from 'services/events/actions'; - -const log = logger('socketio'); - -export const addModelLoadEventListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: socketModelLoadStarted, - effect: (action) => { - const { config, submodel_type } = action.payload.data; - const { name, base, type } = config; - - const extras: string[] = [base, type]; - - if (submodel_type) { - extras.push(submodel_type); - } - - const message = `Model load started: ${name} (${extras.join(', ')})`; - - log.debug(action.payload, message); - }, - }); - - startAppListening({ - actionCreator: socketModelLoadComplete, - effect: (action) => { - const { config, submodel_type } = action.payload.data; - const { name, base, type } = config; - - const extras: string[] = [base, type]; - if (submodel_type) { - extras.push(submodel_type); - } - - const message = `Model load complete: ${name} (${extras.join(', ')})`; - - log.debug(action.payload, message); - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueEvents.tsx b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueEvents.tsx deleted file mode 100644 index 0b37104ca6d..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueEvents.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { deepClone } from 'common/util/deepClone'; -import { $nodeExecutionStates } from 'features/nodes/hooks/useExecutionState'; -import { zNodeStatus } from 'features/nodes/types/invocation'; -import ErrorToastDescription, { getTitleFromErrorType } from 'features/toast/ErrorToastDescription'; -import { toast } from 'features/toast/toast'; -import { forEach } from 'lodash-es'; -import { queueApi, queueItemsAdapter } from 'services/api/endpoints/queue'; -import { socketQueueItemStatusChanged } from 'services/events/actions'; -import { $lastCanvasProgressEvent } from 'services/events/setEventListeners'; - -const log = logger('socketio'); - -export const addSocketQueueEventsListeners = (startAppListening: AppStartListening) => { - // When the queue is cleared or canvas batch is canceled, we should clear the last canvas progress event - startAppListening({ - matcher: queueApi.endpoints.clearQueue.matchFulfilled, - effect: () => { - $lastCanvasProgressEvent.set(null); - }, - }); - - startAppListening({ - actionCreator: socketQueueItemStatusChanged, - effect: (action, { dispatch, getState }) => { - // we've got new status for the queue item, batch and queue - const { - item_id, - session_id, - status, - started_at, - updated_at, - completed_at, - batch_status, - queue_status, - error_type, - error_message, - error_traceback, - origin, - } = action.payload.data; - - log.debug(action.payload, `Queue item ${item_id} status updated: ${status}`); - - // Update this specific queue item in the list of queue items (this is the queue item DTO, without the session) - dispatch( - queueApi.util.updateQueryData('listQueueItems', undefined, (draft) => { - queueItemsAdapter.updateOne(draft, { - id: String(item_id), - changes: { - status, - started_at, - updated_at: updated_at ?? undefined, - completed_at: completed_at ?? undefined, - error_type, - error_message, - error_traceback, - }, - }); - }) - ); - - // Update the queue status (we do not get the processor status here) - dispatch( - queueApi.util.updateQueryData('getQueueStatus', undefined, (draft) => { - if (!draft) { - return; - } - Object.assign(draft.queue, queue_status); - }) - ); - - // Update the batch status - dispatch( - queueApi.util.updateQueryData('getBatchStatus', { batch_id: batch_status.batch_id }, () => batch_status) - ); - - // Invalidate caches for things we cannot update - // TODO: technically, we could possibly update the current session queue item, but feels safer to just request it again - dispatch( - queueApi.util.invalidateTags([ - 'CurrentSessionQueueItem', - 'NextSessionQueueItem', - 'InvocationCacheStatus', - { type: 'SessionQueueItem', id: item_id }, - ]) - ); - - if (status === 'in_progress') { - forEach($nodeExecutionStates.get(), (nes) => { - if (!nes) { - return; - } - const clone = deepClone(nes); - clone.status = zNodeStatus.enum.PENDING; - clone.error = null; - clone.progress = null; - clone.progressImage = null; - clone.outputs = []; - $nodeExecutionStates.setKey(clone.nodeId, clone); - }); - } else if (status === 'failed' && error_type) { - const isLocal = getState().config.isLocal ?? true; - const sessionId = session_id; - if (origin === 'canvas') { - $lastCanvasProgressEvent.set(null); - } - - toast({ - id: `INVOCATION_ERROR_${error_type}`, - title: getTitleFromErrorType(error_type), - status: 'error', - duration: null, - updateDescription: isLocal, - description: ( - - ), - }); - } else if (status === 'canceled' && origin === 'canvas') { - $lastCanvasProgressEvent.set(null); - } - }, - }); -}; diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts index a9423747fe2..fc6f4b085c3 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts @@ -21,7 +21,7 @@ export const getImageUsage = (nodes: NodesState, canvasV2: CanvasV2State, image_ some(node.data.inputs, (input) => isImageFieldInputInstance(input) && input.value?.image_name === image_name) ); - const isControlAdapterImage = canvasV2.controlAdapters.entities.some( + const isControlAdapterImage = canvasV2.controlLayers.entities.some( (ca) => ca.imageObject?.image.image_name === image_name || ca.processedImageObject?.image.image_name === image_name ); diff --git a/invokeai/frontend/web/src/features/system/store/systemSlice.ts b/invokeai/frontend/web/src/features/system/store/systemSlice.ts index 3c700e683e7..0b6131c92be 100644 --- a/invokeai/frontend/web/src/features/system/store/systemSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/systemSlice.ts @@ -2,25 +2,13 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import type { LogLevelName } from 'roarr'; -import { - socketConnected, - socketDisconnected, - socketGeneratorProgress, - socketInvocationComplete, - socketInvocationStarted, - socketModelLoadComplete, - socketModelLoadStarted, - socketQueueItemStatusChanged, -} from 'services/events/actions'; import type { Language, SystemState } from './types'; const initialSystemState: SystemState = { _version: 1, - isConnected: false, shouldConfirmOnDelete: true, enableImageDebugging: false, - denoiseProgress: null, shouldAntialiasProgressImage: false, consoleLogLevel: 'debug', shouldLogToConsole: true, @@ -28,8 +16,6 @@ const initialSystemState: SystemState = { shouldUseNSFWChecker: false, shouldUseWatermarker: false, shouldEnableInformationalPopovers: true, - status: 'DISCONNECTED', - cancellations: [], }; export const systemSlice = createSlice({ @@ -64,82 +50,6 @@ export const systemSlice = createSlice({ state.shouldEnableInformationalPopovers = action.payload; }, }, - extraReducers(builder) { - /** - * Socket Connected - */ - builder.addCase(socketConnected, (state) => { - state.isConnected = true; - state.denoiseProgress = null; - state.status = 'CONNECTED'; - }); - - /** - * Socket Disconnected - */ - builder.addCase(socketDisconnected, (state) => { - state.isConnected = false; - state.denoiseProgress = null; - state.status = 'DISCONNECTED'; - }); - - /** - * Invocation Started - */ - builder.addCase(socketInvocationStarted, (state) => { - state.cancellations = []; - state.denoiseProgress = null; - state.status = 'PROCESSING'; - }); - - /** - * Generator Progress - */ - builder.addCase(socketGeneratorProgress, (state, action) => { - const { step, total_steps, progress_image, session_id, batch_id, percentage } = action.payload.data; - - if (state.cancellations.includes(session_id)) { - // Do not update the progress if this session has been cancelled. This prevents a race condition where we get a - // progress update after the session has been cancelled. - return; - } - - state.denoiseProgress = { - step, - total_steps, - percentage, - progress_image, - session_id, - batch_id, - }; - - state.status = 'PROCESSING'; - }); - - /** - * Invocation Complete - */ - builder.addCase(socketInvocationComplete, (state) => { - state.denoiseProgress = null; - state.status = 'CONNECTED'; - }); - - builder.addCase(socketModelLoadStarted, (state) => { - state.status = 'LOADING_MODEL'; - }); - - builder.addCase(socketModelLoadComplete, (state) => { - state.status = 'CONNECTED'; - }); - - builder.addCase(socketQueueItemStatusChanged, (state, action) => { - if (['completed', 'canceled', 'failed'].includes(action.payload.data.status)) { - state.status = 'CONNECTED'; - state.denoiseProgress = null; - state.cancellations.push(action.payload.data.session_id); - } - }); - }, }); export const { @@ -168,5 +78,5 @@ export const systemPersistConfig: PersistConfig = { name: systemSlice.name, initialState: initialSystemState, migrate: migrateSystemState, - persistDenylist: ['isConnected', 'denoiseProgress', 'status', 'cancellations'], + persistDenylist: [], }; diff --git a/invokeai/frontend/web/src/features/system/store/types.ts b/invokeai/frontend/web/src/features/system/store/types.ts index d896dee5f53..6aa200ebaac 100644 --- a/invokeai/frontend/web/src/features/system/store/types.ts +++ b/invokeai/frontend/web/src/features/system/store/types.ts @@ -1,18 +1,6 @@ import type { LogLevel } from 'app/logging/logger'; -import type { ProgressImage } from 'services/events/types'; import { z } from 'zod'; -type SystemStatus = 'CONNECTED' | 'DISCONNECTED' | 'PROCESSING' | 'ERROR' | 'LOADING_MODEL'; - -type DenoiseProgress = { - session_id: string; - batch_id: string; - progress_image: ProgressImage | null | undefined; - step: number; - total_steps: number; - percentage: number; -}; - const zLanguage = z.enum([ 'ar', 'az', @@ -42,17 +30,13 @@ export const isLanguage = (v: unknown): v is Language => zLanguage.safeParse(v). export interface SystemState { _version: 1; - isConnected: boolean; shouldConfirmOnDelete: boolean; enableImageDebugging: boolean; - denoiseProgress: DenoiseProgress | null; consoleLogLevel: LogLevel; shouldLogToConsole: boolean; shouldAntialiasProgressImage: boolean; language: Language; shouldUseNSFWChecker: boolean; shouldUseWatermarker: boolean; - status: SystemStatus; shouldEnableInformationalPopovers: boolean; - cancellations: string[]; } diff --git a/invokeai/frontend/web/src/services/events/actions.ts b/invokeai/frontend/web/src/services/events/actions.ts deleted file mode 100644 index b36fadcf799..00000000000 --- a/invokeai/frontend/web/src/services/events/actions.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { createAction } from '@reduxjs/toolkit'; -import type { - BatchEnqueuedEvent, - BulkDownloadCompleteEvent, - BulkDownloadFailedEvent, - BulkDownloadStartedEvent, - DownloadCancelledEvent, - DownloadCompleteEvent, - DownloadErrorEvent, - DownloadProgressEvent, - DownloadStartedEvent, - InvocationCompleteEvent, - InvocationDenoiseProgressEvent, - InvocationErrorEvent, - InvocationStartedEvent, - ModelInstallCancelledEvent, - ModelInstallCompleteEvent, - ModelInstallDownloadProgressEvent, - ModelInstallDownloadsCompleteEvent, - ModelInstallDownloadStartedEvent, - ModelInstallErrorEvent, - ModelInstallStartedEvent, - ModelLoadCompleteEvent, - ModelLoadStartedEvent, - QueueClearedEvent, - QueueItemStatusChangedEvent, -} from 'services/events/types'; - -const createSocketAction = (name: string) => - createAction(`socket/${name}`); - -export const socketConnected = createSocketAction('Connected'); -export const socketDisconnected = createSocketAction('Disconnected'); -export const socketInvocationStarted = createSocketAction('InvocationStartedEvent'); -export const socketInvocationComplete = createSocketAction('InvocationCompleteEvent'); -export const socketInvocationError = createSocketAction('InvocationErrorEvent'); -export const socketGeneratorProgress = createSocketAction( - 'InvocationDenoiseProgressEvent' -); -export const socketModelLoadStarted = createSocketAction('ModelLoadStartedEvent'); -export const socketModelLoadComplete = createSocketAction('ModelLoadCompleteEvent'); -export const socketDownloadStarted = createSocketAction('DownloadStartedEvent'); -export const socketDownloadProgress = createSocketAction('DownloadProgressEvent'); -export const socketDownloadComplete = createSocketAction('DownloadCompleteEvent'); -export const socketDownloadCancelled = createSocketAction('DownloadCancelledEvent'); -export const socketDownloadError = createSocketAction('DownloadErrorEvent'); -export const socketModelInstallStarted = createSocketAction('ModelInstallStartedEvent'); -export const socketModelInstallDownloadProgress = createSocketAction( - 'ModelInstallDownloadProgressEvent' -); -export const socketModelInstallDownloadStarted = createSocketAction( - 'ModelInstallDownloadStartedEvent' -); -export const socketModelInstallDownloadsComplete = createSocketAction( - 'ModelInstallDownloadsCompleteEvent' -); -export const socketModelInstallComplete = createSocketAction('ModelInstallCompleteEvent'); -export const socketModelInstallError = createSocketAction('ModelInstallErrorEvent'); -export const socketModelInstallCancelled = createSocketAction('ModelInstallCancelledEvent'); -export const socketQueueItemStatusChanged = - createSocketAction('QueueItemStatusChangedEvent'); -export const socketQueueCleared = createSocketAction('QueueClearedEvent'); -export const socketBatchEnqueued = createSocketAction('BatchEnqueuedEvent'); -export const socketBulkDownloadStarted = createSocketAction('BulkDownloadStartedEvent'); -export const socketBulkDownloadComplete = createSocketAction('BulkDownloadCompleteEvent'); -export const socketBulkDownloadError = createSocketAction('BulkDownloadFailedEvent'); diff --git a/invokeai/frontend/web/src/services/events/setEventListeners.tsx b/invokeai/frontend/web/src/services/events/setEventListeners.tsx index 379b2800329..213249da99b 100644 --- a/invokeai/frontend/web/src/services/events/setEventListeners.tsx +++ b/invokeai/frontend/web/src/services/events/setEventListeners.tsx @@ -1,4 +1,5 @@ import { ExternalLink } from '@invoke-ai/ui-library'; +import { createAction } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import { $baseUrl } from 'app/store/nanostores/baseUrl'; import { $bulkDownloadId } from 'app/store/nanostores/bulkDownloadId'; @@ -21,10 +22,11 @@ import { imagesApi } from 'services/api/endpoints/images'; import { modelsApi } from 'services/api/endpoints/models'; import { queueApi, queueItemsAdapter } from 'services/api/endpoints/queue'; import { getCategories, getListImagesUrl } from 'services/api/util'; -import { socketConnected } from 'services/events/actions'; import type { ClientToServerEvents, InvocationDenoiseProgressEvent, ServerToClientEvents } from 'services/events/types'; import type { Socket } from 'socket.io-client'; +export const socketConnected = createAction('socket/connected'); + const log = logger('socketio'); type SetEventListenersArg = { From e956ea54826a6d88256ec7738e7526c8a59cb27b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 17 Aug 2024 16:09:25 +1000 Subject: [PATCH 369/678] fix(ui): respect image size in staging preview --- .../web/src/features/controlLayers/konva/CanvasStagingArea.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts index 2ce21d239e6..e07833ce070 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts @@ -3,7 +3,7 @@ import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasPreview } from 'features/controlLayers/konva/CanvasPreview'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import type { StagingAreaImage } from 'features/controlLayers/store/types'; +import { imageDTOToImageWithDims, type StagingAreaImage } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; @@ -75,7 +75,7 @@ export class CanvasStagingArea { } if (!this.image.isLoading && !this.image.isError) { - await this.image.updateImageSource(imageDTO.image_name); + await this.image.update({...this.image.state, image: imageDTOToImageWithDims(imageDTO)}, true); this.manager.stateApi.$lastCanvasProgressEvent.set(null); } this.image.konva.group.visible(shouldShowStagedImage); From 3dcb33026a9261a1507b543f16236a31d0568daf Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 17 Aug 2024 16:17:12 +1000 Subject: [PATCH 370/678] fix(ui): discard selected staging image not all other images --- .../web/src/features/controlLayers/store/sessionReducers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts index 7b21cb12c63..94c42aedea9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts @@ -26,7 +26,7 @@ export const sessionReducers = { }, sessionStagedImageDiscarded: (state, action: PayloadAction<{ index: number }>) => { const { index } = action.payload; - state.session.stagedImages = state.session.stagedImages.splice(index, 1); + state.session.stagedImages.splice(index, 1); state.session.selectedStagedImageIndex = Math.min( state.session.selectedStagedImageIndex, state.session.stagedImages.length - 1 From 7b2a5c3a3056e141de319b465fbd309abf7c0725 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 17 Aug 2024 16:18:01 +1000 Subject: [PATCH 371/678] fix(ui): use style preset prompts correctly --- .../features/nodes/util/graph/generation/buildSD1Graph.ts | 6 +++--- .../features/nodes/util/graph/generation/buildSDXLGraph.ts | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index 4ac130d15ef..2471d24e066 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -28,7 +28,7 @@ import { addSeamless } from 'features/nodes/util/graph/generation/addSeamless'; import { addTextToImage } from 'features/nodes/util/graph/generation/addTextToImage'; import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermarker'; import { Graph } from 'features/nodes/util/graph/generation/Graph'; -import { getBoardField, getSizes } from 'features/nodes/util/graph/graphBuilderUtils'; +import { getBoardField, getPresetModifiedPrompts, getSizes } from 'features/nodes/util/graph/graphBuilderUtils'; import type { Invocation } from 'services/api/types'; import { isNonRefinerMainModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; @@ -53,12 +53,12 @@ export const buildSD1Graph = async (state: RootState, manager: CanvasManager): P vaePrecision, seed, vae, - positivePrompt, - negativePrompt, } = params; assert(model, 'No model found in state'); + const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state); + const { originalSize, scaledSize } = getSizes(bbox); const g = new Graph(CONTROL_LAYERS_GRAPH); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index 031873c1e91..94a195b7d4a 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -51,8 +51,6 @@ export const buildSDXLGraph = async (state: RootState, manager: CanvasManager): shouldUseCpuNoise, vaePrecision, vae, - positivePrompt, - negativePrompt, refinerModel, refinerStart, } = params; @@ -61,7 +59,7 @@ export const buildSDXLGraph = async (state: RootState, manager: CanvasManager): const { originalSize, scaledSize } = getSizes(bbox); - const { positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state); + const { positivePrompt, negativePrompt, positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state); const g = new Graph(SDXL_CONTROL_LAYERS_GRAPH); const modelLoader = g.addNode({ From a5e8705ea3bf7b8daedde169534f176b82603b78 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 17 Aug 2024 16:26:39 +1000 Subject: [PATCH 372/678] fix(ui): dynamic prompts recalcs when presets are loaded --- .../middleware/listenerMiddleware/listeners/promptChanged.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts index c5c2caa2277..cce2b61cc6c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts @@ -24,8 +24,6 @@ const matcher = isAnyOf( maxPromptsReset, socketConnected, activeStylePresetIdChanged, - stylePresetsApi.endpoints.deleteStylePreset.matchFulfilled, - stylePresetsApi.endpoints.updateStylePreset.matchFulfilled, stylePresetsApi.endpoints.listStylePresets.matchFulfilled ); From 7baad9c72e80aec51059407aafeccfa99ba1f4a0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 18 Aug 2024 21:15:15 +1000 Subject: [PATCH 373/678] feat(ui): tweak mask patterns --- .../konva/patterns/pattern-crosshatch.svg | 10 ++++------ .../controlLayers/konva/patterns/pattern-diagonal.svg | 8 +++----- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-crosshatch.svg b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-crosshatch.svg index 4bf6edaad91..80aefc57418 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-crosshatch.svg +++ b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-crosshatch.svg @@ -4,10 +4,8 @@ xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;"> - - - - - - + + + + diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-diagonal.svg b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-diagonal.svg index 642bb7bdee1..534f93f71ba 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-diagonal.svg +++ b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-diagonal.svg @@ -1,11 +1,9 @@ - - - - - + + From 105c5a7fd4e57a29ac6fc4c44a29d79a5ae4063b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 18 Aug 2024 23:37:49 +1000 Subject: [PATCH 374/678] feat(ui): revise app layout strategy, add interaction scopes for hotkeys --- .../frontend/web/src/app/components/App.tsx | 6 +- .../web/src/common/hooks/interactionScopes.ts | 163 ++++++++++ .../web/src/common/hooks/useGlobalHotkeys.ts | 29 +- .../components/CanvasDropArea.tsx | 7 + .../components/CanvasResetViewButton.tsx | 6 +- .../ControlLayersEditor.stories.tsx | 10 +- .../components/ControlLayersEditor.tsx | 22 +- .../components/StageComponent.tsx | 2 +- .../StagingArea/StagingAreaToolbar.tsx | 59 ++-- ...eryContent.tsx => GalleryPanelContent.tsx} | 29 +- .../ImageGrid/GallerySelectionCountTag.tsx | 20 +- .../ImageViewer/CurrentImageButtons.tsx | 40 +-- .../ImageViewer/ImageComparisonDroppable.tsx | 17 +- .../components/ImageViewer/ImageViewer.tsx | 23 +- .../components/ImageViewer/useImageViewer.ts | 44 ++- .../gallery/hooks/useGalleryHotkeys.ts | 43 ++- .../features/nodes/components/NodeEditor.tsx | 68 +--- .../flow/AddNodePopover/AddNodePopover.tsx | 11 +- .../features/nodes/components/flow/Flow.tsx | 17 +- .../queue/components/QueueTabContent.tsx | 13 +- .../src/features/ui/components/AppContent.tsx | 181 +++++++++++ .../src/features/ui/components/InvokeTabs.tsx | 303 ------------------ .../src/features/ui/components/TabButton.tsx | 32 ++ .../features/ui/components/TabMountGate.tsx | 22 ++ .../ui/components/TabVisibilityGate.tsx | 29 ++ .../features/ui/components/VerticalNavBar.tsx | 47 +++ .../features/ui/components/tabs/NodesTab.tsx | 37 ++- .../features/ui/components/tabs/QueueTab.tsx | 7 +- .../ui/components/tabs/ResizeHandle.tsx | 19 -- .../ui/components/tabs/TextToImageTab.tsx | 14 +- .../ui/components/tabs/UpscalingTab.tsx | 18 +- .../web/src/features/ui/hooks/usePanel.ts | 78 ++--- .../web/src/features/ui/store/uiSlice.ts | 4 + 33 files changed, 807 insertions(+), 613 deletions(-) create mode 100644 invokeai/frontend/web/src/common/hooks/interactionScopes.ts rename invokeai/frontend/web/src/features/gallery/components/{ImageGalleryContent.tsx => GalleryPanelContent.tsx} (85%) create mode 100644 invokeai/frontend/web/src/features/ui/components/AppContent.tsx delete mode 100644 invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx create mode 100644 invokeai/frontend/web/src/features/ui/components/TabButton.tsx create mode 100644 invokeai/frontend/web/src/features/ui/components/TabMountGate.tsx create mode 100644 invokeai/frontend/web/src/features/ui/components/TabVisibilityGate.tsx create mode 100644 invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index c7c623488e6..7f37c2bfca8 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -9,6 +9,7 @@ import ImageUploadOverlay from 'common/components/ImageUploadOverlay'; import { useClearStorage } from 'common/hooks/useClearStorage'; import { useFullscreenDropzone } from 'common/hooks/useFullscreenDropzone'; import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys'; +import { useScopeFocusWatcher } from 'common/hooks/interactionScopes'; import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardModal'; import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal'; import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal'; @@ -17,7 +18,7 @@ import { StylePresetModal } from 'features/stylePresets/components/StylePresetFo import { activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice'; import { configChanged } from 'features/system/store/configSlice'; import { languageSelector } from 'features/system/store/systemSelectors'; -import InvokeTabs from 'features/ui/components/InvokeTabs'; +import { AppContent } from 'features/ui/components/AppContent'; import type { InvokeTabName } from 'features/ui/store/tabMap'; import { setActiveTab } from 'features/ui/store/uiSlice'; import { useGetAndLoadLibraryWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadLibraryWorkflow'; @@ -107,6 +108,7 @@ const App = ({ useStarterModelsToast(); useSyncQueueStatus(); + useScopeFocusWatcher(); return ( @@ -119,7 +121,7 @@ const App = ({ {...dropzone.getRootProps()} > - + {dropzone.isDragActive && isHandlingUpload && ( diff --git a/invokeai/frontend/web/src/common/hooks/interactionScopes.ts b/invokeai/frontend/web/src/common/hooks/interactionScopes.ts new file mode 100644 index 00000000000..0e4aa906a41 --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/interactionScopes.ts @@ -0,0 +1,163 @@ +import { logger } from 'app/logging/logger'; +import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; +import { objectKeys } from 'common/util/objectKeys'; +import { isEqual } from 'lodash-es'; +import type { Atom } from 'nanostores'; +import { atom, computed } from 'nanostores'; +import type { RefObject } from 'react'; +import { useEffect, useMemo } from 'react'; + +const log = logger('system'); + +const _INTERACTION_SCOPES = ['gallery', 'canvas', 'workflows', 'imageViewer'] as const; + +type InteractionScope = (typeof _INTERACTION_SCOPES)[number]; + +export const $activeScopes = atom>(new Set()); + +type InteractionScopeData = { + targets: Set; + $isActive: Atom; +}; + +export const INTERACTION_SCOPES: Record = _INTERACTION_SCOPES.reduce( + (acc, region) => { + acc[region] = { + targets: new Set(), + $isActive: computed($activeScopes, (activeScopes) => activeScopes.has(region)), + }; + return acc; + }, + {} as Record +); + +const formatScopes = (interactionScopes: Set) => { + if (interactionScopes.size === 0) { + return 'none'; + } + return Array.from(interactionScopes).join(', '); +}; + +export const addScope = (scope: InteractionScope) => { + const currentScopes = $activeScopes.get(); + if (currentScopes.has(scope)) { + return; + } + const newScopes = new Set(currentScopes); + newScopes.add(scope); + $activeScopes.set(newScopes); + log.trace(`Added scope ${scope}: ${formatScopes($activeScopes.get())}`); +}; + +export const removeScope = (scope: InteractionScope) => { + const currentScopes = $activeScopes.get(); + if (!currentScopes.has(scope)) { + return; + } + const newScopes = new Set(currentScopes); + newScopes.delete(scope); + $activeScopes.set(newScopes); + log.trace(`Removed scope ${scope}: ${formatScopes($activeScopes.get())}`); +}; + +export const setScopes = (scopes: InteractionScope[]) => { + const newScopes = new Set(scopes); + $activeScopes.set(newScopes); + log.trace(`Set scopes: ${formatScopes($activeScopes.get())}`); +}; + +export const clearScopes = () => { + $activeScopes.set(new Set()); + log.trace(`Cleared scopes`); +}; + +export const useScopeOnFocus = (scope: InteractionScope, ref: RefObject) => { + useEffect(() => { + const element = ref.current; + + if (!element) { + return; + } + + INTERACTION_SCOPES[scope].targets.add(element); + + return () => { + INTERACTION_SCOPES[scope].targets.delete(element); + }; + }, [ref, scope]); +}; + +type UseScopeOnMountOptions = { + mount?: boolean; + unmount?: boolean; +}; + +const defaultUseScopeOnMountOptions: UseScopeOnMountOptions = { + mount: true, + unmount: true, +}; + +export const useScopeOnMount = (scope: InteractionScope, options?: UseScopeOnMountOptions) => { + useEffect(() => { + const { mount, unmount } = { ...defaultUseScopeOnMountOptions, ...options }; + + if (mount) { + addScope(scope); + } + + return () => { + if (unmount) { + removeScope(scope); + } + }; + }, [options, scope]); +}; + +export const useScopeImperativeApi = (scope: InteractionScope) => { + const api = useMemo(() => { + return { + add: () => { + addScope(scope); + }, + remove: () => { + removeScope(scope); + }, + }; + }, [scope]); + + return api; +}; + +const handleFocusEvent = (_event: FocusEvent) => { + const activeElement = document.activeElement; + if (!(activeElement instanceof HTMLElement)) { + return; + } + + const newActiveScopes = new Set(); + + for (const scope of objectKeys(INTERACTION_SCOPES)) { + for (const element of INTERACTION_SCOPES[scope].targets) { + if (element.contains(activeElement)) { + newActiveScopes.add(scope); + } + } + } + + const oldActiveScopes = $activeScopes.get(); + if (!isEqual(oldActiveScopes, newActiveScopes)) { + $activeScopes.set(newActiveScopes); + log.trace(`Scopes changed: ${formatScopes($activeScopes.get())}`); + } +}; + +export const useScopeFocusWatcher = () => { + useAssertSingleton('useScopeFocusWatcher'); + + useEffect(() => { + window.addEventListener('focus', handleFocusEvent, true); + return () => { + window.removeEventListener('focus', handleFocusEvent, true); + }; + }, []); +}; diff --git a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts index 487622f9b79..9ed33fc26c6 100644 --- a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts +++ b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts @@ -1,4 +1,5 @@ import { useAppDispatch } from 'app/store/storeHooks'; +import { addScope, removeScope, setScopes } from 'common/hooks/interactionScopes'; import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem'; import { useClearQueue } from 'features/queue/hooks/useClearQueue'; import { useQueueBack } from 'features/queue/hooks/useQueueBack'; @@ -16,7 +17,7 @@ export const useGlobalHotkeys = () => { ['ctrl+enter', 'meta+enter'], queueBack, { - enabled: () => !isDisabledQueueBack && !isLoadingQueueBack, + enabled: !isDisabledQueueBack && !isLoadingQueueBack, preventDefault: true, enableOnFormTags: ['input', 'textarea', 'select'], }, @@ -29,7 +30,7 @@ export const useGlobalHotkeys = () => { ['ctrl+shift+enter', 'meta+shift+enter'], queueFront, { - enabled: () => !isDisabledQueueFront && !isLoadingQueueFront, + enabled: !isDisabledQueueFront && !isLoadingQueueFront, preventDefault: true, enableOnFormTags: ['input', 'textarea', 'select'], }, @@ -46,7 +47,7 @@ export const useGlobalHotkeys = () => { ['shift+x'], cancelQueueItem, { - enabled: () => !isDisabledCancelQueueItem && !isLoadingCancelQueueItem, + enabled: !isDisabledCancelQueueItem && !isLoadingCancelQueueItem, preventDefault: true, }, [cancelQueueItem, isDisabledCancelQueueItem, isLoadingCancelQueueItem] @@ -58,7 +59,7 @@ export const useGlobalHotkeys = () => { ['ctrl+shift+x', 'meta+shift+x'], clearQueue, { - enabled: () => !isDisabledClearQueue && !isLoadingClearQueue, + enabled: !isDisabledClearQueue && !isLoadingClearQueue, preventDefault: true, }, [clearQueue, isDisabledClearQueue, isLoadingClearQueue] @@ -68,6 +69,8 @@ export const useGlobalHotkeys = () => { '1', () => { dispatch(setActiveTab('generation')); + addScope('canvas'); + removeScope('workflows'); }, [dispatch] ); @@ -75,25 +78,39 @@ export const useGlobalHotkeys = () => { useHotkeys( '2', () => { - dispatch(setActiveTab('workflows')); + dispatch(setActiveTab('upscaling')); + removeScope('canvas'); + removeScope('workflows'); }, [dispatch] ); useHotkeys( '3', + () => { + dispatch(setActiveTab('workflows')); + removeScope('canvas'); + addScope('workflows'); + }, + [dispatch] + ); + + useHotkeys( + '4', () => { if (isModelManagerEnabled) { dispatch(setActiveTab('models')); + setScopes([]); } }, [dispatch, isModelManagerEnabled] ); useHotkeys( - isModelManagerEnabled ? '4' : '3', + isModelManagerEnabled ? '5' : '4', () => { dispatch(setActiveTab('queue')); + setScopes([]); }, [dispatch, isModelManagerEnabled] ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx index b5a1c636a56..5c90c82a6f7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx @@ -1,6 +1,7 @@ import { Flex } from '@invoke-ai/ui-library'; import IAIDroppable from 'common/components/IAIDroppable'; import type { AddLayerFromImageDropData } from 'features/dnd/types'; +import { useIsImageViewerOpen } from 'features/gallery/components/ImageViewer/useImageViewer'; import { memo } from 'react'; const addLayerFromImageDropData: AddLayerFromImageDropData = { @@ -9,6 +10,12 @@ const addLayerFromImageDropData: AddLayerFromImageDropData = { }; export const CanvasDropArea = memo(() => { + const isImageViewerOpen = useIsImageViewerOpen(); + + if (isImageViewerOpen) { + return null; + } + return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasResetViewButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasResetViewButton.tsx index 5df90004fe2..585f2088039 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasResetViewButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasResetViewButton.tsx @@ -1,5 +1,6 @@ import { $shift, IconButton } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; +import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes'; import { $canvasManager } from 'features/controlLayers/konva/CanvasManager'; import { memo, useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; @@ -9,6 +10,7 @@ import { PiArrowCounterClockwiseBold } from 'react-icons/pi'; export const CanvasResetViewButton = memo(() => { const { t } = useTranslation(); const canvasManager = useStore($canvasManager); + const isCanvasActive = useStore(INTERACTION_SCOPES.canvas.$isActive); const resetZoom = useCallback(() => { if (!canvasManager) { @@ -32,8 +34,8 @@ export const CanvasResetViewButton = memo(() => { } }, [resetView, resetZoom]); - useHotkeys('r', resetView); - useHotkeys('shift+r', resetZoom); + useHotkeys('r', resetView, { enabled: isCanvasActive }, [isCanvasActive]); + useHotkeys('shift+r', resetZoom, { enabled: isCanvasActive }, [isCanvasActive]); return ( = { +const meta: Meta = { title: 'Feature/ControlLayers', tags: ['autodocs'], - component: ControlLayersEditor, + component: CanvasEditor, }; export default meta; -type Story = StoryObj; +type Story = StoryObj; const Component = () => { return ( - + ); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx index 71e35263b5e..d10af98f7aa 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx @@ -1,18 +1,27 @@ /* eslint-disable i18next/no-literal-string */ import { Flex } from '@invoke-ai/ui-library'; +import { useScopeOnFocus } from 'common/hooks/interactionScopes'; import { CanvasDropArea } from 'features/controlLayers/components/CanvasDropArea'; import { ControlLayersToolbar } from 'features/controlLayers/components/ControlLayersToolbar'; import { StageComponent } from 'features/controlLayers/components/StageComponent'; import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar'; -import { memo } from 'react'; +import { memo, useRef } from 'react'; + +export const CanvasEditor = memo(() => { + const ref = useRef(null); + useScopeOnFocus('canvas', ref); -export const ControlLayersEditor = memo(() => { return ( { - {/* - - */} ); }); -ControlLayersEditor.displayName = 'ControlLayersEditor'; +CanvasEditor.displayName = 'CanvasEditor'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index 9870e3c72d9..5fb20b6fd76 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -80,7 +80,7 @@ export const StageComponent = memo(({ asPreview = false }: Props) => { ); return ( - + {canvasBackgroundStyle === 'checkerboard' && ( { const dispatch = useAppDispatch(); - const stagingArea = useAppSelector((s) => s.canvasV2.session); + const session = useAppSelector((s) => s.canvasV2.session); const shouldShowStagedImage = useStore($shouldShowStagedImage); - const images = useMemo(() => stagingArea.stagedImages, [stagingArea]); + const images = useMemo(() => session.stagedImages, [session]); const selectedImage = useMemo(() => { - return images[stagingArea.selectedStagedImageIndex] ?? null; - }, [images, stagingArea.selectedStagedImageIndex]); - + return images[session.selectedStagedImageIndex] ?? null; + }, [images, session.selectedStagedImageIndex]); + const isCanvasActive = useStore(INTERACTION_SCOPES.canvas.$isActive); // const [changeIsImageIntermediate] = useChangeImageIsIntermediateMutation(); const { t } = useTranslation(); @@ -60,8 +61,8 @@ export const StagingAreaToolbarContent = memo(() => { if (!selectedImage) { return; } - dispatch(sessionStagingAreaImageAccepted({ index: stagingArea.selectedStagedImageIndex })); - }, [dispatch, selectedImage, stagingArea.selectedStagedImageIndex]); + dispatch(sessionStagingAreaImageAccepted({ index: session.selectedStagedImageIndex })); + }, [dispatch, selectedImage, session.selectedStagedImageIndex]); const onDiscardOne = useCallback(() => { if (!selectedImage) { @@ -70,9 +71,9 @@ export const StagingAreaToolbarContent = memo(() => { if (images.length === 1) { dispatch(sessionStagingAreaReset()); } else { - dispatch(sessionStagedImageDiscarded({ index: stagingArea.selectedStagedImageIndex })); + dispatch(sessionStagedImageDiscarded({ index: session.selectedStagedImageIndex })); } - }, [selectedImage, images.length, dispatch, stagingArea.selectedStagedImageIndex]); + }, [selectedImage, images.length, dispatch, session.selectedStagedImageIndex]); const onDiscardAll = useCallback(() => { dispatch(sessionStagingAreaReset()); @@ -95,25 +96,43 @@ export const StagingAreaToolbarContent = memo(() => { ] ); - useHotkeys(['left'], onPrev, { - preventDefault: true, - }); + useHotkeys( + ['left'], + onPrev, + { + preventDefault: true, + enabled: isCanvasActive, + }, + [isCanvasActive] + ); - useHotkeys(['right'], onNext, { - preventDefault: true, - }); + useHotkeys( + ['right'], + onNext, + { + preventDefault: true, + enabled: isCanvasActive, + }, + [isCanvasActive] + ); - useHotkeys(['enter'], onAccept, { - preventDefault: true, - }); + useHotkeys( + ['enter'], + onAccept, + { + preventDefault: true, + enabled: isCanvasActive, + }, + [isCanvasActive] + ); const counterText = useMemo(() => { if (images.length > 0) { - return `${(stagingArea.selectedStagedImageIndex ?? 0) + 1} of ${images.length}`; + return `${(session.selectedStagedImageIndex ?? 0) + 1} of ${images.length}`; } else { return `0 of 0`; } - }, [images.length, stagingArea.selectedStagedImageIndex]); + }, [images.length, session.selectedStagedImageIndex]); return ( <> diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryPanelContent.tsx similarity index 85% rename from invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx rename to invokeai/frontend/web/src/features/gallery/components/GalleryPanelContent.tsx index e8f42ae65e8..30765f20d9a 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/GalleryPanelContent.tsx @@ -1,5 +1,6 @@ import { Box, Button, Collapse, Divider, Flex, IconButton, useDisclosure } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useScopeOnFocus } from 'common/hooks/interactionScopes'; import { GalleryHeader } from 'features/gallery/components/GalleryHeader'; import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice'; import ResizeHandle from 'features/ui/components/tabs/ResizeHandle'; @@ -18,19 +19,21 @@ import GallerySettingsPopover from './GallerySettingsPopover/GallerySettingsPopo const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0 }; -const ImageGalleryContent = () => { +const GalleryPanelContent = () => { const { t } = useTranslation(); const boardSearchText = useAppSelector((s) => s.gallery.boardSearchText); const dispatch = useAppDispatch(); const boardSearchDisclosure = useDisclosure({ defaultIsOpen: !!boardSearchText.length }); const panelGroupRef = useRef(null); + const ref = useRef(null); + useScopeOnFocus('gallery', ref); const boardsListPanelOptions = useMemo( () => ({ + id: 'boards-list-panel', unit: 'pixels', minSize: 128, - defaultSize: 256, - fallbackMinSizePct: 20, + defaultSize: 20, panelGroupRef, panelGroupDirection: 'vertical', }), @@ -55,7 +58,7 @@ const ImageGalleryContent = () => { }, [boardSearchText.length, boardSearchDisclosure, boardsListPanel, dispatch]); return ( - + @@ -90,15 +93,7 @@ const ImageGalleryContent = () => { - + @@ -109,11 +104,7 @@ const ImageGalleryContent = () => { - + @@ -122,4 +113,4 @@ const ImageGalleryContent = () => { ); }; -export default memo(ImageGalleryContent); +export default memo(GalleryPanelContent); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GallerySelectionCountTag.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GallerySelectionCountTag.tsx index 6e111e59c07..439388e2c5a 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GallerySelectionCountTag.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GallerySelectionCountTag.tsx @@ -1,16 +1,25 @@ import { Tag, TagCloseButton, TagLabel } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { $activeScopes } from 'common/hooks/interactionScopes'; import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages'; import { selectionChanged } from 'features/gallery/store/gallerySlice'; +import { $isGalleryPanelOpen } from 'features/ui/store/uiSlice'; +import { computed } from 'nanostores'; import { useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; +const $isSelectAllEnabled = computed([$activeScopes, $isGalleryPanelOpen], (activeScopes, isGalleryPanelOpen) => { + return activeScopes.has('gallery') && !activeScopes.has('workflows') && isGalleryPanelOpen; +}); + export const GallerySelectionCountTag = () => { const dispatch = useAppDispatch(); const { selection } = useAppSelector((s) => s.gallery); const { t } = useTranslation(); const { imageDTOs } = useGalleryImages(); + const isSelectAllEnabled = useStore($isSelectAllEnabled); const onClearSelection = useCallback(() => { dispatch(selectionChanged([])); @@ -20,7 +29,16 @@ export const GallerySelectionCountTag = () => { dispatch(selectionChanged([...selection, ...imageDTOs])); }, [dispatch, selection, imageDTOs]); - useHotkeys(['ctrl+a', 'meta+a'], onSelectPage, { preventDefault: true }, [onSelectPage]); + useHotkeys(['ctrl+a', 'meta+a'], onSelectPage, { preventDefault: true, enabled: isSelectAllEnabled }, [ + onSelectPage, + isSelectAllEnabled, + ]); + + useHotkeys('esc', onClearSelection, { enabled: selection.length > 0 && isSelectAllEnabled }, [ + onClearSelection, + selection, + isSelectAllEnabled, + ]); if (selection.length <= 1) { return null; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx index 9ccd69b898a..65131dd3e5e 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx @@ -4,6 +4,7 @@ import { skipToken } from '@reduxjs/toolkit/query'; import { $isConnected } from 'app/hooks/useSocketIO'; import { adHocPostProcessingRequested } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes'; import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton'; import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice'; import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems'; @@ -46,7 +47,7 @@ const CurrentImageButtons = () => { const isUpscalingEnabled = useFeatureStatus('upscaling'); const isQueueMutationInProgress = useIsQueueMutationInProgress(); const { t } = useTranslation(); - + const isImageViewerActive = useStore(INTERACTION_SCOPES.imageViewer.$isActive); const { currentData: imageDTO } = useGetImageDTOQuery(lastSelectedImage?.image_name ?? skipToken); const { recallAll, remix, recallSeed, recallPrompts, hasMetadata, hasSeed, hasPrompts, isLoadingMetadata } = @@ -61,18 +62,9 @@ const CurrentImageButtons = () => { getAndLoadEmbeddedWorkflow(lastSelectedImage.image_name); }, [getAndLoadEmbeddedWorkflow, lastSelectedImage]); - useHotkeys('w', handleLoadWorkflow, [lastSelectedImage]); - useHotkeys('a', recallAll, [recallAll]); - useHotkeys('s', recallSeed, [recallSeed]); - useHotkeys('p', recallPrompts, [recallPrompts]); - useHotkeys('r', remix, [remix]); - const handleUseSize = useCallback(() => { parseAndRecallImageDimensions(lastSelectedImage); }, [lastSelectedImage]); - - useHotkeys('d', handleUseSize, [handleUseSize]); - const handleSendToImageToImage = useCallback(() => { if (!imageDTO) { return; @@ -81,9 +73,6 @@ const CurrentImageButtons = () => { dispatch(sentImageToImg2Img()); dispatch(setActiveTab('generation')); }, [dispatch, imageDTO]); - - useHotkeys('shift+i', handleSendToImageToImage, [imageDTO]); - const handleClickUpscale = useCallback(() => { if (!imageDTO) { return; @@ -98,24 +87,21 @@ const CurrentImageButtons = () => { dispatch(imagesToDeleteSelected(selection)); }, [dispatch, imageDTO, selection]); + useHotkeys('w', handleLoadWorkflow, { enabled: isImageViewerActive }, [lastSelectedImage, isImageViewerActive]); + useHotkeys('a', recallAll, { enabled: isImageViewerActive }, [recallAll, isImageViewerActive]); + useHotkeys('s', recallSeed, { enabled: isImageViewerActive }, [recallSeed, isImageViewerActive]); + useHotkeys('p', recallPrompts, { enabled: isImageViewerActive }, [recallPrompts, isImageViewerActive]); + useHotkeys('r', remix, { enabled: isImageViewerActive }, [remix, isImageViewerActive]); + useHotkeys('d', handleUseSize, { enabled: isImageViewerActive }, [handleUseSize, isImageViewerActive]); + useHotkeys('shift+i', handleSendToImageToImage, { enabled: isImageViewerActive }, [imageDTO, isImageViewerActive]); useHotkeys( 'Shift+U', - () => { - handleClickUpscale(); - }, - { - enabled: () => Boolean(isUpscalingEnabled && !shouldDisableToolbarButtons && isConnected), - }, - [isUpscalingEnabled, imageDTO, shouldDisableToolbarButtons, isConnected] + handleClickUpscale, + { enabled: Boolean(isUpscalingEnabled && isImageViewerActive && isConnected) }, + [isUpscalingEnabled, imageDTO, shouldDisableToolbarButtons, isConnected, isImageViewerActive] ); - useHotkeys( - 'delete', - () => { - handleDelete(); - }, - [dispatch, imageDTO] - ); + useHotkeys('delete', handleDelete, { enabled: isImageViewerActive }, [imageDTO, isImageViewerActive]); return ( <> diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonDroppable.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonDroppable.tsx index 3678c920c06..2998c7d7253 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonDroppable.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonDroppable.tsx @@ -1,21 +1,14 @@ import { Flex } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import IAIDroppable from 'common/components/IAIDroppable'; -import type { CurrentImageDropData, SelectForCompareDropData } from 'features/dnd/types'; -import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer'; +import type { SelectForCompareDropData } from 'features/dnd/types'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { selectComparisonImages } from './common'; -const setCurrentImageDropData: CurrentImageDropData = { - id: 'current-image', - actionType: 'SET_CURRENT_IMAGE', -}; - export const ImageComparisonDroppable = memo(() => { const { t } = useTranslation(); - const imageViewer = useImageViewer(); const { firstImage, secondImage } = useAppSelector(selectComparisonImages); const selectForCompareDropData = useMemo( () => ({ @@ -29,14 +22,6 @@ export const ImageComparisonDroppable = memo(() => { [firstImage?.image_name, secondImage?.image_name] ); - if (!imageViewer.isOpen) { - return ( - - - - ); - } - return ( diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx index 530431fc4c4..f8657d78112 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx @@ -1,19 +1,25 @@ import { Box, Flex } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useScopeOnFocus, useScopeOnMount } from 'common/hooks/interactionScopes'; import { CompareToolbar } from 'features/gallery/components/ImageViewer/CompareToolbar'; import CurrentImagePreview from 'features/gallery/components/ImageViewer/CurrentImagePreview'; import { ImageComparison } from 'features/gallery/components/ImageViewer/ImageComparison'; +import { ImageComparisonDroppable } from 'features/gallery/components/ImageViewer/ImageComparisonDroppable'; import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar'; -import { memo } from 'react'; +import { memo, useRef } from 'react'; import { useMeasure } from 'react-use'; -import { useImageViewer } from './useImageViewer'; - export const ImageViewer = memo(() => { - const imageViewer = useImageViewer(); + const isComparing = useAppSelector((s) => s.gallery.imageToCompare !== null); const [containerRef, containerDims] = useMeasure(); + const ref = useRef(null); + useScopeOnFocus('imageViewer', ref); + useScopeOnMount('imageViewer'); return ( { alignItems="center" justifyContent="center" > - {imageViewer.isComparing && } - {!imageViewer.isComparing && } + {isComparing && } + {!isComparing && } - {!imageViewer.isComparing && } - {imageViewer.isComparing && } + {!isComparing && } + {isComparing && } + ); }); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.ts b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.ts index 1e1567e70ab..dfbc05107b4 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.ts +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.ts @@ -2,30 +2,60 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { imageToCompareChanged, isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; import { useCallback } from 'react'; +export const useIsImageViewerOpen = () => { + const isOpen = useAppSelector((s) => { + const tab = s.ui.activeTab; + const workflowsMode = s.workflow.mode; + if (tab === 'models' || tab === 'queue') { + return false; + } + if (tab === 'workflows' && workflowsMode === 'edit') { + return false; + } + if (tab === 'workflows' && workflowsMode === 'view') { + return true; + } + if (tab === 'upscaling') { + return true; + } + return s.gallery.isImageViewerOpen; + }); + return isOpen; +}; + export const useImageViewer = () => { const dispatch = useAppDispatch(); const isComparing = useAppSelector((s) => s.gallery.imageToCompare !== null); - const isOpen = useAppSelector((s) => s.gallery.isImageViewerOpen); + const isNaturallyOpen = useAppSelector((s) => s.gallery.isImageViewerOpen); + const isForcedOpen = useAppSelector( + (s) => s.ui.activeTab === 'upscaling' || (s.ui.activeTab === 'workflows' && s.workflow.mode === 'view') + ); const onClose = useCallback(() => { - if (isComparing && isOpen) { + if (isForcedOpen) { + return; + } + if (isComparing && isNaturallyOpen) { dispatch(imageToCompareChanged(null)); } else { dispatch(isImageViewerOpenChanged(false)); } - }, [dispatch, isComparing, isOpen]); + }, [dispatch, isComparing, isForcedOpen, isNaturallyOpen]); const onOpen = useCallback(() => { dispatch(isImageViewerOpenChanged(true)); }, [dispatch]); const onToggle = useCallback(() => { - if (isComparing && isOpen) { + if (isForcedOpen) { + return; + } + if (isComparing && isNaturallyOpen) { dispatch(imageToCompareChanged(null)); } else { - dispatch(isImageViewerOpenChanged(!isOpen)); + dispatch(isImageViewerOpenChanged(!isNaturallyOpen)); } - }, [dispatch, isComparing, isOpen]); + }, [dispatch, isComparing, isForcedOpen, isNaturallyOpen]); - return { isOpen, onOpen, onClose, onToggle, isComparing }; + return { isOpen: isNaturallyOpen || isForcedOpen, onOpen, onClose, onToggle, isComparing }; }; diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts index 8199d8f63ee..81a5f34987d 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts @@ -1,19 +1,35 @@ +import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; +import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; +import { $activeScopes } from 'common/hooks/interactionScopes'; import { useGalleryNavigation } from 'features/gallery/hooks/useGalleryNavigation'; import { useGalleryPagination } from 'features/gallery/hooks/useGalleryPagination'; import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors'; +import { $isGalleryPanelOpen } from 'features/ui/store/uiSlice'; +import { computed } from 'nanostores'; import { useHotkeys } from 'react-hotkeys-hook'; import { useListImagesQuery } from 'services/api/endpoints/images'; +const $leftRightHotkeysEnabled = computed($activeScopes, (activeScopes) => { + // The left and right hotkeys can be used when the gallery is focused and the canvas is not focused, OR when the image viewer is focused. + return (!activeScopes.has('staging-area') && !activeScopes.has('canvas')) || activeScopes.has('imageViewer'); +}); + +const $upDownHotkeysEnabled = computed([$activeScopes, $isGalleryPanelOpen], (activeScopes, isGalleryPanelOpen) => { + // The up and down hotkeys can be used when the gallery is focused and the canvas is not focused, and the gallery panel is open. + return !activeScopes.has('staging-area') && !activeScopes.has('canvas') && isGalleryPanelOpen; +}); + /** * Registers gallery hotkeys. This hook is a singleton. */ export const useGalleryHotkeys = () => { - const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); - + useAssertSingleton('useGalleryHotkeys'); const { goNext, goPrev, isNextEnabled, isPrevEnabled } = useGalleryPagination(); const queryArgs = useAppSelector(selectListImagesQueryArgs); const queryResult = useListImagesQuery(queryArgs); + const leftRightHotkeysEnabled = useStore($leftRightHotkeysEnabled); + const upDownHotkeysEnabled = useStore($upDownHotkeysEnabled); const { handleLeftImage, @@ -35,15 +51,13 @@ export const useGalleryHotkeys = () => { } handleLeftImage(e.altKey); }, - [handleLeftImage, isOnFirstImageOfView, goPrev, isPrevEnabled, queryResult.isFetching] + { preventDefault: true, enabled: leftRightHotkeysEnabled }, + [handleLeftImage, isOnFirstImageOfView, goPrev, isPrevEnabled, queryResult.isFetching, leftRightHotkeysEnabled] ); useHotkeys( ['right', 'alt+right'], (e) => { - if (isStaging) { - return; - } if (isOnLastImageOfView && isNextEnabled && !queryResult.isFetching) { goNext(e.altKey ? 'alt+arrow' : 'arrow'); return; @@ -52,38 +66,33 @@ export const useGalleryHotkeys = () => { handleRightImage(e.altKey); } }, - [isStaging, isOnLastImageOfView, goNext, isNextEnabled, queryResult.isFetching, handleRightImage] + { preventDefault: true, enabled: leftRightHotkeysEnabled }, + [isOnLastImageOfView, goNext, isNextEnabled, queryResult.isFetching, handleRightImage, leftRightHotkeysEnabled] ); useHotkeys( ['up', 'alt+up'], (e) => { - if (isStaging) { - return; - } if (isOnFirstRow && isPrevEnabled && !queryResult.isFetching) { goPrev(e.altKey ? 'alt+arrow' : 'arrow'); return; } handleUpImage(e.altKey); }, - { preventDefault: true }, - [isStaging, handleUpImage, isOnFirstRow, goPrev, isPrevEnabled, queryResult.isFetching] + { preventDefault: true, enabled: upDownHotkeysEnabled }, + [handleUpImage, isOnFirstRow, goPrev, isPrevEnabled, queryResult.isFetching, upDownHotkeysEnabled] ); useHotkeys( ['down', 'alt+down'], (e) => { - if (isStaging) { - return; - } if (isOnLastRow && isNextEnabled && !queryResult.isFetching) { goNext(e.altKey ? 'alt+arrow' : 'arrow'); return; } handleDownImage(e.altKey); }, - { preventDefault: true }, - [isStaging, isOnLastRow, goNext, isNextEnabled, queryResult.isFetching, handleDownImage] + { preventDefault: true, enabled: upDownHotkeysEnabled }, + [isOnLastRow, goNext, isNextEnabled, queryResult.isFetching, handleDownImage, upDownHotkeysEnabled] ); }; diff --git a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx index 737adb52e74..27e2006b08a 100644 --- a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx @@ -5,9 +5,6 @@ import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import TopPanel from 'features/nodes/components/flow/panels/TopPanel/TopPanel'; import { LoadWorkflowFromGraphModal } from 'features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal'; import { SaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog/SaveWorkflowAsDialog'; -import type { AnimationProps } from 'framer-motion'; -import { AnimatePresence, motion } from 'framer-motion'; -import type { CSSProperties } from 'react'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { MdDeviceHub } from 'react-icons/md'; @@ -18,28 +15,6 @@ import { Flow } from './flow/Flow'; import BottomLeftPanel from './flow/panels/BottomLeftPanel/BottomLeftPanel'; import MinimapPanel from './flow/panels/MinimapPanel/MinimapPanel'; -const isReadyMotionStyles: CSSProperties = { - position: 'relative', - width: '100%', - height: '100%', -}; -const notIsReadyMotionStyles: CSSProperties = { - position: 'absolute', - width: '100%', - height: '100%', -}; -const initial: AnimationProps['initial'] = { - opacity: 0, -}; -const animate: AnimationProps['animate'] = { - opacity: 1, - transition: { duration: 0.2 }, -}; -const exit: AnimationProps['exit'] = { - opacity: 0, - transition: { duration: 0.2 }, -}; - const NodeEditor = () => { const { data, isLoading } = useGetOpenAPISchemaQuery(); const { t } = useTranslation(); @@ -53,37 +28,18 @@ const NodeEditor = () => { alignItems="center" justifyContent="center" > - - {data && ( - - - - - - - - - - )} - - - {isLoading && ( - - - - - - )} - + {data && ( + <> + + + + + + + + + )} + {isLoading && } ); }; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx index 6da87f4e98c..923c44b5277 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx @@ -5,6 +5,7 @@ import { Combobox, Flex, Popover, PopoverAnchor, PopoverBody, PopoverContent } f import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppStore } from 'app/store/storeHooks'; import type { SelectInstance } from 'chakra-react-select'; +import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes'; import { useBuildNode } from 'features/nodes/hooks/useBuildNode'; import { $cursorPos, @@ -67,6 +68,7 @@ const AddNodePopover = () => { const pendingConnection = useStore($pendingConnection); const isOpen = useStore($isAddNodePopoverOpen); const store = useAppStore(); + const isWorkflowsActive = useStore(INTERACTION_SCOPES.workflows.$isActive); const filteredTemplates = useMemo(() => { // If we have a connection in progress, we need to filter the node choices @@ -214,14 +216,7 @@ const AddNodePopover = () => { } }, []); - const handleHotkeyClose: HotkeyCallback = useCallback(() => { - if ($isAddNodePopoverOpen.get()) { - closeAddNodePopover(); - } - }, []); - - useHotkeys(['shift+a', 'space'], handleHotkeyOpen); - useHotkeys(['escape'], handleHotkeyClose, { enableOnFormTags: ['TEXTAREA'] }); + useHotkeys(['shift+a', 'space'], handleHotkeyOpen, { enabled: isWorkflowsActive }, [isWorkflowsActive]); const noOptionsMessage = useCallback(() => t('nodes.noMatchingNodes'), [t]); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index 727dad9617b..a6a5cb7981d 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -1,6 +1,7 @@ import { useGlobalMenuClose, useToken } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks'; +import { INTERACTION_SCOPES, useScopeImperativeApi } from 'common/hooks/interactionScopes'; import { useConnection } from 'features/nodes/hooks/useConnection'; import { useCopyPaste } from 'features/nodes/hooks/useCopyPaste'; import { useSyncExecutionState } from 'features/nodes/hooks/useExecutionState'; @@ -79,16 +80,13 @@ export const Flow = memo(() => { const cancelConnection = useReactFlowStore(selectCancelConnection); const updateNodeInternals = useUpdateNodeInternals(); const store = useAppStore(); + const isWorkflowsActive = useStore(INTERACTION_SCOPES.workflows.$isActive); + const workflowsScopeApi = useScopeImperativeApi('workflows'); + useWorkflowWatcher(); useSyncExecutionState(); const [borderRadius] = useToken('radii', ['base']); - - const flowStyles = useMemo( - () => ({ - borderRadius, - }), - [borderRadius] - ); + const flowStyles = useMemo(() => ({ borderRadius }), [borderRadius]); const onNodesChange: OnNodesChange = useCallback( (nodeChanges) => { @@ -121,7 +119,8 @@ export const Flow = memo(() => { const { onCloseGlobal } = useGlobalMenuClose(); const handlePaneClick = useCallback(() => { onCloseGlobal(); - }, [onCloseGlobal]); + workflowsScopeApi.add(); + }, [onCloseGlobal, workflowsScopeApi]); const onInit: OnInit = useCallback((flow) => { $flow.set(flow); @@ -237,7 +236,7 @@ export const Flow = memo(() => { }, [dispatch, store] ); - useHotkeys(['Ctrl+a', 'Meta+a'], onSelectAllHotkey); + useHotkeys(['Ctrl+a', 'Meta+a'], onSelectAllHotkey, { enabled: isWorkflowsActive }, [isWorkflowsActive]); const onPasteHotkey = useCallback( (e: KeyboardEvent) => { diff --git a/invokeai/frontend/web/src/features/queue/components/QueueTabContent.tsx b/invokeai/frontend/web/src/features/queue/components/QueueTabContent.tsx index 2dae5e6ebe4..c2f4c75c7a2 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueTabContent.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueTabContent.tsx @@ -1,5 +1,7 @@ import { Box, Flex } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; +import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { memo } from 'react'; import InvocationCacheStatus from './InvocationCacheStatus'; @@ -9,9 +11,18 @@ import QueueTabQueueControls from './QueueTabQueueControls'; const QueueTabContent = () => { const isInvocationCacheEnabled = useFeatureStatus('invocationCache'); + const activeTabName = useAppSelector(activeTabNameSelector); return ( - + } onClickCapture={handleSendToUpscale} id="send-to-upscale"> {t('parameters.sendToUpscale')} - + } onClickCapture={handleSendToCanvas} id="send-to-canvas"> {t('parameters.sendToUnifiedCanvas')} diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index e66a98ce069..4b4b6aeba8b 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -33,10 +33,7 @@ import { setSteps, vaeSelected, } from 'features/controlLayers/store/canvasV2Slice'; -import type { - CanvasRasterLayerState, - LoRA, -} from 'features/controlLayers/store/types'; +import type { CanvasRasterLayerState, LoRA } from 'features/controlLayers/store/types'; import { setHrfEnabled, setHrfMethod, setHrfStrength } from 'features/hrf/store/hrfSlice'; import type { ControlNetConfigMetadata, diff --git a/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx b/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx index 44286d517b2..f0fd3b3afab 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx @@ -8,12 +8,9 @@ import { ParamSDXLNegativeStylePrompt } from 'features/sdxl/components/SDXLPromp import { ParamSDXLPositiveStylePrompt } from 'features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt'; import { memo } from 'react'; -const concatPromptsSelector = createSelector( - [selectCanvasV2Slice], - (canvasV2) => { - return canvasV2.params.model?.base !== 'sdxl' || canvasV2.params.shouldConcatPrompts; - } -); +const concatPromptsSelector = createSelector([selectCanvasV2Slice], (canvasV2) => { + return canvasV2.params.model?.base !== 'sdxl' || canvasV2.params.shouldConcatPrompts; +}); export const Prompts = memo(() => { const shouldConcatPrompts = useAppSelector(concatPromptsSelector); diff --git a/invokeai/frontend/web/src/features/system/hooks/useFeatureStatus.ts b/invokeai/frontend/web/src/features/system/hooks/useFeatureStatus.ts index 15be057b49e..0126d0d94e6 100644 --- a/invokeai/frontend/web/src/features/system/hooks/useFeatureStatus.ts +++ b/invokeai/frontend/web/src/features/system/hooks/useFeatureStatus.ts @@ -2,7 +2,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import type { AppFeature, SDFeature } from 'app/types/invokeai'; import { selectConfigSlice } from 'features/system/store/configSlice'; -import type { TabName } from "features/ui/store/uiTypes"; +import type { TabName } from 'features/ui/store/uiTypes'; import { useMemo } from 'react'; export const useFeatureStatus = (feature: AppFeature | SDFeature | TabName) => { diff --git a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx index 5f36b22b5e4..6765fec3f7d 100644 --- a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx +++ b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx @@ -19,7 +19,7 @@ import type { UsePanelOptions } from 'features/ui/hooks/usePanel'; import { usePanel } from 'features/ui/hooks/usePanel'; import { usePanelStorage } from 'features/ui/hooks/usePanelStorage'; import { $isGalleryPanelOpen, $isParametersPanelOpen } from 'features/ui/store/uiSlice'; -import type { TabName } from "features/ui/store/uiTypes"; +import type { TabName } from 'features/ui/store/uiTypes'; import type { CSSProperties } from 'react'; import { memo, useMemo, useRef } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; diff --git a/invokeai/frontend/web/src/features/ui/components/TabButton.tsx b/invokeai/frontend/web/src/features/ui/components/TabButton.tsx index 8d7071483c8..fa3d5732d7e 100644 --- a/invokeai/frontend/web/src/features/ui/components/TabButton.tsx +++ b/invokeai/frontend/web/src/features/ui/components/TabButton.tsx @@ -2,7 +2,7 @@ import { IconButton, Tooltip } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { setActiveTab } from 'features/ui/store/uiSlice'; -import type { TabName } from "features/ui/store/uiTypes"; +import type { TabName } from 'features/ui/store/uiTypes'; import { memo, type ReactElement, useCallback } from 'react'; export const TabButton = memo(({ tab, icon, label }: { tab: TabName; icon: ReactElement; label: string }) => { diff --git a/invokeai/frontend/web/src/features/ui/components/TabMountGate.tsx b/invokeai/frontend/web/src/features/ui/components/TabMountGate.tsx index 85d8324ea7f..5d8224e8f5f 100644 --- a/invokeai/frontend/web/src/features/ui/components/TabMountGate.tsx +++ b/invokeai/frontend/web/src/features/ui/components/TabMountGate.tsx @@ -1,7 +1,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { selectConfigSlice } from 'features/system/store/configSlice'; -import type { TabName } from "features/ui/store/uiTypes"; +import type { TabName } from 'features/ui/store/uiTypes'; import type { PropsWithChildren } from 'react'; import { memo, useMemo } from 'react'; diff --git a/invokeai/frontend/web/src/features/ui/components/TabVisibilityGate.tsx b/invokeai/frontend/web/src/features/ui/components/TabVisibilityGate.tsx index 1984480430c..f790618ca6b 100644 --- a/invokeai/frontend/web/src/features/ui/components/TabVisibilityGate.tsx +++ b/invokeai/frontend/web/src/features/ui/components/TabVisibilityGate.tsx @@ -1,6 +1,6 @@ import { Box } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; -import type { TabName } from "features/ui/store/uiTypes"; +import type { TabName } from 'features/ui/store/uiTypes'; import type { PropsWithChildren } from 'react'; import { memo } from 'react'; diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts index 32c34856576..092defd869d 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts @@ -4,7 +4,7 @@ import type { PersistConfig, RootState } from 'app/store/store'; import { workflowLoadRequested } from 'features/nodes/store/actions'; import { atom } from 'nanostores'; -import type { TabName , UIState } from "./uiTypes"; +import type { TabName, UIState } from './uiTypes'; const initialUIState: UIState = { _version: 2, diff --git a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts index 378626180fc..4e863fffdc5 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts @@ -1,4 +1,3 @@ - export type TabName = 'generation' | 'upscaling' | 'workflows' | 'models' | 'queue'; export interface UIState { From 5797797904e4ae56a8dd9adc119005d79cebd1bf Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 23 Aug 2024 16:55:36 +1000 Subject: [PATCH 448/678] chore(ui): lint --- .../src/features/controlLayers/konva/CanvasFilterModule.ts | 7 +++---- .../src/features/controlLayers/konva/CanvasLayerAdapter.ts | 2 +- .../src/features/controlLayers/konva/CanvasMaskAdapter.ts | 2 +- .../controlLayers/konva/CanvasProgressImageModule.ts | 4 ++-- invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts | 2 -- 5 files changed, 7 insertions(+), 10 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts index 03e238e044a..3f1274c5306 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts @@ -1,4 +1,4 @@ -import type { SerializableObject, SerializableObject } from 'common/types'; +import type { SerializableObject } from 'common/types'; import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; @@ -7,8 +7,7 @@ import { IMAGE_FILTERS, imageDTOToImageObject } from 'features/controlLayers/sto import { atom } from 'nanostores'; import type { Logger } from 'roarr'; import { getImageDTO } from 'services/api/endpoints/images'; -import type { BatchConfig, ImageDTO } from 'services/api/types'; -import type { InvocationCompleteEvent } from 'services/events/types'; +import type { BatchConfig, ImageDTO, S } from 'services/api/types'; import { assert } from 'tsafe'; const TYPE = 'entity_filter_preview'; @@ -63,7 +62,7 @@ export class CanvasFilterModule { const batch = this.buildBatchConfig(imageDTO, config, nodeId); // Listen for the filter processing completion event - const listener = async (event: InvocationCompleteEvent) => { + const listener = async (event: S['InvocationCompleteEvent']) => { if (event.origin !== this.id || event.invocation_source_id !== nodeId) { return; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts index ed14e9a89c7..af9d8462bbc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts @@ -1,4 +1,4 @@ -import type { SerializableObject, SerializableObject } from 'common/types'; +import type { SerializableObject } from 'common/types'; import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts index 94324d26de0..3573a79a17c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts @@ -1,4 +1,4 @@ -import type { SerializableObject, SerializableObject } from 'common/types'; +import type { SerializableObject } from 'common/types'; import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImageModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImageModule.ts index bc4967ccca3..b2b8a6977d9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImageModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImageModule.ts @@ -5,7 +5,7 @@ import type { CanvasPreviewModule } from 'features/controlLayers/konva/CanvasPre import { getPrefixedId, loadImage } from 'features/controlLayers/konva/util'; import Konva from 'konva'; import type { Logger } from 'roarr'; -import type { InvocationDenoiseProgressEvent } from 'services/events/types'; +import type { S } from 'services/api/types'; export class CanvasProgressImageModule { readonly type = 'progress_image'; @@ -30,7 +30,7 @@ export class CanvasProgressImageModule { isError: boolean = false; imageElement: HTMLImageElement | null = null; - lastProgressEvent: InvocationDenoiseProgressEvent | null = null; + lastProgressEvent: S['InvocationDenoiseProgressEvent'] | null = null; mutex: Mutex = new Mutex(); diff --git a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts index 73c28545e71..4aab4ab74e9 100644 --- a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts +++ b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts @@ -13,8 +13,6 @@ export const isValidDrop = (overData?: TypesafeDroppableData | null, activeData? } switch (actionType) { - case 'SET_CURRENT_IMAGE': - case 'SET_CA_IMAGE': case 'SET_IPA_IMAGE': case 'SET_RG_IP_ADAPTER_IMAGE': case 'ADD_RASTER_LAYER_FROM_IMAGE': From 87150b7c6ba52a02a53e116dc6a78ea0016130b0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 23 Aug 2024 17:02:46 +1000 Subject: [PATCH 449/678] feat(ui): add log debug button --- invokeai/frontend/web/public/locales/en.json | 1 + .../Settings/CanvasSettingsLogDebugInfo.tsx | 19 +++++++++++++++++++ .../Settings/CanvasSettingsPopover.tsx | 2 ++ .../controlLayers/konva/CanvasManager.ts | 4 ++-- 4 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsLogDebugInfo.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index ee0f629509a..bace0d040ea 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1731,6 +1731,7 @@ "hidingType": "Hiding {{type}}", "showingType": "Showing {{type}}", "dynamicGrid": "Dynamic Grid", + "logDebugInfo": "Log Debug Info", "fill": { "fillStyle": "Fill Style", "solid": "Solid", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsLogDebugInfo.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsLogDebugInfo.tsx new file mode 100644 index 00000000000..01323b2fa32 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsLogDebugInfo.tsx @@ -0,0 +1,19 @@ +import { Button } from '@invoke-ai/ui-library'; +import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const CanvasSettingsLogDebugInfoButton = memo(() => { + const { t } = useTranslation(); + const canvasManager = useCanvasManager(); + const onClick = useCallback(() => { + canvasManager.logDebugInfo(); + }, [canvasManager]); + return ( + + ); +}); + +CanvasSettingsLogDebugInfoButton.displayName = 'CanvasSettingsLogDebugInfoButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsPopover.tsx index 8ad4aeb22ae..34f1a11e4ec 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsPopover.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsPopover.tsx @@ -14,6 +14,7 @@ import { CanvasSettingsClearCachesButton } from 'features/controlLayers/componen import { CanvasSettingsClipToBboxCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox'; import { CanvasSettingsDynamicGridSwitch } from 'features/controlLayers/components/Settings/CanvasSettingsDynamicGridSwitch'; import { CanvasSettingsInvertScrollCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox'; +import { CanvasSettingsLogDebugInfoButton } from 'features/controlLayers/components/Settings/CanvasSettingsLogDebugInfo'; import { CanvasSettingsRecalculateRectsButton } from 'features/controlLayers/components/Settings/CanvasSettingsRecalculateRectsButton'; import { CanvasSettingsResetButton } from 'features/controlLayers/components/Settings/CanvasSettingsResetButton'; import { memo } from 'react'; @@ -58,6 +59,7 @@ const DebugSettings = () => { + ); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index c033165435e..09d641decb0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -152,10 +152,10 @@ export class CanvasManager { logDebugInfo() { // eslint-disable-next-line no-console - console.log(this); + console.log('Canvas manager', this); for (const adapter of this.adapters.getAll()) { // eslint-disable-next-line no-console - console.log(adapter); + console.log(adapter.id, adapter); } } From bc8bf989f35bacd26f0f5137ffec511dab2c15d3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 23 Aug 2024 17:10:32 +1000 Subject: [PATCH 450/678] feat(ui): update entity list menu --- invokeai/frontend/web/public/locales/en.json | 1 + .../components/AddLayerButton.tsx | 57 ------------- .../components/CanvasEntityListMenu.tsx | 80 +++++++++++++++++++ .../controlLayers/store/canvasV2Slice.ts | 2 +- .../ParametersPanelTextToImage.tsx | 4 +- 5 files changed, 84 insertions(+), 60 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityListMenu.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index bace0d040ea..faed7ce4b6e 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1657,6 +1657,7 @@ "autoSave": "Auto-save to Gallery", "resetCanvas": "Reset Canvas", "resetAll": "Reset All", + "deleteAll": "Delete All", "clearCaches": "Clear Caches", "recalculateRects": "Recalculate Rects", "clipToBbox": "Clip Strokes to Bbox", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx deleted file mode 100644 index b9d3b0fc623..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { IconButton, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { useDefaultControlAdapter, useDefaultIPAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter'; -import { - controlLayerAdded, - inpaintMaskAdded, - ipaAdded, - rasterLayerAdded, - rgAdded, -} from 'features/controlLayers/store/canvasV2Slice'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiPlusBold } from 'react-icons/pi'; - -export const AddLayerButton = memo(() => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const defaultControlAdapter = useDefaultControlAdapter(); - const defaultIPAdapter = useDefaultIPAdapter(); - const addInpaintMask = useCallback(() => { - dispatch(inpaintMaskAdded()); - }, [dispatch]); - const addRegionalGuidance = useCallback(() => { - dispatch(rgAdded()); - }, [dispatch]); - const addRasterLayer = useCallback(() => { - dispatch(rasterLayerAdded({ isSelected: true })); - }, [dispatch]); - const addControlLayer = useCallback(() => { - dispatch(controlLayerAdded({ isSelected: true, overrides: { controlAdapter: defaultControlAdapter } })); - }, [defaultControlAdapter, dispatch]); - const addIPAdapter = useCallback(() => { - dispatch(ipaAdded({ ipAdapter: defaultIPAdapter })); - }, [defaultIPAdapter, dispatch]); - - return ( - - } - variant="link" - data-testid="control-layers-add-layer-menu-button" - alignSelf="stretch" - /> - - {t('controlLayers.inpaintMask', { count: 1 })} - {t('controlLayers.regionalGuidance', { count: 1 })} - {t('controlLayers.rasterLayer', { count: 1 })} - {t('controlLayers.controlLayer', { count: 1 })} - {t('controlLayers.ipAdapter', { count: 1 })} - - - ); -}); - -AddLayerButton.displayName = 'AddLayerButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityListMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityListMenu.tsx new file mode 100644 index 00000000000..1ef30d92c34 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityListMenu.tsx @@ -0,0 +1,80 @@ +import { IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuList } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useDefaultControlAdapter, useDefaultIPAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter'; +import { + allEntitiesDeleted, + controlLayerAdded, + inpaintMaskAdded, + ipaAdded, + rasterLayerAdded, + rgAdded, +} from 'features/controlLayers/store/canvasV2Slice'; +import { selectEntityCount } from 'features/controlLayers/store/selectors'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiDotsThreeOutlineFill, PiPlusBold, PiTrashSimpleBold } from 'react-icons/pi'; + +export const CanvasEntityListMenu = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const hasEntities = useAppSelector((s) => { + const count = selectEntityCount(s); + return count > 0; + }); + const defaultControlAdapter = useDefaultControlAdapter(); + const defaultIPAdapter = useDefaultIPAdapter(); + const addInpaintMask = useCallback(() => { + dispatch(inpaintMaskAdded()); + }, [dispatch]); + const addRegionalGuidance = useCallback(() => { + dispatch(rgAdded()); + }, [dispatch]); + const addRasterLayer = useCallback(() => { + dispatch(rasterLayerAdded({ isSelected: true })); + }, [dispatch]); + const addControlLayer = useCallback(() => { + dispatch(controlLayerAdded({ isSelected: true, overrides: { controlAdapter: defaultControlAdapter } })); + }, [defaultControlAdapter, dispatch]); + const addIPAdapter = useCallback(() => { + dispatch(ipaAdded({ ipAdapter: defaultIPAdapter })); + }, [defaultIPAdapter, dispatch]); + const deleteAll = useCallback(() => { + dispatch(allEntitiesDeleted()); + }, [dispatch]); + + return ( + + } + variant="link" + data-testid="control-layers-add-layer-menu-button" + alignSelf="stretch" + /> + + } onClick={addInpaintMask}> + {t('controlLayers.inpaintMask', { count: 1 })} + + } onClick={addRegionalGuidance}> + {t('controlLayers.regionalGuidance', { count: 1 })} + + } onClick={addRasterLayer}> + {t('controlLayers.rasterLayer', { count: 1 })} + + } onClick={addControlLayer}> + {t('controlLayers.controlLayer', { count: 1 })} + + } onClick={addIPAdapter}> + {t('controlLayers.ipAdapter', { count: 1 })} + + + } color="error.300" isDisabled={!hasEntities}> + {t('controlLayers.deleteAll', { count: 1 })} + + + + ); +}); + +CanvasEntityListMenu.displayName = 'CanvasEntityListMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 3fe07ff5bc0..3b32084905f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -439,7 +439,6 @@ export const { invertScrollChanged, toolChanged, toolBufferChanged, - allEntitiesDeleted, clipToBboxChanged, canvasReset, settingsDynamicGridToggled, @@ -460,6 +459,7 @@ export const { entityArrangedBackwardOne, entityArrangedToBack, entityOpacityChanged, + allEntitiesDeleted, allEntitiesOfTypeIsHiddenToggled, // bbox bboxChanged, diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx index 3faa5204711..1b39f40227a 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx @@ -3,7 +3,7 @@ import { Box, Flex, Spacer, Tab, TabList, TabPanel, TabPanels, Tabs } from '@inv import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants'; -import { AddLayerButton } from 'features/controlLayers/components/AddLayerButton'; +import { CanvasEntityListMenu } from 'features/controlLayers/components/CanvasEntityListMenu'; import { CanvasPanelContent } from 'features/controlLayers/components/CanvasPanelContent'; import { $isPreviewVisible } from 'features/controlLayers/store/canvasV2Slice'; import { selectEntityCount } from 'features/controlLayers/store/selectors'; @@ -102,7 +102,7 @@ const ParametersPanelTextToImage = () => { {controlLayersTitle} - + From 51cd435ad83158592c6c897a573984c6a4a088f6 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 23 Aug 2024 17:11:47 +1000 Subject: [PATCH 451/678] tidy(ui): regional guidance buttons --- .../RegionalGuidanceAddPromptsIPAdapterButtons.tsx} | 2 +- .../RegionalGuidance/RegionalGuidanceSettings.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) rename invokeai/frontend/web/src/features/controlLayers/components/{AddPromptButtons.tsx => RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons.tsx} (96%) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons.tsx similarity index 96% rename from invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons.tsx index aa05176e380..07104989c01 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons.tsx @@ -17,7 +17,7 @@ type AddPromptButtonProps = { id: string; }; -export const AddPromptButtons = ({ id }: AddPromptButtonProps) => { +export const RegionalGuidanceAddPromptsIPAdapterButtons = ({ id }: AddPromptButtonProps) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const defaultIPAdapter = useDefaultIPAdapter(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx index fb359e06c5b..54165cfbeb8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx @@ -1,7 +1,7 @@ import { Divider } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; -import { AddPromptButtons } from 'features/controlLayers/components/AddPromptButtons'; import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper'; +import { RegionalGuidanceAddPromptsIPAdapterButtons } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers'; import { memo } from 'react'; @@ -24,7 +24,9 @@ export const RegionalGuidanceSettings = memo(() => { return ( - {!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && } + {!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && ( + + )} {hasPositivePrompt && ( <> From cff871e8a6893bdcb859a527f09cd10de56dce19 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 23 Aug 2024 17:14:18 +1000 Subject: [PATCH 452/678] tidy(ui): "eye dropper" -> "color picker" --- invokeai/frontend/web/public/locales/en.json | 2 +- .../components/Tool/ToolChooser.tsx | 4 +-- .../components/Tool/ToolEyeDropperButton.tsx | 12 ++++----- .../controlLayers/konva/CanvasToolModule.ts | 26 +++++++++---------- .../features/controlLayers/konva/events.ts | 4 +-- .../src/features/controlLayers/store/types.ts | 2 +- 6 files changed, 25 insertions(+), 25 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index faed7ce4b6e..c2ce7701ef5 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1750,7 +1750,7 @@ "move": "Move", "view": "View", "transform": "Transform", - "eyeDropper": "Eye Dropper" + "colorPicker": "Color Picker" }, "filter": { "filter": "Filter", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolChooser.tsx index 44409e8374b..bb3a424d4ea 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolChooser.tsx @@ -1,7 +1,7 @@ import { ButtonGroup } from '@invoke-ai/ui-library'; import { ToolBboxButton } from 'features/controlLayers/components/Tool/ToolBboxButton'; import { ToolBrushButton } from 'features/controlLayers/components/Tool/ToolBrushButton'; -import { ToolEyeDropperButton } from 'features/controlLayers/components/Tool/ToolEyeDropperButton'; +import { ToolColorPickerButton } from 'features/controlLayers/components/Tool/ToolEyeDropperButton'; import { ToolMoveButton } from 'features/controlLayers/components/Tool/ToolMoveButton'; import { ToolRectButton } from 'features/controlLayers/components/Tool/ToolRectButton'; import { useCanvasDeleteLayerHotkey } from 'features/controlLayers/hooks/useCanvasDeleteLayerHotkey'; @@ -23,7 +23,7 @@ export const ToolChooser: React.FC = () => { - + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEyeDropperButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEyeDropperButton.tsx index d1783b93ab7..de791a8afc3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEyeDropperButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEyeDropperButton.tsx @@ -7,11 +7,11 @@ import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiEyedropperBold } from 'react-icons/pi'; -export const ToolEyeDropperButton = memo(() => { +export const ToolColorPickerButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const isTransforming = useIsTransforming(); - const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'eyeDropper'); + const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'colorPicker'); const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); const isDisabled = useMemo(() => { @@ -19,15 +19,15 @@ export const ToolEyeDropperButton = memo(() => { }, [isStaging, isTransforming]); const onClick = useCallback(() => { - dispatch(toolChanged('eyeDropper')); + dispatch(toolChanged('colorPicker')); }, [dispatch]); useHotkeys('i', onClick, { enabled: !isDisabled || isSelected }, [onClick, isSelected, isDisabled]); return ( } colorScheme={isSelected ? 'invokeBlue' : 'base'} variant="outline" @@ -37,4 +37,4 @@ export const ToolEyeDropperButton = memo(() => { ); }); -ToolEyeDropperButton.displayName = 'ToolEyeDropperButton'; +ToolColorPickerButton.displayName = 'ToolColorPickerButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts index b2915d1e0b4..fc06cadb44b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts @@ -36,7 +36,7 @@ export class CanvasToolModule { innerBorderCircle: Konva.Circle; outerBorderCircle: Konva.Circle; }; - eyeDropper: { + colorPicker: { group: Konva.Group; fillCircle: Konva.Circle; transparentCenterCircle: Konva.Circle; @@ -102,10 +102,10 @@ export class CanvasToolModule { strokeEnabled: true, }), }, - eyeDropper: { - group: new Konva.Group({ name: `${this.type}:eyeDropper_group`, listening: false }), + colorPicker: { + group: new Konva.Group({ name: `${this.type}:color_picker_group`, listening: false }), fillCircle: new Konva.Circle({ - name: `${this.type}:eyeDropper_fill_circle`, + name: `${this.type}:color_picker_fill_circle`, listening: false, fill: '', radius: 20, @@ -114,7 +114,7 @@ export class CanvasToolModule { strokeScaleEnabled: false, }), transparentCenterCircle: new Konva.Circle({ - name: `${this.type}:eyeDropper_fill_circle`, + name: `${this.type}:color_picker_fill_circle`, listening: false, strokeEnabled: false, fill: 'white', @@ -133,9 +133,9 @@ export class CanvasToolModule { this.konva.eraser.group.add(this.konva.eraser.outerBorderCircle); this.konva.group.add(this.konva.eraser.group); - this.konva.eyeDropper.group.add(this.konva.eyeDropper.fillCircle); - this.konva.eyeDropper.group.add(this.konva.eyeDropper.transparentCenterCircle); - this.konva.group.add(this.konva.eyeDropper.group); + this.konva.colorPicker.group.add(this.konva.colorPicker.fillCircle); + this.konva.colorPicker.group.add(this.konva.colorPicker.transparentCenterCircle); + this.konva.group.add(this.konva.colorPicker.group); this.subscriptions.add( this.manager.stateApi.$stageAttrs.listen(() => { @@ -179,7 +179,7 @@ export class CanvasToolModule { setToolVisibility = (tool: Tool) => { this.konva.brush.group.visible(tool === 'brush'); this.konva.eraser.group.visible(tool === 'eraser'); - this.konva.eyeDropper.group.visible(tool === 'eyeDropper'); + this.konva.colorPicker.group.visible(tool === 'colorPicker'); }; render() { @@ -206,8 +206,8 @@ export class CanvasToolModule { // Bbox tool gets default } else if (tool === 'bbox') { stage.container.style.cursor = 'default'; - } else if (tool === 'eyeDropper') { - // Eyedropper gets none + } else if (tool === 'colorPicker') { + // Color picker gets none stage.container.style.cursor = 'none'; } else if (isDrawable) { if (tool === 'move') { @@ -281,12 +281,12 @@ export class CanvasToolModule { radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, }); } else if (cursorPos && colorUnderCursor) { - this.konva.eyeDropper.fillCircle.setAttrs({ + this.konva.colorPicker.fillCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, fill: rgbaColorToString(colorUnderCursor), }); - this.konva.eyeDropper.transparentCenterCircle.setAttrs({ + this.konva.colorPicker.transparentCenterCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index a514d65e982..fc5c112abba 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -193,7 +193,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { const pos = updateLastCursorPos(stage, $lastCursorPos.set); const selectedEntity = getSelectedEntity(); - if (toolState.selected === 'eyeDropper') { + if (toolState.selected === 'colorPicker') { const color = getColorUnderCursor(stage); manager.stateApi.$colorUnderCursor.set(color); if (color) { @@ -343,7 +343,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { const pos = updateLastCursorPos(stage, $lastCursorPos.set); const selectedEntity = getSelectedEntity(); - if (toolState.selected === 'eyeDropper') { + if (toolState.selected === 'colorPicker') { const color = getColorUnderCursor(stage); manager.stateApi.$colorUnderCursor.set(color); } else { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 8f386f6f7c6..b9ac58c8196 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -456,7 +456,7 @@ export const IMAGE_FILTERS: { [key in FilterConfig['type']]: ImageFilterData; const zPoints = z.array(z.number()).refine((points) => points.length % 2 === 0, { From 66424c3c9311bf7424d1b98f8647734b395af05d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 23 Aug 2024 17:17:40 +1000 Subject: [PATCH 453/678] feat(ui): fix delete layer hotkey --- .../controlLayers/hooks/useCanvasDeleteLayerHotkey.ts | 5 ++++- .../gallery/components/ImageViewer/CurrentImageButtons.tsx | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts index 2d248b15ec8..0baa6823d93 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts @@ -28,5 +28,8 @@ export function useCanvasDeleteLayerHotkey() { [selectedEntityIdentifier, isStaging] ); - useHotkeys('shift+d', deleteSelectedLayer, { enabled: isDeleteEnabled }, [isDeleteEnabled, deleteSelectedLayer]); + useHotkeys(['delete', 'backspace'], deleteSelectedLayer, { enabled: isDeleteEnabled }, [ + isDeleteEnabled, + deleteSelectedLayer, + ]); } diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx index 65131dd3e5e..1364307f8c1 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx @@ -101,7 +101,7 @@ const CurrentImageButtons = () => { [isUpscalingEnabled, imageDTO, shouldDisableToolbarButtons, isConnected, isImageViewerActive] ); - useHotkeys('delete', handleDelete, { enabled: isImageViewerActive }, [imageDTO, isImageViewerActive]); + useHotkeys(['delete', 'backspace'], handleDelete, { enabled: isImageViewerActive }, [imageDTO, isImageViewerActive]); return ( <> From 9486c50e67c54954ee54a17bb3ddeb404167c08f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 23 Aug 2024 17:30:41 +1000 Subject: [PATCH 454/678] fix(ui): select next entity in the list when deleting --- .../controlLayers/store/canvasV2Slice.ts | 106 ++++++++---------- 1 file changed, 48 insertions(+), 58 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 3b32084905f..801ccbe4aa6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -3,7 +3,6 @@ import { createAction, createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { deepClone } from 'common/util/deepClone'; -import { exhaustiveCheck } from 'features/controlLayers/konva/util'; import { bboxReducers } from 'features/controlLayers/store/bboxReducers'; import { compositingReducers } from 'features/controlLayers/store/compositingReducers'; import { controlLayersReducers } from 'features/controlLayers/store/controlLayersReducers'; @@ -157,21 +156,31 @@ export function selectEntity(state: CanvasV2State, { id, type }: CanvasEntityIde } function selectAllEntitiesOfType(state: CanvasV2State, type: CanvasEntityState['type']): CanvasEntityState[] { - if (type === 'raster_layer') { - return state.rasterLayers.entities; - } else if (type === 'control_layer') { - return state.controlLayers.entities; - } else if (type === 'inpaint_mask') { - return state.inpaintMasks.entities; - } else if (type === 'regional_guidance') { - return state.regions.entities; - } else if (type === 'ip_adapter') { - return state.ipAdapters.entities; - } else { - assert(false, 'Not implemented'); + switch (type) { + case 'raster_layer': + return state.rasterLayers.entities; + case 'control_layer': + return state.controlLayers.entities; + case 'inpaint_mask': + return state.inpaintMasks.entities; + case 'regional_guidance': + return state.regions.entities; + case 'ip_adapter': + return state.ipAdapters.entities; } } +function selectAllEntities(state: CanvasV2State): CanvasEntityState[] { + // These are in the same order as they are displayed in the list! + return [ + ...state.inpaintMasks.entities.toReversed(), + ...state.regions.entities.toReversed(), + ...state.ipAdapters.entities.toReversed(), + ...state.controlLayers.entities.toReversed(), + ...state.rasterLayers.entities.toReversed(), + ]; +} + export const canvasV2Slice = createSlice({ name: 'canvasV2', initialState, @@ -288,49 +297,33 @@ export const canvasV2Slice = createSlice({ entityDeleted: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; - const firstInpaintMaskEntity = state.inpaintMasks.entities[0]; - - let selectedEntityIdentifier: CanvasV2State['selectedEntityIdentifier'] = firstInpaintMaskEntity - ? getEntityIdentifier(firstInpaintMaskEntity) - : null; - - if (entityIdentifier.type === 'raster_layer') { - const index = state.rasterLayers.entities.findIndex((layer) => layer.id === entityIdentifier.id); - state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => layer.id !== entityIdentifier.id); - const nextRasterLayer = state.rasterLayers.entities[index]; - if (nextRasterLayer) { - selectedEntityIdentifier = { type: nextRasterLayer.type, id: nextRasterLayer.id }; - } - } else if (entityIdentifier.type === 'control_layer') { - const index = state.controlLayers.entities.findIndex((layer) => layer.id === entityIdentifier.id); - state.controlLayers.entities = state.controlLayers.entities.filter((rg) => rg.id !== entityIdentifier.id); - const nextControlLayer = state.controlLayers.entities[index]; - if (nextControlLayer) { - selectedEntityIdentifier = { type: nextControlLayer.type, id: nextControlLayer.id }; - } - } else if (entityIdentifier.type === 'regional_guidance') { - const index = state.regions.entities.findIndex((layer) => layer.id === entityIdentifier.id); - state.regions.entities = state.regions.entities.filter((rg) => rg.id !== entityIdentifier.id); - const region = state.regions.entities[index]; - if (region) { - selectedEntityIdentifier = { type: region.type, id: region.id }; + let selectedEntityIdentifier: CanvasV2State['selectedEntityIdentifier'] = null; + const allEntities = selectAllEntities(state); + const index = allEntities.findIndex((entity) => entity.id === entityIdentifier.id); + const nextIndex = allEntities.length > 1 ? (index + 1) % allEntities.length : -1; + if (nextIndex !== -1) { + const nextEntity = allEntities[nextIndex]; + if (nextEntity) { + selectedEntityIdentifier = getEntityIdentifier(nextEntity); } - } else if (entityIdentifier.type === 'ip_adapter') { - const index = state.ipAdapters.entities.findIndex((layer) => layer.id === entityIdentifier.id); - state.ipAdapters.entities = state.ipAdapters.entities.filter((rg) => rg.id !== entityIdentifier.id); - const entity = state.ipAdapters.entities[index]; - if (entity) { - selectedEntityIdentifier = { type: entity.type, id: entity.id }; - } - } else if (entityIdentifier.type === 'inpaint_mask') { - const index = state.inpaintMasks.entities.findIndex((layer) => layer.id === entityIdentifier.id); - state.inpaintMasks.entities = state.inpaintMasks.entities.filter((rg) => rg.id !== entityIdentifier.id); - const entity = state.inpaintMasks.entities[index]; - if (entity) { - selectedEntityIdentifier = { type: entity.type, id: entity.id }; - } - } else { - assert(false, 'Not implemented'); + } + + switch (entityIdentifier.type) { + case 'raster_layer': + state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => layer.id !== entityIdentifier.id); + break; + case 'control_layer': + state.controlLayers.entities = state.controlLayers.entities.filter((rg) => rg.id !== entityIdentifier.id); + break; + case 'regional_guidance': + state.regions.entities = state.regions.entities.filter((rg) => rg.id !== entityIdentifier.id); + break; + case 'ip_adapter': + state.ipAdapters.entities = state.ipAdapters.entities.filter((rg) => rg.id !== entityIdentifier.id); + break; + case 'inpaint_mask': + state.inpaintMasks.entities = state.inpaintMasks.entities.filter((rg) => rg.id !== entityIdentifier.id); + break; } state.selectedEntityIdentifier = selectedEntityIdentifier; @@ -397,9 +390,6 @@ export const canvasV2Slice = createSlice({ case 'ip_adapter': // no-op break; - default: { - exhaustiveCheck(type); - } } }, allEntitiesDeleted: (state) => { From e826f8a0203a47127b488b01e6ab7e380c035b24 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 23 Aug 2024 18:13:35 +1000 Subject: [PATCH 455/678] fix(ui): sdxl graph builder --- .../nodes/util/graph/generation/buildSDXLGraph.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index e1384a88263..601e9671a7b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -207,7 +207,7 @@ export const buildSDXLGraph = async ( ); } - const controlNetCollector = g.createNode({ + const controlNetCollector = g.addNode({ type: 'collect', id: getPrefixedId('control_net_collector'), }); @@ -220,11 +220,12 @@ export const buildSDXLGraph = async ( modelConfig.base ); if (controlNetResult.addedControlNets > 0) { - g.addNode(controlNetCollector); g.addEdge(controlNetCollector, 'collection', denoise, 'control'); + } else { + g.deleteNode(controlNetCollector.id); } - const t2iAdapterCollector = g.createNode({ + const t2iAdapterCollector = g.addNode({ type: 'collect', id: getPrefixedId('t2i_adapter_collector'), }); @@ -237,11 +238,12 @@ export const buildSDXLGraph = async ( modelConfig.base ); if (t2iAdapterResult.addedT2IAdapters > 0) { - g.addNode(t2iAdapterCollector); g.addEdge(t2iAdapterCollector, 'collection', denoise, 't2i_adapter'); + } else { + g.deleteNode(t2iAdapterCollector.id); } - const ipAdapterCollector = g.createNode({ + const ipAdapterCollector = g.addNode({ type: 'collect', id: getPrefixedId('ip_adapter_collector'), }); @@ -264,8 +266,9 @@ export const buildSDXLGraph = async ( const totalIPAdaptersAdded = ipAdapterResult.addedIPAdapters + regionsResult.reduce((acc, r) => acc + r.addedIPAdapters, 0); if (totalIPAdaptersAdded > 0) { - g.addNode(ipAdapterCollector); g.addEdge(ipAdapterCollector, 'collection', denoise, 'ip_adapter'); + } else { + g.deleteNode(ipAdapterCollector.id); } if (state.system.shouldUseNSFWChecker) { From e0e816599201c0d12f47db726897feacf783b059 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 23 Aug 2024 18:23:45 +1000 Subject: [PATCH 456/678] fix(ui): upscale tab graph --- .../listeners/enqueueRequestedUpscale.ts | 4 +- .../graph/buildMultidiffusionUpscaleGraph.ts | 107 +++++++++--------- 2 files changed, 57 insertions(+), 54 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedUpscale.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedUpscale.ts index dc870a9f8b5..959a1bf5ae7 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedUpscale.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedUpscale.ts @@ -14,9 +14,9 @@ export const addEnqueueRequestedUpscale = (startAppListening: AppStartListening) const { shouldShowProgressInViewer } = state.ui; const { prepend } = action.payload; - const graph = await buildMultidiffusionUpscaleGraph(state); + const { g, noise, posCond } = await buildMultidiffusionUpscaleGraph(state); - const batchConfig = prepareLinearUIBatch(state, graph, prepend); + const batchConfig = prepareLinearUIBatch(state, g, prepend, noise, posCond); const req = dispatch( queueApi.endpoints.enqueueBatch.initiate(batchConfig, { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildMultidiffusionUpscaleGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildMultidiffusionUpscaleGraph.ts index f07d4cbc793..d9c29d92f5b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildMultidiffusionUpscaleGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildMultidiffusionUpscaleGraph.ts @@ -3,13 +3,16 @@ import { getPrefixedId } from 'features/controlLayers/konva/util'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { addSDXLLoRAs } from 'features/nodes/util/graph/generation/addSDXLLoRAs'; import { Graph } from 'features/nodes/util/graph/generation/Graph'; +import type { Invocation } from 'services/api/types'; import { isNonRefinerMainModelConfig, isSpandrelImageToImageModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; import { addLoRAs } from './generation/addLoRAs'; import { getBoardField, getPresetModifiedPrompts } from './graphBuilderUtils'; -export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise => { +export const buildMultidiffusionUpscaleGraph = async ( + state: RootState +): Promise<{ g: Graph; noise: Invocation<'noise'>; posCond: Invocation<'compel' | 'sdxl_compel_prompt'> }> => { const { model, cfgScale: cfg_scale, scheduler, steps, vaePrecision, seed, vae } = state.canvasV2.params; const { upscaleModel, upscaleInitialImage, structure, creativity, tileControlnetModel, scale } = state.upscale; @@ -20,7 +23,7 @@ export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise const g = new Graph(); - const upscaleNode = g.addNode({ + const spandrelAutoscale = g.addNode({ type: 'spandrel_image_to_image_autoscale', id: getPrefixedId('spandrel_autoscale'), image: upscaleInitialImage, @@ -29,34 +32,34 @@ export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise scale, }); - const unsharpMaskNode2 = g.addNode({ + const unsharpMask = g.addNode({ type: 'unsharp_mask', id: getPrefixedId('unsharp_2'), radius: 2, strength: 60, }); - g.addEdge(upscaleNode, 'image', unsharpMaskNode2, 'image'); + g.addEdge(spandrelAutoscale, 'image', unsharpMask, 'image'); - const noiseNode = g.addNode({ + const noise = g.addNode({ type: 'noise', id: getPrefixedId('noise'), seed, }); - g.addEdge(unsharpMaskNode2, 'width', noiseNode, 'width'); - g.addEdge(unsharpMaskNode2, 'height', noiseNode, 'height'); + g.addEdge(unsharpMask, 'width', noise, 'width'); + g.addEdge(unsharpMask, 'height', noise, 'height'); - const i2lNode = g.addNode({ + const i2l = g.addNode({ type: 'i2l', id: getPrefixedId('i2l'), fp32: vaePrecision === 'fp32', tiled: true, }); - g.addEdge(unsharpMaskNode2, 'image', i2lNode, 'image'); + g.addEdge(unsharpMask, 'image', i2l, 'image'); - const l2iNode = g.addNode({ + const l2i = g.addNode({ type: 'l2i', id: getPrefixedId('l2i'), fp32: vaePrecision === 'fp32', @@ -65,7 +68,7 @@ export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise is_intermediate: false, }); - const tiledMultidiffusionNode = g.addNode({ + const tiledMultidiffusion = g.addNode({ type: 'tiled_multi_diffusion_denoise_latents', id: getPrefixedId('tiled_multidiffusion_denoise_latents'), tile_height: 1024, // is this dependent on base model @@ -78,37 +81,37 @@ export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise denoising_end: 1, }); - let posCondNode; - let negCondNode; - let modelNode; + let posCond; + let negCond; + let modelLoader; if (model.base === 'sdxl') { const { positivePrompt, negativePrompt, positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state); - posCondNode = g.addNode({ + posCond = g.addNode({ type: 'sdxl_compel_prompt', id: getPrefixedId('pos_cond'), prompt: positivePrompt, style: positiveStylePrompt, }); - negCondNode = g.addNode({ + negCond = g.addNode({ type: 'sdxl_compel_prompt', id: getPrefixedId('neg_cond'), prompt: negativePrompt, style: negativeStylePrompt, }); - modelNode = g.addNode({ + modelLoader = g.addNode({ type: 'sdxl_model_loader', id: getPrefixedId('sdxl_model_loader'), model, }); - g.addEdge(modelNode, 'clip', posCondNode, 'clip'); - g.addEdge(modelNode, 'clip', negCondNode, 'clip'); - g.addEdge(modelNode, 'clip2', posCondNode, 'clip2'); - g.addEdge(modelNode, 'clip2', negCondNode, 'clip2'); - g.addEdge(modelNode, 'unet', tiledMultidiffusionNode, 'unet'); - addSDXLLoRAs(state, g, tiledMultidiffusionNode, modelNode, null, posCondNode, negCondNode); + g.addEdge(modelLoader, 'clip', posCond, 'clip'); + g.addEdge(modelLoader, 'clip', negCond, 'clip'); + g.addEdge(modelLoader, 'clip2', posCond, 'clip2'); + g.addEdge(modelLoader, 'clip2', negCond, 'clip2'); + g.addEdge(modelLoader, 'unet', tiledMultidiffusion, 'unet'); + addSDXLLoRAs(state, g, tiledMultidiffusion, modelLoader, null, posCond, negCond); g.upsertMetadata({ positive_prompt: positivePrompt, @@ -119,17 +122,17 @@ export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise } else { const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state); - posCondNode = g.addNode({ + posCond = g.addNode({ type: 'compel', id: getPrefixedId('pos_cond'), prompt: positivePrompt, }); - negCondNode = g.addNode({ + negCond = g.addNode({ type: 'compel', id: getPrefixedId('neg_cond'), prompt: negativePrompt, }); - modelNode = g.addNode({ + modelLoader = g.addNode({ type: 'main_model_loader', id: getPrefixedId('sd1_model_loader'), model, @@ -139,11 +142,11 @@ export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise id: getPrefixedId('clip_skip'), }); - g.addEdge(modelNode, 'clip', clipSkipNode, 'clip'); - g.addEdge(clipSkipNode, 'clip', posCondNode, 'clip'); - g.addEdge(clipSkipNode, 'clip', negCondNode, 'clip'); - g.addEdge(modelNode, 'unet', tiledMultidiffusionNode, 'unet'); - addLoRAs(state, g, tiledMultidiffusionNode, modelNode, null, clipSkipNode, posCondNode, negCondNode); + g.addEdge(modelLoader, 'clip', clipSkipNode, 'clip'); + g.addEdge(clipSkipNode, 'clip', posCond, 'clip'); + g.addEdge(clipSkipNode, 'clip', negCond, 'clip'); + g.addEdge(modelLoader, 'unet', tiledMultidiffusion, 'unet'); + addLoRAs(state, g, tiledMultidiffusion, modelLoader, null, clipSkipNode, posCond, negCond); g.upsertMetadata({ positive_prompt: positivePrompt, @@ -172,30 +175,30 @@ export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise upscale_scale: scale, }); - g.setMetadataReceivingNode(l2iNode); - g.addEdgeToMetadata(upscaleNode, 'width', 'width'); - g.addEdgeToMetadata(upscaleNode, 'height', 'height'); + g.setMetadataReceivingNode(l2i); + g.addEdgeToMetadata(spandrelAutoscale, 'width', 'width'); + g.addEdgeToMetadata(spandrelAutoscale, 'height', 'height'); - let vaeNode; + let vaeLoader; if (vae) { - vaeNode = g.addNode({ + vaeLoader = g.addNode({ type: 'vae_loader', id: getPrefixedId('vae'), vae_model: vae, }); } - g.addEdge(vaeNode || modelNode, 'vae', i2lNode, 'vae'); - g.addEdge(vaeNode || modelNode, 'vae', l2iNode, 'vae'); + g.addEdge(vaeLoader || modelLoader, 'vae', i2l, 'vae'); + g.addEdge(vaeLoader || modelLoader, 'vae', l2i, 'vae'); - g.addEdge(noiseNode, 'noise', tiledMultidiffusionNode, 'noise'); - g.addEdge(i2lNode, 'latents', tiledMultidiffusionNode, 'latents'); - g.addEdge(posCondNode, 'conditioning', tiledMultidiffusionNode, 'positive_conditioning'); - g.addEdge(negCondNode, 'conditioning', tiledMultidiffusionNode, 'negative_conditioning'); + g.addEdge(noise, 'noise', tiledMultidiffusion, 'noise'); + g.addEdge(i2l, 'latents', tiledMultidiffusion, 'latents'); + g.addEdge(posCond, 'conditioning', tiledMultidiffusion, 'positive_conditioning'); + g.addEdge(negCond, 'conditioning', tiledMultidiffusion, 'negative_conditioning'); - g.addEdge(tiledMultidiffusionNode, 'latents', l2iNode, 'latents'); + g.addEdge(tiledMultidiffusion, 'latents', l2i, 'latents'); - const controlnetNode1 = g.addNode({ + const controlNet1 = g.addNode({ id: 'controlnet_1', type: 'controlnet', control_model: tileControlnetModel, @@ -206,9 +209,9 @@ export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise end_step_percent: (structure + 10) * 0.025 + 0.3, }); - g.addEdge(unsharpMaskNode2, 'image', controlnetNode1, 'image'); + g.addEdge(unsharpMask, 'image', controlNet1, 'image'); - const controlnetNode2 = g.addNode({ + const controlNet2 = g.addNode({ id: 'controlnet_2', type: 'controlnet', control_model: tileControlnetModel, @@ -219,16 +222,16 @@ export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise end_step_percent: 0.85, }); - g.addEdge(unsharpMaskNode2, 'image', controlnetNode2, 'image'); + g.addEdge(unsharpMask, 'image', controlNet2, 'image'); - const collectNode = g.addNode({ + const controlNetCollector = g.addNode({ type: 'collect', id: getPrefixedId('controlnet_collector'), }); - g.addEdge(controlnetNode1, 'control', collectNode, 'item'); - g.addEdge(controlnetNode2, 'control', collectNode, 'item'); + g.addEdge(controlNet1, 'control', controlNetCollector, 'item'); + g.addEdge(controlNet2, 'control', controlNetCollector, 'item'); - g.addEdge(collectNode, 'collection', tiledMultidiffusionNode, 'control'); + g.addEdge(controlNetCollector, 'collection', tiledMultidiffusion, 'control'); - return g; + return { g, noise, posCond }; }; From 18d9263d60235ed16f638e94c2d1b50c470cc89d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 23 Aug 2024 18:24:04 +1000 Subject: [PATCH 457/678] tidy(ui): more cleanup --- .../listeners/imageDeletionListeners.ts | 6 ++-- .../listeners/imageDropped.ts | 33 +------------------ .../RasterLayerMenuItemsRasterToControl.tsx | 7 ++-- .../deleteImageModal/store/selectors.ts | 16 +++------ 4 files changed, 12 insertions(+), 50 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts index 858a8cea0b6..d6c284f3c06 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts @@ -15,6 +15,8 @@ import type { ImageDTO } from 'services/api/types'; const log = logger('gallery'); +//TODO(psyche): handle image deletion (canvas sessions?) + // Some utils to delete images from different parts of the app const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { state.nodes.present.nodes.forEach((node) => { @@ -49,8 +51,8 @@ const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: Im // }; const deleteIPAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { - state.canvasV2.ipAdapters.entities.forEach(({ id, imageObject }) => { - if (imageObject?.image.image_name === imageDTO.image_name) { + state.canvasV2.ipAdapters.entities.forEach(({ id, ipAdapter }) => { + if (ipAdapter.image?.image_name === imageDTO.image_name) { dispatch(ipaImageChanged({ id, imageDTO: null })); } }); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index c2d2cb620d4..658b219dbb1 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -11,12 +11,7 @@ import type { CanvasControlLayerState, CanvasRasterLayerState } from 'features/c import { imageDTOToImageObject } from 'features/controlLayers/store/types'; import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import { isValidDrop } from 'features/dnd/util/isValidDrop'; -import { - imageSelected, - imageToCompareChanged, - isImageViewerOpenChanged, - selectionChanged, -} from 'features/gallery/store/gallerySlice'; +import { imageToCompareChanged, isImageViewerOpenChanged, selectionChanged } from 'features/gallery/store/gallerySlice'; import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice'; import { imagesApi } from 'services/api/endpoints/images'; @@ -47,32 +42,6 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => log.debug({ activeData, overData }, `Unknown payload dropped`); } - /** - * Image dropped on current image - */ - if ( - overData.actionType === 'SET_CURRENT_IMAGE' && - activeData.payloadType === 'IMAGE_DTO' && - activeData.payload.imageDTO - ) { - dispatch(imageSelected(activeData.payload.imageDTO)); - dispatch(isImageViewerOpenChanged(true)); - return; - } - - // /** - // * Image dropped on Control Adapter Layer - // */ - // if ( - // overData.actionType === 'SET_CA_IMAGE' && - // activeData.payloadType === 'IMAGE_DTO' && - // activeData.payload.imageDTO - // ) { - // const { id } = overData.context; - // dispatch(caImageChanged({ id, imageDTO: activeData.payload.imageDTO })); - // return; - // } - /** * Image dropped on IP Adapter Layer */ diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl.tsx index 9513f0409c8..57c97b3d876 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl.tsx @@ -1,7 +1,6 @@ import { MenuItem } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { useDefaultControlAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter'; import { rasterLayerConvertedToControlLayer } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -12,11 +11,9 @@ export const RasterLayerMenuItemsRasterToControl = memo(() => { const dispatch = useAppDispatch(); const entityIdentifier = useEntityIdentifierContext(); - const defaultControlAdapter = useDefaultControlAdapter(); - const convertRasterLayerToControlLayer = useCallback(() => { - dispatch(rasterLayerConvertedToControlLayer({ id: entityIdentifier.id, controlAdapter: defaultControlAdapter })); - }, [dispatch, defaultControlAdapter, entityIdentifier.id]); + dispatch(rasterLayerConvertedToControlLayer({ id: entityIdentifier.id })); + }, [dispatch, entityIdentifier.id]); return ( }> diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts index fc6f4b085c3..c5491f8bc38 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts @@ -9,28 +9,22 @@ import { isInvocationNode } from 'features/nodes/types/invocation'; import { some } from 'lodash-es'; import type { ImageUsage } from './types'; - +// TODO(psyche): handle image deletion (canvas sessions?) export const getImageUsage = (nodes: NodesState, canvasV2: CanvasV2State, image_name: string) => { - const isLayerImage = canvasV2.rasterLayers.entities.some((layer) => - layer.objects.some((obj) => obj.type === 'image' && obj.image.image_name === image_name) - ); - const isNodesImage = nodes.nodes .filter(isInvocationNode) .some((node) => some(node.data.inputs, (input) => isImageFieldInputInstance(input) && input.value?.image_name === image_name) ); - const isControlAdapterImage = canvasV2.controlLayers.entities.some( - (ca) => ca.imageObject?.image.image_name === image_name || ca.processedImageObject?.image.image_name === image_name + const isIPAdapterImage = canvasV2.ipAdapters.entities.some( + ({ ipAdapter }) => ipAdapter.image?.image_name === image_name ); - const isIPAdapterImage = canvasV2.ipAdapters.entities.some((ipa) => ipa.imageObject?.image.image_name === image_name); - const imageUsage: ImageUsage = { - isLayerImage, + isLayerImage: false, isNodesImage, - isControlAdapterImage, + isControlAdapterImage: false, isIPAdapterImage, }; From 64f7c7bf362427623c42b76029898318a9f605f7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 23 Aug 2024 18:33:55 +1000 Subject: [PATCH 458/678] fix(ui): staging area actions --- .../addCommitStagingAreaImageListener.ts | 2 +- .../components/ResetAllEntitiesButton.tsx | 28 ------------------- .../StagingArea/StagingAreaToolbar.tsx | 21 ++++++-------- .../web/src/services/api/endpoints/images.ts | 4 +-- 4 files changed, 10 insertions(+), 45 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ResetAllEntitiesButton.tsx diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts index 8363c0ff66a..70540b13c5c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts @@ -67,7 +67,7 @@ export const addStagingListeners = (startAppListening: AppStartListening) => { objects: [imageObject], }; - api.dispatch(rasterLayerAdded({ overrides })); + api.dispatch(rasterLayerAdded({ overrides, isSelected: true })); api.dispatch(sessionStagingAreaReset()); }, }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ResetAllEntitiesButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ResetAllEntitiesButton.tsx deleted file mode 100644 index 6851f7c5ab8..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ResetAllEntitiesButton.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Button } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { allEntitiesDeleted } from 'features/controlLayers/store/canvasV2Slice'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiTrashSimpleBold } from 'react-icons/pi'; - -export const ResetAllEntitiesButton = memo(() => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const onClick = useCallback(() => { - dispatch(allEntitiesDeleted()); - }, [dispatch]); - - return ( - - ); -}); - -ResetAllEntitiesButton.displayName = 'ResetAllEntitiesButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx index c0cac8214f2..2893e66711b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx @@ -23,6 +23,7 @@ import { PiTrashSimpleBold, PiXBold, } from 'react-icons/pi'; +import { useChangeImageIsIntermediateMutation } from 'services/api/endpoints/images'; export const StagingAreaToolbar = memo(() => { const dispatch = useAppDispatch(); @@ -33,7 +34,7 @@ export const StagingAreaToolbar = memo(() => { return images[session.selectedStagedImageIndex] ?? null; }, [images, session.selectedStagedImageIndex]); const isCanvasActive = useStore(INTERACTION_SCOPES.canvas.$isActive); - // const [changeIsImageIntermediate] = useChangeImageIsIntermediateMutation(); + const [changeIsImageIntermediate] = useChangeImageIsIntermediateMutation(); const { t } = useTranslation(); @@ -71,18 +72,12 @@ export const StagingAreaToolbar = memo(() => { $shouldShowStagedImage.set(!shouldShowStagedImage); }, [shouldShowStagedImage]); - const onSaveStagingImage = useCallback( - () => { - // if (!imageDTO) { - // return; - // } - // changeIsImageIntermediate({ imageDTO, is_intermediate: false }); - }, - [ - // changeIsImageIntermediate, - // imageDTO - ] - ); + const onSaveStagingImage = useCallback(() => { + if (!selectedImage) { + return; + } + changeIsImageIntermediate({ imageDTO: selectedImage.imageDTO, is_intermediate: false }); + }, [changeIsImageIntermediate, selectedImage]); useHotkeys( ['left'], diff --git a/invokeai/frontend/web/src/services/api/endpoints/images.ts b/invokeai/frontend/web/src/services/api/endpoints/images.ts index 5dfc400fa04..a94ca912b77 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/images.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/images.ts @@ -162,7 +162,7 @@ export const imagesApi = api.injectEndpoints({ }), invalidatesTags: (result, error, { imageDTO }) => { const categories = getCategories(imageDTO); - const boardId = imageDTO.board_id ?? undefined; + const boardId = imageDTO.board_id ?? 'none'; return [ { type: 'Image', id: imageDTO.image_name }, @@ -557,8 +557,6 @@ export const { useClearIntermediatesMutation, useAddImagesToBoardMutation, useRemoveImagesFromBoardMutation, - useAddImageToBoardMutation, - useRemoveImageFromBoardMutation, useChangeImageIsIntermediateMutation, useDeleteBoardAndImagesMutation, useDeleteBoardMutation, From 84aa2b94e559b515a6bd48a1193685c23310947d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 23 Aug 2024 18:41:24 +1000 Subject: [PATCH 459/678] fix(ui): staging area interaction scopes --- invokeai/frontend/web/src/common/hooks/interactionScopes.ts | 2 +- .../components/StagingArea/StagingAreaToolbar.tsx | 3 ++- .../web/src/features/gallery/hooks/useGalleryHotkeys.ts | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/invokeai/frontend/web/src/common/hooks/interactionScopes.ts b/invokeai/frontend/web/src/common/hooks/interactionScopes.ts index 0e4aa906a41..9140b616c44 100644 --- a/invokeai/frontend/web/src/common/hooks/interactionScopes.ts +++ b/invokeai/frontend/web/src/common/hooks/interactionScopes.ts @@ -9,7 +9,7 @@ import { useEffect, useMemo } from 'react'; const log = logger('system'); -const _INTERACTION_SCOPES = ['gallery', 'canvas', 'workflows', 'imageViewer'] as const; +const _INTERACTION_SCOPES = ['gallery', 'canvas', 'stagingArea', 'workflows', 'imageViewer'] as const; type InteractionScope = (typeof _INTERACTION_SCOPES)[number]; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx index 2893e66711b..45e51f59fc9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx @@ -1,7 +1,7 @@ import { Button, ButtonGroup, IconButton } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes'; +import { INTERACTION_SCOPES, useScopeOnMount } from 'common/hooks/interactionScopes'; import { $shouldShowStagedImage, sessionNextStagedImageSelected, @@ -35,6 +35,7 @@ export const StagingAreaToolbar = memo(() => { }, [images, session.selectedStagedImageIndex]); const isCanvasActive = useStore(INTERACTION_SCOPES.canvas.$isActive); const [changeIsImageIntermediate] = useChangeImageIsIntermediateMutation(); + useScopeOnMount('stagingArea'); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts index b8e9ed5b7e2..ec71b844623 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts @@ -12,12 +12,12 @@ import { useListImagesQuery } from 'services/api/endpoints/images'; const $leftRightHotkeysEnabled = computed($activeScopes, (activeScopes) => { // The left and right hotkeys can be used when the gallery is focused and the canvas is not focused, OR when the image viewer is focused. - return (!activeScopes.has('staging-area') && !activeScopes.has('canvas')) || activeScopes.has('imageViewer'); + return !activeScopes.has('canvas') || activeScopes.has('imageViewer'); }); const $upDownHotkeysEnabled = computed([$activeScopes, $isGalleryPanelOpen], (activeScopes, isGalleryPanelOpen) => { // The up and down hotkeys can be used when the gallery is focused and the canvas is not focused, and the gallery panel is open. - return !activeScopes.has('staging-area') && !activeScopes.has('canvas') && isGalleryPanelOpen; + return !activeScopes.has('canvas') && isGalleryPanelOpen; }); /** From c06998184ea0ee1111eb551b9120ce080b42b1f3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 23 Aug 2024 18:53:11 +1000 Subject: [PATCH 460/678] chore(ui): lint --- .../src/app/store/createMemoizedSelector.ts | 3 +-- .../web/src/common/hooks/interactionScopes.ts | 5 ---- .../contexts/EntityAdapterContext.tsx | 26 +++++++++---------- .../components/MetadataControlNets.tsx | 4 +-- .../components/MetadataIPAdapters.tsx | 9 +++++-- .../components/MetadataT2IAdapters.tsx | 4 +-- .../web/src/features/metadata/types.ts | 2 +- 7 files changed, 25 insertions(+), 28 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/createMemoizedSelector.ts b/invokeai/frontend/web/src/app/store/createMemoizedSelector.ts index fb49d796e10..bd6428db519 100644 --- a/invokeai/frontend/web/src/app/store/createMemoizedSelector.ts +++ b/invokeai/frontend/web/src/app/store/createMemoizedSelector.ts @@ -1,4 +1,4 @@ -import { createDraftSafeSelectorCreator, createSelector, createSelectorCreator, lruMemoize } from '@reduxjs/toolkit'; +import { createDraftSafeSelectorCreator, createSelectorCreator, lruMemoize } from '@reduxjs/toolkit'; import type { GetSelectorsOptions } from '@reduxjs/toolkit/dist/entities/state_selectors'; import type { RootState } from 'app/store/store'; import { isEqual } from 'lodash-es'; @@ -21,5 +21,4 @@ export const getSelectorsOptions: GetSelectorsOptions = { }), }; -export const createAppSelector = createSelector.withTypes(); export const createMemoizedAppSelector = createMemoizedSelector.withTypes(); diff --git a/invokeai/frontend/web/src/common/hooks/interactionScopes.ts b/invokeai/frontend/web/src/common/hooks/interactionScopes.ts index 9140b616c44..3f98b8d5611 100644 --- a/invokeai/frontend/web/src/common/hooks/interactionScopes.ts +++ b/invokeai/frontend/web/src/common/hooks/interactionScopes.ts @@ -66,11 +66,6 @@ export const setScopes = (scopes: InteractionScope[]) => { log.trace(`Set scopes: ${formatScopes($activeScopes.get())}`); }; -export const clearScopes = () => { - $activeScopes.set(new Set()); - log.trace(`Cleared scopes`); -}; - export const useScopeOnFocus = (scope: InteractionScope, ref: RefObject) => { useEffect(() => { const element = ref.current; diff --git a/invokeai/frontend/web/src/features/controlLayers/contexts/EntityAdapterContext.tsx b/invokeai/frontend/web/src/features/controlLayers/contexts/EntityAdapterContext.tsx index a185d6fb695..cd0c455b55c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/contexts/EntityAdapterContext.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/contexts/EntityAdapterContext.tsx @@ -35,13 +35,12 @@ export const EntityLayerAdapterGate = memo(({ children }: PropsWithChildren) => EntityLayerAdapterGate.displayName = 'EntityLayerAdapterGate'; -/** @knipignore */ -export const useEntityLayerAdapter = (): CanvasLayerAdapter => { - const adapter = useContext(EntityAdapterContext); - assert(adapter, 'useEntityLayerAdapter must be used within a EntityLayerAdapterGate'); - assert(adapter.type === 'layer_adapter', 'useEntityLayerAdapter must be used with a layer adapter'); - return adapter; -}; +// export const useEntityLayerAdapter = (): CanvasLayerAdapter => { +// const adapter = useContext(EntityAdapterContext); +// assert(adapter, 'useEntityLayerAdapter must be used within a EntityLayerAdapterGate'); +// assert(adapter.type === 'layer_adapter', 'useEntityLayerAdapter must be used with a layer adapter'); +// return adapter; +// }; export const EntityMaskAdapterGate = memo(({ children }: PropsWithChildren) => { const canvasManager = useCanvasManager(); @@ -69,13 +68,12 @@ export const EntityMaskAdapterGate = memo(({ children }: PropsWithChildren) => { EntityMaskAdapterGate.displayName = 'EntityMaskAdapterGate'; -/** @knipignore */ -export const useEntityMaskAdapter = (): CanvasMaskAdapter => { - const adapter = useContext(EntityAdapterContext); - assert(adapter, 'useEntityMaskAdapter must be used within a CanvasMaskAdapterGate'); - assert(adapter.type === 'mask_adapter', 'useEntityMaskAdapter must be used with a mask adapter'); - return adapter; -}; +// export const useEntityMaskAdapter = (): CanvasMaskAdapter => { +// const adapter = useContext(EntityAdapterContext); +// assert(adapter, 'useEntityMaskAdapter must be used within a CanvasMaskAdapterGate'); +// assert(adapter.type === 'mask_adapter', 'useEntityMaskAdapter must be used with a mask adapter'); +// return adapter; +// }; export const useEntityAdapter = (): CanvasLayerAdapter | CanvasMaskAdapter => { const adapter = useContext(EntityAdapterContext); diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataControlNets.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataControlNets.tsx index ee9618ff74c..4933fa7b53a 100644 --- a/invokeai/frontend/web/src/features/metadata/components/MetadataControlNets.tsx +++ b/invokeai/frontend/web/src/features/metadata/components/MetadataControlNets.tsx @@ -26,9 +26,9 @@ export const MetadataControlNets = ({ metadata }: Props) => { return ( <> - {controlNets.map((controlNet) => ( + {controlNets.map((controlNet, i) => ( { return ( <> - {ipAdapters.map((ipAdapter) => ( - + {ipAdapters.map((ipAdapter, i) => ( + ))} ); diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataT2IAdapters.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataT2IAdapters.tsx index 82575783e9d..52dbc4a2785 100644 --- a/invokeai/frontend/web/src/features/metadata/components/MetadataT2IAdapters.tsx +++ b/invokeai/frontend/web/src/features/metadata/components/MetadataT2IAdapters.tsx @@ -26,9 +26,9 @@ export const MetadataT2IAdapters = ({ metadata }: Props) => { return ( <> - {t2iAdapters.map((t2iAdapter) => ( + {t2iAdapters.map((t2iAdapter, i) => ( Date: Fri, 23 Aug 2024 19:22:29 +1000 Subject: [PATCH 461/678] fix(ui): rip out broken recall logic, NO TS ERRORS --- .../features/controlLayers/konva/naming.ts | 16 -- .../controlLayers/store/canvasV2Slice.ts | 10 +- .../ImageMetadataActions.tsx | 8 - .../components/MetadataControlNets.tsx | 72 ----- .../components/MetadataIPAdapters.tsx | 72 ----- .../metadata/components/MetadataLayers.tsx | 68 ----- .../components/MetadataT2IAdapters.tsx | 72 ----- .../web/src/features/metadata/types.ts | 4 - .../src/features/metadata/util/handlers.ts | 101 +------ .../metadata/util/modelFetchingHelpers.ts | 22 +- .../web/src/features/metadata/util/parsers.ts | 262 ++++++------------ .../src/features/metadata/util/recallers.ts | 197 +------------ .../src/features/metadata/util/validators.ts | 44 +-- .../parameters/types/parameterSchemas.ts | 15 - 14 files changed, 110 insertions(+), 853 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/naming.ts delete mode 100644 invokeai/frontend/web/src/features/metadata/components/MetadataControlNets.tsx delete mode 100644 invokeai/frontend/web/src/features/metadata/components/MetadataIPAdapters.tsx delete mode 100644 invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx delete mode 100644 invokeai/frontend/web/src/features/metadata/components/MetadataT2IAdapters.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts deleted file mode 100644 index a60e8456dc0..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * This file contains IDs, names, and ID getters for konva layers and objects. - */ - -// Getters for non-singleton layer and object IDs -export const getRGId = (entityId: string) => `region_${entityId}`; -export const getLayerId = (entityId: string) => `layer_${entityId}`; -export const getBrushLineId = (entityId: string, lineId: string, isBuffer?: boolean) => - `${isBuffer ? 'buffer_' : ''}brush_line_${lineId}`; -export const getEraserLineId = (entityId: string, lineId: string, isBuffer?: boolean) => - `${isBuffer ? 'buffer_' : ''}eraser_line_${lineId}`; -export const getRectShapeId = (entityId: string, rectId: string, isBuffer?: boolean) => - `${isBuffer ? 'buffer_' : ''}rect_${rectId}`; -export const getImageObjectId = (entityId: string, imageId: string) => `image_${imageId}`; -export const getCAId = (entityId: string) => `control_adapter_${entityId}`; -export const getIPAId = (entityId: string) => `ip_adapter_${entityId}`; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 801ccbe4aa6..b2529e9712a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -463,11 +463,11 @@ export const { bboxSizeOptimized, // Raster layers rasterLayerAdded, - rasterLayerRecalled, + // rasterLayerRecalled, rasterLayerConvertedToControlLayer, // Control layers controlLayerAdded, - controlLayerRecalled, + // controlLayerRecalled, controlLayerConvertedToRasterLayer, controlLayerModelChanged, controlLayerControlModeChanged, @@ -476,7 +476,7 @@ export const { controlLayerWithTransparencyEffectToggled, // IP Adapters ipaAdded, - ipaRecalled, + // ipaRecalled, ipaImageChanged, ipaMethodChanged, ipaModelChanged, @@ -485,7 +485,7 @@ export const { ipaBeginEndStepPctChanged, // Regions rgAdded, - rgRecalled, + // rgRecalled, rgPositivePromptChanged, rgNegativePromptChanged, rgFillColorChanged, @@ -545,7 +545,7 @@ export const { loraAllDeleted, // Inpaint mask inpaintMaskAdded, - inpaintMaskRecalled, + // inpaintMaskRecalled, inpaintMaskFillColorChanged, inpaintMaskFillStyleChanged, // Staging diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx index 9c6b77075db..10d2656746c 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx @@ -1,10 +1,6 @@ import { useAppSelector } from 'app/store/storeHooks'; -import { MetadataControlNets } from 'features/metadata/components/MetadataControlNets'; -import { MetadataIPAdapters } from 'features/metadata/components/MetadataIPAdapters'; import { MetadataItem } from 'features/metadata/components/MetadataItem'; -import { MetadataLayers } from 'features/metadata/components/MetadataLayers'; import { MetadataLoRAs } from 'features/metadata/components/MetadataLoRAs'; -import { MetadataT2IAdapters } from 'features/metadata/components/MetadataT2IAdapters'; import { handlers } from 'features/metadata/util/handlers'; import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { memo } from 'react'; @@ -49,10 +45,6 @@ const ImageMetadataActions = (props: Props) => { - {activeTabName === 'generation' && } - {activeTabName !== 'generation' && } - {activeTabName !== 'generation' && } - {activeTabName !== 'generation' && } ); }; diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataControlNets.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataControlNets.tsx deleted file mode 100644 index 4933fa7b53a..00000000000 --- a/invokeai/frontend/web/src/features/metadata/components/MetadataControlNets.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { MetadataItemView } from 'features/metadata/components/MetadataItemView'; -import type { ControlNetConfigMetadata, MetadataHandlers } from 'features/metadata/types'; -import { handlers } from 'features/metadata/util/handlers'; -import { useCallback, useEffect, useMemo, useState } from 'react'; - -type Props = { - metadata: unknown; -}; - -export const MetadataControlNets = ({ metadata }: Props) => { - const [controlNets, setControlNets] = useState([]); - - useEffect(() => { - const parse = async () => { - try { - const parsed = await handlers.controlNets.parse(metadata); - setControlNets(parsed); - } catch (e) { - setControlNets([]); - } - }; - parse(); - }, [metadata]); - - const label = useMemo(() => handlers.controlNets.getLabel(), []); - - return ( - <> - {controlNets.map((controlNet, i) => ( - - ))} - - ); -}; - -const MetadataViewControlNet = ({ - label, - controlNet, - handlers, -}: { - label: string; - controlNet: ControlNetConfigMetadata; - handlers: MetadataHandlers; -}) => { - const onRecall = useCallback(() => { - if (!handlers.recallItem) { - return; - } - handlers.recallItem(controlNet, true); - }, [handlers, controlNet]); - - const [renderedValue, setRenderedValue] = useState(null); - useEffect(() => { - const _renderValue = async () => { - if (!handlers.renderItemValue) { - setRenderedValue(null); - return; - } - const rendered = await handlers.renderItemValue(controlNet); - setRenderedValue(rendered); - }; - - _renderValue(); - }, [handlers, controlNet]); - - return ; -}; diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataIPAdapters.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataIPAdapters.tsx deleted file mode 100644 index e6e6f555aa6..00000000000 --- a/invokeai/frontend/web/src/features/metadata/components/MetadataIPAdapters.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { MetadataItemView } from 'features/metadata/components/MetadataItemView'; -import type { IPAdapterConfigMetadata, MetadataHandlers } from 'features/metadata/types'; -import { handlers } from 'features/metadata/util/handlers'; -import { useCallback, useEffect, useMemo, useState } from 'react'; - -type Props = { - metadata: unknown; -}; - -export const MetadataIPAdapters = ({ metadata }: Props) => { - const [ipAdapters, setIPAdapters] = useState([]); - - useEffect(() => { - const parse = async () => { - try { - const parsed = await handlers.ipAdapters.parse(metadata); - setIPAdapters(parsed); - } catch (e) { - setIPAdapters([]); - } - }; - parse(); - }, [metadata]); - - const label = useMemo(() => handlers.ipAdapters.getLabel(), []); - - return ( - <> - {ipAdapters.map((ipAdapter, i) => ( - - ))} - - ); -}; - -const MetadataViewIPAdapter = ({ - label, - ipAdapter, - handlers, -}: { - label: string; - ipAdapter: IPAdapterConfigMetadata; - handlers: MetadataHandlers; -}) => { - const onRecall = useCallback(() => { - if (!handlers.recallItem) { - return; - } - handlers.recallItem(ipAdapter, true); - }, [handlers, ipAdapter]); - - const [renderedValue, setRenderedValue] = useState(null); - useEffect(() => { - const _renderValue = async () => { - if (!handlers.renderItemValue) { - setRenderedValue(null); - return; - } - const rendered = await handlers.renderItemValue(ipAdapter); - setRenderedValue(rendered); - }; - - _renderValue(); - }, [handlers, ipAdapter]); - - return ; -}; diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx deleted file mode 100644 index c1b2f85d8a4..00000000000 --- a/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import type { CanvasRasterLayerState } from 'features/controlLayers/store/types'; -import { MetadataItemView } from 'features/metadata/components/MetadataItemView'; -import type { MetadataHandlers } from 'features/metadata/types'; -import { handlers } from 'features/metadata/util/handlers'; -import { useCallback, useEffect, useMemo, useState } from 'react'; - -type Props = { - metadata: unknown; -}; - -export const MetadataLayers = ({ metadata }: Props) => { - const [layers, setLayers] = useState([]); - - useEffect(() => { - const parse = async () => { - try { - const parsed = await handlers.layers.parse(metadata); - setLayers(parsed); - } catch (e) { - setLayers([]); - } - }; - parse(); - }, [metadata]); - - const label = useMemo(() => handlers.layers.getLabel(), []); - - return ( - <> - {layers.map((layer) => ( - - ))} - - ); -}; - -const MetadataViewLayer = ({ - label, - layer, - handlers, -}: { - label: string; - layer: CanvasRasterLayerState; - handlers: MetadataHandlers; -}) => { - const onRecall = useCallback(() => { - if (!handlers.recallItem) { - return; - } - handlers.recallItem(layer, true); - }, [handlers, layer]); - - const [renderedValue, setRenderedValue] = useState(null); - useEffect(() => { - const _renderValue = async () => { - if (!handlers.renderItemValue) { - setRenderedValue(null); - return; - } - const rendered = await handlers.renderItemValue(layer); - setRenderedValue(rendered); - }; - - _renderValue(); - }, [handlers, layer]); - - return ; -}; diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataT2IAdapters.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataT2IAdapters.tsx deleted file mode 100644 index 52dbc4a2785..00000000000 --- a/invokeai/frontend/web/src/features/metadata/components/MetadataT2IAdapters.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { MetadataItemView } from 'features/metadata/components/MetadataItemView'; -import type { MetadataHandlers, T2IAdapterConfigMetadata } from 'features/metadata/types'; -import { handlers } from 'features/metadata/util/handlers'; -import { useCallback, useEffect, useMemo, useState } from 'react'; - -type Props = { - metadata: unknown; -}; - -export const MetadataT2IAdapters = ({ metadata }: Props) => { - const [t2iAdapters, setT2IAdapters] = useState([]); - - useEffect(() => { - const parse = async () => { - try { - const parsed = await handlers.t2iAdapters.parse(metadata); - setT2IAdapters(parsed); - } catch (e) { - setT2IAdapters([]); - } - }; - parse(); - }, [metadata]); - - const label = useMemo(() => handlers.t2iAdapters.getLabel(), []); - - return ( - <> - {t2iAdapters.map((t2iAdapter, i) => ( - - ))} - - ); -}; - -const MetadataViewT2IAdapter = ({ - label, - t2iAdapter, - handlers, -}: { - label: string; - t2iAdapter: T2IAdapterConfigMetadata; - handlers: MetadataHandlers; -}) => { - const onRecall = useCallback(() => { - if (!handlers.recallItem) { - return; - } - handlers.recallItem(t2iAdapter, true); - }, [handlers, t2iAdapter]); - - const [renderedValue, setRenderedValue] = useState(null); - useEffect(() => { - const _renderValue = async () => { - if (!handlers.renderItemValue) { - setRenderedValue(null); - return; - } - const rendered = await handlers.renderItemValue(t2iAdapter); - setRenderedValue(rendered); - }; - - _renderValue(); - }, [handlers, t2iAdapter]); - - return ; -}; diff --git a/invokeai/frontend/web/src/features/metadata/types.ts b/invokeai/frontend/web/src/features/metadata/types.ts index 5a4bba35c8d..a1f9f5a2264 100644 --- a/invokeai/frontend/web/src/features/metadata/types.ts +++ b/invokeai/frontend/web/src/features/metadata/types.ts @@ -148,7 +148,3 @@ export type BuildMetadataHandlers = ( export type ControlNetConfigMetadata = O.NonNullable; export type T2IAdapterConfigMetadata = O.NonNullable; export type IPAdapterConfigMetadata = O.NonNullable; -export type AnyControlAdapterConfigMetadata = - | ControlNetConfigMetadata - | T2IAdapterConfigMetadata - | IPAdapterConfigMetadata; diff --git a/invokeai/frontend/web/src/features/metadata/util/handlers.ts b/invokeai/frontend/web/src/features/metadata/util/handlers.ts index 12d8af35a9c..760a738df43 100644 --- a/invokeai/frontend/web/src/features/metadata/util/handlers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/handlers.ts @@ -2,9 +2,8 @@ import { getStore } from 'app/store/nanostores/store'; import { deepClone } from 'common/util/deepClone'; import { objectKeys } from 'common/util/objectKeys'; import { shouldConcatPromptsChanged } from 'features/controlLayers/store/canvasV2Slice'; -import type { CanvasRasterLayerState, LoRA } from 'features/controlLayers/store/types'; +import type { LoRA } from 'features/controlLayers/store/types'; import type { - AnyControlAdapterConfigMetadata, BuildMetadataHandlers, MetadataGetLabelFunc, MetadataHandlers, @@ -19,7 +18,6 @@ import type { ModelIdentifierField } from 'features/nodes/types/common'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { size } from 'lodash-es'; -import { assert } from 'tsafe'; import { parsers } from './parsers'; import { recallers } from './recallers'; @@ -40,57 +38,6 @@ const renderLoRAValue: MetadataRenderValueFunc = async (value) => { return `${value.model.key} (${value.model.base.toUpperCase()}) - ${value.weight}`; } }; -const renderControlAdapterValue: MetadataRenderValueFunc = async (value) => { - try { - const modelConfig = await fetchModelConfig(value.model.key ?? 'none'); - return `${modelConfig.name} (${modelConfig.base.toUpperCase()}) - ${value.weight}`; - } catch { - return `${value.model.key} (${value.model.base.toUpperCase()}) - ${value.weight}`; - } -}; -const renderLayerValue: MetadataRenderValueFunc = (layer) => { - if (layer.type === 'initial_image_layer') { - let rendered = t('controlLayers.globalInitialImageLayer'); - if (layer.image) { - rendered += ` (${layer.image})`; - } - return rendered; - } - if (layer.type === 'control_adapter_layer') { - let rendered = t('controlLayers.globalControlAdapterLayer'); - const model = layer.controlAdapter.model; - if (model) { - rendered += ` (${model.name} - ${model.base.toUpperCase()})`; - } - return rendered; - } - if (layer.type === 'ip_adapter_layer') { - let rendered = t('controlLayers.globalIPAdapterLayer'); - const model = layer.ipAdapter.model; - if (model) { - rendered += ` (${model.name} - ${model.base.toUpperCase()})`; - } - return rendered; - } - if (layer.type === 'regional_guidance_layer') { - const rendered = t('controlLayers.regionalGuidanceLayer'); - const items: string[] = []; - if (layer.positivePrompt) { - items.push(`Positive: ${layer.positivePrompt}`); - } - if (layer.negativePrompt) { - items.push(`Negative: ${layer.negativePrompt}`); - } - if (layer.ipAdapters.length > 0) { - items.push(`${layer.ipAdapters.length} IP Adapters`); - } - return `${rendered} (${items.join(', ')})`; - } - assert(false, 'Unknown layer type'); -}; -const renderLayersValue: MetadataRenderValueFunc = (layers) => { - return `${layers.length} ${t('controlLayers.layers', { count: layers.length })}`; -}; const parameterSetToast = (parameter: string) => { toast({ @@ -328,26 +275,6 @@ export const handlers = { }), // Arrays of models - controlNets: buildHandlers({ - getLabel: () => t('common.controlNet'), - parser: parsers.controlNets, - itemParser: parsers.controlNet, - recaller: recallers.controlNets, - itemRecaller: recallers.controlNet, - validator: validators.controlNets, - itemValidator: validators.controlNet, - renderItemValue: renderControlAdapterValue, - }), - ipAdapters: buildHandlers({ - getLabel: () => t('common.ipAdapter'), - parser: parsers.ipAdapters, - itemParser: parsers.ipAdapter, - recaller: recallers.ipAdapters, - itemRecaller: recallers.ipAdapter, - validator: validators.ipAdapters, - itemValidator: validators.ipAdapter, - renderItemValue: renderControlAdapterValue, - }), loras: buildHandlers({ getLabel: () => t('models.lora'), parser: parsers.loras, @@ -358,28 +285,6 @@ export const handlers = { itemValidator: validators.lora, renderItemValue: renderLoRAValue, }), - t2iAdapters: buildHandlers({ - getLabel: () => t('common.t2iAdapter'), - parser: parsers.t2iAdapters, - itemParser: parsers.t2iAdapter, - recaller: recallers.t2iAdapters, - itemRecaller: recallers.t2iAdapter, - validator: validators.t2iAdapters, - itemValidator: validators.t2iAdapter, - renderItemValue: renderControlAdapterValue, - }), - layers: buildHandlers({ - getLabel: () => t('controlLayers.layers_one'), - parser: parsers.layers, - itemParser: parsers.layer, - recaller: recallers.layers, - itemRecaller: recallers.layer, - validator: validators.layers, - itemValidator: validators.layer, - renderItemValue: renderLayerValue, - renderValue: renderLayersValue, - getIsVisible: (value) => value.length > 0, - }), } as const; type ParsedValue = Awaited>; @@ -406,9 +311,9 @@ export const parseAndRecallImageDimensions = (metadata: unknown) => { }; // These handlers should be omitted when recalling to control layers -const TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = ['controlNets', 'ipAdapters', 't2iAdapters', 'strength']; +const TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = ['strength']; // These handlers should be omitted when recalling to the rest of the app -const NOT_TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = ['layers']; +const NOT_TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = []; export const parseAndRecallAllMetadata = async ( metadata: unknown, diff --git a/invokeai/frontend/web/src/features/metadata/util/modelFetchingHelpers.ts b/invokeai/frontend/web/src/features/metadata/util/modelFetchingHelpers.ts index 4bd2436c0bc..bdf6a3bd215 100644 --- a/invokeai/frontend/web/src/features/metadata/util/modelFetchingHelpers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/modelFetchingHelpers.ts @@ -76,17 +76,17 @@ const fetchModelConfigByAttrs = async (name: string, base: BaseModelType, type: * @returns A promise that resolves to the model config. * @throws {ModelConfigNotFoundError} If the model config is unable to be fetched. */ -export const fetchModelConfigByIdentifier = async (identifier: ModelIdentifierField): Promise => { - try { - return await fetchModelConfig(identifier.key); - } catch { - try { - return await fetchModelConfigByAttrs(identifier.name, identifier.base, identifier.type); - } catch { - throw new ModelConfigNotFoundError(`Unable to retrieve model config for identifier ${identifier}`); - } - } -}; +// export const fetchModelConfigByIdentifier = async (identifier: ModelIdentifierField): Promise => { +// try { +// return await fetchModelConfig(identifier.key); +// } catch { +// try { +// return await fetchModelConfigByAttrs(identifier.name, identifier.base, identifier.type); +// } catch { +// throw new ModelConfigNotFoundError(`Unable to retrieve model config for identifier ${identifier}`); +// } +// } +// }; /** * Fetches the model config for a given model key and type, and ensures that the model config is of a specific type. diff --git a/invokeai/frontend/web/src/features/metadata/util/parsers.ts b/invokeai/frontend/web/src/features/metadata/util/parsers.ts index 5c6831d9f79..60f9eea8333 100644 --- a/invokeai/frontend/web/src/features/metadata/util/parsers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/parsers.ts @@ -1,18 +1,18 @@ -import { getCAId, getImageObjectId, getIPAId, getLayerId } from 'features/controlLayers/konva/naming'; +import { getPrefixedId } from 'features/controlLayers/konva/util'; import { defaultLoRAConfig } from 'features/controlLayers/store/lorasReducers'; import type { - CanvasControlAdapterState, + CanvasControlLayerState, + CanvasInpaintMaskState, CanvasIPAdapterState, CanvasRasterLayerState, + CanvasRegionalGuidanceState, LoRA, } from 'features/controlLayers/store/types'; import { - IMAGE_FILTERS, imageDTOToImageWithDims, initialControlNet, initialIPAdapter, initialT2IAdapter, - isFilterType, zCanvasRasterLayerState, } from 'features/controlLayers/store/types'; import type { @@ -76,8 +76,6 @@ import { isT2IAdapterModelConfig, isVAEModelConfig, } from 'services/api/types'; -import { assert } from 'tsafe'; -import { v4 as uuidv4 } from 'uuid'; export const MetadataParsePendingToken = Symbol('pending'); export const MetadataParseFailedToken = Symbol('failed'); @@ -222,6 +220,7 @@ const parseLoRA: MetadataParseFunc = async (metadataItem) => { const loraModelConfig = await fetchModelConfigWithTypeGuard(key, isLoRAModelConfig); return { + id: getPrefixedId('lora'), model: zModelIdentifierField.parse(loraModelConfig), weight: isParameterLoRAWeight(weight) ? weight : defaultLoRAConfig.weight, isEnabled: true, @@ -245,14 +244,6 @@ const parseControlNet: MetadataParseFunc = async (meta const control_model = await getProperty(metadataItem, 'control_model'); const key = await getModelKey(control_model, 'controlnet'); const controlNetModel = await fetchModelConfigWithTypeGuard(key, isControlNetModelConfig); - const image = zControlField.shape.image - .nullish() - .catch(null) - .parse(await getProperty(metadataItem, 'image')); - const processedImage = zControlField.shape.image - .nullish() - .catch(null) - .parse(await getProperty(metadataItem, 'processed_image')); const control_weight = zControlField.shape.control_weight .nullish() .catch(null) @@ -269,28 +260,16 @@ const parseControlNet: MetadataParseFunc = async (meta .nullish() .catch(null) .parse(await getProperty(metadataItem, 'control_mode')); - const resize_mode = zControlField.shape.resize_mode - .nullish() - .catch(null) - .parse(await getProperty(metadataItem, 'resize_mode')); - - const { processorType, processorNode } = buildControlAdapterProcessor(controlNetModel); const controlNet: ControlNetConfigMetadata = { type: 'controlnet', - isEnabled: true, model: zModelIdentifierField.parse(controlNetModel), weight: typeof control_weight === 'number' ? control_weight : initialControlNet.weight, - beginStepPct: begin_step_percent ?? initialControlNet.beginStepPct, - endStepPct: end_step_percent ?? initialControlNet.endStepPct, + beginEndStepPct: [ + begin_step_percent ?? initialControlNet.beginEndStepPct[0], + end_step_percent ?? initialControlNet.beginEndStepPct[1], + ], controlMode: control_mode ?? initialControlNet.controlMode, - resizeMode: resize_mode ?? initialControlNet.resizeMode, - controlImage: image?.image_name ?? null, - processedControlImage: processedImage?.image_name ?? null, - processorType, - processorNode, - shouldAutoConfig: true, - id: uuidv4(), }; return controlNet; @@ -314,14 +293,6 @@ const parseT2IAdapter: MetadataParseFunc = async (meta const key = await getModelKey(t2i_adapter_model, 't2i_adapter'); const t2iAdapterModel = await fetchModelConfigWithTypeGuard(key, isT2IAdapterModelConfig); - const image = zT2IAdapterField.shape.image - .nullish() - .catch(null) - .parse(await getProperty(metadataItem, 'image')); - const processedImage = zT2IAdapterField.shape.image - .nullish() - .catch(null) - .parse(await getProperty(metadataItem, 'processed_image')); const weight = zT2IAdapterField.shape.weight .nullish() .catch(null) @@ -334,27 +305,15 @@ const parseT2IAdapter: MetadataParseFunc = async (meta .nullish() .catch(null) .parse(await getProperty(metadataItem, 'end_step_percent')); - const resize_mode = zT2IAdapterField.shape.resize_mode - .nullish() - .catch(null) - .parse(await getProperty(metadataItem, 'resize_mode')); - - const { processorType, processorNode } = buildControlAdapterProcessor(t2iAdapterModel); const t2iAdapter: T2IAdapterConfigMetadata = { type: 't2i_adapter', - isEnabled: true, model: zModelIdentifierField.parse(t2iAdapterModel), weight: typeof weight === 'number' ? weight : initialT2IAdapter.weight, - beginStepPct: begin_step_percent ?? initialT2IAdapter.beginStepPct, - endStepPct: end_step_percent ?? initialT2IAdapter.endStepPct, - resizeMode: resize_mode ?? initialT2IAdapter.resizeMode, - controlImage: image?.image_name ?? null, - processedControlImage: processedImage?.image_name ?? null, - processorType, - processorNode, - shouldAutoConfig: true, - id: uuidv4(), + beginEndStepPct: [ + begin_step_percent ?? initialT2IAdapter.beginEndStepPct[0], + end_step_percent ?? initialT2IAdapter.beginEndStepPct[1], + ], }; return t2iAdapter; @@ -378,10 +337,10 @@ const parseIPAdapter: MetadataParseFunc = async (metada const key = await getModelKey(ip_adapter_model, 'ip_adapter'); const ipAdapterModel = await fetchModelConfigWithTypeGuard(key, isIPAdapterModelConfig); - const image = zIPAdapterField.shape.image - .nullish() - .catch(null) - .parse(await getProperty(metadataItem, 'image')); + // const image = zIPAdapterField.shape.image + // .nullish() + // .catch(null) + // .parse(await getProperty(metadataItem, 'image')); const weight = zIPAdapterField.shape.weight .nullish() .catch(null) @@ -400,16 +359,15 @@ const parseIPAdapter: MetadataParseFunc = async (metada .parse(await getProperty(metadataItem, 'end_step_percent')); const ipAdapter: IPAdapterConfigMetadata = { - id: uuidv4(), - type: 'ip_adapter', - isEnabled: true, model: zModelIdentifierField.parse(ipAdapterModel), clipVisionModel: 'ViT-H', - controlImage: image?.image_name ?? null, + image: null, //TODO(psyche): need an ImageWithDims weight: weight ?? initialIPAdapter.weight, method: method ?? initialIPAdapter.method, - beginStepPct: begin_step_percent ?? initialIPAdapter.beginStepPct, - endStepPct: end_step_percent ?? initialIPAdapter.endStepPct, + beginEndStepPct: [ + begin_step_percent ?? initialIPAdapter.beginEndStepPct[0], + end_step_percent ?? initialIPAdapter.beginEndStepPct[1], + ], }; return ipAdapter; @@ -429,16 +387,35 @@ const parseAllIPAdapters: MetadataParseFunc = async ( }; //#region Control Layers -const parseLayer: MetadataParseFunc = (metadataItem) => - zCanvasRasterLayerState.parseAsync(metadataItem); - -const parseLayers: MetadataParseFunc = async (metadata) => { +const parseLayer: MetadataParseFunc< + | CanvasRasterLayerState + | CanvasControlLayerState + | CanvasIPAdapterState + | CanvasRegionalGuidanceState + | CanvasInpaintMaskState +> = (metadataItem) => zCanvasRasterLayerState.parseAsync(metadataItem); + +const parseLayers: MetadataParseFunc< + ( + | CanvasRasterLayerState + | CanvasControlLayerState + | CanvasIPAdapterState + | CanvasRegionalGuidanceState + | CanvasInpaintMaskState + )[] +> = async (metadata) => { // We need to support recalling pre-Control Layers metadata into Control Layers. A separate set of parsers handles // taking pre-CL metadata and parsing it into layers. It doesn't always map 1-to-1, so this is best-effort. For // example, CL Control Adapters don't support resize mode, so we simply omit that property. try { - const layers: CanvasRasterLayerState[] = []; + const layers: ( + | CanvasRasterLayerState + | CanvasControlLayerState + | CanvasIPAdapterState + | CanvasRegionalGuidanceState + | CanvasInpaintMaskState + )[] = []; try { const control_layers = await getProperty(metadata, 'control_layers'); @@ -458,7 +435,7 @@ const parseLayers: MetadataParseFunc = async (metadata controlNetsRaw.map(async (cn) => await parseControlNetToControlAdapterLayer(cn)) ); const controlNetsAsLayers = controlNetsParseResults - .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') .map((result) => result.value); layers.push(...controlNetsAsLayers); } catch { @@ -471,7 +448,7 @@ const parseLayers: MetadataParseFunc = async (metadata t2iAdaptersRaw.map(async (cn) => await parseT2IAdapterToControlAdapterLayer(cn)) ); const t2iAdaptersAsLayers = t2iAdaptersParseResults - .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') .map((result) => result.value); layers.push(...t2iAdaptersAsLayers); } catch { @@ -491,62 +468,16 @@ const parseLayers: MetadataParseFunc = async (metadata // no-op } - try { - const initialImageLayer = await parseInitialImageToInitialImageLayer(metadata); - layers.push(initialImageLayer); - } catch { - // no-op - } - return layers; } catch { return []; } }; -const parseInitialImageToInitialImageLayer: MetadataParseFunc = async (metadata) => { - // TODO(psyche): recall denoise strength - // const denoisingStrength = await getProperty(metadata, 'strength', isParameterStrength); - const imageName = await getProperty(metadata, 'init_image', isString); - const imageDTO = await getImageDTO(imageName); - assert(imageDTO, 'ImageDTO is null'); - const id = getLayerId(uuidv4()); - const layer: CanvasRasterLayerState = { - id, - type: 'raster_layer', - bbox: null, - bboxNeedsUpdate: true, - x: 0, - y: 0, - isEnabled: true, - opacity: 1, - objects: [ - { - type: 'image', - id: getImageObjectId(id, imageDTO.image_name), - width: imageDTO.width, - height: imageDTO.height, - image: imageDTOToImageWithDims(imageDTO), - x: 0, - y: 0, - }, - ], - }; - return layer; -}; - -const parseControlNetToControlAdapterLayer: MetadataParseFunc = async (metadataItem) => { +const parseControlNetToControlAdapterLayer: MetadataParseFunc = async (metadataItem) => { const control_model = await getProperty(metadataItem, 'control_model'); const key = await getModelKey(control_model, 'controlnet'); const controlNetModel = await fetchModelConfigWithTypeGuard(key, isControlNetModelConfig); - const image = zControlField.shape.image - .nullish() - .catch(null) - .parse(await getProperty(metadataItem, 'image')); - const processedImage = zControlField.shape.image - .nullish() - .catch(null) - .parse(await getProperty(metadataItem, 'processed_image')); const control_weight = zControlField.shape.control_weight .nullish() .catch(null) @@ -564,52 +495,37 @@ const parseControlNetToControlAdapterLayer: MetadataParseFunc = async (metadataItem) => { +const parseT2IAdapterToControlAdapterLayer: MetadataParseFunc = async (metadataItem) => { const t2i_adapter_model = await getProperty(metadataItem, 't2i_adapter_model'); const key = await getModelKey(t2i_adapter_model, 't2i_adapter'); const t2iAdapterModel = await fetchModelConfigWithTypeGuard(key, isT2IAdapterModelConfig); - const image = zT2IAdapterField.shape.image - .nullish() - .catch(null) - .parse(await getProperty(metadataItem, 'image')); - const processedImage = zT2IAdapterField.shape.image - .nullish() - .catch(null) - .parse(await getProperty(metadataItem, 'processed_image')); const weight = zT2IAdapterField.shape.weight .nullish() .catch(null) @@ -623,33 +539,26 @@ const parseT2IAdapterToControlAdapterLayer: MetadataParseFunc = const imageDTO = image ? await getImageDTO(image.image_name) : null; const layer: CanvasIPAdapterState = { - id: getIPAId(uuidv4()), + id: getPrefixedId('ip_adapter'), type: 'ip_adapter', isEnabled: true, - model: zModelIdentifierField.parse(ipAdapterModel), - weight: typeof weight === 'number' ? weight : initialIPAdapter.weight, - beginEndStepPct, - imageObject: imageDTO ? imageDTOToImageWithDims(imageDTO) : null, - clipVisionModel: initialIPAdapter.clipVisionModel, // TODO: This needs to be added to the zIPAdapterField... - method: method ?? initialIPAdapter.method, + name: null, + ipAdapter: { + model: zModelIdentifierField.parse(ipAdapterModel), + weight: typeof weight === 'number' ? weight : initialIPAdapter.weight, + beginEndStepPct, + clipVisionModel: initialIPAdapter.clipVisionModel, // TODO: This needs to be added to the zIPAdapterField... + method: method ?? initialIPAdapter.method, + image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null, + }, }; return layer; diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index 4b4b6aeba8b..41d092d0c5e 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -1,13 +1,4 @@ -import { logger } from 'app/logging/logger'; import { getStore } from 'app/store/nanostores/store'; -import { deepClone } from 'common/util/deepClone'; -import { - getBrushLineId, - getEraserLineId, - getImageObjectId, - getRectShapeId, - getRGId, -} from 'features/controlLayers/konva/naming'; import { bboxHeightChanged, bboxWidthChanged, @@ -17,7 +8,6 @@ import { negativePromptChanged, positivePrompt2Changed, positivePromptChanged, - rasterLayerRecalled, refinerModelChanged, setCfgRescaleMultiplier, setCfgScale, @@ -33,14 +23,9 @@ import { setSteps, vaeSelected, } from 'features/controlLayers/store/canvasV2Slice'; -import type { CanvasRasterLayerState, LoRA } from 'features/controlLayers/store/types'; +import type { LoRA } from 'features/controlLayers/store/types'; import { setHrfEnabled, setHrfMethod, setHrfStrength } from 'features/hrf/store/hrfSlice'; -import type { - ControlNetConfigMetadata, - IPAdapterConfigMetadata, - MetadataRecallFunc, - T2IAdapterConfigMetadata, -} from 'features/metadata/types'; +import type { MetadataRecallFunc } from 'features/metadata/types'; import { modelSelected } from 'features/parameters/store/actions'; import type { ParameterCFGRescaleMultiplier, @@ -64,10 +49,6 @@ import type { ParameterVAEModel, ParameterWidth, } from 'features/parameters/types/parameterSchemas'; -import { getImageDTO } from 'services/api/endpoints/images'; -import { v4 as uuidv4 } from 'uuid'; - -const log = logger('metadata'); const recallPositivePrompt: MetadataRecallFunc = (positivePrompt) => { getStore().dispatch(positivePromptChanged(positivePrompt)); @@ -190,172 +171,6 @@ const recallAllLoRAs: MetadataRecallFunc = (loras) => { }); }; -const recallControlNet: MetadataRecallFunc = (controlNet) => { - getStore().dispatch(controlAdapterRecalled(controlNet)); -}; - -const recallControlNets: MetadataRecallFunc = (controlNets) => { - const { dispatch } = getStore(); - dispatch(controlNetsReset()); - if (!controlNets.length) { - return; - } - controlNets.forEach((controlNet) => { - dispatch(controlAdapterRecalled(controlNet)); - }); -}; - -const recallT2IAdapter: MetadataRecallFunc = (t2iAdapter) => { - getStore().dispatch(controlAdapterRecalled(t2iAdapter)); -}; - -const recallT2IAdapters: MetadataRecallFunc = (t2iAdapters) => { - const { dispatch } = getStore(); - dispatch(t2iAdaptersReset()); - if (!t2iAdapters.length) { - return; - } - t2iAdapters.forEach((t2iAdapter) => { - dispatch(controlAdapterRecalled(t2iAdapter)); - }); -}; - -const recallIPAdapter: MetadataRecallFunc = (ipAdapter) => { - getStore().dispatch(controlAdapterRecalled(ipAdapter)); -}; - -const recallIPAdapters: MetadataRecallFunc = (ipAdapters) => { - const { dispatch } = getStore(); - dispatch(ipAdaptersReset()); - if (!ipAdapters.length) { - return; - } - ipAdapters.forEach((ipAdapter) => { - dispatch(controlAdapterRecalled(ipAdapter)); - }); -}; - -// const recallCA: MetadataRecallFunc = async (ca) => { -// const { dispatch } = getStore(); -// const clone = deepClone(ca); -// if (clone.image) { -// const imageDTO = await getImageDTO(clone.image.name); -// if (!imageDTO) { -// clone.image = null; -// } -// } -// if (clone.processedImage) { -// const imageDTO = await getImageDTO(clone.processedImage.name); -// if (!imageDTO) { -// clone.processedImage = null; -// } -// } -// if (clone.model) { -// try { -// await fetchModelConfigByIdentifier(clone.model); -// } catch { -// // MODEL SMITED! -// clone.model = null; -// } -// } -// // No clobber -// clone.id = getCAId(uuidv4()); -// // dispatch(caRecalled({ data: clone })); -// return; -// }; - -// const recallIPA: MetadataRecallFunc = async (ipa) => { -// const { dispatch } = getStore(); -// const clone = deepClone(ipa); -// if (clone.imageObject) { -// const imageDTO = await getImageDTO(clone.imageObject.name); -// if (!imageDTO) { -// clone.imageObject = null; -// } -// } -// if (clone.model) { -// try { -// await fetchModelConfigByIdentifier(clone.model); -// } catch { -// // MODEL SMITED! -// clone.model = null; -// } -// } -// // No clobber -// clone.id = getIPAId(uuidv4()); -// dispatch(ipaRecalled({ data: clone })); -// return; -// }; - -// const recallRG: MetadataRecallFunc = async (rg) => { -// const { dispatch } = getStore(); -// const clone = deepClone(rg); -// // Strip out the uploaded mask image property - this is an intermediate image -// clone.imageCache = null; - -// for (const ipAdapter of clone.ipAdapters) { -// if (ipAdapter.imageObject) { -// const imageDTO = await getImageDTO(ipAdapter.imageObject.name); -// if (!imageDTO) { -// ipAdapter.imageObject = null; -// } -// } -// if (ipAdapter.model) { -// try { -// await fetchModelConfigByIdentifier(ipAdapter.model); -// } catch { -// // MODEL SMITED! -// ipAdapter.model = null; -// } -// } -// // No clobber -// ipAdapter.id = uuidv4(); -// } -// clone.id = getRGId(uuidv4()); -// dispatch(rgRecalled({ data: clone })); -// return; -// }; - -//#region Control Layers -const recallLayer: MetadataRecallFunc = async (layer) => { - const { dispatch } = getStore(); - const clone = deepClone(layer); - const invalidObjects: string[] = []; - for (const obj of clone.objects) { - if (obj.type === 'image') { - const imageDTO = await getImageDTO(obj.image.image_name); - if (!imageDTO) { - invalidObjects.push(obj.id); - } - } - } - clone.objects = clone.objects.filter(({ id }) => !invalidObjects.includes(id)); - for (const obj of clone.objects) { - if (obj.type === 'brush_line') { - obj.id = getBrushLineId(clone.id, uuidv4()); - } else if (obj.type === 'eraser_line') { - obj.id = getEraserLineId(clone.id, uuidv4()); - } else if (obj.type === 'image') { - obj.id = getImageObjectId(clone.id, uuidv4()); - } else if (obj.type === 'rect') { - obj.id = getRectShapeId(clone.id, uuidv4()); - } else { - log.error(`Unknown object type ${obj.type}`); - } - } - clone.id = getRGId(uuidv4()); - dispatch(rasterLayerRecalled({ data: clone })); - return; -}; - -const recallLayers: MetadataRecallFunc = (layers) => { - const { dispatch } = getStore(); - dispatch(rasterLayerAllDeleted()); - for (const l of layers) { - recallLayer(l); - } -}; - export const recallers = { positivePrompt: recallPositivePrompt, negativePrompt: recallNegativePrompt, @@ -383,12 +198,4 @@ export const recallers = { vae: recallVAE, lora: recallLoRA, loras: recallAllLoRAs, - controlNets: recallControlNets, - controlNet: recallControlNet, - t2iAdapters: recallT2IAdapters, - t2iAdapter: recallT2IAdapter, - ipAdapters: recallIPAdapters, - ipAdapter: recallIPAdapter, - layer: recallLayer, - layers: recallLayers, } as const; diff --git a/invokeai/frontend/web/src/features/metadata/util/validators.ts b/invokeai/frontend/web/src/features/metadata/util/validators.ts index 41510a8fcf2..2defbbfb0e5 100644 --- a/invokeai/frontend/web/src/features/metadata/util/validators.ts +++ b/invokeai/frontend/web/src/features/metadata/util/validators.ts @@ -1,5 +1,5 @@ import { getStore } from 'app/store/nanostores/store'; -import type { CanvasRasterLayerState, LoRA } from 'features/controlLayers/store/types'; +import type { LoRA } from 'features/controlLayers/store/types'; import type { ControlNetConfigMetadata, IPAdapterConfigMetadata, @@ -9,7 +9,6 @@ import type { import { InvalidModelConfigError } from 'features/metadata/util/modelFetchingHelpers'; import type { ParameterSDXLRefinerModel, ParameterVAEModel } from 'features/parameters/types/parameterSchemas'; import type { BaseModelType } from 'services/api/types'; -import { assert } from 'tsafe'; /** * Checks the given base model type against the currently-selected model's base type and throws an error if they are @@ -21,7 +20,7 @@ const validateBaseCompatibility = (base?: BaseModelType, message?: string) => { if (!base) { throw new InvalidModelConfigError(message || 'Missing base'); } - const currentBase = getStore().getState().params.model?.base; + const currentBase = getStore().getState().canvasV2.params.model?.base; if (currentBase && base !== currentBase) { throw new InvalidModelConfigError(message || `Incompatible base models: ${base} and ${currentBase}`); } @@ -129,43 +128,6 @@ const validateIPAdapters: MetadataValidateFunc = (ipA }); }; -const validateLayer: MetadataValidateFunc = (layer) => { - if (layer.type === 'control_adapter_layer') { - const model = layer.controlAdapter.model; - assert(model, 'Control Adapter layer missing model'); - validateBaseCompatibility(model.base, 'Layer incompatible with currently-selected model'); - } - if (layer.type === 'ip_adapter_layer') { - const model = layer.ipAdapter.model; - assert(model, 'IP Adapter layer missing model'); - validateBaseCompatibility(model.base, 'Layer incompatible with currently-selected model'); - } - if (layer.type === 'regional_guidance_layer') { - for (const ipa of layer.ipAdapters) { - const model = ipa.model; - assert(model, 'IP Adapter layer missing model'); - validateBaseCompatibility(model.base, 'Layer incompatible with currently-selected model'); - } - } - - return layer; -}; - -const validateLayers: MetadataValidateFunc = async (layers) => { - const validatedLayers: CanvasRasterLayerState[] = []; - for (const l of layers) { - try { - const validated = await validateLayer(l); - validatedLayers.push(validated); - } catch { - // This is a no-op - we want to continue validating the rest of the layers, and an empty list is valid. - } - } - return new Promise((resolve) => { - resolve(validatedLayers); - }); -}; - export const validators = { refinerModel: validateRefinerModel, vaeModel: validateVAEModel, @@ -177,6 +139,4 @@ export const validators = { t2iAdapters: validateT2IAdapters, ipAdapter: validateIPAdapter, ipAdapters: validateIPAdapters, - layer: validateLayer, - layers: validateLayers, } as const; diff --git a/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts b/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts index 01d28ee63f0..3f767874ef5 100644 --- a/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts +++ b/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts @@ -111,21 +111,6 @@ const zParameterLoRAModel = zModelIdentifierField; export type ParameterLoRAModel = z.infer; // #endregion -// #region ControlNet Model -const zParameterControlNetModel = zModelIdentifierField; -export type ParameterControlNetModel = z.infer; -// #endregion - -// #region IP Adapter Model -const zParameterIPAdapterModel = zModelIdentifierField; -export type ParameterIPAdapterModel = z.infer; -// #endregion - -// #region T2I Adapter Model -const zParameterT2IAdapterModel = zModelIdentifierField; -export type ParameterT2IAdapterModel = z.infer; -// #endregion - // #region VAE Model const zParameterSpandrelImageToImageModel = zModelIdentifierField; export type ParameterSpandrelImageToImageModel = z.infer; From 2ca5aeda96cd3c309d0c354ee19e53f5ffc56b10 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 23 Aug 2024 19:44:58 +1000 Subject: [PATCH 462/678] feat(ui): use singleton for clear q confirm dialog --- .../frontend/web/src/app/components/App.tsx | 2 + .../web/src/common/hooks/useBoolean.ts | 31 +++++++++++++ .../queue/components/ClearQueueButton.tsx | 9 ++-- .../ClearQueueConfirmationAlertDialog.tsx | 21 +++++---- .../queue/components/ClearQueueIconButton.tsx | 46 +++++++++---------- .../components/QueueActionsMenuButton.tsx | 8 ++-- .../queue/components/QueueControls.tsx | 3 -- .../FloatingParametersPanelButtons.tsx | 8 +--- .../WorkflowLibraryMenu.tsx | 2 +- 9 files changed, 77 insertions(+), 53 deletions(-) diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index 4c553b03ab0..c9345e1039b 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -14,6 +14,7 @@ import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardMo import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal'; import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal'; import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast'; +import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog'; import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal'; import { activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice'; import { configChanged } from 'features/system/store/configSlice'; @@ -132,6 +133,7 @@ const App = ({ + ); diff --git a/invokeai/frontend/web/src/common/hooks/useBoolean.ts b/invokeai/frontend/web/src/common/hooks/useBoolean.ts index 123e48cd755..46e96d424ce 100644 --- a/invokeai/frontend/web/src/common/hooks/useBoolean.ts +++ b/invokeai/frontend/web/src/common/hooks/useBoolean.ts @@ -1,3 +1,4 @@ +import type { WritableAtom } from 'nanostores'; import { useCallback, useMemo, useState } from 'react'; export const useBoolean = (initialValue: boolean) => { @@ -19,3 +20,33 @@ export const useBoolean = (initialValue: boolean) => { return api; }; + +export const buildUseBoolean = ($boolean: WritableAtom) => { + return () => { + const setTrue = useCallback(() => { + $boolean.set(true); + }, []); + const setFalse = useCallback(() => { + $boolean.set(false); + }, []); + const set = useCallback((value: boolean) => { + $boolean.set(value); + }, []); + const toggle = useCallback(() => { + $boolean.set(!$boolean.get()); + }, []); + + const api = useMemo( + () => ({ + setTrue, + setFalse, + set, + toggle, + $boolean, + }), + [set, setFalse, setTrue, toggle] + ); + + return api; + }; +}; diff --git a/invokeai/frontend/web/src/features/queue/components/ClearQueueButton.tsx b/invokeai/frontend/web/src/features/queue/components/ClearQueueButton.tsx index c899dd04822..5b2806c93a8 100644 --- a/invokeai/frontend/web/src/features/queue/components/ClearQueueButton.tsx +++ b/invokeai/frontend/web/src/features/queue/components/ClearQueueButton.tsx @@ -1,6 +1,6 @@ import type { ButtonProps } from '@invoke-ai/ui-library'; -import { Button, useDisclosure } from '@invoke-ai/ui-library'; -import ClearQueueConfirmationAlertDialog from 'features/queue/components/ClearQueueConfirmationAlertDialog'; +import { Button } from '@invoke-ai/ui-library'; +import { useClearQueueConfirmationAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog'; import { useClearQueue } from 'features/queue/hooks/useClearQueue'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -10,7 +10,7 @@ type Props = ButtonProps; const ClearQueueButton = (props: Props) => { const { t } = useTranslation(); - const disclosure = useDisclosure(); + const dialogState = useClearQueueConfirmationAlertDialog(); const { isLoading, isDisabled } = useClearQueue(); return ( @@ -21,13 +21,12 @@ const ClearQueueButton = (props: Props) => { tooltip={t('queue.clearTooltip')} leftIcon={} colorScheme="error" - onClick={disclosure.onOpen} + onClick={dialogState.setTrue} data-testid={t('queue.clear')} {...props} > {t('queue.clear')} - ); }; diff --git a/invokeai/frontend/web/src/features/queue/components/ClearQueueConfirmationAlertDialog.tsx b/invokeai/frontend/web/src/features/queue/components/ClearQueueConfirmationAlertDialog.tsx index 291419d024f..b90d66073ee 100644 --- a/invokeai/frontend/web/src/features/queue/components/ClearQueueConfirmationAlertDialog.tsx +++ b/invokeai/frontend/web/src/features/queue/components/ClearQueueConfirmationAlertDialog.tsx @@ -1,21 +1,24 @@ -import type { UseDisclosureReturn } from '@invoke-ai/ui-library'; import { ConfirmationAlertDialog, Text } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { buildUseBoolean } from 'common/hooks/useBoolean'; import { useClearQueue } from 'features/queue/hooks/useClearQueue'; +import { atom } from 'nanostores'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -type Props = { - disclosure: UseDisclosureReturn; -}; +const $boolean = atom(false); +export const useClearQueueConfirmationAlertDialog = buildUseBoolean($boolean); -const ClearQueueButton = ({ disclosure }: Props) => { +export const ClearQueueConfirmationsAlertDialog = memo(() => { const { t } = useTranslation(); + const dialogState = useClearQueueConfirmationAlertDialog(); + const isOpen = useStore(dialogState.$boolean); const { clearQueue } = useClearQueue(); return ( { {t('queue.clearQueueAlertDialog2')} ); -}; +}); -export default memo(ClearQueueButton); +ClearQueueConfirmationsAlertDialog.displayName = 'ClearQueueConfirmationsAlertDialog'; diff --git a/invokeai/frontend/web/src/features/queue/components/ClearQueueIconButton.tsx b/invokeai/frontend/web/src/features/queue/components/ClearQueueIconButton.tsx index 41843ad66c5..39a84c0216c 100644 --- a/invokeai/frontend/web/src/features/queue/components/ClearQueueIconButton.tsx +++ b/invokeai/frontend/web/src/features/queue/components/ClearQueueIconButton.tsx @@ -1,19 +1,17 @@ import type { IconButtonProps } from '@invoke-ai/ui-library'; -import { IconButton, useDisclosure, useShiftModifier } from '@invoke-ai/ui-library'; -import ClearQueueConfirmationAlertDialog from 'features/queue/components/ClearQueueConfirmationAlertDialog'; +import { IconButton, useShiftModifier } from '@invoke-ai/ui-library'; +import { useClearQueueConfirmationAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog'; import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem'; import { useClearQueue } from 'features/queue/hooks/useClearQueue'; +import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiTrashSimpleBold, PiXBold } from 'react-icons/pi'; type ClearQueueButtonProps = Omit; -type ClearQueueIconButtonProps = ClearQueueButtonProps & { - onOpen: () => void; -}; - -export const ClearAllQueueIconButton = ({ onOpen, ...props }: ClearQueueIconButtonProps) => { +export const ClearAllQueueIconButton = memo((props: ClearQueueButtonProps) => { const { t } = useTranslation(); + const dialogState = useClearQueueConfirmationAlertDialog(); const { isLoading, isDisabled } = useClearQueue(); return ( @@ -24,14 +22,16 @@ export const ClearAllQueueIconButton = ({ onOpen, ...props }: ClearQueueIconButt tooltip={t('queue.clearTooltip')} icon={} colorScheme="error" - onClick={onOpen} + onClick={dialogState.setTrue} data-testid={t('queue.clear')} {...props} /> ); -}; +}); + +ClearAllQueueIconButton.displayName = 'ClearAllQueueIconButton'; -const ClearSingleQueueItemIconButton = (props: ClearQueueButtonProps) => { +const ClearSingleQueueItemIconButton = memo((props: ClearQueueButtonProps) => { const { t } = useTranslation(); const { cancelQueueItem, isLoading, isDisabled } = useCancelCurrentQueueItem(); @@ -48,22 +48,20 @@ const ClearSingleQueueItemIconButton = (props: ClearQueueButtonProps) => { {...props} /> ); -}; +}); -export const ClearQueueIconButton = (props: ClearQueueButtonProps) => { +ClearSingleQueueItemIconButton.displayName = 'ClearSingleQueueItemIconButton'; + +export const ClearQueueIconButton = memo((props: ClearQueueButtonProps) => { // Show the single item clear button when shift is pressed // Otherwise show the clear queue button const shift = useShiftModifier(); - const disclosure = useDisclosure(); - return ( - <> - {shift ? ( - - ) : ( - - )} - - - ); -}; + if (shift) { + return ; + } + + return ; +}); + +ClearQueueIconButton.displayName = 'ClearQueueIconButton'; diff --git a/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx b/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx index 101c82376cc..99456b042f4 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx @@ -10,7 +10,7 @@ import { useDisclosure, } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; -import ClearQueueConfirmationAlertDialog from 'features/queue/components/ClearQueueConfirmationAlertDialog'; +import { useClearQueueConfirmationAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog'; import { useClearQueue } from 'features/queue/hooks/useClearQueue'; import { usePauseProcessor } from 'features/queue/hooks/usePauseProcessor'; import { useResumeProcessor } from 'features/queue/hooks/useResumeProcessor'; @@ -26,7 +26,7 @@ export const QueueActionsMenuButton = memo(() => { const { isOpen, onOpen, onClose } = useDisclosure(); const dispatch = useAppDispatch(); const { t } = useTranslation(); - const clearQueueDisclosure = useDisclosure(); + const dialogState = useClearQueueConfirmationAlertDialog(); const isPauseEnabled = useFeatureStatus('pauseQueue'); const isResumeEnabled = useFeatureStatus('resumeQueue'); const { queueSize } = useGetQueueStatusQuery(undefined, { @@ -51,15 +51,13 @@ export const QueueActionsMenuButton = memo(() => { return ( - - } /> } - onClick={clearQueueDisclosure.onOpen} + onClick={dialogState.setTrue} isLoading={isLoadingClearQueue} isDisabled={isDisabledClearQueue} > diff --git a/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx b/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx index 28a12808ea2..570708642db 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx @@ -17,9 +17,6 @@ const QueueControls = () => { - {/* - {isResumeEnabled && } - {isPauseEnabled && } */} diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx index 5a8273b7fc9..58e1461ec7b 100644 --- a/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx +++ b/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx @@ -1,7 +1,6 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library'; -import { ButtonGroup, Flex, Icon, IconButton, Portal, spinAnimation, useDisclosure } from '@invoke-ai/ui-library'; +import { ButtonGroup, Flex, Icon, IconButton, Portal, spinAnimation } from '@invoke-ai/ui-library'; import CancelCurrentQueueItemIconButton from 'features/queue/components/CancelCurrentQueueItemIconButton'; -import ClearQueueConfirmationAlertDialog from 'features/queue/components/ClearQueueConfirmationAlertDialog'; import { ClearAllQueueIconButton } from 'features/queue/components/ClearQueueIconButton'; import { QueueButtonTooltip } from 'features/queue/components/QueueButtonTooltip'; import { useQueueBack } from 'features/queue/hooks/useQueueBack'; @@ -36,8 +35,6 @@ const FloatingSidePanelButtons = (props: Props) => { [isDisabled, queueStatus?.processor.is_processing] ); - const disclosure = useDisclosure(); - if (!props.panelApi.isCollapsed) { return null; } @@ -76,8 +73,7 @@ const FloatingSidePanelButtons = (props: Props) => { - - + ); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu.tsx index 73e9f5d4ba6..38f44314d62 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu.tsx @@ -25,7 +25,7 @@ const WorkflowLibraryMenu = () => { const shift = useShiftModifier(); useGlobalMenuClose(onClose); return ( - + Date: Fri, 23 Aug 2024 19:52:04 +1000 Subject: [PATCH 463/678] tidy(app): clean up app changes for canvas v2 --- invokeai/app/api/routers/session_queue.py | 3 +-- invokeai/app/invocations/image.py | 1 + .../app/services/session_queue/session_queue_base.py | 3 +-- .../app/services/session_queue/session_queue_common.py | 9 --------- .../app/services/session_queue/session_queue_sqlite.py | 3 +-- 5 files changed, 4 insertions(+), 15 deletions(-) diff --git a/invokeai/app/api/routers/session_queue.py b/invokeai/app/api/routers/session_queue.py index f7d29a88c55..409f13cc285 100644 --- a/invokeai/app/api/routers/session_queue.py +++ b/invokeai/app/api/routers/session_queue.py @@ -15,7 +15,6 @@ ClearResult, EnqueueBatchResult, PruneResult, - QueueItemOrigin, SessionQueueItem, SessionQueueItemDTO, SessionQueueStatus, @@ -114,7 +113,7 @@ async def cancel_by_batch_ids( ) async def cancel_by_origin( queue_id: str = Path(description="The queue id to perform this operation on"), - origin: QueueItemOrigin = Query(description="The origin to cancel all queue items for"), + origin: str = Query(description="The origin to cancel all queue items for"), ) -> CancelByOriginResult: """Immediately cancels all queue items with the given origin""" return ApiDependencies.invoker.services.session_queue.cancel_by_origin(queue_id=queue_id, origin=origin) diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index fc446f860fe..3ddd3a30518 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -1027,6 +1027,7 @@ class CanvasV2MaskAndCropOutput(ImageOutput): tags=["image", "mask", "id"], category="image", version="1.0.0", + classification=Classification.Prototype, ) class CanvasV2MaskAndCropInvocation(BaseInvocation, WithMetadata, WithBoard): """Handles Canvas V2 image output masking and cropping""" diff --git a/invokeai/app/services/session_queue/session_queue_base.py b/invokeai/app/services/session_queue/session_queue_base.py index 9658117048e..93bd66e1779 100644 --- a/invokeai/app/services/session_queue/session_queue_base.py +++ b/invokeai/app/services/session_queue/session_queue_base.py @@ -13,7 +13,6 @@ IsEmptyResult, IsFullResult, PruneResult, - QueueItemOrigin, SessionQueueItem, SessionQueueItemDTO, SessionQueueStatus, @@ -98,7 +97,7 @@ def cancel_by_batch_ids(self, queue_id: str, batch_ids: list[str]) -> CancelByBa pass @abstractmethod - def cancel_by_origin(self, queue_id: str, origin: QueueItemOrigin) -> CancelByOriginResult: + def cancel_by_origin(self, queue_id: str, origin: str) -> CancelByOriginResult: """Cancels all queue items with the given batch origin""" pass diff --git a/invokeai/app/services/session_queue/session_queue_common.py b/invokeai/app/services/session_queue/session_queue_common.py index a87684cbedc..1a546dab9c8 100644 --- a/invokeai/app/services/session_queue/session_queue_common.py +++ b/invokeai/app/services/session_queue/session_queue_common.py @@ -1,6 +1,5 @@ import datetime import json -from enum import Enum from itertools import chain, product from typing import Generator, Iterable, Literal, NamedTuple, Optional, TypeAlias, Union, cast @@ -22,7 +21,6 @@ WorkflowWithoutID, WorkflowWithoutIDValidator, ) -from invokeai.app.util.metaenum import MetaEnum from invokeai.app.util.misc import uuid_string # region Errors @@ -60,13 +58,6 @@ class SessionQueueItemNotFoundError(ValueError): ] -class QueueItemOrigin(str, Enum, metaclass=MetaEnum): - """The origin of a batch. For example, a batch can be created from the canvas or workflows tab.""" - - CANVAS = "canvas" - WORKFLOWS = "workflows" - - class NodeFieldValue(BaseModel): node_path: str = Field(description="The node into which this batch data item will be substituted.") field_name: str = Field(description="The field into which this batch data item will be substituted.") diff --git a/invokeai/app/services/session_queue/session_queue_sqlite.py b/invokeai/app/services/session_queue/session_queue_sqlite.py index 38f8eaa4228..265c6065a59 100644 --- a/invokeai/app/services/session_queue/session_queue_sqlite.py +++ b/invokeai/app/services/session_queue/session_queue_sqlite.py @@ -17,7 +17,6 @@ IsEmptyResult, IsFullResult, PruneResult, - QueueItemOrigin, SessionQueueItem, SessionQueueItemDTO, SessionQueueItemNotFoundError, @@ -427,7 +426,7 @@ def cancel_by_batch_ids(self, queue_id: str, batch_ids: list[str]) -> CancelByBa self.__lock.release() return CancelByBatchIDsResult(canceled=count) - def cancel_by_origin(self, queue_id: str, origin: QueueItemOrigin) -> CancelByOriginResult: + def cancel_by_origin(self, queue_id: str, origin: str) -> CancelByOriginResult: try: current_queue_item = self.get_current(queue_id) self.__lock.acquire() From 4f9c4f7b403332a572a471649a946fd3d472ac43 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 23 Aug 2024 19:52:37 +1000 Subject: [PATCH 464/678] chore(ui): typegen --- invokeai/frontend/web/src/services/api/schema.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index 7d0011cd70e..104ae2ad18f 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -12247,12 +12247,6 @@ export type components = { */ queue_id: string; }; - /** - * QueueItemOrigin - * @description The origin of a batch. For example, a batch can be created from the canvas or workflows tab. - * @enum {string} - */ - QueueItemOrigin: "canvas" | "workflows"; /** * QueueItemStatusChangedEvent * @description Event model for queue_item_status_changed @@ -18517,7 +18511,7 @@ export interface operations { parameters: { query: { /** @description The origin to cancel all queue items for */ - origin: components["schemas"]["QueueItemOrigin"]; + origin: string; }; header?: never; path: { From da5b99c840447904176ee0b8e53206b798cd5907 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 23 Aug 2024 19:55:02 +1000 Subject: [PATCH 465/678] feat(ui): tweak layout of staging area toolbar --- .../features/controlLayers/components/ControlLayersEditor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx index 8c4cd6aa6b5..682e7570769 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx @@ -32,7 +32,7 @@ export const CanvasEditor = memo(() => { > - + From f355d907e6d4d2b0f8b255f3dabc89101adb035a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 23 Aug 2024 20:24:40 +1000 Subject: [PATCH 466/678] fix(ui): filter preview offset --- .../features/controlLayers/konva/CanvasFilterModule.ts | 3 ++- .../features/controlLayers/konva/CanvasObjectRenderer.ts | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts index 3f1274c5306..a0900210487 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts @@ -79,7 +79,7 @@ export class CanvasFilterModule { this.imageState = imageDTOToImageObject(imageDTO); adapter.renderer.clearBuffer(); - await adapter.renderer.setBuffer(this.imageState); + await adapter.renderer.setBuffer(this.imageState, true); adapter.renderer.hideObjects(); this.$isProcessing.set(false); @@ -131,6 +131,7 @@ export class CanvasFilterModule { if (adapter) { adapter.renderer.clearBuffer(); adapter.renderer.showObjects(); + adapter.transformer.updatePosition(); this.$adapter.set(null); } this.imageState = null; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index 6d4d6a017f6..b9d9dab456f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -390,12 +390,18 @@ export class CanvasObjectRenderer { /** * Sets the buffer object state to render. * @param objectState The object state to set as the buffer. + * @param resetBufferOffset Whether to reset the buffer's offset to 0,0. This is necessary when previewing filters. + * When previewing a filter, the buffer object is an image of the same size as the entity, so it should be rendered + * at the top-left corner of the entity. * @returns A promise that resolves to a boolean, indicating if the object was rendered. */ - setBuffer = async (objectState: AnyObjectState): Promise => { + setBuffer = async (objectState: AnyObjectState, resetBufferOffset: boolean = false): Promise => { this.log.trace('Setting buffer'); this.bufferState = objectState; + if (resetBufferOffset) { + this.konva.bufferGroup.offset({ x: 0, y: 0 }); + } return await this.renderBufferObject(); }; From 5ea3c11883a4657af614e99fdd96d404d44dced3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 23 Aug 2024 20:32:49 +1000 Subject: [PATCH 467/678] feat(ui): disable most interaction while filtering --- .../features/controlLayers/components/Filters/Filter.tsx | 4 ++-- .../controlLayers/components/Tool/ToolBboxButton.tsx | 6 ++++-- .../controlLayers/components/Tool/ToolBrushButton.tsx | 6 ++++-- .../controlLayers/components/Tool/ToolChooser.tsx | 2 +- ...ToolEyeDropperButton.tsx => ToolColorPickerButton.tsx} | 6 ++++-- .../controlLayers/components/Tool/ToolEraserButton.tsx | 6 ++++-- .../controlLayers/components/Tool/ToolMoveButton.tsx | 6 ++++-- .../controlLayers/components/Tool/ToolRectButton.tsx | 6 ++++-- .../controlLayers/components/Tool/ToolViewButton.tsx | 6 ++++-- .../src/features/controlLayers/hooks/useIsFiltering.ts | 8 ++++++++ .../features/controlLayers/konva/CanvasFilterModule.ts | 4 +++- 11 files changed, 42 insertions(+), 18 deletions(-) rename invokeai/frontend/web/src/features/controlLayers/components/Tool/{ToolEyeDropperButton.tsx => ToolColorPickerButton.tsx} (86%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/useIsFiltering.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx index b49e23110d7..c88ed9c3edd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx @@ -11,8 +11,8 @@ import { PiCheckBold, PiShootingStarBold, PiXBold } from 'react-icons/pi'; export const Filter = memo(() => { const { t } = useTranslation(); const canvasManager = useCanvasManager(); - const adapter = useStore(canvasManager.filter.$adapter); const config = useStore(canvasManager.filter.$config); + const isFiltering = useStore(canvasManager.filter.$isFiltering); const isProcessing = useStore(canvasManager.filter.$isProcessing); const previewFilter = useCallback(() => { @@ -41,7 +41,7 @@ export const Filter = memo(() => { [canvasManager.filter.$config] ); - if (!adapter) { + if (!isFiltering) { return null; } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBboxButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBboxButton.tsx index 05ae60117a0..07841e9d062 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBboxButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBboxButton.tsx @@ -1,5 +1,6 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering'; import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming'; import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback, useMemo } from 'react'; @@ -10,12 +11,13 @@ import { PiBoundingBoxBold } from 'react-icons/pi'; export const ToolBboxButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); + const isFiltering = useIsFiltering(); const isTransforming = useIsTransforming(); const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'bbox'); const isDisabled = useMemo(() => { - return isTransforming || isStaging; - }, [isStaging, isTransforming]); + return isTransforming || isFiltering || isStaging; + }, [isFiltering, isStaging, isTransforming]); const onClick = useCallback(() => { dispatch(toolChanged('bbox')); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushButton.tsx index 4efa2c35917..666b110ef42 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushButton.tsx @@ -1,5 +1,6 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering'; import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming'; import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; import { isDrawableEntityType } from 'features/controlLayers/store/types'; @@ -11,6 +12,7 @@ import { PiPaintBrushBold } from 'react-icons/pi'; export const ToolBrushButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); + const isFiltering = useIsFiltering(); const isTransforming = useIsTransforming(); const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'brush'); @@ -22,8 +24,8 @@ export const ToolBrushButton = memo(() => { }); const isDisabled = useMemo(() => { - return isTransforming || isStaging || !isDrawingToolAllowed; - }, [isDrawingToolAllowed, isStaging, isTransforming]); + return isTransforming || isFiltering || isStaging || !isDrawingToolAllowed; + }, [isDrawingToolAllowed, isFiltering, isStaging, isTransforming]); const onClick = useCallback(() => { dispatch(toolChanged('brush')); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolChooser.tsx index bb3a424d4ea..6e2b294289a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolChooser.tsx @@ -1,7 +1,7 @@ import { ButtonGroup } from '@invoke-ai/ui-library'; import { ToolBboxButton } from 'features/controlLayers/components/Tool/ToolBboxButton'; import { ToolBrushButton } from 'features/controlLayers/components/Tool/ToolBrushButton'; -import { ToolColorPickerButton } from 'features/controlLayers/components/Tool/ToolEyeDropperButton'; +import { ToolColorPickerButton } from 'features/controlLayers/components/Tool/ToolColorPickerButton'; import { ToolMoveButton } from 'features/controlLayers/components/Tool/ToolMoveButton'; import { ToolRectButton } from 'features/controlLayers/components/Tool/ToolRectButton'; import { useCanvasDeleteLayerHotkey } from 'features/controlLayers/hooks/useCanvasDeleteLayerHotkey'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEyeDropperButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolColorPickerButton.tsx similarity index 86% rename from invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEyeDropperButton.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolColorPickerButton.tsx index de791a8afc3..5e271e8b829 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEyeDropperButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolColorPickerButton.tsx @@ -1,5 +1,6 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering'; import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming'; import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback, useMemo } from 'react'; @@ -10,13 +11,14 @@ import { PiEyedropperBold } from 'react-icons/pi'; export const ToolColorPickerButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); + const isFiltering = useIsFiltering(); const isTransforming = useIsTransforming(); const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'colorPicker'); const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); const isDisabled = useMemo(() => { - return isTransforming || isStaging; - }, [isStaging, isTransforming]); + return isTransforming || isFiltering || isStaging; + }, [isFiltering, isStaging, isTransforming]); const onClick = useCallback(() => { dispatch(toolChanged('colorPicker')); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserButton.tsx index 62d66507695..eb10687d2d0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserButton.tsx @@ -1,5 +1,6 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering'; import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming'; import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; import { isDrawableEntityType } from 'features/controlLayers/store/types'; @@ -11,6 +12,7 @@ import { PiEraserBold } from 'react-icons/pi'; export const ToolEraserButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); + const isFiltering = useIsFiltering(); const isTransforming = useIsTransforming(); const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'eraser'); @@ -21,8 +23,8 @@ export const ToolEraserButton = memo(() => { return isDrawableEntityType(s.canvasV2.selectedEntityIdentifier.type); }); const isDisabled = useMemo(() => { - return isTransforming || isStaging || !isDrawingToolAllowed; - }, [isDrawingToolAllowed, isStaging, isTransforming]); + return isTransforming || isFiltering || isStaging || !isDrawingToolAllowed; + }, [isDrawingToolAllowed, isFiltering, isStaging, isTransforming]); const onClick = useCallback(() => { dispatch(toolChanged('eraser')); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolMoveButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolMoveButton.tsx index d4912758d0d..8a9b00ae414 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolMoveButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolMoveButton.tsx @@ -1,5 +1,6 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering'; import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming'; import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; import { isDrawableEntityType } from 'features/controlLayers/store/types'; @@ -11,6 +12,7 @@ import { PiCursorBold } from 'react-icons/pi'; export const ToolMoveButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); + const isFiltering = useIsFiltering(); const isTransforming = useIsTransforming(); const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'move'); const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); @@ -21,8 +23,8 @@ export const ToolMoveButton = memo(() => { return isDrawableEntityType(s.canvasV2.selectedEntityIdentifier.type); }); const isDisabled = useMemo(() => { - return isTransforming || isStaging || !isDrawingToolAllowed; - }, [isDrawingToolAllowed, isStaging, isTransforming]); + return isTransforming || isFiltering || isStaging || !isDrawingToolAllowed; + }, [isDrawingToolAllowed, isFiltering, isStaging, isTransforming]); const onClick = useCallback(() => { dispatch(toolChanged('move')); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolRectButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolRectButton.tsx index dd57f074f5f..9c908d16f85 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolRectButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolRectButton.tsx @@ -1,5 +1,6 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering'; import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming'; import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; import { isDrawableEntityType } from 'features/controlLayers/store/types'; @@ -12,6 +13,7 @@ export const ToolRectButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'rect'); + const isFiltering = useIsFiltering(); const isTransforming = useIsTransforming(); const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); const isDrawingToolAllowed = useAppSelector((s) => { @@ -22,8 +24,8 @@ export const ToolRectButton = memo(() => { }); const isDisabled = useMemo(() => { - return isTransforming || isStaging || !isDrawingToolAllowed; - }, [isDrawingToolAllowed, isStaging, isTransforming]); + return isTransforming || isFiltering || isStaging || !isDrawingToolAllowed; + }, [isDrawingToolAllowed, isFiltering, isStaging, isTransforming]); const onClick = useCallback(() => { dispatch(toolChanged('rect')); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolViewButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolViewButton.tsx index 3fe209a78ba..be45717be67 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolViewButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolViewButton.tsx @@ -1,5 +1,6 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering'; import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming'; import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback, useMemo } from 'react'; @@ -11,11 +12,12 @@ export const ToolViewButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const isTransforming = useIsTransforming(); + const isFiltering = useIsFiltering(); const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'view'); const isDisabled = useMemo(() => { - return isTransforming || isStaging; - }, [isStaging, isTransforming]); + return isTransforming || isFiltering || isStaging; + }, [isFiltering, isStaging, isTransforming]); const onClick = useCallback(() => { dispatch(toolChanged('view')); }, [dispatch]); diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useIsFiltering.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useIsFiltering.ts new file mode 100644 index 00000000000..66fe0123565 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useIsFiltering.ts @@ -0,0 +1,8 @@ +import { useStore } from '@nanostores/react'; +import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; + +export const useIsFiltering = () => { + const canvasManager = useCanvasManager(); + const isFiltering = useStore(canvasManager.filter.$isFiltering); + return isFiltering; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts index a0900210487..b6e95a1d8b3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts @@ -4,7 +4,7 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { CanvasEntityIdentifier, CanvasImageState, FilterConfig } from 'features/controlLayers/store/types'; import { IMAGE_FILTERS, imageDTOToImageObject } from 'features/controlLayers/store/types'; -import { atom } from 'nanostores'; +import { atom, computed } from 'nanostores'; import type { Logger } from 'roarr'; import { getImageDTO } from 'services/api/endpoints/images'; import type { BatchConfig, ImageDTO, S } from 'services/api/types'; @@ -23,6 +23,7 @@ export class CanvasFilterModule { imageState: CanvasImageState | null = null; $adapter = atom(null); + $isFiltering = computed(this.$adapter, (adapter) => Boolean(adapter)); $isProcessing = atom(false); $config = atom(IMAGE_FILTERS.canny_image_processor.buildDefaults()); @@ -46,6 +47,7 @@ export class CanvasFilterModule { return; } this.$adapter.set(entity.adapter); + this.manager.stateApi.setTool('view'); }; previewFilter = async () => { From 7c68b889eb0ec3f525545cd01ce2a6af02d64694 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 23 Aug 2024 20:34:19 +1000 Subject: [PATCH 468/678] chore: bump version v4.2.9.dev20240823 --- invokeai/version/invokeai_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/version/invokeai_version.py b/invokeai/version/invokeai_version.py index d386f167fd8..6a6a202f164 100644 --- a/invokeai/version/invokeai_version.py +++ b/invokeai/version/invokeai_version.py @@ -1 +1 @@ -__version__ = "4.2.9" +__version__ = "4.2.9.dev20240823" From a040d1d2a66eb5ed18bbc271560d0807d621215a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 23 Aug 2024 23:24:16 +1000 Subject: [PATCH 469/678] fix(ui): new rectangles don't trigger rerender --- .../web/src/features/controlLayers/store/canvasV2Slice.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index b2529e9712a..fa6391c1d86 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -266,6 +266,8 @@ export const canvasV2Slice = createSlice({ assert(false, `Cannot add a brush line to a non-drawable entity of type ${entity.type}`); } + // TODO(psyche): If we add the object without splatting, the renderer will see it as the same object and not + // re-render it (reference equality check). I don't like this behaviour. entity.objects.push({ ...brushLine, points: simplifyFlatNumbersArray(brushLine.points) }); }, entityEraserLineAdded: (state, action: PayloadAction) => { @@ -279,6 +281,8 @@ export const canvasV2Slice = createSlice({ assert(false, `Cannot add a eraser line to a non-drawable entity of type ${entity.type}`); } + // TODO(psyche): If we add the object without splatting, the renderer will see it as the same object and not + // re-render it (reference equality check). I don't like this behaviour. entity.objects.push({ ...eraserLine, points: simplifyFlatNumbersArray(eraserLine.points) }); }, entityRectAdded: (state, action: PayloadAction) => { @@ -292,7 +296,9 @@ export const canvasV2Slice = createSlice({ assert(false, `Cannot add a rect to a non-drawable entity of type ${entity.type}`); } - entity.objects.push(rect); + // TODO(psyche): If we add the object without splatting, the renderer will see it as the same object and not + // re-render it (reference equality check). I don't like this behaviour. + entity.objects.push({ ...rect }); }, entityDeleted: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; From 20a0ed81c5dbd81b69e2d3fa539c92056b750931 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 24 Aug 2024 08:54:20 +1000 Subject: [PATCH 470/678] feat(ui): colored mask preview image --- .../common/CanvasEntityPreviewImage.tsx | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityPreviewImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityPreviewImage.tsx index 161eed1fdab..142f2cdacc9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityPreviewImage.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityPreviewImage.tsx @@ -1,13 +1,36 @@ import { Box, chakra, Flex } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { useEntityAdapter } from 'features/controlLayers/contexts/EntityAdapterContext'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants'; -import { memo, useEffect, useRef } from 'react'; +import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useEffect, useMemo, useRef } from 'react'; +import { useSelector } from 'react-redux'; const ChakraCanvas = chakra.canvas; +const PADDING = 4; + export const CanvasEntityPreviewImage = memo(() => { + const entityIdentifier = useEntityIdentifierContext(); const adapter = useEntityAdapter(); + const selectMaskColor = useMemo( + () => + createSelector(selectCanvasV2Slice, (state) => { + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return null; + } + if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { + return rgbColorToString(entity.fill.color); + } + return null; + }), + [entityIdentifier] + ); + const maskColor = useSelector(selectMaskColor); const containerRef = useRef(null); const canvasRef = useRef(null); const cache = useStore(adapter.renderer.$canvasCache); @@ -26,8 +49,25 @@ export const CanvasEntityPreviewImage = memo(() => { canvasRef.current.width = rect.width; canvasRef.current.height = rect.height; - ctx.drawImage(canvas, rect.x, rect.y, rect.width, rect.height, 0, 0, rect.width, rect.height); - }, [adapter.transformer, adapter.transformer.nodeRect, adapter.transformer.pixelRect, cache]); + const scale = containerRef.current.offsetWidth / rect.width; + + const sx = rect.x; + const sy = rect.y; + const sWidth = rect.width; + const sHeight = rect.height; + const dx = PADDING / scale; + const dy = PADDING / scale; + const dWidth = rect.width - (PADDING * 2) / scale; + const dHeight = rect.height - (PADDING * 2) / scale; + + ctx.drawImage(canvas, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight); + + if (maskColor) { + ctx.fillStyle = maskColor; + ctx.globalCompositeOperation = 'source-in'; + ctx.fillRect(0, 0, rect.width, rect.height); + } + }, [adapter.transformer, adapter.transformer.nodeRect, adapter.transformer.pixelRect, cache, maskColor]); return ( Date: Sat, 24 Aug 2024 10:10:04 +1000 Subject: [PATCH 471/678] feat(ui): better color picker --- .../controlLayers/konva/CanvasToolModule.ts | 198 ++++++++++-------- 1 file changed, 109 insertions(+), 89 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts index fc06cadb44b..275ebe4ea02 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts @@ -2,11 +2,7 @@ import type { SerializableObject } from 'common/types'; import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasPreviewModule } from 'features/controlLayers/konva/CanvasPreviewModule'; -import { - BRUSH_BORDER_INNER_COLOR, - BRUSH_BORDER_OUTER_COLOR, - BRUSH_ERASER_BORDER_WIDTH, -} from 'features/controlLayers/konva/constants'; +import { BRUSH_BORDER_INNER_COLOR, BRUSH_BORDER_OUTER_COLOR } from 'features/controlLayers/konva/constants'; import { alignCoordForTool, getPrefixedId } from 'features/controlLayers/konva/util'; import type { Tool } from 'features/controlLayers/store/types'; import { isDrawableEntity } from 'features/controlLayers/store/types'; @@ -15,6 +11,8 @@ import type { Logger } from 'roarr'; export class CanvasToolModule { readonly type = 'tool_preview'; + static readonly COLOR_PICKER_RADIUS = 25; + static readonly COLOR_PICKER_THICKNESS = 15; id: string; path: string[]; @@ -27,19 +25,21 @@ export class CanvasToolModule { brush: { group: Konva.Group; fillCircle: Konva.Circle; - innerBorderCircle: Konva.Circle; - outerBorderCircle: Konva.Circle; + innerBorder: Konva.Ring; + outerBorder: Konva.Ring; }; eraser: { group: Konva.Group; fillCircle: Konva.Circle; - innerBorderCircle: Konva.Circle; - outerBorderCircle: Konva.Circle; + innerBorder: Konva.Ring; + outerBorder: Konva.Ring; }; colorPicker: { group: Konva.Group; - fillCircle: Konva.Circle; - transparentCenterCircle: Konva.Circle; + newColor: Konva.Ring; + oldColor: Konva.Arc; + innerBorder: Konva.Ring; + outerBorder: Konva.Ring; }; }; @@ -63,19 +63,21 @@ export class CanvasToolModule { listening: false, strokeEnabled: false, }), - innerBorderCircle: new Konva.Circle({ - name: `${this.type}:brush_inner_border_circle`, + innerBorder: new Konva.Ring({ + name: `${this.type}:brush_inner_border_ring`, listening: false, - stroke: BRUSH_BORDER_INNER_COLOR, - strokeWidth: BRUSH_ERASER_BORDER_WIDTH, - strokeEnabled: true, + innerRadius: 0, + outerRadius: 0, + fill: BRUSH_BORDER_INNER_COLOR, + strokeEnabled: false, }), - outerBorderCircle: new Konva.Circle({ - name: `${this.type}:brush_outer_border_circle`, + outerBorder: new Konva.Ring({ + name: `${this.type}:brush_outer_border_ring`, listening: false, - stroke: BRUSH_BORDER_OUTER_COLOR, - strokeWidth: BRUSH_ERASER_BORDER_WIDTH, - strokeEnabled: true, + innerRadius: 0, + outerRadius: 0, + fill: BRUSH_BORDER_OUTER_COLOR, + strokeEnabled: false, }), }, eraser: { @@ -87,54 +89,70 @@ export class CanvasToolModule { fill: 'white', globalCompositeOperation: 'destination-out', }), - innerBorderCircle: new Konva.Circle({ - name: `${this.type}:eraser_inner_border_circle`, + innerBorder: new Konva.Ring({ + name: `${this.type}:eraser_inner_border_ring`, listening: false, - stroke: BRUSH_BORDER_INNER_COLOR, - strokeWidth: BRUSH_ERASER_BORDER_WIDTH, - strokeEnabled: true, + innerRadius: 0, + outerRadius: 0, + fill: BRUSH_BORDER_INNER_COLOR, + strokeEnabled: false, }), - outerBorderCircle: new Konva.Circle({ - name: `${this.type}:eraser_outer_border_circle`, - listening: false, - stroke: BRUSH_BORDER_OUTER_COLOR, - strokeWidth: BRUSH_ERASER_BORDER_WIDTH, - strokeEnabled: true, + outerBorder: new Konva.Ring({ + name: `${this.type}:eraser_outer_border_ring`, + innerRadius: 0, + outerRadius: 0, + fill: BRUSH_BORDER_OUTER_COLOR, + strokeEnabled: false, }), }, colorPicker: { group: new Konva.Group({ name: `${this.type}:color_picker_group`, listening: false }), - fillCircle: new Konva.Circle({ - name: `${this.type}:color_picker_fill_circle`, - listening: false, - fill: '', - radius: 20, - strokeWidth: 1, - stroke: 'black', - strokeScaleEnabled: false, + newColor: new Konva.Ring({ + name: `${this.type}:color_picker_new_color_ring`, + innerRadius: 0, + outerRadius: 0, + strokeEnabled: false, }), - transparentCenterCircle: new Konva.Circle({ - name: `${this.type}:color_picker_fill_circle`, + oldColor: new Konva.Arc({ + name: `${this.type}:color_picker_old_color_arc`, + innerRadius: 0, + outerRadius: 0, + angle: 180, + strokeEnabled: false, + }), + innerBorder: new Konva.Ring({ + name: `${this.type}:color_picker_inner_border_ring`, listening: false, + innerRadius: 0, + outerRadius: 0, + fill: BRUSH_BORDER_INNER_COLOR, + strokeEnabled: false, + }), + outerBorder: new Konva.Ring({ + name: `${this.type}:color_picker_outer_border_ring`, + innerRadius: 0, + outerRadius: 0, + fill: BRUSH_BORDER_OUTER_COLOR, strokeEnabled: false, - fill: 'white', - radius: 5, - globalCompositeOperation: 'destination-out', }), }, }; - this.konva.brush.group.add(this.konva.brush.fillCircle); - this.konva.brush.group.add(this.konva.brush.innerBorderCircle); - this.konva.brush.group.add(this.konva.brush.outerBorderCircle); + this.konva.brush.group.add(this.konva.brush.fillCircle, this.konva.brush.innerBorder, this.konva.brush.outerBorder); this.konva.group.add(this.konva.brush.group); - this.konva.eraser.group.add(this.konva.eraser.fillCircle); - this.konva.eraser.group.add(this.konva.eraser.innerBorderCircle); - this.konva.eraser.group.add(this.konva.eraser.outerBorderCircle); + this.konva.eraser.group.add( + this.konva.eraser.fillCircle, + this.konva.eraser.innerBorder, + this.konva.eraser.outerBorder + ); this.konva.group.add(this.konva.eraser.group); - this.konva.colorPicker.group.add(this.konva.colorPicker.fillCircle); - this.konva.colorPicker.group.add(this.konva.colorPicker.transparentCenterCircle); + this.konva.colorPicker.group.add( + this.konva.colorPicker.newColor, + this.konva.colorPicker.oldColor, + this.konva.colorPicker.innerBorder, + this.konva.colorPicker.outerBorder + ); this.konva.group.add(this.konva.colorPicker.group); this.subscriptions.add( @@ -159,21 +177,38 @@ export class CanvasToolModule { scaleTool = () => { const toolState = this.manager.stateApi.getToolState(); - const scale = this.manager.stage.getScale(); + const onePixel = this.manager.stage.getScaledPixels(1); + const twoPixels = this.manager.stage.getScaledPixels(2); const brushRadius = toolState.brush.width / 2; - this.konva.brush.innerBorderCircle.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale); - this.konva.brush.outerBorderCircle.setAttrs({ - strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, - radius: brushRadius + BRUSH_ERASER_BORDER_WIDTH / scale, - }); + this.konva.brush.innerBorder.innerRadius(brushRadius); + this.konva.brush.innerBorder.outerRadius(brushRadius + onePixel); + + this.konva.brush.outerBorder.innerRadius(brushRadius + onePixel); + this.konva.brush.outerBorder.outerRadius(brushRadius + twoPixels); const eraserRadius = toolState.eraser.width / 2; - this.konva.eraser.innerBorderCircle.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale); - this.konva.eraser.outerBorderCircle.setAttrs({ - strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, - radius: eraserRadius + BRUSH_ERASER_BORDER_WIDTH / scale, - }); + this.konva.eraser.innerBorder.innerRadius(eraserRadius); + this.konva.eraser.innerBorder.outerRadius(eraserRadius + onePixel); + + this.konva.eraser.outerBorder.innerRadius(eraserRadius + onePixel); + this.konva.eraser.outerBorder.outerRadius(eraserRadius + twoPixels); + + const colorPickerInnerRadius = this.manager.stage.getScaledPixels(CanvasToolModule.COLOR_PICKER_RADIUS); + const colorPickerOuterRadius = this.manager.stage.getScaledPixels( + CanvasToolModule.COLOR_PICKER_RADIUS + CanvasToolModule.COLOR_PICKER_THICKNESS + ); + this.konva.colorPicker.oldColor.innerRadius(colorPickerInnerRadius); + this.konva.colorPicker.oldColor.outerRadius(colorPickerOuterRadius); + + this.konva.colorPicker.newColor.innerRadius(colorPickerInnerRadius); + this.konva.colorPicker.newColor.outerRadius(colorPickerOuterRadius); + + this.konva.colorPicker.innerBorder.innerRadius(colorPickerOuterRadius); + this.konva.colorPicker.innerBorder.outerRadius(colorPickerOuterRadius + onePixel); + + this.konva.colorPicker.outerBorder.innerRadius(colorPickerOuterRadius + onePixel); + this.konva.colorPicker.outerBorder.outerRadius(colorPickerOuterRadius + twoPixels); }; setToolVisibility = (tool: Tool) => { @@ -238,7 +273,7 @@ export class CanvasToolModule { if (cursorPos && tool === 'brush') { const brushPreviewFill = this.manager.stateApi.getBrushPreviewFill(); const alignedCursorPos = alignCoordForTool(cursorPos, toolState.brush.width); - const scale = stage.getScale(); + // Update the fill circle const radius = toolState.brush.width / 2; @@ -248,20 +283,10 @@ export class CanvasToolModule { radius, fill: isDrawing ? '' : rgbaColorToString(brushPreviewFill), }); - - // Update the inner border of the brush preview - this.konva.brush.innerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius }); - - // Update the outer border of the brush preview - this.konva.brush.outerBorderCircle.setAttrs({ - x: cursorPos.x, - y: cursorPos.y, - radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, - }); + this.konva.brush.innerBorder.setAttrs({ x: cursorPos.x, y: cursorPos.y }); + this.konva.brush.outerBorder.setAttrs({ x: cursorPos.x, y: cursorPos.y }); } else if (cursorPos && tool === 'eraser') { const alignedCursorPos = alignCoordForTool(cursorPos, toolState.eraser.width); - - const scale = stage.getScale(); // Update the fill circle const radius = toolState.eraser.width / 2; this.konva.eraser.fillCircle.setAttrs({ @@ -270,26 +295,21 @@ export class CanvasToolModule { radius, fill: 'white', }); - - // Update the inner border of the eraser preview - this.konva.eraser.innerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius }); - - // Update the outer border of the eraser preview - this.konva.eraser.outerBorderCircle.setAttrs({ - x: cursorPos.x, - y: cursorPos.y, - radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, - }); + this.konva.eraser.innerBorder.setAttrs({ x: cursorPos.x, y: cursorPos.y }); + this.konva.eraser.outerBorder.setAttrs({ x: cursorPos.x, y: cursorPos.y }); } else if (cursorPos && colorUnderCursor) { - this.konva.colorPicker.fillCircle.setAttrs({ + this.konva.colorPicker.newColor.setAttrs({ x: cursorPos.x, y: cursorPos.y, fill: rgbaColorToString(colorUnderCursor), }); - this.konva.colorPicker.transparentCenterCircle.setAttrs({ + this.konva.colorPicker.oldColor.setAttrs({ x: cursorPos.x, y: cursorPos.y, + fill: rgbaColorToString(toolState.fill), }); + this.konva.colorPicker.innerBorder.setAttrs({ x: cursorPos.x, y: cursorPos.y }); + this.konva.colorPicker.outerBorder.setAttrs({ x: cursorPos.x, y: cursorPos.y }); } this.scaleTool(); From 6cbaf7e0ae2b1944be2c0d898cde227c1a3c16a5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 24 Aug 2024 10:10:21 +1000 Subject: [PATCH 472/678] fix(ui): calculate renderable entities correctly in tool module --- .../controlLayers/konva/CanvasStateApiModule.ts | 12 ++++++++++++ .../controlLayers/konva/CanvasToolModule.ts | 2 +- .../controlLayers/store/canvasV2Slice.ts | 17 ++++++++++++++++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index 0f77f607136..41e30d017e4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -25,6 +25,7 @@ import { entitySelected, eraserWidthChanged, fillChanged, + selectAllRenderableEntities, toolBufferChanged, toolChanged, } from 'features/controlLayers/store/canvasV2Slice'; @@ -198,6 +199,17 @@ export class CanvasStateApiModule { return null; } + getRenderedEntityCount = () => { + const renderableEntities = selectAllRenderableEntities(this.getState()); + let count = 0; + for (const entity of renderableEntities) { + if (entity.isEnabled) { + count++; + } + } + return count; + }; + getSelectedEntity = () => { const state = this.getState(); if (state.selectedEntityIdentifier) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts index 275ebe4ea02..9e3d2aedaa3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts @@ -219,7 +219,7 @@ export class CanvasToolModule { render() { const stage = this.manager.stage; - const renderedEntityCount: number = 1; // TODO(psyche): this.manager should be renderable entity count + const renderedEntityCount = this.manager.stateApi.getRenderedEntityCount(); const toolState = this.manager.stateApi.getToolState(); const selectedEntity = this.manager.stateApi.getSelectedEntity(); const cursorPos = this.manager.stateApi.$lastCursorPos.get(); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index fa6391c1d86..a9a04b163c2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -24,8 +24,12 @@ import { atom } from 'nanostores'; import { assert } from 'tsafe'; import type { + CanvasControlLayerState, CanvasEntityIdentifier, CanvasEntityState, + CanvasInpaintMaskState, + CanvasRasterLayerState, + CanvasRegionalGuidanceState, CanvasV2State, Coordinate, EntityBrushLineAddedPayload, @@ -170,7 +174,7 @@ function selectAllEntitiesOfType(state: CanvasV2State, type: CanvasEntityState[' } } -function selectAllEntities(state: CanvasV2State): CanvasEntityState[] { +export function selectAllEntities(state: CanvasV2State): CanvasEntityState[] { // These are in the same order as they are displayed in the list! return [ ...state.inpaintMasks.entities.toReversed(), @@ -181,6 +185,17 @@ function selectAllEntities(state: CanvasV2State): CanvasEntityState[] { ]; } +export function selectAllRenderableEntities( + state: CanvasV2State +): (CanvasRasterLayerState | CanvasControlLayerState | CanvasInpaintMaskState | CanvasRegionalGuidanceState)[] { + return [ + ...state.rasterLayers.entities, + ...state.controlLayers.entities, + ...state.inpaintMasks.entities, + ...state.regions.entities, + ]; +} + export const canvasV2Slice = createSlice({ name: 'canvasV2', initialState, From 12a00de2ec8cc4686a4b878080afcb70e0a9a8c4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 24 Aug 2024 10:16:27 +1000 Subject: [PATCH 473/678] fix(ui): color picker ignores alpha --- .../konva/CanvasStateApiModule.ts | 3 ++- .../controlLayers/konva/CanvasToolModule.ts | 11 +++++----- .../features/controlLayers/konva/events.ts | 20 +++++++++++-------- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index 41e30d017e4..83ba26d8d20 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -44,6 +44,7 @@ import type { EntityRectAddedPayload, Rect, RgbaColor, + RgbColor, Tool, } from 'features/controlLayers/store/types'; import { RGBA_BLACK } from 'features/controlLayers/store/types'; @@ -249,7 +250,7 @@ export class CanvasStateApiModule { $currentFill: WritableAtom = atom(); $selectedEntity: WritableAtom = atom(); $selectedEntityIdentifier: WritableAtom = atom(); - $colorUnderCursor: WritableAtom = atom(); + $colorUnderCursor: WritableAtom = atom(RGBA_BLACK); // Read-write state, ephemeral interaction state $isDrawing = $isDrawing; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts index 9e3d2aedaa3..ada23a724dd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts @@ -1,5 +1,5 @@ import type { SerializableObject } from 'common/types'; -import { rgbaColorToString } from 'common/util/colorCodeTransformers'; +import { rgbaColorToString, rgbColorToString } from 'common/util/colorCodeTransformers'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasPreviewModule } from 'features/controlLayers/konva/CanvasPreviewModule'; import { BRUSH_BORDER_INNER_COLOR, BRUSH_BORDER_OUTER_COLOR } from 'features/controlLayers/konva/constants'; @@ -225,7 +225,6 @@ export class CanvasToolModule { const cursorPos = this.manager.stateApi.$lastCursorPos.get(); const isDrawing = this.manager.stateApi.$isDrawing.get(); const isMouseDown = this.manager.stateApi.$isMouseDown.get(); - const colorUnderCursor = this.manager.stateApi.$colorUnderCursor.get(); const tool = toolState.selected; @@ -297,16 +296,18 @@ export class CanvasToolModule { }); this.konva.eraser.innerBorder.setAttrs({ x: cursorPos.x, y: cursorPos.y }); this.konva.eraser.outerBorder.setAttrs({ x: cursorPos.x, y: cursorPos.y }); - } else if (cursorPos && colorUnderCursor) { + } else if (cursorPos && tool === 'colorPicker') { + const colorUnderCursor = this.manager.stateApi.$colorUnderCursor.get(); + this.konva.colorPicker.newColor.setAttrs({ x: cursorPos.x, y: cursorPos.y, - fill: rgbaColorToString(colorUnderCursor), + fill: rgbColorToString(colorUnderCursor), }); this.konva.colorPicker.oldColor.setAttrs({ x: cursorPos.x, y: cursorPos.y, - fill: rgbaColorToString(toolState.fill), + fill: rgbColorToString(toolState.fill), }); this.konva.colorPicker.innerBorder.setAttrs({ x: cursorPos.x, y: cursorPos.y }); this.konva.colorPicker.outerBorder.setAttrs({ x: cursorPos.x, y: cursorPos.y }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index fc5c112abba..af64a2d7773 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -12,7 +12,7 @@ import type { CanvasRegionalGuidanceState, CanvasV2State, Coordinate, - RgbaColor, + RgbColor, Tool, } from 'features/controlLayers/store/types'; import type Konva from 'konva'; @@ -115,7 +115,7 @@ const getLastPointOfLastLineOfEntity = ( return { x, y }; }; -const getColorUnderCursor = (stage: Konva.Stage): RgbaColor | null => { +const getColorUnderCursor = (stage: Konva.Stage): RgbColor | null => { const pos = stage.getPointerPosition(); if (!pos) { return null; @@ -126,12 +126,12 @@ const getColorUnderCursor = (stage: Konva.Stage): RgbaColor | null => { if (!ctx) { return null; } - const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data; - if (r === undefined || g === undefined || b === undefined || a === undefined) { + const [r, g, b, _a] = ctx.getImageData(0, 0, 1, 1).data; + if (r === undefined || g === undefined || b === undefined) { return null; } - return { r, g, b, a }; + return { r, g, b }; }; export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { @@ -195,9 +195,11 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (toolState.selected === 'colorPicker') { const color = getColorUnderCursor(stage); - manager.stateApi.$colorUnderCursor.set(color); if (color) { - manager.stateApi.setFill(color); + manager.stateApi.$colorUnderCursor.set(color); + } + if (color) { + manager.stateApi.setFill({ ...color, a: 1 }); } manager.preview.tool.render(); } else { @@ -345,7 +347,9 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { if (toolState.selected === 'colorPicker') { const color = getColorUnderCursor(stage); - manager.stateApi.$colorUnderCursor.set(color); + if (color) { + manager.stateApi.$colorUnderCursor.set(color); + } } else { const isDrawable = selectedEntity?.state.isEnabled; if (pos && isDrawable && !$spaceKey.get() && getIsPrimaryMouseDown(e)) { From a22bf4c29627307f22b50ca026a06893cc4a006b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 24 Aug 2024 10:51:34 +1000 Subject: [PATCH 474/678] feat(ui): add crosshair to color picker --- .../controlLayers/konva/CanvasToolModule.ts | 201 +++++++++++++----- 1 file changed, 152 insertions(+), 49 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts index ada23a724dd..a01c0d9cd48 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts @@ -13,6 +13,10 @@ export class CanvasToolModule { readonly type = 'tool_preview'; static readonly COLOR_PICKER_RADIUS = 25; static readonly COLOR_PICKER_THICKNESS = 15; + static readonly COLOR_PICKER_CROSSHAIR_SPACE = 5; + static readonly COLOR_PICKER_CROSSHAIR_INNER_THICKNESS = 1.5; + static readonly COLOR_PICKER_CROSSHAIR_OUTER_THICKNESS = 3; + static readonly COLOR_PICKER_CROSSHAIR_SIZE = 10; id: string; path: string[]; @@ -40,6 +44,14 @@ export class CanvasToolModule { oldColor: Konva.Arc; innerBorder: Konva.Ring; outerBorder: Konva.Ring; + crosshairNorthInner: Konva.Line; + crosshairNorthOuter: Konva.Line; + crosshairEastInner: Konva.Line; + crosshairEastOuter: Konva.Line; + crosshairSouthInner: Konva.Line; + crosshairSouthOuter: Konva.Line; + crosshairWestInner: Konva.Line; + crosshairWestOuter: Konva.Line; }; }; @@ -135,6 +147,38 @@ export class CanvasToolModule { fill: BRUSH_BORDER_OUTER_COLOR, strokeEnabled: false, }), + crosshairNorthInner: new Konva.Line({ + name: `${this.type}:color_picker_crosshair_north1_line`, + stroke: BRUSH_BORDER_INNER_COLOR, + }), + crosshairNorthOuter: new Konva.Line({ + name: `${this.type}:color_picker_crosshair_north2_line`, + stroke: BRUSH_BORDER_OUTER_COLOR, + }), + crosshairEastInner: new Konva.Line({ + name: `${this.type}:color_picker_crosshair_east1_line`, + stroke: BRUSH_BORDER_INNER_COLOR, + }), + crosshairEastOuter: new Konva.Line({ + name: `${this.type}:color_picker_crosshair_east2_line`, + stroke: BRUSH_BORDER_OUTER_COLOR, + }), + crosshairSouthInner: new Konva.Line({ + name: `${this.type}:color_picker_crosshair_south1_line`, + stroke: BRUSH_BORDER_INNER_COLOR, + }), + crosshairSouthOuter: new Konva.Line({ + name: `${this.type}:color_picker_crosshair_south2_line`, + stroke: BRUSH_BORDER_OUTER_COLOR, + }), + crosshairWestInner: new Konva.Line({ + name: `${this.type}:color_picker_crosshair_west1_line`, + stroke: BRUSH_BORDER_INNER_COLOR, + }), + crosshairWestOuter: new Konva.Line({ + name: `${this.type}:color_picker_crosshair_west2_line`, + stroke: BRUSH_BORDER_OUTER_COLOR, + }), }, }; this.konva.brush.group.add(this.konva.brush.fillCircle, this.konva.brush.innerBorder, this.konva.brush.outerBorder); @@ -151,7 +195,15 @@ export class CanvasToolModule { this.konva.colorPicker.newColor, this.konva.colorPicker.oldColor, this.konva.colorPicker.innerBorder, - this.konva.colorPicker.outerBorder + this.konva.colorPicker.outerBorder, + this.konva.colorPicker.crosshairNorthOuter, + this.konva.colorPicker.crosshairNorthInner, + this.konva.colorPicker.crosshairEastOuter, + this.konva.colorPicker.crosshairEastInner, + this.konva.colorPicker.crosshairSouthOuter, + this.konva.colorPicker.crosshairSouthInner, + this.konva.colorPicker.crosshairWestOuter, + this.konva.colorPicker.crosshairWestInner ); this.konva.group.add(this.konva.colorPicker.group); @@ -175,42 +227,6 @@ export class CanvasToolModule { this.konva.group.destroy(); }; - scaleTool = () => { - const toolState = this.manager.stateApi.getToolState(); - const onePixel = this.manager.stage.getScaledPixels(1); - const twoPixels = this.manager.stage.getScaledPixels(2); - - const brushRadius = toolState.brush.width / 2; - this.konva.brush.innerBorder.innerRadius(brushRadius); - this.konva.brush.innerBorder.outerRadius(brushRadius + onePixel); - - this.konva.brush.outerBorder.innerRadius(brushRadius + onePixel); - this.konva.brush.outerBorder.outerRadius(brushRadius + twoPixels); - - const eraserRadius = toolState.eraser.width / 2; - this.konva.eraser.innerBorder.innerRadius(eraserRadius); - this.konva.eraser.innerBorder.outerRadius(eraserRadius + onePixel); - - this.konva.eraser.outerBorder.innerRadius(eraserRadius + onePixel); - this.konva.eraser.outerBorder.outerRadius(eraserRadius + twoPixels); - - const colorPickerInnerRadius = this.manager.stage.getScaledPixels(CanvasToolModule.COLOR_PICKER_RADIUS); - const colorPickerOuterRadius = this.manager.stage.getScaledPixels( - CanvasToolModule.COLOR_PICKER_RADIUS + CanvasToolModule.COLOR_PICKER_THICKNESS - ); - this.konva.colorPicker.oldColor.innerRadius(colorPickerInnerRadius); - this.konva.colorPicker.oldColor.outerRadius(colorPickerOuterRadius); - - this.konva.colorPicker.newColor.innerRadius(colorPickerInnerRadius); - this.konva.colorPicker.newColor.outerRadius(colorPickerOuterRadius); - - this.konva.colorPicker.innerBorder.innerRadius(colorPickerOuterRadius); - this.konva.colorPicker.innerBorder.outerRadius(colorPickerOuterRadius + onePixel); - - this.konva.colorPicker.outerBorder.innerRadius(colorPickerOuterRadius + onePixel); - this.konva.colorPicker.outerBorder.outerRadius(colorPickerOuterRadius + twoPixels); - }; - setToolVisibility = (tool: Tool) => { this.konva.brush.group.visible(tool === 'brush'); this.konva.eraser.group.visible(tool === 'eraser'); @@ -223,7 +239,6 @@ export class CanvasToolModule { const toolState = this.manager.stateApi.getToolState(); const selectedEntity = this.manager.stateApi.getSelectedEntity(); const cursorPos = this.manager.stateApi.$lastCursorPos.get(); - const isDrawing = this.manager.stateApi.$isDrawing.get(); const isMouseDown = this.manager.stateApi.$isMouseDown.get(); const tool = toolState.selected; @@ -272,48 +287,136 @@ export class CanvasToolModule { if (cursorPos && tool === 'brush') { const brushPreviewFill = this.manager.stateApi.getBrushPreviewFill(); const alignedCursorPos = alignCoordForTool(cursorPos, toolState.brush.width); - - // Update the fill circle + const onePixel = this.manager.stage.getScaledPixels(1); + const twoPixels = this.manager.stage.getScaledPixels(2); const radius = toolState.brush.width / 2; + // The circle is scaled this.konva.brush.fillCircle.setAttrs({ x: alignedCursorPos.x, y: alignedCursorPos.y, radius, - fill: isDrawing ? '' : rgbaColorToString(brushPreviewFill), + fill: rgbaColorToString(brushPreviewFill), + }); + + // But the borders are in screen-pixels + this.konva.brush.innerBorder.setAttrs({ + x: cursorPos.x, + y: cursorPos.y, + innerRadius: radius, + outerRadius: radius + onePixel, + }); + this.konva.brush.outerBorder.setAttrs({ + x: cursorPos.x, + y: cursorPos.y, + innerRadius: radius + onePixel, + outerRadius: radius + twoPixels, }); - this.konva.brush.innerBorder.setAttrs({ x: cursorPos.x, y: cursorPos.y }); - this.konva.brush.outerBorder.setAttrs({ x: cursorPos.x, y: cursorPos.y }); } else if (cursorPos && tool === 'eraser') { const alignedCursorPos = alignCoordForTool(cursorPos, toolState.eraser.width); - // Update the fill circle + const onePixel = this.manager.stage.getScaledPixels(1); + const twoPixels = this.manager.stage.getScaledPixels(2); const radius = toolState.eraser.width / 2; + + // The circle is scaled this.konva.eraser.fillCircle.setAttrs({ x: alignedCursorPos.x, y: alignedCursorPos.y, radius, fill: 'white', }); - this.konva.eraser.innerBorder.setAttrs({ x: cursorPos.x, y: cursorPos.y }); - this.konva.eraser.outerBorder.setAttrs({ x: cursorPos.x, y: cursorPos.y }); + + // But the borders are in screen-pixels + this.konva.eraser.innerBorder.setAttrs({ + x: cursorPos.x, + y: cursorPos.y, + innerRadius: radius, + outerRadius: radius + onePixel, + }); + this.konva.eraser.outerBorder.setAttrs({ + x: cursorPos.x, + y: cursorPos.y, + innerRadius: radius + onePixel, + outerRadius: radius + twoPixels, + }); } else if (cursorPos && tool === 'colorPicker') { const colorUnderCursor = this.manager.stateApi.$colorUnderCursor.get(); + const colorPickerInnerRadius = this.manager.stage.getScaledPixels(CanvasToolModule.COLOR_PICKER_RADIUS); + const colorPickerOuterRadius = this.manager.stage.getScaledPixels( + CanvasToolModule.COLOR_PICKER_RADIUS + CanvasToolModule.COLOR_PICKER_THICKNESS + ); + const onePixel = this.manager.stage.getScaledPixels(1); + const twoPixels = this.manager.stage.getScaledPixels(2); this.konva.colorPicker.newColor.setAttrs({ x: cursorPos.x, y: cursorPos.y, fill: rgbColorToString(colorUnderCursor), + innerRadius: colorPickerInnerRadius, + outerRadius: colorPickerOuterRadius, }); this.konva.colorPicker.oldColor.setAttrs({ x: cursorPos.x, y: cursorPos.y, fill: rgbColorToString(toolState.fill), + innerRadius: colorPickerInnerRadius, + outerRadius: colorPickerOuterRadius, + }); + this.konva.colorPicker.innerBorder.setAttrs({ + x: cursorPos.x, + y: cursorPos.y, + innerRadius: colorPickerOuterRadius, + outerRadius: colorPickerOuterRadius + onePixel, + }); + this.konva.colorPicker.outerBorder.setAttrs({ + x: cursorPos.x, + y: cursorPos.y, + innerRadius: colorPickerOuterRadius + onePixel, + outerRadius: colorPickerOuterRadius + twoPixels, + }); + + const size = this.manager.stage.getScaledPixels(CanvasToolModule.COLOR_PICKER_CROSSHAIR_SIZE); + const space = this.manager.stage.getScaledPixels(CanvasToolModule.COLOR_PICKER_CROSSHAIR_SPACE); + const innerThickness = this.manager.stage.getScaledPixels( + CanvasToolModule.COLOR_PICKER_CROSSHAIR_INNER_THICKNESS + ); + const outerThickness = this.manager.stage.getScaledPixels( + CanvasToolModule.COLOR_PICKER_CROSSHAIR_OUTER_THICKNESS + ); + this.konva.colorPicker.crosshairNorthOuter.setAttrs({ + strokeWidth: outerThickness, + points: [cursorPos.x, cursorPos.y - size, cursorPos.x, cursorPos.y - space], + }); + this.konva.colorPicker.crosshairNorthInner.setAttrs({ + strokeWidth: innerThickness, + points: [cursorPos.x, cursorPos.y - size, cursorPos.x, cursorPos.y - space], + }); + this.konva.colorPicker.crosshairEastOuter.setAttrs({ + strokeWidth: outerThickness, + points: [cursorPos.x + space, cursorPos.y, cursorPos.x + size, cursorPos.y], + }); + this.konva.colorPicker.crosshairEastInner.setAttrs({ + strokeWidth: innerThickness, + points: [cursorPos.x + space, cursorPos.y, cursorPos.x + size, cursorPos.y], + }); + this.konva.colorPicker.crosshairSouthOuter.setAttrs({ + strokeWidth: outerThickness, + points: [cursorPos.x, cursorPos.y + space, cursorPos.x, cursorPos.y + size], + }); + this.konva.colorPicker.crosshairSouthInner.setAttrs({ + strokeWidth: innerThickness, + points: [cursorPos.x, cursorPos.y + space, cursorPos.x, cursorPos.y + size], + }); + this.konva.colorPicker.crosshairWestOuter.setAttrs({ + strokeWidth: outerThickness, + points: [cursorPos.x - space, cursorPos.y, cursorPos.x - size, cursorPos.y], + }); + this.konva.colorPicker.crosshairWestInner.setAttrs({ + strokeWidth: innerThickness, + points: [cursorPos.x - space, cursorPos.y, cursorPos.x - size, cursorPos.y], }); - this.konva.colorPicker.innerBorder.setAttrs({ x: cursorPos.x, y: cursorPos.y }); - this.konva.colorPicker.outerBorder.setAttrs({ x: cursorPos.x, y: cursorPos.y }); } - this.scaleTool(); this.setToolVisibility(tool); } } From e013aff9fedac558838f2b352ee4d14e0d66f45f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 24 Aug 2024 11:14:58 +1000 Subject: [PATCH 475/678] fix(ui): newly-added entities are selected --- .../components/CanvasEntityListMenu.tsx | 15 ++++------ .../store/controlLayersReducers.ts | 10 +++---- .../store/inpaintMaskReducers.ts | 15 +++++----- .../controlLayers/store/ipAdaptersReducers.ts | 29 +++++++++++++------ .../store/rasterLayersReducers.ts | 10 +++---- .../controlLayers/store/regionsReducers.ts | 24 ++++++++++----- 6 files changed, 60 insertions(+), 43 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityListMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityListMenu.tsx index 1ef30d92c34..93577d353ea 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityListMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityListMenu.tsx @@ -1,6 +1,5 @@ import { IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useDefaultControlAdapter, useDefaultIPAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter'; import { allEntitiesDeleted, controlLayerAdded, @@ -21,23 +20,21 @@ export const CanvasEntityListMenu = memo(() => { const count = selectEntityCount(s); return count > 0; }); - const defaultControlAdapter = useDefaultControlAdapter(); - const defaultIPAdapter = useDefaultIPAdapter(); const addInpaintMask = useCallback(() => { - dispatch(inpaintMaskAdded()); + dispatch(inpaintMaskAdded({ isSelected: true })); }, [dispatch]); const addRegionalGuidance = useCallback(() => { - dispatch(rgAdded()); + dispatch(rgAdded({ isSelected: true })); }, [dispatch]); const addRasterLayer = useCallback(() => { dispatch(rasterLayerAdded({ isSelected: true })); }, [dispatch]); const addControlLayer = useCallback(() => { - dispatch(controlLayerAdded({ isSelected: true, overrides: { controlAdapter: defaultControlAdapter } })); - }, [defaultControlAdapter, dispatch]); + dispatch(controlLayerAdded({ isSelected: true })); + }, [dispatch]); const addIPAdapter = useCallback(() => { - dispatch(ipaAdded({ ipAdapter: defaultIPAdapter })); - }, [defaultIPAdapter, dispatch]); + dispatch(ipaAdded({ isSelected: true })); + }, [dispatch]); const deleteAll = useCallback(() => { dispatch(allEntitiesDeleted()); }, [dispatch]); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts index aefd73db93f..d6399846912 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts @@ -14,7 +14,7 @@ import type { ControlNetConfig, T2IAdapterConfig, } from './types'; -import { initialControlNet } from './types'; +import { getEntityIdentifier, initialControlNet } from './types'; const selectControlLayerEntity = (state: CanvasV2State, id: string) => state.controlLayers.entities.find((entity) => entity.id === id); @@ -31,7 +31,7 @@ export const controlLayersReducers = { action: PayloadAction<{ id: string; overrides?: Partial; isSelected?: boolean }> ) => { const { id, overrides, isSelected } = action.payload; - const layer: CanvasControlLayerState = { + const entity: CanvasControlLayerState = { id, name: null, type: 'control_layer', @@ -42,10 +42,10 @@ export const controlLayersReducers = { position: { x: 0, y: 0 }, controlAdapter: deepClone(initialControlNet), }; - merge(layer, overrides); - state.controlLayers.entities.push(layer); + merge(entity, overrides); + state.controlLayers.entities.push(entity); if (isSelected) { - state.selectedEntityIdentifier = { type: 'control_layer', id }; + state.selectedEntityIdentifier = getEntityIdentifier(entity); } }, prepare: (payload: { overrides?: Partial; isSelected?: boolean }) => ({ diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts index 75d57be7da9..9952c8a3458 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts @@ -1,11 +1,12 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import type { - CanvasInpaintMaskState, - CanvasV2State, - EntityIdentifierPayload, - FillStyle, - RgbColor, +import { + type CanvasInpaintMaskState, + type CanvasV2State, + type EntityIdentifierPayload, + type FillStyle, + getEntityIdentifier, + type RgbColor, } from 'features/controlLayers/store/types'; import { merge } from 'lodash-es'; import { assert } from 'tsafe'; @@ -41,7 +42,7 @@ export const inpaintMaskReducers = { merge(entity, overrides); state.inpaintMasks.entities.push(entity); if (isSelected) { - state.selectedEntityIdentifier = { type: 'inpaint_mask', id }; + state.selectedEntityIdentifier = getEntityIdentifier(entity); } }, prepare: (payload?: { overrides?: Partial; isSelected?: boolean }) => ({ diff --git a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts index 7893e46694a..1fa95139751 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts @@ -1,11 +1,14 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import { deepClone } from 'common/util/deepClone'; +import { getPrefixedId } from 'features/controlLayers/konva/util'; import { zModelIdentifierField } from 'features/nodes/types/common'; +import { merge } from 'lodash-es'; import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; -import type { CanvasIPAdapterState, CanvasV2State, CLIPVisionModelV2, IPAdapterConfig, IPMethodV2 } from './types'; -import { imageDTOToImageWithDims } from './types'; +import type { CanvasIPAdapterState, CanvasV2State, CLIPVisionModelV2, IPMethodV2 } from './types'; +import { getEntityIdentifier, imageDTOToImageWithDims, initialIPAdapter } from './types'; const selectIPAdapterEntity = (state: CanvasV2State, id: string) => state.ipAdapters.entities.find((ipa) => ipa.id === id); @@ -17,19 +20,27 @@ export const selectIPAdapterEntityOrThrow = (state: CanvasV2State, id: string) = export const ipAdaptersReducers = { ipaAdded: { - reducer: (state, action: PayloadAction<{ id: string; ipAdapter: IPAdapterConfig }>) => { - const { id, ipAdapter } = action.payload; - const layer: CanvasIPAdapterState = { + reducer: ( + state, + action: PayloadAction<{ id: string; overrides?: Partial; isSelected?: boolean }> + ) => { + const { id, overrides, isSelected } = action.payload; + const entity: CanvasIPAdapterState = { id, type: 'ip_adapter', name: null, isEnabled: true, - ipAdapter, + ipAdapter: deepClone(initialIPAdapter), }; - state.ipAdapters.entities.push(layer); - state.selectedEntityIdentifier = { type: 'ip_adapter', id }; + merge(entity, overrides); + state.ipAdapters.entities.push(entity); + if (isSelected) { + state.selectedEntityIdentifier = getEntityIdentifier(entity); + } }, - prepare: (payload: { ipAdapter: IPAdapterConfig }) => ({ payload: { id: uuidv4(), ...payload } }), + prepare: (payload?: { overrides?: Partial; isSelected?: boolean }) => ({ + payload: { ...payload, id: getPrefixedId('ip_adapter') }, + }), }, ipaRecalled: (state, action: PayloadAction<{ data: CanvasIPAdapterState }>) => { const { data } = action.payload; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts index 05c5257787a..24270555008 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts @@ -4,7 +4,7 @@ import { getPrefixedId } from 'features/controlLayers/konva/util'; import { merge } from 'lodash-es'; import type { CanvasControlLayerState, CanvasRasterLayerState, CanvasV2State } from './types'; -import { initialControlNet } from './types'; +import { getEntityIdentifier, initialControlNet } from './types'; const selectRasterLayerEntity = (state: CanvasV2State, id: string) => state.rasterLayers.entities.find((layer) => layer.id === id); @@ -16,7 +16,7 @@ export const rasterLayersReducers = { action: PayloadAction<{ id: string; overrides?: Partial; isSelected?: boolean }> ) => { const { id, overrides, isSelected } = action.payload; - const layer: CanvasRasterLayerState = { + const entity: CanvasRasterLayerState = { id, name: null, type: 'raster_layer', @@ -25,10 +25,10 @@ export const rasterLayersReducers = { opacity: 1, position: { x: 0, y: 0 }, }; - merge(layer, overrides); - state.rasterLayers.entities.push(layer); + merge(entity, overrides); + state.rasterLayers.entities.push(entity); if (isSelected) { - state.selectedEntityIdentifier = { type: 'raster_layer', id }; + state.selectedEntityIdentifier = getEntityIdentifier(entity); } }, prepare: (payload: { overrides?: Partial; isSelected?: boolean }) => ({ diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index 4b3a9ecf927..faf12dd3f7a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -8,9 +8,9 @@ import type { RegionalGuidanceIPAdapterConfig, RgbColor, } from 'features/controlLayers/store/types'; -import { imageDTOToImageWithDims } from 'features/controlLayers/store/types'; +import { getEntityIdentifier, imageDTOToImageWithDims } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; -import { isEqual } from 'lodash-es'; +import { isEqual, merge } from 'lodash-es'; import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; @@ -56,9 +56,12 @@ const getRGMaskFill = (state: CanvasV2State): RgbColor => { export const regionsReducers = { rgAdded: { - reducer: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const rg: CanvasRegionalGuidanceState = { + reducer: ( + state, + action: PayloadAction<{ id: string; overrides?: Partial; isSelected?: boolean }> + ) => { + const { id, overrides, isSelected } = action.payload; + const entity: CanvasRegionalGuidanceState = { id, name: null, type: 'regional_guidance', @@ -75,10 +78,15 @@ export const regionsReducers = { negativePrompt: null, ipAdapters: [], }; - state.regions.entities.push(rg); - state.selectedEntityIdentifier = { type: 'regional_guidance', id }; + merge(entity, overrides); + state.regions.entities.push(entity); + if (isSelected) { + state.selectedEntityIdentifier = getEntityIdentifier(entity); + } }, - prepare: () => ({ payload: { id: getPrefixedId('regional_guidance') } }), + prepare: (payload?: { overrides?: Partial; isSelected?: boolean }) => ({ + payload: { ...payload, id: getPrefixedId('regional_guidance') }, + }), }, rgRecalled: (state, action: PayloadAction<{ data: CanvasRegionalGuidanceState }>) => { const { data } = action.payload; From 775353fe827f576a9289d3643823cc10720017c2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 24 Aug 2024 11:28:05 +1000 Subject: [PATCH 476/678] fix(ui): extraneous entity preview updates --- .../konva/CanvasObjectRenderer.ts | 12 ++++---- .../controlLayers/konva/CanvasTransformer.ts | 28 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index b9d9dab456f..f5b7b574046 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -27,6 +27,7 @@ import type { import { imageDTOToImageObject } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { GroupConfig } from 'konva/lib/Group'; +import { debounce } from 'lodash-es'; import { atom } from 'nanostores'; import type { Logger } from 'roarr'; import { getImageDTO, uploadImage } from 'services/api/endpoints/images'; @@ -209,9 +210,7 @@ export class CanvasObjectRenderer { this.log.trace('Caching object group'); this.konva.objectGroup.clearCache(); this.konva.objectGroup.cache({ pixelRatio: 1 }); - if (!this.parent.transformer.isPendingRectCalculation) { - this.parent.renderer.updatePreviewCanvas(); - } + this.parent.renderer.updatePreviewCanvas(); } }; @@ -542,7 +541,10 @@ export class CanvasObjectRenderer { return imageDTO; }; - updatePreviewCanvas = () => { + updatePreviewCanvas = debounce(() => { + if (this.parent.transformer.isPendingRectCalculation) { + return; + } if (this.parent.transformer.pixelRect.width === 0 || this.parent.transformer.pixelRect.height === 0) { return; } @@ -558,7 +560,7 @@ export class CanvasObjectRenderer { }; this.$canvasCache.set({ rect, canvas }); } - }; + }, 300); cloneObjectGroup = (attrs?: GroupConfig): Konva.Group => { const clone = this.konva.objectGroup.clone(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index ac690bd2e8e..1df7aa7532b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -602,7 +602,6 @@ export class CanvasTransformer { if (this.isPendingRectCalculation) { this.syncInteractionState(); - this.parent.renderer.updatePreviewCanvas(); return; } @@ -613,20 +612,19 @@ export class CanvasTransformer { // The layer is fully transparent but has objects - reset it this.manager.stateApi.resetEntity({ entityIdentifier: this.parent.getEntityIdentifier() }); this.syncInteractionState(); - this.parent.renderer.updatePreviewCanvas(); - return; + } else { + this.syncInteractionState(); + this.update(this.parent.state.position, this.pixelRect); + const groupAttrs: Partial = { + x: this.parent.state.position.x + this.pixelRect.x, + y: this.parent.state.position.y + this.pixelRect.y, + offsetX: this.pixelRect.x, + offsetY: this.pixelRect.y, + }; + this.parent.renderer.konva.objectGroup.setAttrs(groupAttrs); + this.parent.renderer.konva.bufferGroup.setAttrs(groupAttrs); } - this.syncInteractionState(); - this.update(this.parent.state.position, this.pixelRect); - const groupAttrs: Partial = { - x: this.parent.state.position.x + this.pixelRect.x, - y: this.parent.state.position.y + this.pixelRect.y, - offsetX: this.pixelRect.x, - offsetY: this.pixelRect.y, - }; - this.parent.renderer.konva.objectGroup.setAttrs(groupAttrs); - this.parent.renderer.konva.bufferGroup.setAttrs(groupAttrs); this.parent.renderer.updatePreviewCanvas(); }; @@ -649,8 +647,8 @@ export class CanvasTransformer { if (!this.parent.renderer.needsPixelBbox()) { this.nodeRect = { ...rect }; this.pixelRect = { ...rect }; - this.isPendingRectCalculation = false; this.log.trace({ nodeRect: this.nodeRect, pixelRect: this.pixelRect }, 'Got bbox from client rect'); + this.isPendingRectCalculation = false; this.updateBbox(); return; } @@ -674,8 +672,8 @@ export class CanvasTransformer { this.nodeRect = getEmptyRect(); this.pixelRect = getEmptyRect(); } - this.isPendingRectCalculation = false; this.log.trace({ nodeRect: this.nodeRect, pixelRect: this.pixelRect, extents }, `Got bbox from worker`); + this.isPendingRectCalculation = false; this.updateBbox(); } ); From e6960320dd7e0076ca140c93ed708cbb0e619fd4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 24 Aug 2024 11:55:43 +1000 Subject: [PATCH 477/678] feat(nodes): CanvasV2MaskAndCropInvocation can paste generated image back on source This is needed for `Generate` mode. --- invokeai/app/invocations/image.py | 40 ++++++++++++++----------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index 3ddd3a30518..2c70a141fbc 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -1032,7 +1032,11 @@ class CanvasV2MaskAndCropOutput(ImageOutput): class CanvasV2MaskAndCropInvocation(BaseInvocation, WithMetadata, WithBoard): """Handles Canvas V2 image output masking and cropping""" - image: ImageField = InputField(description="The image to apply the mask to") + source_image: ImageField | None = InputField( + default=None, + description="The source image onto which the masked generated image is pasted. If omitted, the masked generated image is returned with transparency.", + ) + generated_image: ImageField = InputField(description="The image to apply the mask to") mask: ImageField = InputField(description="The mask to apply") mask_blur: int = InputField(default=0, ge=0, description="The amount to blur the mask by") @@ -1046,33 +1050,25 @@ def _prepare_mask(self, mask: Image.Image) -> Image.Image: return ImageOps.invert(mask.convert("L")) def invoke(self, context: InvocationContext) -> CanvasV2MaskAndCropOutput: - image = context.images.get_pil(self.image.image_name) mask = self._prepare_mask(context.images.get_pil(self.mask.image_name)) - image.putalpha(mask) + + if self.source_image: + generated_image = context.images.get_pil(self.generated_image.image_name) + source_image = context.images.get_pil(self.source_image.image_name) + source_image.paste(generated_image, (0, 0), mask) + image_dto = context.images.save(image=source_image) + else: + generated_image = context.images.get_pil(self.generated_image.image_name) + generated_image.putalpha(mask) + image_dto = context.images.save(image=generated_image) + # bbox = image.getbbox() # image = image.crop(bbox) - image_dto = context.images.save(image=image) return CanvasV2MaskAndCropOutput( image=ImageField(image_name=image_dto.image_name), offset_x=0, offset_y=0, - width=image.width, - height=image.height, + width=image_dto.width, + height=image_dto.height, ) - - # def invoke(self, context: InvocationContext) -> CanvasV2MaskAndCropOutput: - # image = context.images.get_pil(self.image.image_name) - # mask = self._prepare_mask(context.images.get_pil(self.mask.image_name)) - # image.putalpha(mask) - # bbox = image.getbbox() - # image = image.crop(bbox) - # image_dto = context.images.save(image=image) - - # return CanvasV2MaskAndCropOutput( - # image=ImageField(image_name=image_dto.image_name), - # offset_x=bbox[0], - # offset_y=bbox[1], - # width=image.width, - # height=image.height, - # ) From 44b8480832730c0a2d5b7e4de3a0877f8556f6a7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 24 Aug 2024 11:55:50 +1000 Subject: [PATCH 478/678] chore(ui): typegen --- invokeai/frontend/web/src/services/api/schema.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index 104ae2ad18f..b9ca8a3a60c 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -3214,11 +3214,16 @@ export type components = { * @default true */ use_cache?: boolean; + /** + * @description The source image onto which the masked generated image is pasted. If omitted, the masked generated image is returned with transparency. + * @default null + */ + source_image?: components["schemas"]["ImageField"] | null; /** * @description The image to apply the mask to * @default null */ - image?: components["schemas"]["ImageField"]; + generated_image?: components["schemas"]["ImageField"]; /** * @description The mask to apply * @default null From 03b3139705f504b0bc6ed9b14a3360ee70ac684a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 24 Aug 2024 11:56:24 +1000 Subject: [PATCH 479/678] feat(ui): paste canvas gens back on source in generate mode --- .../nodes/util/graph/generation/addInpaint.ts | 16 ++++++++++++++-- .../nodes/util/graph/generation/addOutpaint.ts | 15 +++++++++++++-- .../nodes/util/graph/generation/buildSD1Graph.ts | 2 ++ .../util/graph/generation/buildSDXLGraph.ts | 2 ++ 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts index ef0aed835bb..35f512a1c97 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts @@ -1,3 +1,4 @@ +import type { RootState } from 'app/store/store'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { CanvasV2State, Dimensions } from 'features/controlLayers/store/types'; @@ -7,6 +8,7 @@ import { isEqual } from 'lodash-es'; import type { Invocation } from 'services/api/types'; export const addInpaint = async ( + state: RootState, g: Graph, manager: CanvasManager, l2i: Invocation<'l2i'>, @@ -22,6 +24,7 @@ export const addInpaint = async ( ): Promise> => { denoise.denoising_start = denoising_start; + const mode = state.canvasV2.session.mode; const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect); const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect); @@ -87,9 +90,13 @@ export const addInpaint = async ( g.addEdge(createGradientMask, 'expanded_mask_area', resizeMaskToOriginalSize, 'image'); // Finally, paste the generated masked image back onto the original image - g.addEdge(resizeImageToOriginalSize, 'image', canvasPasteBack, 'image'); + g.addEdge(resizeImageToOriginalSize, 'image', canvasPasteBack, 'generated_image'); g.addEdge(resizeMaskToOriginalSize, 'image', canvasPasteBack, 'mask'); + if (mode === 'generate') { + canvasPasteBack.source_image = { image_name: initialImage.image_name }; + } + return canvasPasteBack; } else { // No scale before processing, much simpler @@ -114,6 +121,7 @@ export const addInpaint = async ( type: 'canvas_v2_mask_and_crop', mask_blur: compositing.maskBlur, }); + g.addEdge(alphaToMask, 'image', createGradientMask, 'mask'); g.addEdge(i2l, 'latents', denoise, 'latents'); g.addEdge(vaeSource, 'vae', i2l, 'vae'); @@ -122,7 +130,11 @@ export const addInpaint = async ( g.addEdge(createGradientMask, 'denoise_mask', denoise, 'denoise_mask'); g.addEdge(createGradientMask, 'expanded_mask_area', canvasPasteBack, 'mask'); - g.addEdge(l2i, 'image', canvasPasteBack, 'image'); + g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image'); + + if (mode === 'generate') { + canvasPasteBack.source_image = { image_name: initialImage.image_name }; + } return canvasPasteBack; } diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index b2d5b4a18e7..55fd2eac4ea 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts @@ -1,3 +1,4 @@ +import type { RootState } from 'app/store/store'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { CanvasV2State, Dimensions } from 'features/controlLayers/store/types'; @@ -8,6 +9,7 @@ import { isEqual } from 'lodash-es'; import type { Invocation } from 'services/api/types'; export const addOutpaint = async ( + state: RootState, g: Graph, manager: CanvasManager, l2i: Invocation<'l2i'>, @@ -23,6 +25,7 @@ export const addOutpaint = async ( ): Promise> => { denoise.denoising_start = denoising_start; + const mode = state.canvasV2.session.mode; const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect); const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect); const infill = getInfill(g, compositing); @@ -111,9 +114,13 @@ export const addOutpaint = async ( g.addEdge(createGradientMask, 'expanded_mask_area', resizeOutputMaskToOriginalSize, 'image'); // Finally, paste the generated masked image back onto the original image - g.addEdge(resizeOutputImageToOriginalSize, 'image', canvasPasteBack, 'image'); + g.addEdge(resizeOutputImageToOriginalSize, 'image', canvasPasteBack, 'generated_image'); g.addEdge(resizeOutputMaskToOriginalSize, 'image', canvasPasteBack, 'mask'); + if (mode === 'generate') { + canvasPasteBack.source_image = { image_name: initialImage.image_name }; + } + return canvasPasteBack; } else { infill.image = { image_name: initialImage.image_name }; @@ -158,7 +165,11 @@ export const addOutpaint = async ( g.addEdge(modelLoader, 'unet', createGradientMask, 'unet'); g.addEdge(createGradientMask, 'denoise_mask', denoise, 'denoise_mask'); g.addEdge(createGradientMask, 'expanded_mask_area', canvasPasteBack, 'mask'); - g.addEdge(l2i, 'image', canvasPasteBack, 'image'); + g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image'); + + if (mode === 'generate') { + canvasPasteBack.source_image = { image_name: initialImage.image_name }; + } return canvasPasteBack; } diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index b0aad315caf..237acfca13f 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -173,6 +173,7 @@ export const buildSD1Graph = async ( } else if (generationMode === 'inpaint') { const { compositing } = state.canvasV2; canvasOutput = await addInpaint( + state, g, manager, l2i, @@ -189,6 +190,7 @@ export const buildSD1Graph = async ( } else if (generationMode === 'outpaint') { const { compositing } = state.canvasV2; canvasOutput = await addOutpaint( + state, g, manager, l2i, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index 601e9671a7b..6e8c8a7248b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -176,6 +176,7 @@ export const buildSDXLGraph = async ( } else if (generationMode === 'inpaint') { const { compositing } = state.canvasV2; canvasOutput = await addInpaint( + state, g, manager, l2i, @@ -192,6 +193,7 @@ export const buildSDXLGraph = async ( } else if (generationMode === 'outpaint') { const { compositing } = state.canvasV2; canvasOutput = await addOutpaint( + state, g, manager, l2i, From ee94b0ce17cfc7576212d1712256b24474fcbf83 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 24 Aug 2024 12:20:26 +1000 Subject: [PATCH 480/678] feat(ui): autocomplete on getPrefixeId --- .../frontend/web/src/features/controlLayers/konva/util.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index adc8504bf19..003448929c7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -1,4 +1,4 @@ -import type { Coordinate, Rect } from 'features/controlLayers/store/types'; +import type { CanvasEntityIdentifier, Coordinate, Rect } from 'features/controlLayers/store/types'; import type Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; import type { Vector2d } from 'konva/lib/types'; @@ -335,7 +335,7 @@ export function loadImage(src: string): Promise { */ export const nanoid = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 10); -export function getPrefixedId(prefix: string): string { +export function getPrefixedId(prefix: CanvasEntityIdentifier['type'] | (string & Record)): string { return `${prefix}:${nanoid()}`; } From 86a5e2538c0a42f315c12ea44682010b45e77cba Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 24 Aug 2024 12:20:35 +1000 Subject: [PATCH 481/678] feat(ui): duplicate entity --- invokeai/frontend/web/public/locales/en.json | 1 + .../ControlLayer/ControlLayerMenuItems.tsx | 2 + .../IPAdapter/IPAdapterMenuItems.tsx | 2 + .../InpaintMask/InpaintMaskMenuItems.tsx | 2 + .../RasterLayer/RasterLayerMenuItems.tsx | 2 + .../RegionalGuidanceMenuItems.tsx | 2 + .../common/CanvasEntityMenuItemsDuplicate.tsx | 25 ++++++++++++ .../controlLayers/store/canvasV2Slice.ts | 38 +++++++++++++++++++ 8 files changed, 74 insertions(+) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index c2ce7701ef5..abfafcdbf75 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1662,6 +1662,7 @@ "recalculateRects": "Recalculate Rects", "clipToBbox": "Clip Strokes to Bbox", "addLayer": "Add Layer", + "duplicate": "Duplicate", "moveToFront": "Move to Front", "moveToBack": "Move to Back", "moveForward": "Move Forward", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItems.tsx index 97ab0eb4cec..accf350bea8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItems.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItems.tsx @@ -1,6 +1,7 @@ import { MenuDivider } from '@invoke-ai/ui-library'; import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange'; import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete'; +import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate'; import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter'; import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform'; import { ControlLayerMenuItemsControlToRaster } from 'features/controlLayers/components/ControlLayer/ControlLayerMenuItemsControlToRaster'; @@ -17,6 +18,7 @@ export const ControlLayerMenuItems = memo(() => { + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterMenuItems.tsx index 315d3b06c4a..d9ac61e4fb3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterMenuItems.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterMenuItems.tsx @@ -1,6 +1,7 @@ import { MenuDivider } from '@invoke-ai/ui-library'; import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange'; import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete'; +import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate'; import { memo } from 'react'; export const IPAdapterMenuItems = memo(() => { @@ -8,6 +9,7 @@ export const IPAdapterMenuItems = memo(() => { <> + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItems.tsx index ac19e78ffa8..925ba30e354 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItems.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItems.tsx @@ -1,6 +1,7 @@ import { MenuDivider } from '@invoke-ai/ui-library'; import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange'; import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete'; +import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate'; import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform'; import { memo } from 'react'; @@ -11,6 +12,7 @@ export const InpaintMaskMenuItems = memo(() => { + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItems.tsx index d787fc39f40..723db3ab818 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItems.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItems.tsx @@ -1,6 +1,7 @@ import { MenuDivider } from '@invoke-ai/ui-library'; import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange'; import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete'; +import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate'; import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter'; import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform'; import { RasterLayerMenuItemsRasterToControl } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl'; @@ -15,6 +16,7 @@ export const RasterLayerMenuItems = memo(() => { + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItems.tsx index 2ae566688b6..ac21fd6357a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItems.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItems.tsx @@ -1,6 +1,7 @@ import { MenuDivider } from '@invoke-ai/ui-library'; import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange'; import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete'; +import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate'; import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform'; import { RegionalGuidanceMenuItemsAddPromptsAndIPAdapter } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter'; import { RegionalGuidanceMenuItemsAutoNegative } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAutoNegative'; @@ -16,6 +17,7 @@ export const RegionalGuidanceMenuItems = memo(() => { + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate.tsx new file mode 100644 index 00000000000..dd84a84f428 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate.tsx @@ -0,0 +1,25 @@ +import { MenuItem } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { entityDuplicated } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiCopyFill } from 'react-icons/pi'; + +export const CanvasEntityMenuItemsDuplicate = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const entityIdentifier = useEntityIdentifierContext(); + + const onClick = useCallback(() => { + dispatch(entityDuplicated({ entityIdentifier })); + }, [dispatch, entityIdentifier]); + + return ( + }> + {t('controlLayers.duplicate')} + + ); +}); + +CanvasEntityMenuItemsDuplicate.displayName = 'CanvasEntityMenuItemsDuplicate'; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index a9a04b163c2..702e42786bf 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -3,6 +3,7 @@ import { createAction, createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { deepClone } from 'common/util/deepClone'; +import { getPrefixedId } from 'features/controlLayers/konva/util'; import { bboxReducers } from 'features/controlLayers/store/bboxReducers'; import { compositingReducers } from 'features/controlLayers/store/compositingReducers'; import { controlLayersReducers } from 'features/controlLayers/store/controlLayersReducers'; @@ -237,6 +238,42 @@ export const canvasV2Slice = createSlice({ assert(false, 'Not implemented'); } }, + entityDuplicated: (state, action: PayloadAction) => { + const { entityIdentifier } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } + + const newEntity = deepClone(entity); + if (newEntity.name) { + newEntity.name = `${newEntity.name} (Copy)`; + } + switch (newEntity.type) { + case 'raster_layer': + newEntity.id = getPrefixedId('raster_layer'); + state.rasterLayers.entities.push(newEntity); + break; + case 'control_layer': + newEntity.id = getPrefixedId('control_layer'); + state.controlLayers.entities.push(newEntity); + break; + case 'regional_guidance': + newEntity.id = getPrefixedId('regional_guidance'); + state.regions.entities.push(newEntity); + break; + case 'ip_adapter': + newEntity.id = getPrefixedId('ip_adapter'); + state.ipAdapters.entities.push(newEntity); + break; + case 'inpaint_mask': + newEntity.id = getPrefixedId('inpaint_mask'); + state.inpaintMasks.entities.push(newEntity); + break; + } + + state.selectedEntityIdentifier = getEntityIdentifier(newEntity); + }, entityIsEnabledToggled: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; const entity = selectEntity(state, entityIdentifier); @@ -460,6 +497,7 @@ export const { entityReset, entityIsEnabledToggled, entityMoved, + entityDuplicated, entityRasterized, entityBrushLineAdded, entityEraserLineAdded, From 51663c54399d8d048798f5093b2bae21a1cdbc81 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 24 Aug 2024 12:32:00 +1000 Subject: [PATCH 482/678] feat(ui): add knipignore tag I'm not ready to delete some things but still want to build the app. --- invokeai/frontend/web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index d0ed4ba1973..7693cab9e8c 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -24,7 +24,7 @@ "build": "pnpm run lint && vite build", "typegen": "node scripts/typegen.js", "preview": "vite preview", - "lint:knip": "knip", + "lint:knip": "knip --tags=-knipignore", "lint:dpdm": "dpdm --no-warning --no-tree --transform --exit-code circular:1 src/main.tsx", "lint:eslint": "eslint --max-warnings=0 .", "lint:prettier": "prettier --check .", From bc75b0259bba25ea541f154a25860daef211963d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 24 Aug 2024 12:32:38 +1000 Subject: [PATCH 483/678] fix(ui): lint & fix issues with adding regional ip adapters --- .../components/CanvasAddEntityButtons.tsx | 15 +++++------ ...onalGuidanceAddPromptsIPAdapterButtons.tsx | 7 ++--- ...uidanceMenuItemsAddPromptsAndIPAdapter.tsx | 7 ++--- .../hooks/useLayerControlAdapter.ts | 2 ++ .../features/controlLayers/konva/constants.ts | 5 ---- .../controlLayers/store/canvasV2Slice.ts | 2 +- .../controlLayers/store/regionsReducers.ts | 27 +++++++++++++------ 7 files changed, 32 insertions(+), 33 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx index b98c0e78d66..abb689983af 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx @@ -1,6 +1,5 @@ import { Button, ButtonGroup, Flex } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; -import { useDefaultControlAdapter, useDefaultIPAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter'; import { controlLayerAdded, inpaintMaskAdded, @@ -15,23 +14,21 @@ import { PiPlusBold } from 'react-icons/pi'; export const CanvasAddEntityButtons = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const defaultControlAdapter = useDefaultControlAdapter(); - const defaultIPAdapter = useDefaultIPAdapter(); const addInpaintMask = useCallback(() => { - dispatch(inpaintMaskAdded()); + dispatch(inpaintMaskAdded({ isSelected: true })); }, [dispatch]); const addRegionalGuidance = useCallback(() => { - dispatch(rgAdded()); + dispatch(rgAdded({ isSelected: true })); }, [dispatch]); const addRasterLayer = useCallback(() => { dispatch(rasterLayerAdded({ isSelected: true })); }, [dispatch]); const addControlLayer = useCallback(() => { - dispatch(controlLayerAdded({ isSelected: true, overrides: { controlAdapter: defaultControlAdapter } })); - }, [defaultControlAdapter, dispatch]); + dispatch(controlLayerAdded({ isSelected: true })); + }, [dispatch]); const addIPAdapter = useCallback(() => { - dispatch(ipaAdded({ ipAdapter: defaultIPAdapter })); - }, [defaultIPAdapter, dispatch]); + dispatch(ipaAdded({ isSelected: true })); + }, [dispatch]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons.tsx index 07104989c01..45333207145 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons.tsx @@ -1,8 +1,6 @@ import { Button, Flex } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useDefaultIPAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter'; -import { nanoid } from 'features/controlLayers/konva/util'; import { rgIPAdapterAdded, rgNegativePromptChanged, @@ -20,7 +18,6 @@ type AddPromptButtonProps = { export const RegionalGuidanceAddPromptsIPAdapterButtons = ({ id }: AddPromptButtonProps) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const defaultIPAdapter = useDefaultIPAdapter(); const selectValidActions = useMemo( () => createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { @@ -40,8 +37,8 @@ export const RegionalGuidanceAddPromptsIPAdapterButtons = ({ id }: AddPromptButt dispatch(rgNegativePromptChanged({ id, prompt: '' })); }, [dispatch, id]); const addIPAdapter = useCallback(() => { - dispatch(rgIPAdapterAdded({ id, ipAdapter: { ...defaultIPAdapter, id: nanoid() } })); - }, [defaultIPAdapter, dispatch, id]); + dispatch(rgIPAdapterAdded({ id })); + }, [dispatch, id]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx index bda94d91dee..5556c25929d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx @@ -2,8 +2,6 @@ import { MenuItem } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { useDefaultIPAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter'; -import { nanoid } from 'features/controlLayers/konva/util'; import { rgIPAdapterAdded, rgNegativePromptChanged, @@ -17,7 +15,6 @@ export const RegionalGuidanceMenuItemsAddPromptsAndIPAdapter = memo(() => { const { id } = useEntityIdentifierContext(); const { t } = useTranslation(); const dispatch = useAppDispatch(); - const defaultIPAdapter = useDefaultIPAdapter(); const selectValidActions = useMemo( () => createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { @@ -37,8 +34,8 @@ export const RegionalGuidanceMenuItemsAddPromptsAndIPAdapter = memo(() => { dispatch(rgNegativePromptChanged({ id: id, prompt: '' })); }, [dispatch, id]); const addIPAdapter = useCallback(() => { - dispatch(rgIPAdapterAdded({ id, ipAdapter: { ...defaultIPAdapter, id: nanoid() } })); - }, [defaultIPAdapter, dispatch, id]); + dispatch(rgIPAdapterAdded({ id })); + }, [dispatch, id]); return ( <> diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts index 9f876d5e562..827174c2df6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts @@ -27,6 +27,7 @@ export const useControlLayerControlAdapter = (entityIdentifier: CanvasEntityIden return controlAdapter; }; +/** @knipignore */ export const useDefaultControlAdapter = (): ControlNetConfig | T2IAdapterConfig => { const [modelConfigs] = useControlNetAndT2IAdapterModels(); @@ -47,6 +48,7 @@ export const useDefaultControlAdapter = (): ControlNetConfig | T2IAdapterConfig return defaultControlAdapter; }; +/** @knipignore */ export const useDefaultIPAdapter = (): IPAdapterConfig => { const [modelConfigs] = useIPAdapterModels(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts b/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts index 1af95a77fa8..398ff4b12e4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/constants.ts @@ -15,11 +15,6 @@ export const BRUSH_BORDER_INNER_COLOR = 'rgba(0,0,0,1)'; */ export const BRUSH_BORDER_OUTER_COLOR = 'rgba(255,255,255,0.8)'; -/** - * The border width for the brush preview. - */ -export const BRUSH_ERASER_BORDER_WIDTH = 1; - /** * The target spacing of individual points of brush strokes, as a percentage of the brush size. */ diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 702e42786bf..425e7529ae8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -175,7 +175,7 @@ function selectAllEntitiesOfType(state: CanvasV2State, type: CanvasEntityState[' } } -export function selectAllEntities(state: CanvasV2State): CanvasEntityState[] { +function selectAllEntities(state: CanvasV2State): CanvasEntityState[] { // These are in the same order as they are displayed in the list! return [ ...state.inpaintMasks.entities.toReversed(), diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index faf12dd3f7a..96d47395d68 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -1,4 +1,5 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import { deepClone } from 'common/util/deepClone'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { CanvasV2State, @@ -8,7 +9,7 @@ import type { RegionalGuidanceIPAdapterConfig, RgbColor, } from 'features/controlLayers/store/types'; -import { getEntityIdentifier, imageDTOToImageWithDims } from 'features/controlLayers/store/types'; +import { getEntityIdentifier, imageDTOToImageWithDims, initialIPAdapter } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { isEqual, merge } from 'lodash-es'; import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; @@ -134,13 +135,23 @@ export const regionsReducers = { } rg.autoNegative = !rg.autoNegative; }, - rgIPAdapterAdded: (state, action: PayloadAction<{ id: string; ipAdapter: RegionalGuidanceIPAdapterConfig }>) => { - const { id, ipAdapter } = action.payload; - const entity = selectRegionalGuidanceEntity(state, id); - if (!entity) { - return; - } - entity.ipAdapters.push(ipAdapter); + rgIPAdapterAdded: { + reducer: ( + state, + action: PayloadAction<{ id: string; ipAdapterId: string; overrides?: Partial }> + ) => { + const { id, overrides, ipAdapterId } = action.payload; + const entity = selectRegionalGuidanceEntity(state, id); + if (!entity) { + return; + } + const ipAdapter = { ...deepClone(initialIPAdapter), id: ipAdapterId }; + merge(ipAdapter, overrides); + entity.ipAdapters.push(ipAdapter); + }, + prepare: (payload: { id: string; overrides?: Partial }) => ({ + payload: { ...payload, ipAdapterId: getPrefixedId('regional_guidance_ip_adapter') }, + }), }, rgIPAdapterDeleted: (state, action: PayloadAction<{ id: string; ipAdapterId: string }>) => { const { id, ipAdapterId } = action.payload; From b91f79b0bb5b1a25870ac5446fb1a5f75ef49215 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 24 Aug 2024 12:36:25 +1000 Subject: [PATCH 484/678] chore: release v4.2.9.dev20240824 --- invokeai/version/invokeai_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/version/invokeai_version.py b/invokeai/version/invokeai_version.py index 6a6a202f164..00f96dbbc74 100644 --- a/invokeai/version/invokeai_version.py +++ b/invokeai/version/invokeai_version.py @@ -1 +1 @@ -__version__ = "4.2.9.dev20240823" +__version__ = "4.2.9.dev20240824" From a5e2e78dee76b791a4390a8683e81de4b3e06098 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 24 Aug 2024 14:46:58 +1000 Subject: [PATCH 485/678] feat(ui): add Result type & helpers Wrappers to capture errors and turn into results: - `withResult` wraps a sync function - `withResultAsync` wraps an async function Comments, tests. --- .../web/src/common/util/result.test.ts | 72 +++++++++++++++ .../frontend/web/src/common/util/result.ts | 89 +++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 invokeai/frontend/web/src/common/util/result.test.ts create mode 100644 invokeai/frontend/web/src/common/util/result.ts diff --git a/invokeai/frontend/web/src/common/util/result.test.ts b/invokeai/frontend/web/src/common/util/result.test.ts new file mode 100644 index 00000000000..1237a590244 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/result.test.ts @@ -0,0 +1,72 @@ +import type { Equals } from 'tsafe'; +import { assert } from 'tsafe'; +import { describe, expect, it } from 'vitest'; + +import type { ErrResult, OkResult } from './result'; +import { Err, isErr, isOk, Ok, withResult, withResultAsync } from './result'; // Adjust import as needed + +const promiseify = (fn: () => T): (() => Promise) => { + return () => + new Promise((resolve) => { + resolve(fn()); + }); +}; + +describe('Result Utility Functions', () => { + it('Ok() should create an OkResult', () => { + const result = Ok(42); + expect(result).toEqual({ type: 'Ok', value: 42 }); + expect(isOk(result)).toBe(true); + expect(isErr(result)).toBe(false); + assert, typeof result>>(result); + }); + + it('Err() should create an ErrResult', () => { + const error = new Error('Something went wrong'); + const result = Err(error); + expect(result).toEqual({ type: 'Err', error }); + expect(isOk(result)).toBe(false); + expect(isErr(result)).toBe(true); + assert, typeof result>>(result); + }); + + it('withResult() should return Ok on success', () => { + const fn = () => 42; + const result = withResult(fn); + expect(isOk(result)).toBe(true); + if (isOk(result)) { + expect(result.value).toBe(42); + } + }); + + it('withResult() should return Err on exception', () => { + const fn = () => { + throw new Error('Failure'); + }; + const result = withResult(fn); + expect(isErr(result)).toBe(true); + if (isErr(result)) { + expect(result.error.message).toBe('Failure'); + } + }); + + it('withResultAsync() should return Ok on success', async () => { + const fn = promiseify(() => 42); + const result = await withResultAsync(fn); + expect(isOk(result)).toBe(true); + if (isOk(result)) { + expect(result.value).toBe(42); + } + }); + + it('withResultAsync() should return Err on exception', async () => { + const fn = promiseify(() => { + throw new Error('Async failure'); + }); + const result = await withResultAsync(fn); + expect(isErr(result)).toBe(true); + if (isErr(result)) { + expect(result.error.message).toBe('Async failure'); + } + }); +}); diff --git a/invokeai/frontend/web/src/common/util/result.ts b/invokeai/frontend/web/src/common/util/result.ts new file mode 100644 index 00000000000..6a28e257be1 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/result.ts @@ -0,0 +1,89 @@ +/** + * Represents a successful result. + * @template T The type of the value. + */ +export type OkResult = { type: 'Ok'; value: T }; + +/** + * Represents a failed result. + * @template E The type of the error. + */ +export type ErrResult = { type: 'Err'; error: E }; + +/** + * A union type that represents either a successful result (`Ok`) or a failed result (`Err`). + * @template T The type of the value in the `Ok` case. + * @template E The type of the error in the `Err` case. + */ +export type Result = OkResult | ErrResult; + +/** + * Creates a successful result. + * @template T The type of the value. + * @param {T} value The value to wrap in an `Ok` result. + * @returns {OkResult} The `Ok` result containing the value. + */ +export function Ok(value: T): OkResult { + return { type: 'Ok', value }; +} + +/** + * Creates a failed result. + * @template E The type of the error. + * @param {E} error The error to wrap in an `Err` result. + * @returns {ErrResult} The `Err` result containing the error. + */ +export function Err(error: E): ErrResult { + return { type: 'Err', error }; +} + +/** + * Wraps a synchronous function in a try-catch block, returning a `Result`. + * @template T The type of the value returned by the function. + * @param {() => T} fn The function to execute. + * @returns {Result} An `Ok` result if the function succeeds, or an `Err` result if it throws an error. + */ +export function withResult(fn: () => T): Result { + try { + return Ok(fn()); + } catch (error) { + return Err(error instanceof Error ? error : new Error(String(error))); + } +} + +/** + * Wraps an asynchronous function in a try-catch block, returning a `Promise` of a `Result`. + * @template T The type of the value returned by the function. + * @param {() => Promise} fn The asynchronous function to execute. + * @returns {Promise>} A `Promise` resolving to an `Ok` result if the function succeeds, or an `Err` result if it throws an error. + */ +export async function withResultAsync(fn: () => Promise): Promise> { + try { + const result = await fn(); + return Ok(result); + } catch (error) { + return Err(error instanceof Error ? error : new Error(String(error))); + } +} + +/** + * Type guard to check if a `Result` is an `Ok` result. + * @template T The type of the value in the `Ok` result. + * @template E The type of the error in the `Err` result. + * @param {Result} result The result to check. + * @returns {result is OkResult} `true` if the result is an `Ok` result, otherwise `false`. + */ +export function isOk(result: Result): result is OkResult { + return result.type === 'Ok'; +} + +/** + * Type guard to check if a `Result` is an `Err` result. + * @template T The type of the value in the `Ok` result. + * @template E The type of the error in the `Err` result. + * @param {Result} result The result to check. + * @returns {result is ErrResult} `true` if the result is an `Err` result, otherwise `false`. + */ +export function isErr(result: Result): result is ErrResult { + return result.type === 'Err'; +} From 112d6ead91de30d2130e656822a2eac061059bbe Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 24 Aug 2024 14:48:18 +1000 Subject: [PATCH 486/678] fix(ui): graph building issue w/ controlnet --- .../src/features/nodes/util/graph/generation/buildSD1Graph.ts | 2 +- .../src/features/nodes/util/graph/generation/buildSDXLGraph.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index 237acfca13f..6c5ea865059 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -233,7 +233,7 @@ export const buildSD1Graph = async ( state.canvasV2.controlLayers.entities, g, state.canvasV2.bbox.rect, - controlNetCollector, + t2iAdapterCollector, modelConfig.base ); if (t2iAdapterResult.addedT2IAdapters > 0) { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index 6e8c8a7248b..112a401339a 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -236,7 +236,7 @@ export const buildSDXLGraph = async ( state.canvasV2.controlLayers.entities, g, state.canvasV2.bbox.rect, - controlNetCollector, + t2iAdapterCollector, modelConfig.base ); if (t2iAdapterResult.addedT2IAdapters > 0) { From 4b488de10ea208bc8bf4524ea6d40155dcc976b5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 24 Aug 2024 14:49:17 +1000 Subject: [PATCH 487/678] feat(ui): use new Result utils for enqueueing --- .../listeners/enqueueRequestedLinear.ts | 91 ++++++++++++------- .../graph/generation/addControlAdapters.ts | 4 +- 2 files changed, 60 insertions(+), 35 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index 5412ccf28f1..d2a065ec22a 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -1,6 +1,9 @@ import { logger } from 'app/logging/logger'; import { enqueueRequested } from 'app/store/actions'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +import type { SerializableObject } from 'common/types'; +import type { Result } from 'common/util/result'; +import { isErr, withResult, withResultAsync } from 'common/util/result'; import { $canvasManager } from 'features/controlLayers/konva/CanvasManager'; import { sessionStagingAreaReset, sessionStartedStaging } from 'features/controlLayers/store/canvasV2Slice'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; @@ -27,48 +30,70 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) assert(manager, 'No model found in state'); let didStartStaging = false; + if (!state.canvasV2.session.isStaging && state.canvasV2.session.mode === 'compose') { dispatch(sessionStartedStaging()); didStartStaging = true; } - try { - let g: Graph; - let noise: Invocation<'noise'>; - let posCond: Invocation<'compel' | 'sdxl_compel_prompt'>; - - assert(model, 'No model found in state'); - const base = model.base; - - if (base === 'sdxl') { - const result = await buildSDXLGraph(state, manager); - g = result.g; - noise = result.noise; - posCond = result.posCond; - } else if (base === 'sd-1' || base === 'sd-2') { - const result = await buildSD1Graph(state, manager); - g = result.g; - noise = result.noise; - posCond = result.posCond; - } else { - assert(false, `No graph builders for base ${base}`); - } - - const batchConfig = prepareLinearUIBatch(state, g, prepend, noise, posCond); - - const req = dispatch( - queueApi.endpoints.enqueueBatch.initiate(batchConfig, { - fixedCacheKey: 'enqueueBatch', - }) - ); - req.reset(); - await req.unwrap(); - } catch (error) { - log.error({ error: serializeError(error) }, 'Failed to enqueue batch'); + const abortStaging = () => { if (didStartStaging && getState().canvasV2.session.isStaging) { dispatch(sessionStagingAreaReset()); } + }; + + let buildGraphResult: Result< + { g: Graph; noise: Invocation<'noise'>; posCond: Invocation<'compel' | 'sdxl_compel_prompt'> }, + Error + >; + + assert(model, 'No model found in state'); + const base = model.base; + + switch (base) { + case 'sdxl': + buildGraphResult = await withResultAsync(() => buildSDXLGraph(state, manager)); + break; + case 'sd-1': + case `sd-2`: + buildGraphResult = await withResultAsync(() => buildSD1Graph(state, manager)); + break; + default: + assert(false, `No graph builders for base ${base}`); + } + + if (isErr(buildGraphResult)) { + log.error({ error: serializeError(buildGraphResult.error) }, 'Failed to build graph'); + abortStaging(); + return; } + + const { g, noise, posCond } = buildGraphResult.value; + + const prepareBatchResult = withResult(() => prepareLinearUIBatch(state, g, prepend, noise, posCond)); + + if (isErr(prepareBatchResult)) { + log.error({ error: serializeError(prepareBatchResult.error) }, 'Failed to prepare batch'); + abortStaging(); + return; + } + + const req = dispatch( + queueApi.endpoints.enqueueBatch.initiate(prepareBatchResult.value, { + fixedCacheKey: 'enqueueBatch', + }) + ); + req.reset(); + + const enqueueResult = await withResultAsync(() => req.unwrap()); + + if (isErr(enqueueResult)) { + log.error({ error: serializeError(enqueueResult.error) }, 'Failed to enqueue batch'); + abortStaging(); + return; + } + + log.debug({ batchConfig: prepareBatchResult.value } as SerializableObject, 'Enqueued batch'); }, }); }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts index 8b3630574bb..a178fe2369b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlAdapters.ts @@ -36,7 +36,7 @@ export const addControlNets = async ( const adapter = manager.adapters.controlLayers.get(layer.id); assert(adapter, 'Adapter not found'); const imageDTO = await adapter.renderer.rasterize({ rect: bbox, attrs: { opacity: 1, filters: [] } }); - await addControlNetToGraph(g, layer, imageDTO, collector); + addControlNetToGraph(g, layer, imageDTO, collector); } return result; @@ -69,7 +69,7 @@ export const addT2IAdapters = async ( const adapter = manager.adapters.controlLayers.get(layer.id); assert(adapter, 'Adapter not found'); const imageDTO = await adapter.renderer.rasterize({ rect: bbox, attrs: { opacity: 1, filters: [] } }); - await addT2IAdapterToGraph(g, layer, imageDTO, collector); + addT2IAdapterToGraph(g, layer, imageDTO, collector); } return result; From 839248c74c59483836c90f10f3e009024de22387 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 24 Aug 2024 15:03:43 +1000 Subject: [PATCH 488/678] chore: release v4.2.9.dev3 Instead of using dates, just going to increment. --- invokeai/version/invokeai_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/version/invokeai_version.py b/invokeai/version/invokeai_version.py index 00f96dbbc74..b91bbf2bf67 100644 --- a/invokeai/version/invokeai_version.py +++ b/invokeai/version/invokeai_version.py @@ -1 +1 @@ -__version__ = "4.2.9.dev20240824" +__version__ = "4.2.9.dev3" From af638cf5cec14c5a6b18c7c9137f30855e419222 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 24 Aug 2024 18:35:30 +1000 Subject: [PATCH 489/678] fix(ui): missing vae precision in graph builders --- .../util/graph/generation/addImageToImage.ts | 9 +++++---- .../nodes/util/graph/generation/addInpaint.ts | 16 ++++++++++------ .../nodes/util/graph/generation/addOutpaint.ts | 11 +++++------ .../nodes/util/graph/generation/buildSD1Graph.ts | 11 ++++++----- .../util/graph/generation/buildSDXLGraph.ts | 11 ++++++----- 5 files changed, 32 insertions(+), 26 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts index 9bcd148acf6..5c72b548b07 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts @@ -14,7 +14,8 @@ export const addImageToImage = async ( originalSize: Dimensions, scaledSize: Dimensions, bbox: CanvasV2State['bbox'], - denoising_start: number + denoising_start: number, + fp32: boolean ): Promise> => { denoise.denoising_start = denoising_start; @@ -28,7 +29,7 @@ export const addImageToImage = async ( image: { image_name }, ...scaledSize, }); - const i2l = g.addNode({ id: 'i2l', type: 'i2l' }); + const i2l = g.addNode({ id: 'i2l', type: 'i2l', fp32 }); const resizeImageToOriginalSize = g.addNode({ type: 'img_resize', id: getPrefixedId('initial_image_resize_out'), @@ -43,8 +44,8 @@ export const addImageToImage = async ( // This is the new output node return resizeImageToOriginalSize; } else { - // No need to resize, just denoise - const i2l = g.addNode({ id: 'i2l', type: 'i2l', image: { image_name } }); + // No need to resize, just decode + const i2l = g.addNode({ id: 'i2l', type: 'i2l', image: { image_name }, fp32 }); g.addEdge(vaeSource, 'vae', i2l, 'vae'); g.addEdge(i2l, 'latents', denoise, 'latents'); return l2i; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts index 35f512a1c97..5092deb171d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts @@ -3,7 +3,6 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { CanvasV2State, Dimensions } from 'features/controlLayers/store/types'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; -import type { ParameterPrecision } from 'features/parameters/types/parameterSchemas'; import { isEqual } from 'lodash-es'; import type { Invocation } from 'services/api/types'; @@ -20,7 +19,7 @@ export const addInpaint = async ( bbox: CanvasV2State['bbox'], compositing: CanvasV2State['compositing'], denoising_start: number, - vaePrecision: ParameterPrecision + fp32: boolean ): Promise> => { denoise.denoising_start = denoising_start; @@ -30,7 +29,7 @@ export const addInpaint = async ( if (!isEqual(scaledSize, originalSize)) { // Scale before processing requires some resizing - const i2l = g.addNode({ id: getPrefixedId('i2l'), type: 'i2l' }); + const i2l = g.addNode({ id: getPrefixedId('i2l'), type: 'i2l', fp32 }); const resizeImageToScaledSize = g.addNode({ type: 'img_resize', id: getPrefixedId('resize_image_to_scaled_size'), @@ -64,7 +63,7 @@ export const addInpaint = async ( coherence_mode: compositing.canvasCoherenceMode, minimum_denoise: compositing.canvasCoherenceMinDenoise, edge_radius: compositing.canvasCoherenceEdgeSize, - fp32: vaePrecision === 'fp32', + fp32, }); const canvasPasteBack = g.addNode({ id: getPrefixedId('canvas_v2_mask_and_crop'), @@ -100,7 +99,12 @@ export const addInpaint = async ( return canvasPasteBack; } else { // No scale before processing, much simpler - const i2l = g.addNode({ id: getPrefixedId('i2l'), type: 'i2l', image: { image_name: initialImage.image_name } }); + const i2l = g.addNode({ + id: getPrefixedId('i2l'), + type: 'i2l', + image: { image_name: initialImage.image_name }, + fp32, + }); const alphaToMask = g.addNode({ id: getPrefixedId('alpha_to_mask'), type: 'tomask', @@ -113,7 +117,7 @@ export const addInpaint = async ( coherence_mode: compositing.canvasCoherenceMode, minimum_denoise: compositing.canvasCoherenceMinDenoise, edge_radius: compositing.canvasCoherenceEdgeSize, - fp32: vaePrecision === 'fp32', + fp32, image: { image_name: initialImage.image_name }, }); const canvasPasteBack = g.addNode({ diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index 55fd2eac4ea..ef999ef2cfe 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts @@ -4,7 +4,6 @@ import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { CanvasV2State, Dimensions } from 'features/controlLayers/store/types'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { getInfill } from 'features/nodes/util/graph/graphBuilderUtils'; -import type { ParameterPrecision } from 'features/parameters/types/parameterSchemas'; import { isEqual } from 'lodash-es'; import type { Invocation } from 'services/api/types'; @@ -21,7 +20,7 @@ export const addOutpaint = async ( bbox: CanvasV2State['bbox'], compositing: CanvasV2State['compositing'], denoising_start: number, - vaePrecision: ParameterPrecision + fp32: boolean ): Promise> => { denoise.denoising_start = denoising_start; @@ -76,7 +75,7 @@ export const addOutpaint = async ( coherence_mode: compositing.canvasCoherenceMode, minimum_denoise: compositing.canvasCoherenceMinDenoise, edge_radius: compositing.canvasCoherenceEdgeSize, - fp32: vaePrecision === 'fp32', + fp32, }); g.addEdge(infill, 'image', createGradientMask, 'image'); g.addEdge(resizeInputMaskToScaledSize, 'image', createGradientMask, 'mask'); @@ -85,7 +84,7 @@ export const addOutpaint = async ( g.addEdge(createGradientMask, 'denoise_mask', denoise, 'denoise_mask'); // Decode infilled image and connect to denoise - const i2l = g.addNode({ id: getPrefixedId('i2l'), type: 'i2l' }); + const i2l = g.addNode({ id: getPrefixedId('i2l'), type: 'i2l', fp32 }); g.addEdge(infill, 'image', i2l, 'image'); g.addEdge(vaeSource, 'vae', i2l, 'vae'); g.addEdge(i2l, 'latents', denoise, 'latents'); @@ -125,7 +124,7 @@ export const addOutpaint = async ( } else { infill.image = { image_name: initialImage.image_name }; // No scale before processing, much simpler - const i2l = g.addNode({ id: getPrefixedId('i2l'), type: 'i2l' }); + const i2l = g.addNode({ id: getPrefixedId('i2l'), type: 'i2l', fp32 }); const maskAlphaToMask = g.addNode({ id: getPrefixedId('mask_alpha_to_mask'), type: 'tomask', @@ -147,7 +146,7 @@ export const addOutpaint = async ( coherence_mode: compositing.canvasCoherenceMode, minimum_denoise: compositing.canvasCoherenceMinDenoise, edge_radius: compositing.canvasCoherenceEdgeSize, - fp32: vaePrecision === 'fp32', + fp32, image: { image_name: initialImage.image_name }, }); const canvasPasteBack = g.addNode({ diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index 6c5ea865059..6182de8ee02 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -48,8 +48,8 @@ export const buildSD1Graph = async ( assert(model, 'No model found in state'); + const fp32 = vaePrecision === 'fp32'; const { positivePrompt, negativePrompt } = getPresetModifiedPrompts(state); - const { originalSize, scaledSize } = getSizes(bbox); const g = new Graph(getPrefixedId('sd1_graph')); @@ -102,7 +102,7 @@ export const buildSD1Graph = async ( const l2i = g.addNode({ type: 'l2i', id: getPrefixedId('l2i'), - fp32: vaePrecision === 'fp32', + fp32, }); const vaeLoader = vae?.base === model.base @@ -168,7 +168,8 @@ export const buildSD1Graph = async ( originalSize, scaledSize, bbox, - 1 - params.img2imgStrength + 1 - params.img2imgStrength, + vaePrecision === 'fp32' ); } else if (generationMode === 'inpaint') { const { compositing } = state.canvasV2; @@ -185,7 +186,7 @@ export const buildSD1Graph = async ( bbox, compositing, 1 - params.img2imgStrength, - vaePrecision + vaePrecision === 'fp32' ); } else if (generationMode === 'outpaint') { const { compositing } = state.canvasV2; @@ -202,7 +203,7 @@ export const buildSD1Graph = async ( bbox, compositing, 1 - params.img2imgStrength, - vaePrecision + fp32 ); } diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index 112a401339a..184be31fd6c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -49,8 +49,8 @@ export const buildSDXLGraph = async ( assert(model, 'No model found in state'); + const fp32 = vaePrecision === 'fp32'; const { originalSize, scaledSize } = getSizes(bbox); - const { positivePrompt, negativePrompt, positiveStylePrompt, negativeStylePrompt } = getPresetModifiedPrompts(state); const g = new Graph(getPrefixedId('sdxl_graph')); @@ -100,7 +100,7 @@ export const buildSDXLGraph = async ( const l2i = g.addNode({ type: 'l2i', id: getPrefixedId('l2i'), - fp32: vaePrecision === 'fp32', + fp32, }); const vaeLoader = vae?.base === model.base @@ -171,7 +171,8 @@ export const buildSDXLGraph = async ( originalSize, scaledSize, bbox, - refinerModel ? Math.min(refinerStart, 1 - params.img2imgStrength) : 1 - params.img2imgStrength + refinerModel ? Math.min(refinerStart, 1 - params.img2imgStrength) : 1 - params.img2imgStrength, + fp32 ); } else if (generationMode === 'inpaint') { const { compositing } = state.canvasV2; @@ -188,7 +189,7 @@ export const buildSDXLGraph = async ( bbox, compositing, refinerModel ? Math.min(refinerStart, 1 - params.img2imgStrength) : 1 - params.img2imgStrength, - vaePrecision + fp32 ); } else if (generationMode === 'outpaint') { const { compositing } = state.canvasV2; @@ -205,7 +206,7 @@ export const buildSDXLGraph = async ( bbox, compositing, refinerModel ? Math.min(refinerStart, 1 - params.img2imgStrength) : 1 - params.img2imgStrength, - vaePrecision + fp32 ); } From 8a56702341aac89b60740fc652c6ea8b0cb7e880 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 24 Aug 2024 19:49:43 +1000 Subject: [PATCH 490/678] chore(ui): bump @invoke-ai/ui-library --- invokeai/frontend/web/package.json | 2 +- invokeai/frontend/web/pnpm-lock.yaml | 203 ++++++++++++++------------- 2 files changed, 108 insertions(+), 97 deletions(-) diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 7693cab9e8c..4fdb81ea676 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -58,7 +58,7 @@ "@dnd-kit/sortable": "^8.0.0", "@dnd-kit/utilities": "^3.2.2", "@fontsource-variable/inter": "^5.0.20", - "@invoke-ai/ui-library": "^0.0.31", + "@invoke-ai/ui-library": "^0.0.32", "@nanostores/react": "^0.7.3", "@reduxjs/toolkit": "2.2.3", "@roarr/browser-log-writer": "^1.3.0", diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index 4b2f0f9d6fa..a487b8d6f33 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -24,8 +24,8 @@ dependencies: specifier: ^5.0.20 version: 5.0.20 '@invoke-ai/ui-library': - specifier: ^0.0.31 - version: 0.0.31(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.20)(@types/react@18.3.3)(i18next@23.12.2)(react-dom@18.3.1)(react@18.3.1) + specifier: ^0.0.32 + version: 0.0.32(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.20)(@types/react@18.3.3)(i18next@23.12.2)(react-dom@18.3.1)(react@18.3.1) '@nanostores/react': specifier: ^0.7.3 version: 0.7.3(nanostores@0.11.2)(react@18.3.1) @@ -40,7 +40,7 @@ dependencies: version: 0.5.0 chakra-react-select: specifier: ^4.9.1 - version: 4.9.1(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.13.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + version: 4.9.1(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.13.3)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) compare-versions: specifier: ^6.1.1 version: 6.1.1 @@ -1752,6 +1752,13 @@ packages: dependencies: regenerator-runtime: 0.14.1 + /@babel/runtime@7.25.4: + resolution: {integrity: sha512-DSgLeL/FNcpXuzav5wfYvHCGvynXkJbn3Zvc3823AEe9nPwW9IK4UoCSS5yGymmQzN0pCPvivtgS6/8U2kkm1w==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.1 + dev: false + /@babel/template@7.24.0: resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==} engines: {node: '>=6.9.0'} @@ -1838,7 +1845,7 @@ packages: '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.3.1) '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) '@chakra-ui/transition': 2.1.0(framer-motion@10.18.0)(react@18.3.1) framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) react: 18.3.1 @@ -1854,7 +1861,7 @@ packages: '@chakra-ui/react-context': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false @@ -1872,7 +1879,7 @@ packages: '@chakra-ui/react-children-utils': 2.0.6(react@18.3.1) '@chakra-ui/react-context': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false @@ -1885,7 +1892,7 @@ packages: '@chakra-ui/react-children-utils': 2.0.6(react@18.3.1) '@chakra-ui/react-context': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false @@ -1905,7 +1912,7 @@ packages: '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false @@ -1916,7 +1923,7 @@ packages: react: '>=18' dependencies: '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false @@ -1935,7 +1942,7 @@ packages: '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) '@chakra-ui/visually-hidden': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) '@zag-js/focus-visible': 0.16.0 react: 18.3.1 @@ -1958,7 +1965,7 @@ packages: react: '>=18' dependencies: '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false @@ -1977,7 +1984,7 @@ packages: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false @@ -1992,13 +1999,13 @@ packages: react: 18.3.1 dev: false - /@chakra-ui/css-reset@2.3.0(@emotion/react@11.13.0)(react@18.3.1): + /@chakra-ui/css-reset@2.3.0(@emotion/react@11.13.3)(react@18.3.1): resolution: {integrity: sha512-cQwwBy5O0jzvl0K7PLTLgp8ijqLPKyuEMiDXwYzl95seD3AoeuoCLyzZcJtVqaUZ573PiBdAbY/IlZcwDOItWg==} peerDependencies: '@emotion/react': '>=10.0.35' react: '>=18' dependencies: - '@emotion/react': 11.13.0(@types/react@18.3.3)(react@18.3.1) + '@emotion/react': 11.13.3(@types/react@18.3.3)(react@18.3.1) react: 18.3.1 dev: false @@ -2031,7 +2038,7 @@ packages: '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false @@ -2062,7 +2069,7 @@ packages: '@chakra-ui/react-types': 2.0.7(react@18.3.1) '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false @@ -2085,7 +2092,7 @@ packages: react: '>=18' dependencies: '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false @@ -2096,7 +2103,7 @@ packages: react: '>=18' dependencies: '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false @@ -2108,7 +2115,7 @@ packages: dependencies: '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false @@ -2123,7 +2130,7 @@ packages: '@chakra-ui/react-children-utils': 2.0.6(react@18.3.1) '@chakra-ui/react-context': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false @@ -2139,7 +2146,7 @@ packages: '@chakra-ui/react-children-utils': 2.0.6(react@18.3.1) '@chakra-ui/react-context': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false @@ -2164,7 +2171,7 @@ packages: '@chakra-ui/breakpoint-utils': 2.0.8 '@chakra-ui/react-env': 3.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false @@ -2189,7 +2196,7 @@ packages: '@chakra-ui/react-use-outside-click': 2.2.0(react@18.3.1) '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) '@chakra-ui/transition': 2.1.0(framer-motion@10.18.0)(react@18.3.1) framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) react: 18.3.1 @@ -2216,7 +2223,7 @@ packages: '@chakra-ui/react-use-outside-click': 2.2.0(react@18.3.1) '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) '@chakra-ui/transition': 2.1.0(framer-motion@11.3.24)(react@18.3.1) framer-motion: 11.3.24(react-dom@18.3.1)(react@18.3.1) react: 18.3.1 @@ -2237,7 +2244,7 @@ packages: '@chakra-ui/react-types': 2.0.7(react@18.3.1) '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) '@chakra-ui/transition': 2.1.0(framer-motion@10.18.0)(react@18.3.1) aria-hidden: 1.2.4 framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) @@ -2266,7 +2273,7 @@ packages: '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false @@ -2290,7 +2297,7 @@ packages: '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.3.1) '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false @@ -2312,7 +2319,7 @@ packages: '@chakra-ui/react-use-focus-on-pointer-down': 2.1.0(react@18.3.1) '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) react: 18.3.1 dev: false @@ -2347,11 +2354,11 @@ packages: react: '>=18' dependencies: '@chakra-ui/react-context': 2.1.0(react@18.3.1) - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false - /@chakra-ui/provider@2.4.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react-dom@18.3.1)(react@18.3.1): + /@chakra-ui/provider@2.4.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-w0Tef5ZCJK1mlJorcSjItCSbyvVuqpvyWdxZiVQmE6fvSJR83wZof42ux0+sfWD+I7rHSfj+f9nzhNaEWClysw==} peerDependencies: '@emotion/react': ^11.0.0 @@ -2359,13 +2366,13 @@ packages: react: '>=18' react-dom: '>=18' dependencies: - '@chakra-ui/css-reset': 2.3.0(@emotion/react@11.13.0)(react@18.3.1) + '@chakra-ui/css-reset': 2.3.0(@emotion/react@11.13.3)(react@18.3.1) '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) '@chakra-ui/react-env': 3.1.0(react@18.3.1) - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) '@chakra-ui/utils': 2.0.15 - '@emotion/react': 11.13.0(@types/react@18.3.3)(react@18.3.1) - '@emotion/styled': 11.13.0(@emotion/react@11.13.0)(@types/react@18.3.3)(react@18.3.1) + '@emotion/react': 11.13.3(@types/react@18.3.3)(react@18.3.1) + '@emotion/styled': 11.13.0(@emotion/react@11.13.3)(@types/react@18.3.3)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) dev: false @@ -2381,7 +2388,7 @@ packages: '@chakra-ui/react-types': 2.0.7(react@18.3.1) '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) '@zag-js/focus-visible': 0.16.0 react: 18.3.1 dev: false @@ -2581,7 +2588,7 @@ packages: react: 18.3.1 dev: false - /@chakra-ui/react@2.8.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(@types/react@18.3.3)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1): + /@chakra-ui/react@2.8.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(@types/react@18.3.3)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-Hn0moyxxyCDKuR9ywYpqgX8dvjqwu9ArwpIb9wHNYjnODETjLwazgNIliCVBRcJvysGRiV51U2/JtJVrpeCjUQ==} peerDependencies: '@emotion/react': ^11.0.0 @@ -2600,7 +2607,7 @@ packages: '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/control-box': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/counter': 2.1.0(react@18.3.1) - '@chakra-ui/css-reset': 2.3.0(@emotion/react@11.13.0)(react@18.3.1) + '@chakra-ui/css-reset': 2.3.0(@emotion/react@11.13.3)(react@18.3.1) '@chakra-ui/editable': 3.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/focus-lock': 2.1.0(@types/react@18.3.3)(react@18.3.1) '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) @@ -2619,7 +2626,7 @@ packages: '@chakra-ui/popper': 3.1.0(react@18.3.1) '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) '@chakra-ui/progress': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/provider': 2.4.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/provider': 2.4.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react-dom@18.3.1)(react@18.3.1) '@chakra-ui/radio': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/react-env': 3.1.0(react@18.3.1) '@chakra-ui/select': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) @@ -2631,7 +2638,7 @@ packages: '@chakra-ui/stepper': 2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/styled-system': 2.9.2 '@chakra-ui/switch': 2.1.2(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.3.1) - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) '@chakra-ui/table': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/tabs': 3.0.0(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/tag': 3.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) @@ -2643,8 +2650,8 @@ packages: '@chakra-ui/transition': 2.1.0(framer-motion@10.18.0)(react@18.3.1) '@chakra-ui/utils': 2.0.15 '@chakra-ui/visually-hidden': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@emotion/react': 11.13.0(@types/react@18.3.3)(react@18.3.1) - '@emotion/styled': 11.13.0(@emotion/react@11.13.0)(@types/react@18.3.3)(react@18.3.1) + '@emotion/react': 11.13.3(@types/react@18.3.3)(react@18.3.1) + '@emotion/styled': 11.13.0(@emotion/react@11.13.3)(@types/react@18.3.3)(react@18.3.1) framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -2660,7 +2667,7 @@ packages: dependencies: '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false @@ -2677,7 +2684,7 @@ packages: '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/react-use-previous': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false @@ -2687,7 +2694,7 @@ packages: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false @@ -2707,7 +2714,7 @@ packages: '@chakra-ui/react-use-pan-event': 2.1.0(react@18.3.1) '@chakra-ui/react-use-size': 2.1.0(react@18.3.1) '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false @@ -2718,7 +2725,7 @@ packages: react: '>=18' dependencies: '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false @@ -2731,7 +2738,7 @@ packages: '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/react-context': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false @@ -2744,7 +2751,7 @@ packages: '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/react-context': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false @@ -2765,12 +2772,12 @@ packages: dependencies: '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) react: 18.3.1 dev: false - /@chakra-ui/system@2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1): + /@chakra-ui/system@2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1): resolution: {integrity: sha512-EGtpoEjLrUu4W1fHD+a62XR+hzC5YfsWm+6lO0Kybcga3yYEij9beegO0jZgug27V+Rf7vns95VPVP6mFd/DEQ==} peerDependencies: '@emotion/react': ^11.0.0 @@ -2783,8 +2790,8 @@ packages: '@chakra-ui/styled-system': 2.9.2 '@chakra-ui/theme-utils': 2.0.21 '@chakra-ui/utils': 2.0.15 - '@emotion/react': 11.13.0(@types/react@18.3.3)(react@18.3.1) - '@emotion/styled': 11.13.0(@emotion/react@11.13.0)(@types/react@18.3.3)(react@18.3.1) + '@emotion/react': 11.13.3(@types/react@18.3.3)(react@18.3.1) + '@emotion/styled': 11.13.0(@emotion/react@11.13.3)(@types/react@18.3.3)(react@18.3.1) react: 18.3.1 react-fast-compare: 3.2.2 dev: false @@ -2797,7 +2804,7 @@ packages: dependencies: '@chakra-ui/react-context': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false @@ -2816,7 +2823,7 @@ packages: '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false @@ -2828,7 +2835,7 @@ packages: dependencies: '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/react-context': 2.1.0(react@18.3.1) - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false @@ -2840,7 +2847,7 @@ packages: dependencies: '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false @@ -2891,7 +2898,7 @@ packages: '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 '@chakra-ui/styled-system': 2.9.2 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) '@chakra-ui/theme': 3.3.1(@chakra-ui/styled-system@2.9.2) framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) react: 18.3.1 @@ -2914,7 +2921,7 @@ packages: '@chakra-ui/react-use-event-listener': 2.1.0(react@18.3.1) '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -2957,7 +2964,7 @@ packages: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) react: 18.3.1 dev: false @@ -3040,10 +3047,10 @@ packages: resolution: {integrity: sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw==} dependencies: '@babel/helper-module-imports': 7.24.7 - '@babel/runtime': 7.25.0 + '@babel/runtime': 7.25.4 '@emotion/hash': 0.9.2 '@emotion/memoize': 0.9.0 - '@emotion/serialize': 1.3.0 + '@emotion/serialize': 1.3.1 babel-plugin-macros: 3.1.0 convert-source-map: 1.9.0 escape-string-regexp: 4.0.0 @@ -3131,8 +3138,8 @@ packages: react: 18.3.1 dev: false - /@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1): - resolution: {integrity: sha512-WkL+bw1REC2VNV1goQyfxjx1GYJkcc23CRQkXX+vZNLINyfI7o+uUn/rTGPt/xJ3bJHd5GcljgnxHf4wRw5VWQ==} + /@emotion/react@11.13.3(@types/react@18.3.3)(react@18.3.1): + resolution: {integrity: sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg==} peerDependencies: '@types/react': '*' react: '>=16.8.0' @@ -3140,10 +3147,10 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.25.0 + '@babel/runtime': 7.25.4 '@emotion/babel-plugin': 11.12.0 '@emotion/cache': 11.13.1 - '@emotion/serialize': 1.3.0 + '@emotion/serialize': 1.3.1 '@emotion/use-insertion-effect-with-fallbacks': 1.1.0(react@18.3.1) '@emotion/utils': 1.4.0 '@emotion/weak-memoize': 0.4.0 @@ -3164,12 +3171,12 @@ packages: csstype: 3.1.3 dev: false - /@emotion/serialize@1.3.0: - resolution: {integrity: sha512-jACuBa9SlYajnpIVXB+XOXnfJHyckDfe6fOpORIM6yhBDlqGuExvDdZYHDQGoDf3bZXGv7tNr+LpLjJqiEQ6EA==} + /@emotion/serialize@1.3.1: + resolution: {integrity: sha512-dEPNKzBPU+vFPGa+z3axPRn8XVDetYORmDC0wAiej+TNcOZE70ZMJa0X7JdeoM6q/nWTMZeLpN/fTnD9o8MQBA==} dependencies: '@emotion/hash': 0.9.2 '@emotion/memoize': 0.9.0 - '@emotion/unitless': 0.9.0 + '@emotion/unitless': 0.10.0 '@emotion/utils': 1.4.0 csstype: 3.1.3 dev: false @@ -3182,7 +3189,7 @@ packages: resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} dev: false - /@emotion/styled@11.13.0(@emotion/react@11.13.0)(@types/react@18.3.3)(react@18.3.1): + /@emotion/styled@11.13.0(@emotion/react@11.13.3)(@types/react@18.3.3)(react@18.3.1): resolution: {integrity: sha512-tkzkY7nQhW/zC4hztlwucpT8QEZ6eUzpXDRhww/Eej4tFfO0FxQYWRyg/c5CCXa4d/f174kqeXYjuQRnhzf6dA==} peerDependencies: '@emotion/react': ^11.0.0-rc.0 @@ -3192,11 +3199,11 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.25.0 + '@babel/runtime': 7.25.4 '@emotion/babel-plugin': 11.12.0 '@emotion/is-prop-valid': 1.3.0 - '@emotion/react': 11.13.0(@types/react@18.3.3)(react@18.3.1) - '@emotion/serialize': 1.3.0 + '@emotion/react': 11.13.3(@types/react@18.3.3)(react@18.3.1) + '@emotion/serialize': 1.3.1 '@emotion/use-insertion-effect-with-fallbacks': 1.1.0(react@18.3.1) '@emotion/utils': 1.4.0 '@types/react': 18.3.3 @@ -3205,12 +3212,12 @@ packages: - supports-color dev: false - /@emotion/unitless@0.8.1: - resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==} + /@emotion/unitless@0.10.0: + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} dev: false - /@emotion/unitless@0.9.0: - resolution: {integrity: sha512-TP6GgNZtmtFaFcsOgExdnfxLLpRDla4Q66tnenA9CktvVSdNKDvMVuUah4QvWPIpNjrWsGg3qeGo9a43QooGZQ==} + /@emotion/unitless@0.8.1: + resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==} dev: false /@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@18.3.1): @@ -3564,8 +3571,8 @@ packages: prettier: 3.3.3 dev: true - /@invoke-ai/ui-library@0.0.31(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.20)(@types/react@18.3.3)(i18next@23.12.2)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-7LtOUN/bcGHc8jCRd2m22DvP2eeogqwM/shdXQpLH5RY2FzWJNXlWdVT4hIPGDu7znnk3xvXlZvo6tiGSjbnCQ==} + /@invoke-ai/ui-library@0.0.32(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.20)(@types/react@18.3.3)(i18next@23.12.2)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-JxAoblrDu/cZ4ha9KO4ry5OWvyLUE1Dj28i+ciMaDNUpC/cN+IyiTbUBoFoPaoN5JP8Zpd/MYCcmF2qsziHDzg==} peerDependencies: '@fontsource-variable/inter': ^5.0.16 react: ^18.2.0 @@ -3575,14 +3582,14 @@ packages: '@chakra-ui/icons': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) - '@chakra-ui/react': 2.8.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(@types/react@18.3.3)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/react': 2.8.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(@types/react@18.3.3)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1) '@chakra-ui/styled-system': 2.9.2 '@chakra-ui/theme-tools': 2.1.2(@chakra-ui/styled-system@2.9.2) - '@emotion/react': 11.13.0(@types/react@18.3.3)(react@18.3.1) - '@emotion/styled': 11.13.0(@emotion/react@11.13.0)(@types/react@18.3.3)(react@18.3.1) + '@emotion/react': 11.13.3(@types/react@18.3.3)(react@18.3.1) + '@emotion/styled': 11.13.0(@emotion/react@11.13.3)(@types/react@18.3.3)(react@18.3.1) '@fontsource-variable/inter': 5.0.20 '@nanostores/react': 0.7.3(nanostores@0.11.2)(react@18.3.1) - chakra-react-select: 4.9.1(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.13.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + chakra-react-select: 4.9.1(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.13.3)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) lodash-es: 4.17.21 nanostores: 0.11.2 @@ -5774,7 +5781,7 @@ packages: resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} engines: {node: '>=10'} dependencies: - tslib: 2.6.3 + tslib: 2.7.0 dev: false /aria-query@5.3.0: @@ -6126,7 +6133,7 @@ packages: type-detect: 4.0.8 dev: true - /chakra-react-select@4.9.1(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.13.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1): + /chakra-react-select@4.9.1(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.13.3)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-jmgfN+S/wnTaCp3pW30GYDIZ5J8jWcT1gIbhpw6RdKV+atm/U4/sT+gaHOHHhRL8xeaYip+iI/m8MPGREkve0w==} peerDependencies: '@chakra-ui/form-control': ^2.0.0 @@ -6146,8 +6153,8 @@ packages: '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/menu': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.3.24)(react@18.3.1) '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/system': 2.6.2(@emotion/react@11.13.0)(@emotion/styled@11.13.0)(react@18.3.1) - '@emotion/react': 11.13.0(@types/react@18.3.3)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(react@18.3.1) + '@emotion/react': 11.13.3(@types/react@18.3.3)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-select: 5.8.0(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) @@ -7572,7 +7579,7 @@ packages: resolution: {integrity: sha512-QFaHbhv9WPUeLYBDe/PAuLKJ4Dd9OPvKs9xZBr3yLXnUrDNaVXKu2baDBXe3naPY30hgHYSsf2JW4jzas2mDEQ==} engines: {node: '>=10'} dependencies: - tslib: 2.6.3 + tslib: 2.7.0 dev: false /for-each@0.3.3: @@ -7611,7 +7618,7 @@ packages: dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - tslib: 2.6.3 + tslib: 2.7.0 optionalDependencies: '@emotion/is-prop-valid': 0.8.8 dev: false @@ -9619,7 +9626,7 @@ packages: peerDependencies: react: ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 dependencies: - '@babel/runtime': 7.25.0 + '@babel/runtime': 7.25.4 react: 18.3.1 dev: false @@ -9714,7 +9721,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.25.0 + '@babel/runtime': 7.25.4 '@types/react': 18.3.3 focus-lock: 1.3.5 prop-types: 15.8.1 @@ -9776,7 +9783,7 @@ packages: react-native: optional: true dependencies: - '@babel/runtime': 7.25.0 + '@babel/runtime': 7.25.4 html-parse-stringify: 3.0.1 i18next: 23.12.2 react: 18.3.1 @@ -9838,7 +9845,7 @@ packages: '@types/react': 18.3.3 react: 18.3.1 react-style-singleton: 2.2.1(@types/react@18.3.3)(react@18.3.1) - tslib: 2.6.3 + tslib: 2.7.0 dev: false /react-remove-scroll@2.5.10(@types/react@18.3.3)(react@18.3.1): @@ -9855,7 +9862,7 @@ packages: react: 18.3.1 react-remove-scroll-bar: 2.3.6(@types/react@18.3.3)(react@18.3.1) react-style-singleton: 2.2.1(@types/react@18.3.3)(react@18.3.1) - tslib: 2.6.3 + tslib: 2.7.0 use-callback-ref: 1.3.2(@types/react@18.3.3)(react@18.3.1) use-sidecar: 1.1.2(@types/react@18.3.3)(react@18.3.1) dev: false @@ -9905,7 +9912,7 @@ packages: get-nonce: 1.0.1 invariant: 2.2.4 react: 18.3.1 - tslib: 2.6.3 + tslib: 2.7.0 dev: false /react-transition-group@4.4.5(react-dom@18.3.1)(react@18.3.1): @@ -11045,6 +11052,10 @@ packages: /tslib@2.6.3: resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} + /tslib@2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + dev: false + /tsutils@3.21.0(typescript@5.5.4): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} @@ -11292,7 +11303,7 @@ packages: dependencies: '@types/react': 18.3.3 react: 18.3.1 - tslib: 2.6.3 + tslib: 2.7.0 dev: false /use-debounce@10.0.2(react@18.3.1): @@ -11338,7 +11349,7 @@ packages: '@types/react': 18.3.3 detect-node-es: 1.1.0 react: 18.3.1 - tslib: 2.6.3 + tslib: 2.7.0 dev: false /use-sync-external-store@1.2.0(react@18.3.1): From 8d0a75cb5f10199b91822afab26d74e9d60a55e4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 24 Aug 2024 19:50:36 +1000 Subject: [PATCH 491/678] feat(ui): add context menu to entity list --- .../CanvasEntityList.tsx | 2 +- .../CanvasEntityListMenuButton.tsx | 27 +++++++ .../CanvasEntityListMenuItems.tsx | 67 ++++++++++++++++ .../components/CanvasEntityListMenu.tsx | 77 ------------------- .../components/CanvasPanelContent.tsx | 24 +++++- .../components/common/CanvasEntityHeader.tsx | 2 +- .../ParametersPanelTextToImage.tsx | 4 +- 7 files changed, 118 insertions(+), 85 deletions(-) rename invokeai/frontend/web/src/features/controlLayers/components/{ => CanvasEntityList}/CanvasEntityList.tsx (96%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuButton.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuItems.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityListMenu.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityList.tsx similarity index 96% rename from invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityList.tsx index 2b28bdc61e2..dac827da5d1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityList.tsx @@ -11,7 +11,7 @@ import { memo } from 'react'; export const CanvasEntityList = memo(() => { return ( - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuButton.tsx new file mode 100644 index 00000000000..cb7231c2070 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuButton.tsx @@ -0,0 +1,27 @@ +import { IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library'; +import { CanvasEntityListMenuItems } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuItems'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiDotsThreeOutlineFill } from 'react-icons/pi'; + +export const CanvasEntityListMenuButton = memo(() => { + const { t } = useTranslation(); + + return ( + + } + variant="link" + data-testid="control-layers-add-layer-menu-button" + alignSelf="stretch" + /> + + + + + ); +}); + +CanvasEntityListMenuButton.displayName = 'CanvasEntityListMenuButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuItems.tsx new file mode 100644 index 00000000000..38be1d477da --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuItems.tsx @@ -0,0 +1,67 @@ +import { MenuDivider, MenuItem } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { + allEntitiesDeleted, + controlLayerAdded, + inpaintMaskAdded, + ipaAdded, + rasterLayerAdded, + rgAdded, +} from 'features/controlLayers/store/canvasV2Slice'; +import { selectEntityCount } from 'features/controlLayers/store/selectors'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiPlusBold, PiTrashSimpleBold } from 'react-icons/pi'; + +export const CanvasEntityListMenuItems = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const hasEntities = useAppSelector((s) => { + const count = selectEntityCount(s); + return count > 0; + }); + const addInpaintMask = useCallback(() => { + dispatch(inpaintMaskAdded({ isSelected: true })); + }, [dispatch]); + const addRegionalGuidance = useCallback(() => { + dispatch(rgAdded({ isSelected: true })); + }, [dispatch]); + const addRasterLayer = useCallback(() => { + dispatch(rasterLayerAdded({ isSelected: true })); + }, [dispatch]); + const addControlLayer = useCallback(() => { + dispatch(controlLayerAdded({ isSelected: true })); + }, [dispatch]); + const addIPAdapter = useCallback(() => { + dispatch(ipaAdded({ isSelected: true })); + }, [dispatch]); + const deleteAll = useCallback(() => { + dispatch(allEntitiesDeleted()); + }, [dispatch]); + + return ( + <> + } onClick={addInpaintMask}> + {t('controlLayers.inpaintMask', { count: 1 })} + + } onClick={addRegionalGuidance}> + {t('controlLayers.regionalGuidance', { count: 1 })} + + } onClick={addRasterLayer}> + {t('controlLayers.rasterLayer', { count: 1 })} + + } onClick={addControlLayer}> + {t('controlLayers.controlLayer', { count: 1 })} + + } onClick={addIPAdapter}> + {t('controlLayers.ipAdapter', { count: 1 })} + + + } color="error.300" isDisabled={!hasEntities}> + {t('controlLayers.deleteAll', { count: 1 })} + + + ); +}); + +CanvasEntityListMenuItems.displayName = 'CanvasEntityListMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityListMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityListMenu.tsx deleted file mode 100644 index 93577d353ea..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityListMenu.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuList } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { - allEntitiesDeleted, - controlLayerAdded, - inpaintMaskAdded, - ipaAdded, - rasterLayerAdded, - rgAdded, -} from 'features/controlLayers/store/canvasV2Slice'; -import { selectEntityCount } from 'features/controlLayers/store/selectors'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiDotsThreeOutlineFill, PiPlusBold, PiTrashSimpleBold } from 'react-icons/pi'; - -export const CanvasEntityListMenu = memo(() => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const hasEntities = useAppSelector((s) => { - const count = selectEntityCount(s); - return count > 0; - }); - const addInpaintMask = useCallback(() => { - dispatch(inpaintMaskAdded({ isSelected: true })); - }, [dispatch]); - const addRegionalGuidance = useCallback(() => { - dispatch(rgAdded({ isSelected: true })); - }, [dispatch]); - const addRasterLayer = useCallback(() => { - dispatch(rasterLayerAdded({ isSelected: true })); - }, [dispatch]); - const addControlLayer = useCallback(() => { - dispatch(controlLayerAdded({ isSelected: true })); - }, [dispatch]); - const addIPAdapter = useCallback(() => { - dispatch(ipaAdded({ isSelected: true })); - }, [dispatch]); - const deleteAll = useCallback(() => { - dispatch(allEntitiesDeleted()); - }, [dispatch]); - - return ( - - } - variant="link" - data-testid="control-layers-add-layer-menu-button" - alignSelf="stretch" - /> - - } onClick={addInpaintMask}> - {t('controlLayers.inpaintMask', { count: 1 })} - - } onClick={addRegionalGuidance}> - {t('controlLayers.regionalGuidance', { count: 1 })} - - } onClick={addRasterLayer}> - {t('controlLayers.rasterLayer', { count: 1 })} - - } onClick={addControlLayer}> - {t('controlLayers.controlLayer', { count: 1 })} - - } onClick={addIPAdapter}> - {t('controlLayers.ipAdapter', { count: 1 })} - - - } color="error.300" isDisabled={!hasEntities}> - {t('controlLayers.deleteAll', { count: 1 })} - - - - ); -}); - -CanvasEntityListMenu.displayName = 'CanvasEntityListMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx index 3ecf8f2b6d0..406cba7d7d2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx @@ -1,16 +1,32 @@ +import { Box, ContextMenu, MenuList } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { CanvasAddEntityButtons } from 'features/controlLayers/components/CanvasAddEntityButtons'; -import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList'; +import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityList'; +import { CanvasEntityListMenuItems } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuItems'; import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { selectEntityCount } from 'features/controlLayers/store/selectors'; -import { memo } from 'react'; +import { memo, useCallback } from 'react'; export const CanvasPanelContent = memo(() => { const hasEntities = useAppSelector((s) => selectEntityCount(s) > 0); + const renderMenu = useCallback( + () => ( + + + + ), + [] + ); return ( - {!hasEntities && } - {hasEntities && } + renderMenu={renderMenu}> + {(ref) => ( + + {!hasEntities && } + {hasEntities && } + + )} + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx index 4fe8867fd2a..1948830a7bb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx @@ -56,7 +56,7 @@ export const CanvasEntityHeader = memo(({ children, ...rest }: FlexProps) => { }, [entityIdentifier]); return ( - + {(ref) => ( {children} diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx index 1b39f40227a..dad4fdc215b 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx @@ -3,7 +3,7 @@ import { Box, Flex, Spacer, Tab, TabList, TabPanel, TabPanels, Tabs } from '@inv import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants'; -import { CanvasEntityListMenu } from 'features/controlLayers/components/CanvasEntityListMenu'; +import { CanvasEntityListMenuButton } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuButton'; import { CanvasPanelContent } from 'features/controlLayers/components/CanvasPanelContent'; import { $isPreviewVisible } from 'features/controlLayers/store/canvasV2Slice'; import { selectEntityCount } from 'features/controlLayers/store/selectors'; @@ -102,7 +102,7 @@ const ParametersPanelTextToImage = () => { {controlLayersTitle} - + From a733d720891a6cef5889c5f20e2ec2f07be586b8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 26 Aug 2024 08:55:33 +1000 Subject: [PATCH 492/678] fix(ui): scaled bbox loses sync --- .../controlLayers/store/bboxReducers.ts | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts index d414144a3cf..e6a5fb356e6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts @@ -8,7 +8,14 @@ import { ASPECT_RATIO_MAP, initialAspectRatioState } from 'features/parameters/c import type { AspectRatioID } from 'features/parameters/components/DocumentSize/types'; import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; import type { IRect } from 'konva/lib/types'; -import { pick } from 'lodash-es'; + +const syncScaledSize = (state: CanvasV2State) => { + if (state.bbox.scaleMethod === 'auto') { + const optimalDimension = getOptimalDimension(state.params.model); + const { width, height } = state.bbox.rect; + state.bbox.scaledSize = getScaledBoundingBoxDimensions({ width, height }, optimalDimension); + } +}; export const bboxReducers = { bboxScaledSizeChanged: (state, action: PayloadAction>) => { @@ -16,21 +23,11 @@ export const bboxReducers = { }, bboxScaleMethodChanged: (state, action: PayloadAction) => { state.bbox.scaleMethod = action.payload; - - if (action.payload === 'auto') { - const optimalDimension = getOptimalDimension(state.params.model); - const size = pick(state.bbox.rect, 'width', 'height'); - state.bbox.scaledSize = getScaledBoundingBoxDimensions(size, optimalDimension); - } + syncScaledSize(state); }, bboxChanged: (state, action: PayloadAction) => { state.bbox.rect = action.payload; - - if (state.bbox.scaleMethod === 'auto') { - const optimalDimension = getOptimalDimension(state.params.model); - const size = pick(state.bbox.rect, 'width', 'height'); - state.bbox.scaledSize = getScaledBoundingBoxDimensions(size, optimalDimension); - } + syncScaledSize(state); }, bboxWidthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }>) => { const { width, updateAspectRatio, clamp } = action.payload; @@ -45,6 +42,8 @@ export const bboxReducers = { state.bbox.aspectRatio.id = 'Free'; state.bbox.aspectRatio.isLocked = false; } + + syncScaledSize(state); }, bboxHeightChanged: ( state, @@ -63,6 +62,8 @@ export const bboxReducers = { state.bbox.aspectRatio.id = 'Free'; state.bbox.aspectRatio.isLocked = false; } + + syncScaledSize(state); }, bboxAspectRatioLockToggled: (state) => { state.bbox.aspectRatio.isLocked = !state.bbox.aspectRatio.isLocked; @@ -82,6 +83,8 @@ export const bboxReducers = { state.bbox.rect.width = width; state.bbox.rect.height = height; } + + syncScaledSize(state); }, bboxDimensionsSwapped: (state) => { state.bbox.aspectRatio.value = 1 / state.bbox.aspectRatio.value; @@ -99,6 +102,8 @@ export const bboxReducers = { state.bbox.rect.height = height; state.bbox.aspectRatio.id = ASPECT_RATIO_MAP[state.bbox.aspectRatio.id].inverseID; } + + syncScaledSize(state); }, bboxSizeOptimized: (state) => { const optimalDimension = getOptimalDimension(state.params.model); @@ -111,5 +116,7 @@ export const bboxReducers = { state.bbox.rect.width = optimalDimension; state.bbox.rect.height = optimalDimension; } + + syncScaledSize(state); }, } satisfies SliceCaseReducers; From b4d01659e0f99dbc894ed9f2973b111e90bf7ee1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 26 Aug 2024 08:58:16 +1000 Subject: [PATCH 493/678] fix(ui): color picker resets brush opacity --- .../frontend/web/src/features/controlLayers/konva/events.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index af64a2d7773..4bab491f06d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -199,7 +199,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { manager.stateApi.$colorUnderCursor.set(color); } if (color) { - manager.stateApi.setFill({ ...color, a: 1 }); + manager.stateApi.setFill({ ...toolState.fill, ...color }); } manager.preview.tool.render(); } else { From 72f304baa5387db3329060797f03ae707ba70ecd Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 26 Aug 2024 10:34:59 +1000 Subject: [PATCH 494/678] feat(ui): move events into modules who care about them --- .../controlLayers/konva/CanvasLayerAdapter.ts | 17 + .../controlLayers/konva/CanvasManager.ts | 3 - .../controlLayers/konva/CanvasMaskAdapter.ts | 17 + .../controlLayers/konva/CanvasStageModule.ts | 64 ++ .../controlLayers/konva/CanvasToolModule.ts | 475 +++++++++++++- .../features/controlLayers/konva/events.ts | 588 ------------------ .../src/features/controlLayers/konva/util.ts | 59 ++ 7 files changed, 628 insertions(+), 595 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/events.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts index af9d8462bbc..30f3c4f6be3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts @@ -3,11 +3,15 @@ import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; +import { getLastPointOfLine } from 'features/controlLayers/konva/util'; import type { + CanvasBrushLineState, CanvasControlLayerState, CanvasEntityIdentifier, + CanvasEraserLineState, CanvasRasterLayerState, CanvasV2State, + Coordinate, Rect, } from 'features/controlLayers/store/types'; import { getEntityIdentifier } from 'features/controlLayers/store/types'; @@ -180,6 +184,19 @@ export class CanvasLayerAdapter { return stableHash(arg); }; + getLastPointOfLastLine = (type: CanvasBrushLineState['type'] | CanvasEraserLineState['type']): Coordinate | null => { + const lastObject = this.state.objects[this.state.objects.length - 1]; + if (!lastObject) { + return null; + } + + if (lastObject.type === type) { + return getLastPointOfLine(lastObject.points); + } + + return null; + }; + logDebugInfo(msg = 'Debug info') { const info = { repr: this.repr(), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 09d641decb0..b4131ddb052 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -19,7 +19,6 @@ import type { CanvasLayerAdapter } from './CanvasLayerAdapter'; import type { CanvasMaskAdapter } from './CanvasMaskAdapter'; import { CanvasPreviewModule } from './CanvasPreviewModule'; import { CanvasStateApiModule } from './CanvasStateApiModule'; -import { setStageEventHandlers } from './events'; export const $canvasManager = atom(null); const TYPE = 'manager'; @@ -110,7 +109,6 @@ export class CanvasManager { this.stateApi.$currentFill.set(this.stateApi.getCurrentFill()); this.stateApi.$selectedEntity.set(this.stateApi.getSelectedEntity()); - const cleanupEventHandlers = setStageEventHandlers(this); const cleanupStage = this.stage.initialize(); const cleanupStore = this.store.subscribe(this.renderer.render); @@ -122,7 +120,6 @@ export class CanvasManager { this.background.destroy(); this.preview.destroy(); cleanupStore(); - cleanupEventHandlers(); cleanupStage(); }; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts index 3573a79a17c..ec2a50501e8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts @@ -3,11 +3,15 @@ import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; +import { getLastPointOfLine } from 'features/controlLayers/konva/util'; import type { + CanvasBrushLineState, CanvasEntityIdentifier, + CanvasEraserLineState, CanvasInpaintMaskState, CanvasRegionalGuidanceState, CanvasV2State, + Coordinate, Rect, } from 'features/controlLayers/store/types'; import { getEntityIdentifier } from 'features/controlLayers/store/types'; @@ -136,6 +140,19 @@ export class CanvasMaskAdapter { this.konva.layer.visible(isEnabled); }; + getLastPointOfLastLine = (type: CanvasBrushLineState['type'] | CanvasEraserLineState['type']): Coordinate | null => { + const lastObject = this.state.objects[this.state.objects.length - 1]; + if (!lastObject) { + return null; + } + + if (lastObject.type === type) { + return getLastPointOfLine(lastObject.points); + } + + return null; + }; + repr = () => { return { id: this.id, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts index 624027d1773..995751de10f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts @@ -1,8 +1,10 @@ import type { SerializableObject } from 'common/types'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CANVAS_SCALE_BY } from 'features/controlLayers/konva/constants'; import { getPrefixedId, getRectUnion } from 'features/controlLayers/konva/util'; import type { Coordinate, Dimensions, Rect } from 'features/controlLayers/store/types'; import type Konva from 'konva'; +import type { KonvaEventObject } from 'konva/lib/Node'; import { clamp } from 'lodash-es'; import type { Logger } from 'roarr'; @@ -27,6 +29,18 @@ export class CanvasStageModule { this.konva = { stage }; } + setEventListeners = () => { + this.konva.stage.on('wheel', this.onStageMouseWheel); + this.konva.stage.on('dragmove', this.onStageDragMove); + this.konva.stage.on('dragend', this.onStageDragEnd); + + return () => { + this.konva.stage.off('wheel', this.onStageMouseWheel); + this.konva.stage.off('dragmove', this.onStageDragMove); + this.konva.stage.off('dragend', this.onStageDragEnd); + }; + }; + initialize = () => { this.log.debug('Initializing stage'); this.konva.stage.container(this.container); @@ -34,11 +48,13 @@ export class CanvasStageModule { resizeObserver.observe(this.container); this.fitStageToContainer(); this.fitLayersToStage(); + const cleanupListeners = this.setEventListeners(); return () => { this.log.debug('Destroying stage'); resizeObserver.disconnect(); this.konva.stage.destroy(); + cleanupListeners(); }; }; @@ -172,6 +188,54 @@ export class CanvasStageModule { }); }; + onStageMouseWheel = (e: KonvaEventObject) => { + e.evt.preventDefault(); + + if (e.evt.ctrlKey || e.evt.metaKey) { + return; + } + + // We need the absolute cursor position - not the scaled position + const cursorPos = this.konva.stage.getPointerPosition(); + + if (cursorPos) { + // When wheeling on trackpad, e.evt.ctrlKey is true - in that case, let's reverse the direction + const delta = e.evt.ctrlKey ? -e.evt.deltaY : e.evt.deltaY; + const scale = this.manager.stage.getScale() * CANVAS_SCALE_BY ** delta; + this.manager.stage.setScale(scale, cursorPos); + } + }; + + onStageDragMove = (e: KonvaEventObject) => { + if (e.target !== this.konva.stage) { + return; + } + + this.manager.stateApi.$stageAttrs.set({ + // Stage position should always be an integer, else we get fractional pixels which are blurry + x: Math.floor(this.konva.stage.x()), + y: Math.floor(this.konva.stage.y()), + width: this.konva.stage.width(), + height: this.konva.stage.height(), + scale: this.konva.stage.scaleX(), + }); + }; + + onStageDragEnd = (e: KonvaEventObject) => { + if (e.target !== this.konva.stage) { + return; + } + + this.manager.stateApi.$stageAttrs.set({ + // Stage position should always be an integer, else we get fractional pixels which are blurry + x: Math.floor(this.konva.stage.x()), + y: Math.floor(this.konva.stage.y()), + width: this.konva.stage.width(), + height: this.konva.stage.height(), + scale: this.konva.stage.scaleX(), + }); + }; + /** * Gets the scale of the stage. The stage is always scaled uniformly in x and y. */ diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts index a01c0d9cd48..4123b9b190d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts @@ -2,11 +2,33 @@ import type { SerializableObject } from 'common/types'; import { rgbaColorToString, rgbColorToString } from 'common/util/colorCodeTransformers'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasPreviewModule } from 'features/controlLayers/konva/CanvasPreviewModule'; -import { BRUSH_BORDER_INNER_COLOR, BRUSH_BORDER_OUTER_COLOR } from 'features/controlLayers/konva/constants'; -import { alignCoordForTool, getPrefixedId } from 'features/controlLayers/konva/util'; -import type { Tool } from 'features/controlLayers/store/types'; +import { + BRUSH_BORDER_INNER_COLOR, + BRUSH_BORDER_OUTER_COLOR, + BRUSH_SPACING_TARGET_SCALE, +} from 'features/controlLayers/konva/constants'; +import { + alignCoordForTool, + calculateNewBrushSizeFromWheelDelta, + getIsPrimaryMouseDown, + getLastPointOfLine, + getPrefixedId, + getScaledCursorPosition, + offsetCoord, + validateCandidatePoint, +} from 'features/controlLayers/konva/util'; +import type { + CanvasControlLayerState, + CanvasInpaintMaskState, + CanvasRasterLayerState, + CanvasRegionalGuidanceState, + Coordinate, + RgbColor, + Tool, +} from 'features/controlLayers/store/types'; import { isDrawableEntity } from 'features/controlLayers/store/types'; import Konva from 'konva'; +import type { KonvaEventObject } from 'konva/lib/Node'; import type { Logger } from 'roarr'; export class CanvasToolModule { @@ -25,6 +47,7 @@ export class CanvasToolModule { log: Logger; konva: { + stage: Konva.Stage; group: Konva.Group; brush: { group: Konva.Group; @@ -67,6 +90,7 @@ export class CanvasToolModule { this.path = this.manager.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); this.konva = { + stage: this.manager.stage.konva.stage, group: new Konva.Group({ name: `${this.type}:group`, listening: false }), brush: { group: new Konva.Group({ name: `${this.type}:brush_group`, listening: false }), @@ -218,6 +242,10 @@ export class CanvasToolModule { this.render(); }) ); + + const cleanupListeners = this.setEventListeners(); + + this.subscriptions.add(cleanupListeners); } destroy = () => { @@ -277,7 +305,7 @@ export class CanvasToolModule { stage.setIsDraggable(tool === 'view'); - if (!cursorPos || renderedEntityCount === 0 || !isDrawable) { + if (!cursorPos || renderedEntityCount === 0) { // We can bail early if the mouse isn't over the stage or there are no layers this.konva.group.visible(false); } else { @@ -421,6 +449,445 @@ export class CanvasToolModule { } } + syncLastCursorPos = (): Coordinate | null => { + const pos = getScaledCursorPosition(this.konva.stage); + if (!pos) { + return null; + } + this.manager.stateApi.$lastCursorPos.set(pos); + return pos; + }; + + getColorUnderCursor = (): RgbColor | null => { + const pos = this.konva.stage.getPointerPosition(); + if (!pos) { + return null; + } + const ctx = this.konva.stage + .toCanvas({ x: pos.x, y: pos.y, width: 1, height: 1, imageSmoothingEnabled: false }) + .getContext('2d'); + + if (!ctx) { + return null; + } + + const [r, g, b, _a] = ctx.getImageData(0, 0, 1, 1).data; + + if (r === undefined || g === undefined || b === undefined) { + return null; + } + + return { r, g, b }; + }; + + getClip( + entity: CanvasRegionalGuidanceState | CanvasControlLayerState | CanvasRasterLayerState | CanvasInpaintMaskState + ) { + const settings = this.manager.stateApi.getSettings(); + + if (settings.clipToBbox) { + const { x, y, width, height } = this.manager.stateApi.getBbox().rect; + return { + x: x - entity.position.x, + y: y - entity.position.y, + width, + height, + }; + } else { + const { x, y } = this.manager.stage.getPosition(); + const scale = this.manager.stage.getScale(); + const { width, height } = this.manager.stage.getSize(); + return { + x: -x / scale - entity.position.x, + y: -y / scale - entity.position.y, + width: width / scale, + height: height / scale, + }; + } + } + + setEventListeners = (): (() => void) => { + this.konva.stage.on('mouseenter', this.onStageMouseEnter); + this.konva.stage.on('mousedown', this.onStageMouseDown); + this.konva.stage.on('mouseup', this.onStageMouseUp); + this.konva.stage.on('mousemove', this.onStageMouseMove); + this.konva.stage.on('mouseleave', this.onStageMouseLeave); + this.konva.stage.on('wheel', this.onStageMouseWheel); + + window.addEventListener('keydown', this.onKeyDown); + window.addEventListener('keyup', this.onKeyUp); + + return () => { + this.konva.stage.off('mouseenter', this.onStageMouseEnter); + this.konva.stage.off('mousedown', this.onStageMouseDown); + this.konva.stage.off('mouseup', this.onStageMouseUp); + this.konva.stage.off('mousemove', this.onStageMouseMove); + this.konva.stage.off('mouseleave', this.onStageMouseLeave); + + this.konva.stage.off('wheel', this.onStageMouseWheel); + window.removeEventListener('keydown', this.onKeyDown); + window.removeEventListener('keyup', this.onKeyUp); + }; + }; + + onStageMouseEnter = (_: KonvaEventObject) => { + this.render(); + }; + + onStageMouseDown = async (e: KonvaEventObject) => { + this.manager.stateApi.$isMouseDown.set(true); + const toolState = this.manager.stateApi.getToolState(); + const pos = this.syncLastCursorPos(); + const selectedEntity = this.manager.stateApi.getSelectedEntity(); + + if (toolState.selected === 'colorPicker') { + const color = this.getColorUnderCursor(); + if (color) { + this.manager.stateApi.$colorUnderCursor.set(color); + } + if (color) { + this.manager.stateApi.setFill({ ...toolState.fill, ...color }); + } + this.render(); + } else { + const isDrawable = selectedEntity?.state.isEnabled; + if (pos && isDrawable && !this.manager.stateApi.$spaceKey.get() && getIsPrimaryMouseDown(e)) { + this.manager.stateApi.$lastMouseDownPos.set(pos); + const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); + + if (toolState.selected === 'brush') { + const lastLinePoint = selectedEntity.adapter.getLastPointOfLastLine('brush_line'); + const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); + if (e.evt.shiftKey && lastLinePoint) { + // Create a straight line from the last line point + if (selectedEntity.adapter.renderer.bufferState) { + selectedEntity.adapter.renderer.commitBuffer(); + } + + await selectedEntity.adapter.renderer.setBuffer({ + id: getPrefixedId('brush_line'), + type: 'brush_line', + points: [ + // The last point of the last line is already normalized to the entity's coordinates + lastLinePoint.x, + lastLinePoint.y, + alignedPoint.x, + alignedPoint.y, + ], + strokeWidth: toolState.brush.width, + color: this.manager.stateApi.getCurrentFill(), + clip: this.getClip(selectedEntity.state), + }); + } else { + if (selectedEntity.adapter.renderer.bufferState) { + selectedEntity.adapter.renderer.commitBuffer(); + } + await selectedEntity.adapter.renderer.setBuffer({ + id: getPrefixedId('brush_line'), + type: 'brush_line', + points: [alignedPoint.x, alignedPoint.y], + strokeWidth: toolState.brush.width, + color: this.manager.stateApi.getCurrentFill(), + clip: this.getClip(selectedEntity.state), + }); + } + this.manager.stateApi.$lastAddedPoint.set(alignedPoint); + } + + if (toolState.selected === 'eraser') { + const lastLinePoint = selectedEntity.adapter.getLastPointOfLastLine('eraser_line'); + const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); + if (e.evt.shiftKey && lastLinePoint) { + // Create a straight line from the last line point + if (selectedEntity.adapter.renderer.bufferState) { + selectedEntity.adapter.renderer.commitBuffer(); + } + await selectedEntity.adapter.renderer.setBuffer({ + id: getPrefixedId('eraser_line'), + type: 'eraser_line', + points: [ + // The last point of the last line is already normalized to the entity's coordinates + lastLinePoint.x, + lastLinePoint.y, + alignedPoint.x, + alignedPoint.y, + ], + strokeWidth: toolState.eraser.width, + clip: this.getClip(selectedEntity.state), + }); + } else { + if (selectedEntity.adapter.renderer.bufferState) { + selectedEntity.adapter.renderer.commitBuffer(); + } + await selectedEntity.adapter.renderer.setBuffer({ + id: getPrefixedId('eraser_line'), + type: 'eraser_line', + points: [alignedPoint.x, alignedPoint.y], + strokeWidth: toolState.eraser.width, + clip: this.getClip(selectedEntity.state), + }); + } + this.manager.stateApi.$lastAddedPoint.set(alignedPoint); + } + + if (toolState.selected === 'rect') { + if (selectedEntity.adapter.renderer.bufferState) { + selectedEntity.adapter.renderer.commitBuffer(); + } + await selectedEntity.adapter.renderer.setBuffer({ + id: getPrefixedId('rect'), + type: 'rect', + rect: { x: Math.round(normalizedPoint.x), y: Math.round(normalizedPoint.y), width: 0, height: 0 }, + color: this.manager.stateApi.getCurrentFill(), + }); + } + } + } + }; + + onStageMouseUp = (_: KonvaEventObject) => { + this.manager.stateApi.$isMouseDown.set(false); + const pos = this.manager.stateApi.$lastCursorPos.get(); + const selectedEntity = this.manager.stateApi.getSelectedEntity(); + const isDrawable = selectedEntity?.state.isEnabled; + + if (pos && isDrawable && !this.manager.stateApi.$spaceKey.get()) { + const toolState = this.manager.stateApi.getToolState(); + + if (toolState.selected === 'brush') { + const drawingBuffer = selectedEntity.adapter.renderer.bufferState; + if (drawingBuffer?.type === 'brush_line') { + selectedEntity.adapter.renderer.commitBuffer(); + } else { + selectedEntity.adapter.renderer.clearBuffer(); + } + } + + if (toolState.selected === 'eraser') { + const drawingBuffer = selectedEntity.adapter.renderer.bufferState; + if (drawingBuffer?.type === 'eraser_line') { + selectedEntity.adapter.renderer.commitBuffer(); + } else { + selectedEntity.adapter.renderer.clearBuffer(); + } + } + + if (toolState.selected === 'rect') { + const drawingBuffer = selectedEntity.adapter.renderer.bufferState; + if (drawingBuffer?.type === 'rect') { + selectedEntity.adapter.renderer.commitBuffer(); + } else { + selectedEntity.adapter.renderer.clearBuffer(); + } + } + + this.manager.stateApi.$lastMouseDownPos.set(null); + } + this.render(); + }; + + onStageMouseMove = async (e: KonvaEventObject) => { + const toolState = this.manager.stateApi.getToolState(); + const pos = this.syncLastCursorPos(); + const selectedEntity = this.manager.stateApi.getSelectedEntity(); + + if (toolState.selected === 'colorPicker') { + const color = this.getColorUnderCursor(); + if (color) { + this.manager.stateApi.$colorUnderCursor.set(color); + } + } else { + const isDrawable = selectedEntity?.state.isEnabled; + if (pos && isDrawable && !this.manager.stateApi.$spaceKey.get() && getIsPrimaryMouseDown(e)) { + if (toolState.selected === 'brush') { + const drawingBuffer = selectedEntity.adapter.renderer.bufferState; + if (drawingBuffer) { + if (drawingBuffer.type === 'brush_line') { + const lastPoint = getLastPointOfLine(drawingBuffer.points); + const minDistance = toolState.brush.width * BRUSH_SPACING_TARGET_SCALE; + if (lastPoint && validateCandidatePoint(pos, lastPoint, minDistance)) { + const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); + const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); + // Do not add duplicate points + if (lastPoint.x !== alignedPoint.x || lastPoint.y !== alignedPoint.y) { + drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); + await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); + this.manager.stateApi.$lastAddedPoint.set(alignedPoint); + } + } + } else { + selectedEntity.adapter.renderer.clearBuffer(); + } + } else { + if (selectedEntity.adapter.renderer.bufferState) { + selectedEntity.adapter.renderer.commitBuffer(); + } + const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); + const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); + await selectedEntity.adapter.renderer.setBuffer({ + id: getPrefixedId('brush_line'), + type: 'brush_line', + points: [alignedPoint.x, alignedPoint.y], + strokeWidth: toolState.brush.width, + color: this.manager.stateApi.getCurrentFill(), + clip: this.getClip(selectedEntity.state), + }); + this.manager.stateApi.$lastAddedPoint.set(alignedPoint); + } + } + + if (toolState.selected === 'eraser') { + const drawingBuffer = selectedEntity.adapter.renderer.bufferState; + if (drawingBuffer) { + if (drawingBuffer.type === 'eraser_line') { + const lastPoint = getLastPointOfLine(drawingBuffer.points); + const minDistance = toolState.eraser.width * BRUSH_SPACING_TARGET_SCALE; + if (lastPoint && validateCandidatePoint(pos, lastPoint, minDistance)) { + const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); + const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); + // Do not add duplicate points + if (lastPoint.x !== alignedPoint.x || lastPoint.y !== alignedPoint.y) { + drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); + await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); + this.manager.stateApi.$lastAddedPoint.set(alignedPoint); + } + } + } else { + selectedEntity.adapter.renderer.clearBuffer(); + } + } else { + if (selectedEntity.adapter.renderer.bufferState) { + selectedEntity.adapter.renderer.commitBuffer(); + } + const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); + const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); + await selectedEntity.adapter.renderer.setBuffer({ + id: getPrefixedId('eraser_line'), + type: 'eraser_line', + points: [alignedPoint.x, alignedPoint.y], + strokeWidth: toolState.eraser.width, + clip: this.getClip(selectedEntity.state), + }); + this.manager.stateApi.$lastAddedPoint.set(alignedPoint); + } + } + + if (toolState.selected === 'rect') { + const drawingBuffer = selectedEntity.adapter.renderer.bufferState; + if (drawingBuffer) { + if (drawingBuffer.type === 'rect') { + const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); + drawingBuffer.rect.width = Math.round(normalizedPoint.x - drawingBuffer.rect.x); + drawingBuffer.rect.height = Math.round(normalizedPoint.y - drawingBuffer.rect.y); + await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); + } else { + selectedEntity.adapter.renderer.clearBuffer(); + } + } + } + } + } + + this.render(); + }; + + onStageMouseLeave = async (e: KonvaEventObject) => { + const pos = this.syncLastCursorPos(); + this.manager.stateApi.$lastCursorPos.set(null); + this.manager.stateApi.$lastMouseDownPos.set(null); + const selectedEntity = this.manager.stateApi.getSelectedEntity(); + const toolState = this.manager.stateApi.getToolState(); + const isDrawable = selectedEntity?.state.isEnabled; + + if (pos && isDrawable && !this.manager.stateApi.$spaceKey.get() && getIsPrimaryMouseDown(e)) { + const drawingBuffer = selectedEntity.adapter.renderer.bufferState; + const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); + if (toolState.selected === 'brush' && drawingBuffer?.type === 'brush_line') { + const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); + drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); + await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); + selectedEntity.adapter.renderer.commitBuffer(); + } else if (toolState.selected === 'eraser' && drawingBuffer?.type === 'eraser_line') { + const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); + drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); + await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); + selectedEntity.adapter.renderer.commitBuffer(); + } else if (toolState.selected === 'rect' && drawingBuffer?.type === 'rect') { + drawingBuffer.rect.width = Math.round(normalizedPoint.x - drawingBuffer.rect.x); + drawingBuffer.rect.height = Math.round(normalizedPoint.y - drawingBuffer.rect.y); + await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); + selectedEntity.adapter.renderer.commitBuffer(); + } + } + + this.render(); + }; + + onStageMouseWheel = (e: KonvaEventObject) => { + e.evt.preventDefault(); + + if (!e.evt.ctrlKey && !e.evt.metaKey) { + return; + } + + const toolState = this.manager.stateApi.getToolState(); + + let delta = e.evt.deltaY; + + if (toolState.invertScroll) { + delta = -delta; + } + + // Holding ctrl or meta while scrolling changes the brush size + if (toolState.selected === 'brush') { + this.manager.stateApi.setBrushWidth(calculateNewBrushSizeFromWheelDelta(toolState.brush.width, delta)); + } else if (toolState.selected === 'eraser') { + this.manager.stateApi.setEraserWidth(calculateNewBrushSizeFromWheelDelta(toolState.eraser.width, delta)); + } + + this.render(); + }; + + onKeyDown = (e: KeyboardEvent) => { + if (e.repeat) { + return; + } + if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { + return; + } + if (e.key === 'Escape') { + // Cancel shape drawing on escape + const selectedEntity = this.manager.stateApi.getSelectedEntity(); + if (selectedEntity) { + selectedEntity.adapter.renderer.clearBuffer(); + this.manager.stateApi.$lastMouseDownPos.set(null); + } + } else if (e.key === ' ') { + // Select the view tool on space key down + this.manager.stateApi.setToolBuffer(this.manager.stateApi.getToolState().selected); + this.manager.stateApi.setTool('view'); + this.manager.stateApi.$spaceKey.set(true); + this.manager.stateApi.$lastCursorPos.set(null); + this.manager.stateApi.$lastMouseDownPos.set(null); + } + }; + + onKeyUp = (e: KeyboardEvent) => { + if (e.repeat) { + return; + } + if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { + return; + } + if (e.key === ' ') { + // Revert the tool to the previous tool on space key up + const toolBuffer = this.manager.stateApi.getToolState().selectedBuffer; + this.manager.stateApi.setTool(toolBuffer ?? 'move'); + this.manager.stateApi.setToolBuffer(null); + this.manager.stateApi.$spaceKey.set(false); + } + }; + getLoggingContext = (): SerializableObject => { return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts deleted file mode 100644 index 4bab491f06d..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ /dev/null @@ -1,588 +0,0 @@ -import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { - alignCoordForTool, - getPrefixedId, - getScaledCursorPosition, - offsetCoord, -} from 'features/controlLayers/konva/util'; -import type { - CanvasControlLayerState, - CanvasInpaintMaskState, - CanvasRasterLayerState, - CanvasRegionalGuidanceState, - CanvasV2State, - Coordinate, - RgbColor, - Tool, -} from 'features/controlLayers/store/types'; -import type Konva from 'konva'; -import type { KonvaEventObject } from 'konva/lib/Node'; -import { clamp } from 'lodash-es'; - -import { BRUSH_SPACING_TARGET_SCALE, CANVAS_SCALE_BY } from './constants'; - -/** - * Updates the last cursor position atom with the current cursor position, returning the new position or `null` if the - * cursor is not over the stage. - * @param stage The konva stage - * @param setLastCursorPos The callback to store the cursor pos - */ -const updateLastCursorPos = ( - stage: Konva.Stage, - setLastCursorPos: CanvasManager['stateApi']['$lastCursorPos']['set'] -) => { - const pos = getScaledCursorPosition(stage); - if (!pos) { - return null; - } - setLastCursorPos(pos); - return pos; -}; - -const calculateNewBrushSize = (brushSize: number, delta: number) => { - // This equation was derived by fitting a curve to the desired brush sizes and deltas - // see https://github.com/invoke-ai/InvokeAI/pull/5542#issuecomment-1915847565 - const targetDelta = Math.sign(delta) * 0.7363 * Math.pow(1.0394, brushSize); - // This needs to be clamped to prevent the delta from getting too large - const finalDelta = clamp(targetDelta, -20, 20); - // The new brush size is also clamped to prevent it from getting too large or small - const newBrushSize = clamp(brushSize + finalDelta, 1, 500); - - return newBrushSize; -}; - -const getNextPoint = ( - currentPos: Coordinate, - toolState: CanvasV2State['tool'], - lastAddedPoint: Coordinate | null -): Coordinate | null => { - // Continue the last line - const minSpacingPx = - toolState.selected === 'brush' - ? toolState.brush.width * BRUSH_SPACING_TARGET_SCALE - : toolState.eraser.width * BRUSH_SPACING_TARGET_SCALE; - - if (lastAddedPoint) { - // Dispatching redux events impacts perf substantially - using brush spacing keeps dispatches to a reasonable number - if (Math.hypot(lastAddedPoint.x - currentPos.x, lastAddedPoint.y - currentPos.y) < minSpacingPx) { - return null; - } - } - - return currentPos; -}; - -const getLastPointOfLine = (points: number[]): Coordinate | null => { - if (points.length < 2) { - return null; - } - const x = points[points.length - 2]; - const y = points[points.length - 1]; - if (x === undefined || y === undefined) { - return null; - } - return { x, y }; -}; - -const getLastPointOfLastLineOfEntity = ( - entity: CanvasRasterLayerState | CanvasControlLayerState | CanvasRegionalGuidanceState | CanvasInpaintMaskState, - tool: Tool -): Coordinate | null => { - const lastObject = entity.objects[entity.objects.length - 1]; - - if (!lastObject) { - return null; - } - - if ( - !( - (lastObject.type === 'brush_line' && tool === 'brush') || - (lastObject.type === 'eraser_line' && tool === 'eraser') - ) - ) { - // If the last object type and current tool do not match, we cannot continue the line - return null; - } - - if (lastObject.points.length < 2) { - return null; - } - const x = lastObject.points[lastObject.points.length - 2]; - const y = lastObject.points[lastObject.points.length - 1]; - if (x === undefined || y === undefined) { - return null; - } - return { x, y }; -}; - -const getColorUnderCursor = (stage: Konva.Stage): RgbColor | null => { - const pos = stage.getPointerPosition(); - if (!pos) { - return null; - } - const ctx = stage - .toCanvas({ x: pos.x, y: pos.y, width: 1, height: 1, imageSmoothingEnabled: false }) - .getContext('2d'); - if (!ctx) { - return null; - } - const [r, g, b, _a] = ctx.getImageData(0, 0, 1, 1).data; - if (r === undefined || g === undefined || b === undefined) { - return null; - } - - return { r, g, b }; -}; - -export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { - const stage = manager.stage.konva.stage; - const { - getToolState, - setTool, - setToolBuffer, - $isMouseDown, - $lastMouseDownPos, - $lastCursorPos, - $lastAddedPoint, - $stageAttrs, - $spaceKey, - getBbox, - getSettings, - setBrushWidth, - setEraserWidth, - getCurrentFill, - getSelectedEntity, - } = manager.stateApi; - - function getIsPrimaryMouseDown(e: KonvaEventObject) { - return e.evt.buttons === 1; - } - - function getClip( - entity: CanvasRegionalGuidanceState | CanvasControlLayerState | CanvasRasterLayerState | CanvasInpaintMaskState - ) { - const settings = getSettings(); - const bboxRect = getBbox().rect; - - if (settings.clipToBbox) { - return { - x: bboxRect.x - entity.position.x, - y: bboxRect.y - entity.position.y, - width: bboxRect.width, - height: bboxRect.height, - }; - } else { - return { - x: -stage.x() / stage.scaleX() - entity.position.x, - y: -stage.y() / stage.scaleY() - entity.position.y, - width: stage.width() / stage.scaleX(), - height: stage.height() / stage.scaleY(), - }; - } - } - - //#region mouseenter - stage.on('mouseenter', () => { - manager.preview.tool.render(); - }); - - //#region mousedown - stage.on('mousedown', async (e) => { - $isMouseDown.set(true); - const toolState = getToolState(); - const pos = updateLastCursorPos(stage, $lastCursorPos.set); - const selectedEntity = getSelectedEntity(); - - if (toolState.selected === 'colorPicker') { - const color = getColorUnderCursor(stage); - if (color) { - manager.stateApi.$colorUnderCursor.set(color); - } - if (color) { - manager.stateApi.setFill({ ...toolState.fill, ...color }); - } - manager.preview.tool.render(); - } else { - const isDrawable = selectedEntity?.state.isEnabled; - if (pos && isDrawable && !$spaceKey.get() && getIsPrimaryMouseDown(e)) { - $lastMouseDownPos.set(pos); - const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); - - if (toolState.selected === 'brush') { - const lastLinePoint = getLastPointOfLastLineOfEntity(selectedEntity.state, toolState.selected); - const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); - if (e.evt.shiftKey && lastLinePoint) { - // Create a straight line from the last line point - if (selectedEntity.adapter.renderer.bufferState) { - selectedEntity.adapter.renderer.commitBuffer(); - } - - await selectedEntity.adapter.renderer.setBuffer({ - id: getPrefixedId('brush_line'), - type: 'brush_line', - points: [ - // The last point of the last line is already normalized to the entity's coordinates - lastLinePoint.x, - lastLinePoint.y, - alignedPoint.x, - alignedPoint.y, - ], - strokeWidth: toolState.brush.width, - color: getCurrentFill(), - clip: getClip(selectedEntity.state), - }); - } else { - if (selectedEntity.adapter.renderer.bufferState) { - selectedEntity.adapter.renderer.commitBuffer(); - } - await selectedEntity.adapter.renderer.setBuffer({ - id: getPrefixedId('brush_line'), - type: 'brush_line', - points: [alignedPoint.x, alignedPoint.y], - strokeWidth: toolState.brush.width, - color: getCurrentFill(), - clip: getClip(selectedEntity.state), - }); - } - $lastAddedPoint.set(alignedPoint); - } - - if (toolState.selected === 'eraser') { - const lastLinePoint = getLastPointOfLastLineOfEntity(selectedEntity.state, toolState.selected); - const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); - if (e.evt.shiftKey && lastLinePoint) { - // Create a straight line from the last line point - if (selectedEntity.adapter.renderer.bufferState) { - selectedEntity.adapter.renderer.commitBuffer(); - } - await selectedEntity.adapter.renderer.setBuffer({ - id: getPrefixedId('eraser_line'), - type: 'eraser_line', - points: [ - // The last point of the last line is already normalized to the entity's coordinates - lastLinePoint.x, - lastLinePoint.y, - alignedPoint.x, - alignedPoint.y, - ], - strokeWidth: toolState.eraser.width, - clip: getClip(selectedEntity.state), - }); - } else { - if (selectedEntity.adapter.renderer.bufferState) { - selectedEntity.adapter.renderer.commitBuffer(); - } - await selectedEntity.adapter.renderer.setBuffer({ - id: getPrefixedId('eraser_line'), - type: 'eraser_line', - points: [alignedPoint.x, alignedPoint.y], - strokeWidth: toolState.eraser.width, - clip: getClip(selectedEntity.state), - }); - } - $lastAddedPoint.set(alignedPoint); - } - - if (toolState.selected === 'rect') { - if (selectedEntity.adapter.renderer.bufferState) { - selectedEntity.adapter.renderer.commitBuffer(); - } - await selectedEntity.adapter.renderer.setBuffer({ - id: getPrefixedId('rect'), - type: 'rect', - rect: { x: Math.round(normalizedPoint.x), y: Math.round(normalizedPoint.y), width: 0, height: 0 }, - color: getCurrentFill(), - }); - } - } - } - }); - - //#region mouseup - stage.on('mouseup', () => { - $isMouseDown.set(false); - const pos = $lastCursorPos.get(); - const selectedEntity = getSelectedEntity(); - const isDrawable = selectedEntity?.state.isEnabled; - if (pos && isDrawable && !$spaceKey.get()) { - const toolState = getToolState(); - - if (toolState.selected === 'brush') { - const drawingBuffer = selectedEntity.adapter.renderer.bufferState; - if (drawingBuffer?.type === 'brush_line') { - selectedEntity.adapter.renderer.commitBuffer(); - } else { - selectedEntity.adapter.renderer.clearBuffer(); - } - } - - if (toolState.selected === 'eraser') { - const drawingBuffer = selectedEntity.adapter.renderer.bufferState; - if (drawingBuffer?.type === 'eraser_line') { - selectedEntity.adapter.renderer.commitBuffer(); - } else { - selectedEntity.adapter.renderer.clearBuffer(); - } - } - - if (toolState.selected === 'rect') { - const drawingBuffer = selectedEntity.adapter.renderer.bufferState; - if (drawingBuffer?.type === 'rect') { - selectedEntity.adapter.renderer.commitBuffer(); - } else { - selectedEntity.adapter.renderer.clearBuffer(); - } - } - - $lastMouseDownPos.set(null); - } - manager.preview.tool.render(); - }); - - //#region mousemove - stage.on('mousemove', async (e) => { - const toolState = getToolState(); - const pos = updateLastCursorPos(stage, $lastCursorPos.set); - const selectedEntity = getSelectedEntity(); - - if (toolState.selected === 'colorPicker') { - const color = getColorUnderCursor(stage); - if (color) { - manager.stateApi.$colorUnderCursor.set(color); - } - } else { - const isDrawable = selectedEntity?.state.isEnabled; - if (pos && isDrawable && !$spaceKey.get() && getIsPrimaryMouseDown(e)) { - if (toolState.selected === 'brush') { - const drawingBuffer = selectedEntity.adapter.renderer.bufferState; - if (drawingBuffer) { - if (drawingBuffer.type === 'brush_line') { - const lastPoint = getLastPointOfLine(drawingBuffer.points); - const nextPoint = getNextPoint(pos, toolState, lastPoint); - if (lastPoint && nextPoint) { - const normalizedPoint = offsetCoord(nextPoint, selectedEntity.state.position); - const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); - // Do not add duplicate points - if (lastPoint.x !== alignedPoint.x || lastPoint.y !== alignedPoint.y) { - drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); - await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); - $lastAddedPoint.set(alignedPoint); - } - } - } else { - selectedEntity.adapter.renderer.clearBuffer(); - } - } else { - if (selectedEntity.adapter.renderer.bufferState) { - selectedEntity.adapter.renderer.commitBuffer(); - } - const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); - const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); - await selectedEntity.adapter.renderer.setBuffer({ - id: getPrefixedId('brush_line'), - type: 'brush_line', - points: [alignedPoint.x, alignedPoint.y], - strokeWidth: toolState.brush.width, - color: getCurrentFill(), - clip: getClip(selectedEntity.state), - }); - $lastAddedPoint.set(alignedPoint); - } - } - - if (toolState.selected === 'eraser') { - const drawingBuffer = selectedEntity.adapter.renderer.bufferState; - if (drawingBuffer) { - if (drawingBuffer.type === 'eraser_line') { - const lastPoint = getLastPointOfLine(drawingBuffer.points); - const nextPoint = getNextPoint(pos, toolState, lastPoint); - if (lastPoint && nextPoint) { - const normalizedPoint = offsetCoord(nextPoint, selectedEntity.state.position); - const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); - // Do not add duplicate points - if (lastPoint.x !== alignedPoint.x || lastPoint.y !== alignedPoint.y) { - drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); - await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); - $lastAddedPoint.set(alignedPoint); - } - } - } else { - selectedEntity.adapter.renderer.clearBuffer(); - } - } else { - if (selectedEntity.adapter.renderer.bufferState) { - selectedEntity.adapter.renderer.commitBuffer(); - } - const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); - const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); - await selectedEntity.adapter.renderer.setBuffer({ - id: getPrefixedId('eraser_line'), - type: 'eraser_line', - points: [alignedPoint.x, alignedPoint.y], - strokeWidth: toolState.eraser.width, - clip: getClip(selectedEntity.state), - }); - $lastAddedPoint.set(alignedPoint); - } - } - - if (toolState.selected === 'rect') { - const drawingBuffer = selectedEntity.adapter.renderer.bufferState; - if (drawingBuffer) { - if (drawingBuffer.type === 'rect') { - const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); - drawingBuffer.rect.width = Math.round(normalizedPoint.x - drawingBuffer.rect.x); - drawingBuffer.rect.height = Math.round(normalizedPoint.y - drawingBuffer.rect.y); - await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); - } else { - selectedEntity.adapter.renderer.clearBuffer(); - } - } - } - } - } - - manager.preview.tool.render(); - }); - - //#region mouseleave - stage.on('mouseleave', async (e) => { - const pos = updateLastCursorPos(stage, $lastCursorPos.set); - $lastCursorPos.set(null); - $lastMouseDownPos.set(null); - const selectedEntity = getSelectedEntity(); - const toolState = getToolState(); - const isDrawable = selectedEntity?.state.isEnabled; - - if (pos && isDrawable && !$spaceKey.get() && getIsPrimaryMouseDown(e)) { - const drawingBuffer = selectedEntity.adapter.renderer.bufferState; - const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); - if (toolState.selected === 'brush' && drawingBuffer?.type === 'brush_line') { - const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); - drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); - await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); - selectedEntity.adapter.renderer.commitBuffer(); - } else if (toolState.selected === 'eraser' && drawingBuffer?.type === 'eraser_line') { - const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); - drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); - await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); - selectedEntity.adapter.renderer.commitBuffer(); - } else if (toolState.selected === 'rect' && drawingBuffer?.type === 'rect') { - drawingBuffer.rect.width = Math.round(normalizedPoint.x - drawingBuffer.rect.x); - drawingBuffer.rect.height = Math.round(normalizedPoint.y - drawingBuffer.rect.y); - await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); - selectedEntity.adapter.renderer.commitBuffer(); - } - } - - manager.preview.tool.render(); - }); - - //#region wheel - stage.on('wheel', (e) => { - e.evt.preventDefault(); - - if (e.evt.ctrlKey || e.evt.metaKey) { - const toolState = getToolState(); - let delta = e.evt.deltaY; - if (toolState.invertScroll) { - delta = -delta; - } - // Holding ctrl or meta while scrolling changes the brush size - if (toolState.selected === 'brush') { - setBrushWidth(calculateNewBrushSize(toolState.brush.width, delta)); - } else if (toolState.selected === 'eraser') { - setEraserWidth(calculateNewBrushSize(toolState.eraser.width, delta)); - } - } else { - // We need the absolute cursor position - not the scaled position - const cursorPos = stage.getPointerPosition(); - if (cursorPos) { - // When wheeling on trackpad, e.evt.ctrlKey is true - in that case, let's reverse the direction - const delta = e.evt.ctrlKey ? -e.evt.deltaY : e.evt.deltaY; - const scale = manager.stage.getScale() * CANVAS_SCALE_BY ** delta; - manager.stage.setScale(scale, cursorPos); - } - } - manager.preview.tool.render(); - }); - - //#region dragmove - stage.on('dragmove', (e) => { - if (e.target !== stage) { - return; - } - $stageAttrs.set({ - x: Math.floor(stage.x()), - y: Math.floor(stage.y()), - width: stage.width(), - height: stage.height(), - scale: stage.scaleX(), - }); - }); - - //#region dragend - stage.on('dragend', (e) => { - if (e.target !== stage) { - return; - } // Stage position should always be an integer, else we get fractional pixels which are blurry - $stageAttrs.set({ - x: Math.floor(stage.x()), - y: Math.floor(stage.y()), - width: stage.width(), - height: stage.height(), - scale: stage.scaleX(), - }); - manager.preview.tool.render(); - }); - - //#region key - const onKeyDown = (e: KeyboardEvent) => { - if (e.repeat) { - return; - } - if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { - return; - } - if (e.key === 'Escape') { - // Cancel shape drawing on escape - const selectedEntity = getSelectedEntity(); - if (selectedEntity) { - selectedEntity.adapter.renderer.clearBuffer(); - $lastMouseDownPos.set(null); - } - } else if (e.key === ' ') { - // Select the view tool on space key down - setToolBuffer(getToolState().selected); - setTool('view'); - $spaceKey.set(true); - $lastCursorPos.set(null); - $lastMouseDownPos.set(null); - } - }; - window.addEventListener('keydown', onKeyDown); - - const onKeyUp = (e: KeyboardEvent) => { - if (e.repeat) { - return; - } - if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { - return; - } - if (e.key === ' ') { - // Revert the tool to the previous tool on space key up - const toolBuffer = getToolState().selectedBuffer; - setTool(toolBuffer ?? 'move'); - setToolBuffer(null); - $spaceKey.set(false); - } - manager.preview.tool.render(); - }; - window.addEventListener('keyup', onKeyUp); - - return () => { - stage.off('mousedown mouseup mousemove mouseenter mouseleave wheel dragend'); - window.removeEventListener('keydown', onKeyDown); - window.removeEventListener('keyup', onKeyUp); - }; -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index 003448929c7..eea14e02ba7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -2,6 +2,7 @@ import type { CanvasEntityIdentifier, Coordinate, Rect } from 'features/controlL import type Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; import type { Vector2d } from 'konva/lib/types'; +import { clamp } from 'lodash-es'; import { customAlphabet } from 'nanoid'; import { assert } from 'tsafe'; @@ -129,6 +130,64 @@ export const getIsMouseDown = (e: KonvaEventObject): boolean => e.ev */ export const getIsFocused = (stage: Konva.Stage): boolean => stage.container().contains(document.activeElement); +/** + * Gets the last point of a line as a coordinate. + * @param points An array of numbers representing points as [x1, y1, x2, y2, ...] + * @returns The last point of the line as a coordinate, or null if the line has less than 1 point + */ +export const getLastPointOfLine = (points: number[]): Coordinate | null => { + if (points.length < 2) { + return null; + } + const x = points[points.length - 2]; + const y = points[points.length - 1]; + if (x === undefined || y === undefined) { + return null; + } + return { x, y }; +}; + +export function getIsPrimaryMouseDown(e: KonvaEventObject) { + return e.evt.buttons === 1; +} + +/** + * Calculates the new brush size based on the current brush size and the wheel delta from a mouse wheel event. + * @param brushSize The current brush size + * @param delta The wheel delta + * @returns + */ +export const calculateNewBrushSizeFromWheelDelta = (brushSize: number, delta: number) => { + // This equation was derived by fitting a curve to the desired brush sizes and deltas + // see https://github.com/invoke-ai/InvokeAI/pull/5542#issuecomment-1915847565 + const targetDelta = Math.sign(delta) * 0.7363 * Math.pow(1.0394, brushSize); + // This needs to be clamped to prevent the delta from getting too large + const finalDelta = clamp(targetDelta, -20, 20); + // The new brush size is also clamped to prevent it from getting too large or small + const newBrushSize = clamp(brushSize + finalDelta, 1, 500); + + return newBrushSize; +}; + +/** + * Validates a candidate point by checking if it is at least `minDistance` away from the last point. + * @param candidatePoint The candidate point + * @param lastPoint The last point + * @param minDistance The minimum distance between points + * @returns + */ +export const validateCandidatePoint = ( + candidatePoint: Coordinate, + lastPoint: Coordinate | null, + minDistance: number +): boolean => { + if (!lastPoint) { + return true; + } + + return Math.hypot(lastPoint.x - candidatePoint.x, lastPoint.y - candidatePoint.y) >= minDistance; +}; + /** * Simple util to map an object to its id property. Serves as a minor optimization to avoid recreating a map callback * every time we need to map an object to its id, which happens very often. From 463f3dbb351fe4b96cc162b0b9683d2d62425289 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 26 Aug 2024 18:52:28 +1000 Subject: [PATCH 495/678] feat(ui): normalize all actions to accept an entityIdentifier Previously, canvas actions specific to an entity type only needed the id of that entity type. This allowed you to pass in the id of an entity of the wrong type. All actions for a specific entity now take a full entity identifier, and the entity identifier type can be narrowed. `selectEntity` and `selectEntityOrThrow` now need a full entity identifier, and narrow their return values to a specific entity type _if_ the entity identifier is narrowed. The types for canvas entities are updated with optional type parameters for this purpose. All reducers, actions and components have been updated. --- .../listeners/imageDeletionListeners.ts | 7 +- .../listeners/imageDropped.ts | 12 +- .../listeners/imageUploaded.ts | 6 +- .../listeners/modelsLoaded.ts | 13 +- .../src/common/hooks/useIsReadyToEnqueue.ts | 2 +- .../ControlLayer/ControlLayerBadges.tsx | 6 +- .../ControlLayerControlAdapter.tsx | 18 +- .../ControlLayer/ControlLayerEntityList.tsx | 2 +- .../ControlLayerMenuItemsControlToRaster.tsx | 6 +- ...ontrolLayerMenuItemsTransparencyEffect.tsx | 15 +- .../components/IPAdapter/IPAdapterList.tsx | 2 +- .../IPAdapter/IPAdapterSettings.tsx | 42 +++-- .../InpaintMask/InpaintMaskList.tsx | 2 +- .../InpaintMaskMaskFillColorPicker.tsx | 6 +- .../RasterLayer/RasterLayerEntityList.tsx | 2 +- .../RasterLayerMenuItemsRasterToControl.tsx | 6 +- ...onalGuidanceAddPromptsIPAdapterButtons.tsx | 30 ++-- .../RegionalGuidanceBadges.tsx | 6 +- .../RegionalGuidanceEntityList.tsx | 2 +- .../RegionalGuidanceIPAdapterSettings.tsx | 49 +++--- .../RegionalGuidanceIPAdapters.tsx | 16 +- .../RegionalGuidanceMaskFillColorPicker.tsx | 14 +- ...uidanceMenuItemsAddPromptsAndIPAdapter.tsx | 24 +-- .../RegionalGuidanceMenuItemsAutoNegative.tsx | 10 +- .../RegionalGuidanceNegativePrompt.tsx | 20 +-- .../RegionalGuidancePositivePrompt.tsx | 20 +-- .../RegionalGuidanceSettings.tsx | 22 +-- .../common/CanvasEntityMenuItemsArrange.tsx | 2 +- .../components/common/CanvasEntityOpacity.tsx | 3 +- .../common/CanvasEntityPreviewImage.tsx | 2 +- .../contexts/EntityIdentifierContext.ts | 11 +- .../hooks/useCanvasDeleteLayerHotkey.ts | 3 +- .../hooks/useCanvasResetLayerHotkey.ts | 3 +- .../controlLayers/hooks/useEntityIsEnabled.ts | 2 +- .../hooks/useEntityObjectCount.ts | 2 +- .../hooks/useEntitySelectionColor.ts | 2 +- .../controlLayers/hooks/useEntityTitle.ts | 2 +- .../controlLayers/hooks/useEntityTypeCount.ts | 2 +- .../hooks/useEntityTypeIsHidden.ts | 2 +- .../hooks/useLayerControlAdapter.ts | 7 +- .../konva/CanvasStateApiModule.ts | 2 +- .../controlLayers/store/canvasV2Slice.ts | 66 +------- .../store/controlLayersReducers.ts | 70 ++++---- .../store/inpaintMaskReducers.ts | 38 ++--- .../controlLayers/store/ipAdaptersReducers.ts | 78 +++++---- .../store/rasterLayersReducers.ts | 18 +- .../controlLayers/store/regionsReducers.ts | 144 +++++++++------- .../features/controlLayers/store/selectors.ts | 155 +++++++++++++++++- .../src/features/controlLayers/store/types.ts | 21 ++- .../components/DeleteImageModal.tsx | 2 +- .../deleteImageModal/store/selectors.ts | 2 +- .../components/Boards/DeleteBoardModal.tsx | 2 +- .../src/features/lora/components/LoRAList.tsx | 2 +- .../features/lora/components/LoRASelect.tsx | 3 +- .../parameters/components/Prompts/Prompts.tsx | 2 +- .../queue/components/QueueButtonTooltip.tsx | 2 +- .../AdvancedSettingsAccordion.tsx | 2 +- .../GenerationSettingsAccordion.tsx | 2 +- .../ImageSettingsAccordion.tsx | 2 +- .../RefinerSettingsAccordion.tsx | 2 +- 60 files changed, 588 insertions(+), 430 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts index d6c284f3c06..cf1fc0ff309 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts @@ -2,6 +2,7 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import type { AppDispatch, RootState } from 'app/store/store'; import { entityDeleted, ipaImageChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { getEntityIdentifier } from 'features/controlLayers/store/types'; import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions'; import { isModalOpenChanged } from 'features/deleteImageModal/store/slice'; import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors'; @@ -51,9 +52,9 @@ const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: Im // }; const deleteIPAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { - state.canvasV2.ipAdapters.entities.forEach(({ id, ipAdapter }) => { - if (ipAdapter.image?.image_name === imageDTO.image_name) { - dispatch(ipaImageChanged({ id, imageDTO: null })); + state.canvasV2.ipAdapters.entities.forEach((entity) => { + if (entity.ipAdapter.image?.image_name === imageDTO.image_name) { + dispatch(ipaImageChanged({ entityIdentifier: getEntityIdentifier(entity), imageDTO: null })); } }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index 658b219dbb1..1d6bdacaa69 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -51,7 +51,9 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => activeData.payload.imageDTO ) { const { id } = overData.context; - dispatch(ipaImageChanged({ id, imageDTO: activeData.payload.imageDTO })); + dispatch( + ipaImageChanged({ entityIdentifier: { id, type: 'ip_adapter' }, imageDTO: activeData.payload.imageDTO }) + ); return; } @@ -64,7 +66,13 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => activeData.payload.imageDTO ) { const { id, ipAdapterId } = overData.context; - dispatch(rgIPAdapterImageChanged({ id, ipAdapterId, imageDTO: activeData.payload.imageDTO })); + dispatch( + rgIPAdapterImageChanged({ + entityIdentifier: { id, type: 'regional_guidance' }, + ipAdapterId, + imageDTO: activeData.payload.imageDTO, + }) + ); return; } diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts index e5eaf71c308..c10fc60a4dc 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts @@ -89,14 +89,16 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis if (postUploadAction?.type === 'SET_IPA_IMAGE') { const { id } = postUploadAction; - dispatch(ipaImageChanged({ id, imageDTO })); + dispatch(ipaImageChanged({ entityIdentifier: { id, type: 'ip_adapter' }, imageDTO })); toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage') }); return; } if (postUploadAction?.type === 'SET_RG_IP_ADAPTER_IMAGE') { const { id, ipAdapterId } = postUploadAction; - dispatch(rgIPAdapterImageChanged({ id, ipAdapterId, imageDTO })); + dispatch( + rgIPAdapterImageChanged({ entityIdentifier: { id, type: 'regional_guidance' }, ipAdapterId, imageDTO }) + ); toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage') }); return; } diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts index ade6772b1f3..f83e34b0369 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts @@ -13,6 +13,7 @@ import { rgIPAdapterModelChanged, vaeSelected, } from 'features/controlLayers/store/canvasV2Slice'; +import { getEntityIdentifier } from 'features/controlLayers/store/types'; import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; import { postProcessingModelChanged, upscaleModelChanged } from 'features/parameters/store/upscaleSlice'; import { zParameterModel, zParameterVAEModel } from 'features/parameters/types/parameterSchemas'; @@ -178,7 +179,7 @@ const handleControlAdapterModels: ModelHandler = (models, state, dispatch, _log) if (isModelAvailable) { return; } - dispatch(controlLayerModelChanged({ id: entity.id, modelConfig: null })); + dispatch(controlLayerModelChanged({ entityIdentifier: getEntityIdentifier(entity), modelConfig: null })); }); }; @@ -189,16 +190,18 @@ const handleIPAdapterModels: ModelHandler = (models, state, dispatch, _log) => { if (isModelAvailable) { return; } - dispatch(ipaModelChanged({ id: entity.id, modelConfig: null })); + dispatch(ipaModelChanged({ entityIdentifier: getEntityIdentifier(entity), modelConfig: null })); }); - state.canvasV2.regions.entities.forEach(({ id, ipAdapters }) => { - ipAdapters.forEach(({ id: ipAdapterId, model }) => { + state.canvasV2.regions.entities.forEach((entity) => { + entity.ipAdapters.forEach(({ id: ipAdapterId, model }) => { const isModelAvailable = ipaModels.some((m) => m.key === model?.key); if (isModelAvailable) { return; } - dispatch(rgIPAdapterModelChanged({ id, ipAdapterId, modelConfig: null })); + dispatch( + rgIPAdapterModelChanged({ entityIdentifier: getEntityIdentifier(entity), ipAdapterId, modelConfig: null }) + ); }); }); }; diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index 64d47df650e..cf17a945ed1 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react'; import { $isConnected } from 'app/hooks/useSocketIO'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerBadges.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerBadges.tsx index 28224876ff1..ec68367b3a9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerBadges.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerBadges.tsx @@ -1,15 +1,15 @@ import { Badge } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { selectControlLayerEntityOrThrow } from 'features/controlLayers/store/controlLayersReducers'; +import { selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; export const ControlLayerBadges = memo(() => { - const { id } = useEntityIdentifierContext(); + const entityIdentifier = useEntityIdentifierContext('control_layer'); const { t } = useTranslation(); const withTransparencyEffect = useAppSelector( - (s) => selectControlLayerEntityOrThrow(s.canvasV2, id).withTransparencyEffect + (s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).withTransparencyEffect ); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx index 7df91e0084c..3b9e6d1a312 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx @@ -18,35 +18,35 @@ import type { ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/ export const ControlLayerControlAdapter = memo(() => { const dispatch = useAppDispatch(); - const entityIdentifier = useEntityIdentifierContext(); + const entityIdentifier = useEntityIdentifierContext('control_layer'); const controlAdapter = useControlLayerControlAdapter(entityIdentifier); const onChangeBeginEndStepPct = useCallback( (beginEndStepPct: [number, number]) => { - dispatch(controlLayerBeginEndStepPctChanged({ id: entityIdentifier.id, beginEndStepPct })); + dispatch(controlLayerBeginEndStepPctChanged({ entityIdentifier, beginEndStepPct })); }, - [dispatch, entityIdentifier.id] + [dispatch, entityIdentifier] ); const onChangeControlMode = useCallback( (controlMode: ControlModeV2) => { - dispatch(controlLayerControlModeChanged({ id: entityIdentifier.id, controlMode })); + dispatch(controlLayerControlModeChanged({ entityIdentifier, controlMode })); }, - [dispatch, entityIdentifier.id] + [dispatch, entityIdentifier] ); const onChangeWeight = useCallback( (weight: number) => { - dispatch(controlLayerWeightChanged({ id: entityIdentifier.id, weight })); + dispatch(controlLayerWeightChanged({ entityIdentifier, weight })); }, - [dispatch, entityIdentifier.id] + [dispatch, entityIdentifier] ); const onChangeModel = useCallback( (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => { - dispatch(controlLayerModelChanged({ id: entityIdentifier.id, modelConfig })); + dispatch(controlLayerModelChanged({ entityIdentifier, modelConfig })); }, - [dispatch, entityIdentifier.id] + [dispatch, entityIdentifier] ); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx index f19e4ca6f31..8f94481f867 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx @@ -3,7 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList'; import { ControlLayer } from 'features/controlLayers/components/ControlLayer/ControlLayer'; import { mapId } from 'features/controlLayers/konva/util'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; import { memo } from 'react'; const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsControlToRaster.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsControlToRaster.tsx index e64df4e9e5c..924122fe240 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsControlToRaster.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsControlToRaster.tsx @@ -9,11 +9,11 @@ import { PiLightningBold } from 'react-icons/pi'; export const ControlLayerMenuItemsControlToRaster = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const entityIdentifier = useEntityIdentifierContext(); + const entityIdentifier = useEntityIdentifierContext('control_layer'); const convertControlLayerToRasterLayer = useCallback(() => { - dispatch(controlLayerConvertedToRasterLayer({ id: entityIdentifier.id })); - }, [dispatch, entityIdentifier.id]); + dispatch(controlLayerConvertedToRasterLayer({ entityIdentifier })); + }, [dispatch, entityIdentifier]); return ( }> diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsTransparencyEffect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsTransparencyEffect.tsx index fe53a785fd2..5342e975bae 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsTransparencyEffect.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsTransparencyEffect.tsx @@ -2,11 +2,8 @@ import { MenuItem } from '@invoke-ai/ui-library'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { - controlLayerWithTransparencyEffectToggled, - selectCanvasV2Slice, -} from 'features/controlLayers/store/canvasV2Slice'; -import { selectControlLayerEntityOrThrow } from 'features/controlLayers/store/controlLayersReducers'; +import { controlLayerWithTransparencyEffectToggled } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiDropHalfBold } from 'react-icons/pi'; @@ -14,18 +11,18 @@ import { PiDropHalfBold } from 'react-icons/pi'; export const ControlLayerMenuItemsTransparencyEffect = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const entityIdentifier = useEntityIdentifierContext(); + const entityIdentifier = useEntityIdentifierContext('control_layer'); const selectWithTransparencyEffect = useMemo( () => createSelector(selectCanvasV2Slice, (canvasV2) => { - const entity = selectControlLayerEntityOrThrow(canvasV2, entityIdentifier.id); + const entity = selectEntityOrThrow(canvasV2, entityIdentifier); return entity.withTransparencyEffect; }), - [entityIdentifier.id] + [entityIdentifier] ); const withTransparencyEffect = useAppSelector(selectWithTransparencyEffect); const onToggle = useCallback(() => { - dispatch(controlLayerWithTransparencyEffectToggled({ id: entityIdentifier.id })); + dispatch(controlLayerWithTransparencyEffectToggled({ entityIdentifier })); }, [dispatch, entityIdentifier]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterList.tsx index b0ad9108a8c..cdfcc897b55 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterList.tsx @@ -4,7 +4,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList'; import { IPAdapter } from 'features/controlLayers/components/IPAdapter/IPAdapter'; import { mapId } from 'features/controlLayers/konva/util'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; import { memo } from 'react'; const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx index d0d91646e96..0f8152fd9ff 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx @@ -13,7 +13,7 @@ import { ipaModelChanged, ipaWeightChanged, } from 'features/controlLayers/store/canvasV2Slice'; -import { selectIPAdapterEntityOrThrow } from 'features/controlLayers/store/ipAdaptersReducers'; +import { selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types'; import type { IPAImageDropData } from 'features/dnd/types'; import { memo, useCallback, useMemo } from 'react'; @@ -24,53 +24,59 @@ import { IPAdapterModel } from './IPAdapterModel'; export const IPAdapterSettings = memo(() => { const dispatch = useAppDispatch(); - const { id } = useEntityIdentifierContext(); - const ipAdapter = useAppSelector((s) => selectIPAdapterEntityOrThrow(s.canvasV2, id).ipAdapter); + const entityIdentifier = useEntityIdentifierContext('ip_adapter'); + const ipAdapter = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).ipAdapter); const onChangeBeginEndStepPct = useCallback( (beginEndStepPct: [number, number]) => { - dispatch(ipaBeginEndStepPctChanged({ id, beginEndStepPct })); + dispatch(ipaBeginEndStepPctChanged({ entityIdentifier, beginEndStepPct })); }, - [dispatch, id] + [dispatch, entityIdentifier] ); const onChangeWeight = useCallback( (weight: number) => { - dispatch(ipaWeightChanged({ id, weight })); + dispatch(ipaWeightChanged({ entityIdentifier, weight })); }, - [dispatch, id] + [dispatch, entityIdentifier] ); const onChangeIPMethod = useCallback( (method: IPMethodV2) => { - dispatch(ipaMethodChanged({ id, method })); + dispatch(ipaMethodChanged({ entityIdentifier, method })); }, - [dispatch, id] + [dispatch, entityIdentifier] ); const onChangeModel = useCallback( (modelConfig: IPAdapterModelConfig) => { - dispatch(ipaModelChanged({ id, modelConfig })); + dispatch(ipaModelChanged({ entityIdentifier, modelConfig })); }, - [dispatch, id] + [dispatch, entityIdentifier] ); const onChangeCLIPVisionModel = useCallback( (clipVisionModel: CLIPVisionModelV2) => { - dispatch(ipaCLIPVisionModelChanged({ id, clipVisionModel })); + dispatch(ipaCLIPVisionModelChanged({ entityIdentifier, clipVisionModel })); }, - [dispatch, id] + [dispatch, entityIdentifier] ); const onChangeImage = useCallback( (imageDTO: ImageDTO | null) => { - dispatch(ipaImageChanged({ id, imageDTO })); + dispatch(ipaImageChanged({ entityIdentifier, imageDTO })); }, - [dispatch, id] + [dispatch, entityIdentifier] ); - const droppableData = useMemo(() => ({ actionType: 'SET_IPA_IMAGE', context: { id }, id }), [id]); - const postUploadAction = useMemo(() => ({ type: 'SET_IPA_IMAGE', id }), [id]); + const droppableData = useMemo( + () => ({ actionType: 'SET_IPA_IMAGE', context: { id: entityIdentifier.id }, id: entityIdentifier.id }), + [entityIdentifier.id] + ); + const postUploadAction = useMemo( + () => ({ type: 'SET_IPA_IMAGE', id: entityIdentifier.id }), + [entityIdentifier.id] + ); return ( @@ -95,7 +101,7 @@ export const IPAdapterSettings = memo(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskList.tsx index 42351a0dabb..b4f92759e47 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskList.tsx @@ -3,7 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList'; import { InpaintMask } from 'features/controlLayers/components/InpaintMask/InpaintMask'; import { mapId } from 'features/controlLayers/konva/util'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; import { memo } from 'react'; const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx index 97855daad49..426d5261f55 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx @@ -5,7 +5,7 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { MaskFillStyle } from 'features/controlLayers/components/common/MaskFillStyle'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { inpaintMaskFillColorChanged, inpaintMaskFillStyleChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { selectInpaintMaskEntityOrThrow } from 'features/controlLayers/store/inpaintMaskReducers'; +import { selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import type { FillStyle, RgbColor } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -13,8 +13,8 @@ import { useTranslation } from 'react-i18next'; export const InpaintMaskMaskFillColorPicker = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const entityIdentifier = useEntityIdentifierContext(); - const fill = useAppSelector((s) => selectInpaintMaskEntityOrThrow(s.canvasV2, entityIdentifier.id).fill); + const entityIdentifier = useEntityIdentifierContext('inpaint_mask'); + const fill = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).fill); const onChangeFillColor = useCallback( (color: RgbColor) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerEntityList.tsx index 5c5ee557dc6..82bf728d0b8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerEntityList.tsx @@ -3,7 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList'; import { RasterLayer } from 'features/controlLayers/components/RasterLayer/RasterLayer'; import { mapId } from 'features/controlLayers/konva/util'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; import { memo } from 'react'; const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl.tsx index 57c97b3d876..f844f3aa389 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl.tsx @@ -9,11 +9,11 @@ import { PiLightningBold } from 'react-icons/pi'; export const RasterLayerMenuItemsRasterToControl = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const entityIdentifier = useEntityIdentifierContext(); + const entityIdentifier = useEntityIdentifierContext('raster_layer'); const convertRasterLayerToControlLayer = useCallback(() => { - dispatch(rasterLayerConvertedToControlLayer({ id: entityIdentifier.id })); - }, [dispatch, entityIdentifier.id]); + dispatch(rasterLayerConvertedToControlLayer({ entityIdentifier })); + }, [dispatch, entityIdentifier]); return ( }> diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons.tsx index 45333207145..4e1a302233e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons.tsx @@ -1,44 +1,42 @@ import { Button, Flex } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { rgIPAdapterAdded, rgNegativePromptChanged, rgPositivePromptChanged, - selectCanvasV2Slice, } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPlusBold } from 'react-icons/pi'; -type AddPromptButtonProps = { - id: string; -}; - -export const RegionalGuidanceAddPromptsIPAdapterButtons = ({ id }: AddPromptButtonProps) => { +export const RegionalGuidanceAddPromptsIPAdapterButtons = () => { + const entityIdentifier = useEntityIdentifierContext('regional_guidance'); const { t } = useTranslation(); const dispatch = useAppDispatch(); const selectValidActions = useMemo( () => createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { - const rg = canvasV2.regions.entities.find((rg) => rg.id === id); + const entity = selectEntityOrThrow(canvasV2, entityIdentifier); return { - canAddPositivePrompt: rg?.positivePrompt === null, - canAddNegativePrompt: rg?.negativePrompt === null, + canAddPositivePrompt: entity?.positivePrompt === null, + canAddNegativePrompt: entity?.negativePrompt === null, }; }), - [id] + [entityIdentifier] ); const validActions = useAppSelector(selectValidActions); const addPositivePrompt = useCallback(() => { - dispatch(rgPositivePromptChanged({ id, prompt: '' })); - }, [dispatch, id]); + dispatch(rgPositivePromptChanged({ entityIdentifier, prompt: '' })); + }, [dispatch, entityIdentifier]); const addNegativePrompt = useCallback(() => { - dispatch(rgNegativePromptChanged({ id, prompt: '' })); - }, [dispatch, id]); + dispatch(rgNegativePromptChanged({ entityIdentifier, prompt: '' })); + }, [dispatch, entityIdentifier]); const addIPAdapter = useCallback(() => { - dispatch(rgIPAdapterAdded({ id })); - }, [dispatch, id]); + dispatch(rgIPAdapterAdded({ entityIdentifier })); + }, [dispatch, entityIdentifier]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges.tsx index fb35b5068e0..be928090f1d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges.tsx @@ -1,14 +1,14 @@ import { Badge } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers'; +import { selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; export const RegionalGuidanceBadges = memo(() => { - const { id } = useEntityIdentifierContext(); + const entityIdentifier = useEntityIdentifierContext('regional_guidance'); const { t } = useTranslation(); - const autoNegative = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).autoNegative); + const autoNegative = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).autoNegative); return ( <> diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList.tsx index 8e4a5f588ac..ef6faa51b19 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList.tsx @@ -3,7 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList'; import { RegionalGuidance } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidance'; import { mapId } from 'features/controlLayers/konva/util'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; import { memo } from 'react'; const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx index 47497848bf6..cc60a8a023d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx @@ -5,6 +5,7 @@ import { Weight } from 'features/controlLayers/components/common/Weight'; import { IPAdapterImagePreview } from 'features/controlLayers/components/IPAdapter/IPAdapterImagePreview'; import { IPAdapterMethod } from 'features/controlLayers/components/IPAdapter/IPAdapterMethod'; import { IPAdapterModel } from 'features/controlLayers/components/IPAdapter/IPAdapterModel'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { rgIPAdapterBeginEndStepPctChanged, rgIPAdapterCLIPVisionModelChanged, @@ -14,7 +15,7 @@ import { rgIPAdapterModelChanged, rgIPAdapterWeightChanged, } from 'features/controlLayers/store/canvasV2Slice'; -import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers'; +import { selectRegionalGuidanceIPAdapter } from 'features/controlLayers/store/selectors'; import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types'; import type { RGIPAdapterImageDropData } from 'features/dnd/types'; import { memo, useCallback, useMemo } from 'react'; @@ -23,71 +24,75 @@ import type { ImageDTO, IPAdapterModelConfig, RGIPAdapterImagePostUploadAction } import { assert } from 'tsafe'; type Props = { - id: string; ipAdapterId: string; ipAdapterNumber: number; }; -export const RegionalGuidanceIPAdapterSettings = memo(({ id, ipAdapterId, ipAdapterNumber }: Props) => { +export const RegionalGuidanceIPAdapterSettings = memo(({ ipAdapterId, ipAdapterNumber }: Props) => { + const entityIdentifier = useEntityIdentifierContext('regional_guidance'); const dispatch = useAppDispatch(); const onDeleteIPAdapter = useCallback(() => { - dispatch(rgIPAdapterDeleted({ id, ipAdapterId })); - }, [dispatch, ipAdapterId, id]); + dispatch(rgIPAdapterDeleted({ entityIdentifier, ipAdapterId })); + }, [dispatch, entityIdentifier, ipAdapterId]); const ipAdapter = useAppSelector((s) => { - const ipa = selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).ipAdapters.find((ipa) => ipa.id === ipAdapterId); + const ipa = selectRegionalGuidanceIPAdapter(s.canvasV2, entityIdentifier, ipAdapterId); assert(ipa, `Regional GuidanceIP Adapter with id ${ipAdapterId} not found`); return ipa; }); const onChangeBeginEndStepPct = useCallback( (beginEndStepPct: [number, number]) => { - dispatch(rgIPAdapterBeginEndStepPctChanged({ id, ipAdapterId, beginEndStepPct })); + dispatch(rgIPAdapterBeginEndStepPctChanged({ entityIdentifier, ipAdapterId, beginEndStepPct })); }, - [dispatch, ipAdapterId, id] + [dispatch, entityIdentifier, ipAdapterId] ); const onChangeWeight = useCallback( (weight: number) => { - dispatch(rgIPAdapterWeightChanged({ id, ipAdapterId, weight })); + dispatch(rgIPAdapterWeightChanged({ entityIdentifier, ipAdapterId, weight })); }, - [dispatch, ipAdapterId, id] + [dispatch, entityIdentifier, ipAdapterId] ); const onChangeIPMethod = useCallback( (method: IPMethodV2) => { - dispatch(rgIPAdapterMethodChanged({ id, ipAdapterId, method })); + dispatch(rgIPAdapterMethodChanged({ entityIdentifier, ipAdapterId, method })); }, - [dispatch, ipAdapterId, id] + [dispatch, entityIdentifier, ipAdapterId] ); const onChangeModel = useCallback( (modelConfig: IPAdapterModelConfig) => { - dispatch(rgIPAdapterModelChanged({ id, ipAdapterId, modelConfig })); + dispatch(rgIPAdapterModelChanged({ entityIdentifier, ipAdapterId, modelConfig })); }, - [dispatch, ipAdapterId, id] + [dispatch, entityIdentifier, ipAdapterId] ); const onChangeCLIPVisionModel = useCallback( (clipVisionModel: CLIPVisionModelV2) => { - dispatch(rgIPAdapterCLIPVisionModelChanged({ id, ipAdapterId, clipVisionModel })); + dispatch(rgIPAdapterCLIPVisionModelChanged({ entityIdentifier, ipAdapterId, clipVisionModel })); }, - [dispatch, ipAdapterId, id] + [dispatch, entityIdentifier, ipAdapterId] ); const onChangeImage = useCallback( (imageDTO: ImageDTO | null) => { - dispatch(rgIPAdapterImageChanged({ id, ipAdapterId, imageDTO })); + dispatch(rgIPAdapterImageChanged({ entityIdentifier, ipAdapterId, imageDTO })); }, - [dispatch, ipAdapterId, id] + [dispatch, entityIdentifier, ipAdapterId] ); const droppableData = useMemo( - () => ({ actionType: 'SET_RG_IP_ADAPTER_IMAGE', context: { id, ipAdapterId }, id }), - [ipAdapterId, id] + () => ({ + actionType: 'SET_RG_IP_ADAPTER_IMAGE', + context: { id: entityIdentifier.id, ipAdapterId }, + id: entityIdentifier.id, + }), + [entityIdentifier.id, ipAdapterId] ); const postUploadAction = useMemo( - () => ({ type: 'SET_RG_IP_ADAPTER_IMAGE', id, ipAdapterId }), - [ipAdapterId, id] + () => ({ type: 'SET_RG_IP_ADAPTER_IMAGE', id: entityIdentifier.id, ipAdapterId }), + [entityIdentifier.id, ipAdapterId] ); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapters.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapters.tsx index 6967848d65e..ae47edb3e4f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapters.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapters.tsx @@ -3,25 +3,23 @@ import { EMPTY_ARRAY } from 'app/store/constants'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { RegionalGuidanceIPAdapterSettings } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; -import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { selectCanvasV2Slice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import { Fragment, memo, useMemo } from 'react'; -type Props = { - id: string; -}; +export const RegionalGuidanceIPAdapters = memo(() => { + const entityIdentifier = useEntityIdentifierContext('regional_guidance'); -export const RegionalGuidanceIPAdapters = memo(({ id }: Props) => { const selectIPAdapterIds = useMemo( () => createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { - const ipAdapterIds = selectRegionalGuidanceEntityOrThrow(canvasV2, id).ipAdapters.map(({ id }) => id); + const ipAdapterIds = selectEntityOrThrow(canvasV2, entityIdentifier).ipAdapters.map(({ id }) => id); if (ipAdapterIds.length === 0) { return EMPTY_ARRAY; } return ipAdapterIds; }), - [id] + [entityIdentifier] ); const ipAdapterIds = useAppSelector(selectIPAdapterIds); @@ -35,7 +33,7 @@ export const RegionalGuidanceIPAdapters = memo(({ id }: Props) => { {ipAdapterIds.map((ipAdapterId, index) => ( {index > 0 && } - + ))} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx index f42e5f44e4c..331401b7051 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx @@ -5,27 +5,27 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { MaskFillStyle } from 'features/controlLayers/components/common/MaskFillStyle'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { rgFillColorChanged, rgFillStyleChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers'; +import { selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import type { FillStyle, RgbColor } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; export const RegionalGuidanceMaskFillColorPicker = memo(() => { - const entityIdentifier = useEntityIdentifierContext(); + const entityIdentifier = useEntityIdentifierContext('regional_guidance'); const { t } = useTranslation(); const dispatch = useAppDispatch(); - const fill = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, entityIdentifier.id).fill); + const fill = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).fill); const onChangeFillColor = useCallback( (color: RgbColor) => { - dispatch(rgFillColorChanged({ id: entityIdentifier.id, color })); + dispatch(rgFillColorChanged({ entityIdentifier, color })); }, - [dispatch, entityIdentifier.id] + [dispatch, entityIdentifier] ); const onChangeFillStyle = useCallback( (style: FillStyle) => { - dispatch(rgFillStyleChanged({ id: entityIdentifier.id, style })); + dispatch(rgFillStyleChanged({ entityIdentifier, style })); }, - [dispatch, entityIdentifier.id] + [dispatch, entityIdentifier] ); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx index 5556c25929d..d47b6cc0f93 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx @@ -6,36 +6,36 @@ import { rgIPAdapterAdded, rgNegativePromptChanged, rgPositivePromptChanged, - selectCanvasV2Slice, } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/selectors'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; export const RegionalGuidanceMenuItemsAddPromptsAndIPAdapter = memo(() => { - const { id } = useEntityIdentifierContext(); + const entityIdentifier = useEntityIdentifierContext('regional_guidance'); const { t } = useTranslation(); const dispatch = useAppDispatch(); const selectValidActions = useMemo( () => createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { - const rg = canvasV2.regions.entities.find((rg) => rg.id === id); + const entity = selectEntity(canvasV2, entityIdentifier); return { - canAddPositivePrompt: rg?.positivePrompt === null, - canAddNegativePrompt: rg?.negativePrompt === null, + canAddPositivePrompt: entity?.positivePrompt === null, + canAddNegativePrompt: entity?.negativePrompt === null, }; }), - [id] + [entityIdentifier] ); const validActions = useAppSelector(selectValidActions); const addPositivePrompt = useCallback(() => { - dispatch(rgPositivePromptChanged({ id: id, prompt: '' })); - }, [dispatch, id]); + dispatch(rgPositivePromptChanged({ entityIdentifier, prompt: '' })); + }, [dispatch, entityIdentifier]); const addNegativePrompt = useCallback(() => { - dispatch(rgNegativePromptChanged({ id: id, prompt: '' })); - }, [dispatch, id]); + dispatch(rgNegativePromptChanged({ entityIdentifier, prompt: '' })); + }, [dispatch, entityIdentifier]); const addIPAdapter = useCallback(() => { - dispatch(rgIPAdapterAdded({ id })); - }, [dispatch, id]); + dispatch(rgIPAdapterAdded({ entityIdentifier })); + }, [dispatch, entityIdentifier]); return ( <> diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAutoNegative.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAutoNegative.tsx index 66ab80be8a5..955cea7d66e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAutoNegative.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAutoNegative.tsx @@ -2,19 +2,19 @@ import { MenuItem } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { rgAutoNegativeToggled } from 'features/controlLayers/store/canvasV2Slice'; -import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers'; +import { selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiSelectionInverseBold } from 'react-icons/pi'; export const RegionalGuidanceMenuItemsAutoNegative = memo(() => { - const { id } = useEntityIdentifierContext(); + const entityIdentifier = useEntityIdentifierContext('regional_guidance'); const { t } = useTranslation(); const dispatch = useAppDispatch(); - const autoNegative = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).autoNegative); + const autoNegative = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).autoNegative); const onClick = useCallback(() => { - dispatch(rgAutoNegativeToggled({ id })); - }, [dispatch, id]); + dispatch(rgAutoNegativeToggled({ entityIdentifier })); + }, [dispatch, entityIdentifier]); return ( } onClick={onClick}> diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceNegativePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceNegativePrompt.tsx index f24dedce035..b83539fbc8d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceNegativePrompt.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceNegativePrompt.tsx @@ -1,8 +1,9 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { RegionalGuidanceDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceDeletePromptButton'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { rgNegativePromptChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers'; +import { selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; @@ -10,24 +11,21 @@ import { usePrompt } from 'features/prompt/usePrompt'; import { memo, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; -type Props = { - id: string; -}; - -export const RegionalGuidanceNegativePrompt = memo(({ id }: Props) => { - const prompt = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).negativePrompt ?? ''); +export const RegionalGuidanceNegativePrompt = memo(() => { + const entityIdentifier = useEntityIdentifierContext('regional_guidance'); + const prompt = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).negativePrompt ?? ''); const dispatch = useAppDispatch(); const textareaRef = useRef(null); const { t } = useTranslation(); const _onChange = useCallback( (v: string) => { - dispatch(rgNegativePromptChanged({ id, prompt: v })); + dispatch(rgNegativePromptChanged({ entityIdentifier, prompt: v })); }, - [dispatch, id] + [dispatch, entityIdentifier] ); const onDeletePrompt = useCallback(() => { - dispatch(rgNegativePromptChanged({ id, prompt: null })); - }, [dispatch, id]); + dispatch(rgNegativePromptChanged({ entityIdentifier, prompt: null })); + }, [dispatch, entityIdentifier]); const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown } = usePrompt({ prompt, textareaRef, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidancePositivePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidancePositivePrompt.tsx index 44a371873e4..e699dcf8b2c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidancePositivePrompt.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidancePositivePrompt.tsx @@ -1,8 +1,9 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { RegionalGuidanceDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceDeletePromptButton'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { rgPositivePromptChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers'; +import { selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; @@ -10,24 +11,21 @@ import { usePrompt } from 'features/prompt/usePrompt'; import { memo, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; -type Props = { - id: string; -}; - -export const RegionalGuidancePositivePrompt = memo(({ id }: Props) => { - const prompt = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).positivePrompt ?? ''); +export const RegionalGuidancePositivePrompt = memo(() => { + const entityIdentifier = useEntityIdentifierContext('regional_guidance'); + const prompt = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).positivePrompt ?? ''); const dispatch = useAppDispatch(); const textareaRef = useRef(null); const { t } = useTranslation(); const _onChange = useCallback( (v: string) => { - dispatch(rgPositivePromptChanged({ id, prompt: v })); + dispatch(rgPositivePromptChanged({ entityIdentifier, prompt: v })); }, - [dispatch, id] + [dispatch, entityIdentifier] ); const onDeletePrompt = useCallback(() => { - dispatch(rgPositivePromptChanged({ id, prompt: null })); - }, [dispatch, id]); + dispatch(rgPositivePromptChanged({ entityIdentifier, prompt: null })); + }, [dispatch, entityIdentifier]); const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown } = usePrompt({ prompt, textareaRef, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx index 54165cfbeb8..b2c1e3db988 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx @@ -3,7 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper'; import { RegionalGuidanceAddPromptsIPAdapterButtons } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers'; +import { selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import { memo } from 'react'; import { RegionalGuidanceIPAdapters } from './RegionalGuidanceIPAdapters'; @@ -11,35 +11,31 @@ import { RegionalGuidanceNegativePrompt } from './RegionalGuidanceNegativePrompt import { RegionalGuidancePositivePrompt } from './RegionalGuidancePositivePrompt'; export const RegionalGuidanceSettings = memo(() => { - const { id } = useEntityIdentifierContext(); + const entityIdentifier = useEntityIdentifierContext('regional_guidance'); const hasPositivePrompt = useAppSelector( - (s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).positivePrompt !== null + (s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).positivePrompt !== null ); const hasNegativePrompt = useAppSelector( - (s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).negativePrompt !== null - ); - const hasIPAdapters = useAppSelector( - (s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).ipAdapters.length > 0 + (s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).negativePrompt !== null ); + const hasIPAdapters = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).ipAdapters.length > 0); return ( - {!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && ( - - )} + {!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && } {hasPositivePrompt && ( <> - + {(hasNegativePrompt || hasIPAdapters) && } )} {hasNegativePrompt && ( <> - + {hasIPAdapters && } )} - {hasIPAdapters && } + {hasIPAdapters && } ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx index 6342613ea5c..1582cce92d1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx @@ -7,8 +7,8 @@ import { entityArrangedForwardOne, entityArrangedToBack, entityArrangedToFront, - selectCanvasV2Slice, } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier, CanvasV2State } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityOpacity.tsx index c5be221c6d2..dc5c5895102 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityOpacity.tsx @@ -15,7 +15,8 @@ import { } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { snapToNearest } from 'features/controlLayers/konva/util'; -import { entityOpacityChanged, selectEntity } from 'features/controlLayers/store/canvasV2Slice'; +import { entityOpacityChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { selectEntity } from 'features/controlLayers/store/selectors'; import { isDrawableEntity } from 'features/controlLayers/store/types'; import { clamp, round } from 'lodash-es'; import type { KeyboardEvent } from 'react'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityPreviewImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityPreviewImage.tsx index 142f2cdacc9..2a7630c1a8a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityPreviewImage.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityPreviewImage.tsx @@ -5,7 +5,7 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { useEntityAdapter } from 'features/controlLayers/contexts/EntityAdapterContext'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants'; -import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/selectors'; import { memo, useEffect, useMemo, useRef } from 'react'; import { useSelector } from 'react-redux'; diff --git a/invokeai/frontend/web/src/features/controlLayers/contexts/EntityIdentifierContext.ts b/invokeai/frontend/web/src/features/controlLayers/contexts/EntityIdentifierContext.ts index 04489e8e9d9..d0d049a2b51 100644 --- a/invokeai/frontend/web/src/features/controlLayers/contexts/EntityIdentifierContext.ts +++ b/invokeai/frontend/web/src/features/controlLayers/contexts/EntityIdentifierContext.ts @@ -1,11 +1,16 @@ -import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import type { CanvasEntityIdentifier, CanvasEntityType } from 'features/controlLayers/store/types'; import { createContext, useContext } from 'react'; import { assert } from 'tsafe'; export const EntityIdentifierContext = createContext(null); -export const useEntityIdentifierContext = (): CanvasEntityIdentifier => { +export const useEntityIdentifierContext = ( + type?: T +): CanvasEntityIdentifier => { const entityIdentifier = useContext(EntityIdentifierContext); assert(entityIdentifier, 'useEntityIdentifier must be used within a EntityIdentifierProvider'); - return entityIdentifier; + if (type) { + assert(entityIdentifier.type === type, 'useEntityIdentifier must be used with the correct type'); + } + return entityIdentifier as CanvasEntityIdentifier; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts index 0baa6823d93..88f444401c9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts @@ -1,7 +1,8 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; -import { entityDeleted, selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { entityDeleted } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; import { useCallback, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts index f15d087b0fb..c752a7348cf 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts @@ -1,7 +1,8 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; -import { entityReset, selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { entityReset } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; import { useCallback, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsEnabled.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsEnabled.ts index e37b402ea6a..022cbeb2121 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsEnabled.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsEnabled.ts @@ -1,6 +1,6 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityObjectCount.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityObjectCount.ts index 29f14377ab9..62f3a8e8d00 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityObjectCount.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityObjectCount.ts @@ -1,6 +1,6 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/selectors'; import { type CanvasEntityIdentifier, isDrawableEntity } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntitySelectionColor.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntitySelectionColor.ts index 2815577bff6..15061d28b86 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntitySelectionColor.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntitySelectionColor.ts @@ -1,7 +1,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { rgbColorToString } from 'common/util/colorCodeTransformers'; -import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts index 1c9586ce996..c5649963d87 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts @@ -1,7 +1,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { useEntityObjectCount } from 'features/controlLayers/hooks/useEntityObjectCount'; -import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeCount.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeCount.ts index f2209af4a96..e770f220fbc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeCount.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeCount.ts @@ -1,6 +1,6 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeIsHidden.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeIsHidden.ts index d35bf9efa0b..0e867be2be4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeIsHidden.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeIsHidden.ts @@ -1,6 +1,6 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts index 827174c2df6..93157c3564b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts @@ -1,8 +1,7 @@ import { createMemoizedAppSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { deepClone } from 'common/util/deepClone'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; -import { selectControlLayerEntityOrThrow } from 'features/controlLayers/store/controlLayersReducers'; +import { selectCanvasV2Slice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier, ControlNetConfig, @@ -14,11 +13,11 @@ import { zModelIdentifierField } from 'features/nodes/types/common'; import { useMemo } from 'react'; import { useControlNetAndT2IAdapterModels, useIPAdapterModels } from 'services/api/hooks/modelsByType'; -export const useControlLayerControlAdapter = (entityIdentifier: CanvasEntityIdentifier) => { +export const useControlLayerControlAdapter = (entityIdentifier: CanvasEntityIdentifier<'control_layer'>) => { const selectControlAdapter = useMemo( () => createMemoizedAppSelector(selectCanvasV2Slice, (canvasV2) => { - const layer = selectControlLayerEntityOrThrow(canvasV2, entityIdentifier.id); + const layer = selectEntityOrThrow(canvasV2, entityIdentifier); return layer.controlAdapter; }), [entityIdentifier] diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index 83ba26d8d20..377c15cebb9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -25,10 +25,10 @@ import { entitySelected, eraserWidthChanged, fillChanged, - selectAllRenderableEntities, toolBufferChanged, toolChanged, } from 'features/controlLayers/store/canvasV2Slice'; +import { selectAllRenderableEntities } from 'features/controlLayers/store/selectors'; import type { CanvasControlLayerState, CanvasEntityIdentifier, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 425e7529ae8..611cf496220 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -1,6 +1,6 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createAction, createSlice } from '@reduxjs/toolkit'; -import type { PersistConfig, RootState } from 'app/store/store'; +import type { PersistConfig } from 'app/store/store'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { deepClone } from 'common/util/deepClone'; import { getPrefixedId } from 'features/controlLayers/konva/util'; @@ -13,6 +13,7 @@ import { lorasReducers } from 'features/controlLayers/store/lorasReducers'; import { paramsReducers } from 'features/controlLayers/store/paramsReducers'; import { rasterLayersReducers } from 'features/controlLayers/store/rasterLayersReducers'; import { regionsReducers } from 'features/controlLayers/store/regionsReducers'; +import { selectAllEntities, selectAllEntitiesOfType, selectEntity } from 'features/controlLayers/store/selectors'; import { sessionReducers } from 'features/controlLayers/store/sessionReducers'; import { settingsReducers } from 'features/controlLayers/store/settingsReducers'; import { toolReducers } from 'features/controlLayers/store/toolReducers'; @@ -25,12 +26,7 @@ import { atom } from 'nanostores'; import { assert } from 'tsafe'; import type { - CanvasControlLayerState, CanvasEntityIdentifier, - CanvasEntityState, - CanvasInpaintMaskState, - CanvasRasterLayerState, - CanvasRegionalGuidanceState, CanvasV2State, Coordinate, EntityBrushLineAddedPayload, @@ -143,60 +139,6 @@ const initialState: CanvasV2State = { }, }; -export function selectEntity(state: CanvasV2State, { id, type }: CanvasEntityIdentifier) { - switch (type) { - case 'raster_layer': - return state.rasterLayers.entities.find((entity) => entity.id === id); - case 'control_layer': - return state.controlLayers.entities.find((entity) => entity.id === id); - case 'inpaint_mask': - return state.inpaintMasks.entities.find((entity) => entity.id === id); - case 'regional_guidance': - return state.regions.entities.find((entity) => entity.id === id); - case 'ip_adapter': - return state.ipAdapters.entities.find((entity) => entity.id === id); - default: - return; - } -} - -function selectAllEntitiesOfType(state: CanvasV2State, type: CanvasEntityState['type']): CanvasEntityState[] { - switch (type) { - case 'raster_layer': - return state.rasterLayers.entities; - case 'control_layer': - return state.controlLayers.entities; - case 'inpaint_mask': - return state.inpaintMasks.entities; - case 'regional_guidance': - return state.regions.entities; - case 'ip_adapter': - return state.ipAdapters.entities; - } -} - -function selectAllEntities(state: CanvasV2State): CanvasEntityState[] { - // These are in the same order as they are displayed in the list! - return [ - ...state.inpaintMasks.entities.toReversed(), - ...state.regions.entities.toReversed(), - ...state.ipAdapters.entities.toReversed(), - ...state.controlLayers.entities.toReversed(), - ...state.rasterLayers.entities.toReversed(), - ]; -} - -export function selectAllRenderableEntities( - state: CanvasV2State -): (CanvasRasterLayerState | CanvasControlLayerState | CanvasInpaintMaskState | CanvasRegionalGuidanceState)[] { - return [ - ...state.rasterLayers.entities, - ...state.controlLayers.entities, - ...state.inpaintMasks.entities, - ...state.regions.entities, - ]; -} - export const canvasV2Slice = createSlice({ name: 'canvasV2', initialState, @@ -217,7 +159,7 @@ export const canvasV2Slice = createSlice({ const { entityIdentifier } = action.payload; state.selectedEntityIdentifier = entityIdentifier; }, - entityNameChanged: (state, action: PayloadAction) => { + entityNameChanged: (state, action: PayloadAction>) => { const { entityIdentifier, name } = action.payload; const entity = selectEntity(state, entityIdentifier); if (!entity) { @@ -617,8 +559,6 @@ export const { sessionModeChanged, } = canvasV2Slice.actions; -export const selectCanvasV2Slice = (state: RootState) => state.canvasV2; - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const migrate = (state: any): any => { return state; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts index d6399846912..7ed4a341cee 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts @@ -1,10 +1,10 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { deepClone } from 'common/util/deepClone'; import { getPrefixedId } from 'features/controlLayers/konva/util'; +import { selectEntity } from 'features/controlLayers/store/selectors'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { merge, omit } from 'lodash-es'; import type { ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types'; -import { assert } from 'tsafe'; import type { CanvasControlLayerState, @@ -12,18 +12,11 @@ import type { CanvasV2State, ControlModeV2, ControlNetConfig, + EntityIdentifierPayload, T2IAdapterConfig, } from './types'; import { getEntityIdentifier, initialControlNet } from './types'; -const selectControlLayerEntity = (state: CanvasV2State, id: string) => - state.controlLayers.entities.find((entity) => entity.id === id); -export const selectControlLayerEntityOrThrow = (state: CanvasV2State, id: string) => { - const layer = selectControlLayerEntity(state, id); - assert(layer, `Layer with id ${id} not found`); - return layer; -}; - export const controlLayersReducers = { controlLayerAdded: { reducer: ( @@ -58,9 +51,9 @@ export const controlLayersReducers = { state.selectedEntityIdentifier = { type: 'control_layer', id: data.id }; }, controlLayerConvertedToRasterLayer: { - reducer: (state, action: PayloadAction<{ id: string; newId: string }>) => { - const { id, newId } = action.payload; - const layer = selectControlLayerEntity(state, id); + reducer: (state, action: PayloadAction>) => { + const { entityIdentifier, newId } = action.payload; + const layer = selectEntity(state, entityIdentifier); if (!layer) { return; } @@ -73,26 +66,30 @@ export const controlLayersReducers = { }; // Remove the control layer - state.controlLayers.entities = state.controlLayers.entities.filter((layer) => layer.id !== id); + state.controlLayers.entities = state.controlLayers.entities.filter((layer) => layer.id !== entityIdentifier.id); // Add the new raster layer state.rasterLayers.entities.push(rasterLayerState); state.selectedEntityIdentifier = { type: rasterLayerState.type, id: rasterLayerState.id }; }, - prepare: (payload: { id: string }) => ({ + prepare: (payload: EntityIdentifierPayload) => ({ payload: { ...payload, newId: getPrefixedId('raster_layer') }, }), }, controlLayerModelChanged: ( state, - action: PayloadAction<{ - id: string; - modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null; - }> + action: PayloadAction< + EntityIdentifierPayload< + { + modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null; + }, + 'control_layer' + > + > ) => { - const { id, modelConfig } = action.payload; - const layer = selectControlLayerEntity(state, id); + const { entityIdentifier, modelConfig } = action.payload; + const layer = selectEntity(state, entityIdentifier); if (!layer || !layer.controlAdapter) { return; } @@ -118,17 +115,23 @@ export const controlLayersReducers = { layer.controlAdapter = t2iAdapterConfig; } }, - controlLayerControlModeChanged: (state, action: PayloadAction<{ id: string; controlMode: ControlModeV2 }>) => { - const { id, controlMode } = action.payload; - const layer = selectControlLayerEntity(state, id); + controlLayerControlModeChanged: ( + state, + action: PayloadAction> + ) => { + const { entityIdentifier, controlMode } = action.payload; + const layer = selectEntity(state, entityIdentifier); if (!layer || !layer.controlAdapter || layer.controlAdapter.type !== 'controlnet') { return; } layer.controlAdapter.controlMode = controlMode; }, - controlLayerWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { - const { id, weight } = action.payload; - const layer = selectControlLayerEntity(state, id); + controlLayerWeightChanged: ( + state, + action: PayloadAction> + ) => { + const { entityIdentifier, weight } = action.payload; + const layer = selectEntity(state, entityIdentifier); if (!layer || !layer.controlAdapter) { return; } @@ -136,18 +139,21 @@ export const controlLayersReducers = { }, controlLayerBeginEndStepPctChanged: ( state, - action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }> + action: PayloadAction> ) => { - const { id, beginEndStepPct } = action.payload; - const layer = selectControlLayerEntity(state, id); + const { entityIdentifier, beginEndStepPct } = action.payload; + const layer = selectEntity(state, entityIdentifier); if (!layer || !layer.controlAdapter) { return; } layer.controlAdapter.beginEndStepPct = beginEndStepPct; }, - controlLayerWithTransparencyEffectToggled: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const layer = selectControlLayerEntity(state, id); + controlLayerWithTransparencyEffectToggled: ( + state, + action: PayloadAction> + ) => { + const { entityIdentifier } = action.payload; + const layer = selectEntity(state, entityIdentifier); if (!layer) { return; } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts index 9952c8a3458..9056f082a60 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts @@ -1,23 +1,15 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import { - type CanvasInpaintMaskState, - type CanvasV2State, - type EntityIdentifierPayload, - type FillStyle, - getEntityIdentifier, - type RgbColor, +import { selectEntity } from 'features/controlLayers/store/selectors'; +import type { + CanvasInpaintMaskState, + CanvasV2State, + EntityIdentifierPayload, + FillStyle, + RgbColor, } from 'features/controlLayers/store/types'; +import { getEntityIdentifier } from 'features/controlLayers/store/types'; import { merge } from 'lodash-es'; -import { assert } from 'tsafe'; - -const selectInpaintMaskEntity = (state: CanvasV2State, id: string) => - state.inpaintMasks.entities.find((layer) => layer.id === id); -export const selectInpaintMaskEntityOrThrow = (state: CanvasV2State, id: string) => { - const entity = selectInpaintMaskEntity(state, id); - assert(entity, `Inpaint mask with id ${id} not found`); - return entity; -}; export const inpaintMaskReducers = { inpaintMaskAdded: { @@ -54,17 +46,23 @@ export const inpaintMaskReducers = { state.inpaintMasks.entities = [data]; state.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id }; }, - inpaintMaskFillColorChanged: (state, action: PayloadAction>) => { + inpaintMaskFillColorChanged: ( + state, + action: PayloadAction> + ) => { const { color, entityIdentifier } = action.payload; - const entity = selectInpaintMaskEntity(state, entityIdentifier.id); + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } entity.fill.color = color; }, - inpaintMaskFillStyleChanged: (state, action: PayloadAction>) => { + inpaintMaskFillStyleChanged: ( + state, + action: PayloadAction> + ) => { const { style, entityIdentifier } = action.payload; - const entity = selectInpaintMaskEntity(state, entityIdentifier.id); + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts index 1fa95139751..d3bb67c9ce3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts @@ -1,23 +1,20 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { deepClone } from 'common/util/deepClone'; import { getPrefixedId } from 'features/controlLayers/konva/util'; +import { selectEntity } from 'features/controlLayers/store/selectors'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { merge } from 'lodash-es'; import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; -import { assert } from 'tsafe'; -import { v4 as uuidv4 } from 'uuid'; -import type { CanvasIPAdapterState, CanvasV2State, CLIPVisionModelV2, IPMethodV2 } from './types'; +import type { + CanvasIPAdapterState, + CanvasV2State, + CLIPVisionModelV2, + EntityIdentifierPayload, + IPMethodV2, +} from './types'; import { getEntityIdentifier, imageDTOToImageWithDims, initialIPAdapter } from './types'; -const selectIPAdapterEntity = (state: CanvasV2State, id: string) => - state.ipAdapters.entities.find((ipa) => ipa.id === id); -export const selectIPAdapterEntityOrThrow = (state: CanvasV2State, id: string) => { - const entity = selectIPAdapterEntity(state, id); - assert(entity, `IP Adapter with id ${id} not found`); - return entity; -}; - export const ipAdaptersReducers = { ipaAdded: { reducer: ( @@ -47,52 +44,61 @@ export const ipAdaptersReducers = { state.ipAdapters.entities.push(data); state.selectedEntityIdentifier = { type: 'ip_adapter', id: data.id }; }, - ipaImageChanged: { - reducer: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => { - const { id, imageDTO } = action.payload; - const entity = selectIPAdapterEntity(state, id); - if (!entity) { - return; - } - entity.ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; - }, - prepare: (payload: { id: string; imageDTO: ImageDTO | null }) => ({ payload: { ...payload, objectId: uuidv4() } }), + ipaImageChanged: ( + state, + action: PayloadAction> + ) => { + const { entityIdentifier, imageDTO } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } + entity.ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; }, - ipaMethodChanged: (state, action: PayloadAction<{ id: string; method: IPMethodV2 }>) => { - const { id, method } = action.payload; - const entity = selectIPAdapterEntity(state, id); + ipaMethodChanged: (state, action: PayloadAction>) => { + const { entityIdentifier, method } = action.payload; + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } entity.ipAdapter.method = method; }, - ipaModelChanged: (state, action: PayloadAction<{ id: string; modelConfig: IPAdapterModelConfig | null }>) => { - const { id, modelConfig } = action.payload; - const entity = selectIPAdapterEntity(state, id); + ipaModelChanged: ( + state, + action: PayloadAction> + ) => { + const { entityIdentifier, modelConfig } = action.payload; + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } entity.ipAdapter.model = modelConfig ? zModelIdentifierField.parse(modelConfig) : null; }, - ipaCLIPVisionModelChanged: (state, action: PayloadAction<{ id: string; clipVisionModel: CLIPVisionModelV2 }>) => { - const { id, clipVisionModel } = action.payload; - const entity = selectIPAdapterEntity(state, id); + ipaCLIPVisionModelChanged: ( + state, + action: PayloadAction> + ) => { + const { entityIdentifier, clipVisionModel } = action.payload; + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } entity.ipAdapter.clipVisionModel = clipVisionModel; }, - ipaWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { - const { id, weight } = action.payload; - const entity = selectIPAdapterEntity(state, id); + ipaWeightChanged: (state, action: PayloadAction>) => { + const { entityIdentifier, weight } = action.payload; + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } entity.ipAdapter.weight = weight; }, - ipaBeginEndStepPctChanged: (state, action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }>) => { - const { id, beginEndStepPct } = action.payload; - const entity = selectIPAdapterEntity(state, id); + ipaBeginEndStepPctChanged: ( + state, + action: PayloadAction> + ) => { + const { entityIdentifier, beginEndStepPct } = action.payload; + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts index 24270555008..0db696b84e0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts @@ -1,14 +1,12 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { deepClone } from 'common/util/deepClone'; import { getPrefixedId } from 'features/controlLayers/konva/util'; +import { selectEntity } from 'features/controlLayers/store/selectors'; import { merge } from 'lodash-es'; -import type { CanvasControlLayerState, CanvasRasterLayerState, CanvasV2State } from './types'; +import type { CanvasControlLayerState, CanvasRasterLayerState, CanvasV2State, EntityIdentifierPayload } from './types'; import { getEntityIdentifier, initialControlNet } from './types'; -const selectRasterLayerEntity = (state: CanvasV2State, id: string) => - state.rasterLayers.entities.find((layer) => layer.id === id); - export const rasterLayersReducers = { rasterLayerAdded: { reducer: ( @@ -38,12 +36,12 @@ export const rasterLayersReducers = { rasterLayerRecalled: (state, action: PayloadAction<{ data: CanvasRasterLayerState }>) => { const { data } = action.payload; state.rasterLayers.entities.push(data); - state.selectedEntityIdentifier = { type: 'raster_layer', id: data.id }; + state.selectedEntityIdentifier = getEntityIdentifier(data); }, rasterLayerConvertedToControlLayer: { - reducer: (state, action: PayloadAction<{ id: string; newId: string }>) => { - const { id, newId } = action.payload; - const layer = selectRasterLayerEntity(state, id); + reducer: (state, action: PayloadAction>) => { + const { entityIdentifier, newId } = action.payload; + const layer = selectEntity(state, entityIdentifier); if (!layer) { return; } @@ -58,14 +56,14 @@ export const rasterLayersReducers = { }; // Remove the raster layer - state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => layer.id !== id); + state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => layer.id !== entityIdentifier.id); // Add the converted control layer state.controlLayers.entities.push(controlLayerState); state.selectedEntityIdentifier = { type: controlLayerState.type, id: controlLayerState.id }; }, - prepare: (payload: { id: string }) => ({ + prepare: (payload: EntityIdentifierPayload) => ({ payload: { ...payload, newId: getPrefixedId('control_layer') }, }), }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index 96d47395d68..ff2729fcc55 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -1,9 +1,11 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { deepClone } from 'common/util/deepClone'; import { getPrefixedId } from 'features/controlLayers/konva/util'; +import { selectEntity, selectRegionalGuidanceIPAdapter } from 'features/controlLayers/store/selectors'; import type { CanvasV2State, CLIPVisionModelV2, + EntityIdentifierPayload, FillStyle, IPMethodV2, RegionalGuidanceIPAdapterConfig, @@ -17,22 +19,6 @@ import { assert } from 'tsafe'; import type { CanvasRegionalGuidanceState } from './types'; -const selectRegionalGuidanceEntity = (state: CanvasV2State, id: string) => { - return state.regions.entities.find((rg) => rg.id === id); -}; -const selectRegionalGuidanceIPAdapter = (state: CanvasV2State, id: string, ipAdapterId: string) => { - const entity = state.regions.entities.find((rg) => rg.id === id); - if (!entity) { - return; - } - return entity.ipAdapters.find((ipa) => ipa.id === ipAdapterId); -}; -export const selectRegionalGuidanceEntityOrThrow = (state: CanvasV2State, id: string) => { - const rg = selectRegionalGuidanceEntity(state, id); - assert(rg, `Region with id ${id} not found`); - return rg; -}; - const DEFAULT_MASK_COLORS: RgbColor[] = [ { r: 121, g: 157, b: 219 }, // rgb(121, 157, 219) { r: 131, g: 214, b: 131 }, // rgb(131, 214, 131) @@ -94,42 +80,54 @@ export const regionsReducers = { state.regions.entities.push(data); state.selectedEntityIdentifier = { type: 'regional_guidance', id: data.id }; }, - rgPositivePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => { - const { id, prompt } = action.payload; - const entity = selectRegionalGuidanceEntity(state, id); + rgPositivePromptChanged: ( + state, + action: PayloadAction> + ) => { + const { entityIdentifier, prompt } = action.payload; + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } entity.positivePrompt = prompt; }, - rgNegativePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => { - const { id, prompt } = action.payload; - const entity = selectRegionalGuidanceEntity(state, id); + rgNegativePromptChanged: ( + state, + action: PayloadAction> + ) => { + const { entityIdentifier, prompt } = action.payload; + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } entity.negativePrompt = prompt; }, - rgFillColorChanged: (state, action: PayloadAction<{ id: string; color: RgbColor }>) => { - const { id, color } = action.payload; - const entity = selectRegionalGuidanceEntity(state, id); + rgFillColorChanged: ( + state, + action: PayloadAction> + ) => { + const { entityIdentifier, color } = action.payload; + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } entity.fill.color = color; }, - rgFillStyleChanged: (state, action: PayloadAction<{ id: string; style: FillStyle }>) => { - const { id, style } = action.payload; - const entity = selectRegionalGuidanceEntity(state, id); + rgFillStyleChanged: ( + state, + action: PayloadAction> + ) => { + const { entityIdentifier, style } = action.payload; + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } entity.fill.style = style; }, - rgAutoNegativeToggled: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const rg = selectRegionalGuidanceEntity(state, id); + rgAutoNegativeToggled: (state, action: PayloadAction>) => { + const { entityIdentifier } = action.payload; + const rg = selectEntity(state, entityIdentifier); if (!rg) { return; } @@ -138,10 +136,15 @@ export const regionsReducers = { rgIPAdapterAdded: { reducer: ( state, - action: PayloadAction<{ id: string; ipAdapterId: string; overrides?: Partial }> + action: PayloadAction< + EntityIdentifierPayload< + { ipAdapterId: string; overrides?: Partial }, + 'regional_guidance' + > + > ) => { - const { id, overrides, ipAdapterId } = action.payload; - const entity = selectRegionalGuidanceEntity(state, id); + const { entityIdentifier, overrides, ipAdapterId } = action.payload; + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } @@ -149,13 +152,18 @@ export const regionsReducers = { merge(ipAdapter, overrides); entity.ipAdapters.push(ipAdapter); }, - prepare: (payload: { id: string; overrides?: Partial }) => ({ + prepare: ( + payload: EntityIdentifierPayload<{ overrides?: Partial }, 'regional_guidance'> + ) => ({ payload: { ...payload, ipAdapterId: getPrefixedId('regional_guidance_ip_adapter') }, }), }, - rgIPAdapterDeleted: (state, action: PayloadAction<{ id: string; ipAdapterId: string }>) => { - const { id, ipAdapterId } = action.payload; - const entity = selectRegionalGuidanceEntity(state, id); + rgIPAdapterDeleted: ( + state, + action: PayloadAction> + ) => { + const { entityIdentifier, ipAdapterId } = action.payload; + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } @@ -163,18 +171,23 @@ export const regionsReducers = { }, rgIPAdapterImageChanged: ( state, - action: PayloadAction<{ id: string; ipAdapterId: string; imageDTO: ImageDTO | null }> + action: PayloadAction< + EntityIdentifierPayload<{ ipAdapterId: string; imageDTO: ImageDTO | null }, 'regional_guidance'> + > ) => { - const { id, ipAdapterId, imageDTO } = action.payload; - const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId); + const { entityIdentifier, ipAdapterId, imageDTO } = action.payload; + const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId); if (!ipAdapter) { return; } ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; }, - rgIPAdapterWeightChanged: (state, action: PayloadAction<{ id: string; ipAdapterId: string; weight: number }>) => { - const { id, ipAdapterId, weight } = action.payload; - const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId); + rgIPAdapterWeightChanged: ( + state, + action: PayloadAction> + ) => { + const { entityIdentifier, ipAdapterId, weight } = action.payload; + const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId); if (!ipAdapter) { return; } @@ -182,18 +195,23 @@ export const regionsReducers = { }, rgIPAdapterBeginEndStepPctChanged: ( state, - action: PayloadAction<{ id: string; ipAdapterId: string; beginEndStepPct: [number, number] }> + action: PayloadAction< + EntityIdentifierPayload<{ ipAdapterId: string; beginEndStepPct: [number, number] }, 'regional_guidance'> + > ) => { - const { id, ipAdapterId, beginEndStepPct } = action.payload; - const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId); + const { entityIdentifier, ipAdapterId, beginEndStepPct } = action.payload; + const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId); if (!ipAdapter) { return; } ipAdapter.beginEndStepPct = beginEndStepPct; }, - rgIPAdapterMethodChanged: (state, action: PayloadAction<{ id: string; ipAdapterId: string; method: IPMethodV2 }>) => { - const { id, ipAdapterId, method } = action.payload; - const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId); + rgIPAdapterMethodChanged: ( + state, + action: PayloadAction> + ) => { + const { entityIdentifier, ipAdapterId, method } = action.payload; + const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId); if (!ipAdapter) { return; } @@ -201,14 +219,18 @@ export const regionsReducers = { }, rgIPAdapterModelChanged: ( state, - action: PayloadAction<{ - id: string; - ipAdapterId: string; - modelConfig: IPAdapterModelConfig | null; - }> + action: PayloadAction< + EntityIdentifierPayload< + { + ipAdapterId: string; + modelConfig: IPAdapterModelConfig | null; + }, + 'regional_guidance' + > + > ) => { - const { id, ipAdapterId, modelConfig } = action.payload; - const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId); + const { entityIdentifier, ipAdapterId, modelConfig } = action.payload; + const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId); if (!ipAdapter) { return; } @@ -216,10 +238,12 @@ export const regionsReducers = { }, rgIPAdapterCLIPVisionModelChanged: ( state, - action: PayloadAction<{ id: string; ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }> + action: PayloadAction< + EntityIdentifierPayload<{ ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }, 'regional_guidance'> + > ) => { - const { id, ipAdapterId, clipVisionModel } = action.payload; - const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId); + const { entityIdentifier, ipAdapterId, clipVisionModel } = action.payload; + const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId); if (!ipAdapter) { return; } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts index d78417d55ed..93e837ee2c2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts @@ -1,7 +1,32 @@ import { createSelector } from '@reduxjs/toolkit'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import type { RootState } from 'app/store/store'; +import type { + CanvasControlLayerState, + CanvasEntityIdentifier, + CanvasEntityState, + CanvasInpaintMaskState, + CanvasRasterLayerState, + CanvasRegionalGuidanceState, + CanvasV2State, +} from 'features/controlLayers/store/types'; import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; +import { assert } from 'tsafe'; +/** + * Selects the canvasV2 slice from the root state + */ +export const selectCanvasV2Slice = (state: RootState) => state.canvasV2; + +/** + * Selects the total canvas entity count: + * - Regions + * - IP adapters + * - Raster layers + * - Control layers + * - Inpaint masks + * + * It does not check for validity of the entities. + */ export const selectEntityCount = createSelector(selectCanvasV2Slice, (canvasV2) => { return ( canvasV2.regions.entities.length + @@ -12,6 +37,134 @@ export const selectEntityCount = createSelector(selectCanvasV2Slice, (canvasV2) ); }); +/** + * Selects the optimal dimension for the canvas based on the currently-model + */ export const selectOptimalDimension = createSelector(selectCanvasV2Slice, (canvasV2) => { return getOptimalDimension(canvasV2.params.model); }); + +/** + * Selects a single entity from the canvasV2 slice. If the entity identifier is narrowed to a specific type, the + * return type will be narrowed as well. + */ +export function selectEntity( + state: CanvasV2State, + entityIdentifier: T +): Extract | undefined { + const { id, type } = entityIdentifier; + + let entity: CanvasEntityState | undefined = undefined; + + switch (type) { + case 'raster_layer': + entity = state.rasterLayers.entities.find((entity) => entity.id === id); + break; + case 'control_layer': + entity = state.controlLayers.entities.find((entity) => entity.id === id); + break; + case 'inpaint_mask': + entity = state.inpaintMasks.entities.find((entity) => entity.id === id); + break; + case 'regional_guidance': + entity = state.regions.entities.find((entity) => entity.id === id); + break; + case 'ip_adapter': + entity = state.ipAdapters.entities.find((entity) => entity.id === id); + break; + } + + // This cast is safe, but TS seems to be unable to infer the type + return entity as Extract; +} + +/** + * Selected an entity from the canvasV2 slice. If the entity is not found, an error is thrown. + * Wrapper around {@link selectEntity}. + */ +export function selectEntityOrThrow( + state: CanvasV2State, + entityIdentifier: T +): Extract { + const entity = selectEntity(state, entityIdentifier); + assert(entity, `Entity with id ${entityIdentifier.id} not found`); + return entity; +} + +/** + * Selects all entities of the given type. + */ +export function selectAllEntitiesOfType( + state: CanvasV2State, + type: T +): Extract[] { + let entities: CanvasEntityState[] = []; + + switch (type) { + case 'raster_layer': + entities = state.rasterLayers.entities; + break; + case 'control_layer': + entities = state.controlLayers.entities; + break; + case 'inpaint_mask': + entities = state.inpaintMasks.entities; + break; + case 'regional_guidance': + entities = state.regions.entities; + break; + case 'ip_adapter': + entities = state.ipAdapters.entities; + break; + } + + // This cast is safe, but TS seems to be unable to infer the type + return entities as Extract[]; +} + +/** + * Selects all entities, in the order they are displayed in the list. + */ +export function selectAllEntities(state: CanvasV2State): CanvasEntityState[] { + // These are in the same order as they are displayed in the list! + return [ + ...state.inpaintMasks.entities.toReversed(), + ...state.regions.entities.toReversed(), + ...state.ipAdapters.entities.toReversed(), + ...state.controlLayers.entities.toReversed(), + ...state.rasterLayers.entities.toReversed(), + ]; +} + +/** + * Selects all _renderable_ entities: + * - Raster layers + * - Control layers + * - Inpaint masks + * - Regional guidance + */ +export function selectAllRenderableEntities( + state: CanvasV2State +): (CanvasRasterLayerState | CanvasControlLayerState | CanvasInpaintMaskState | CanvasRegionalGuidanceState)[] { + return [ + ...state.rasterLayers.entities, + ...state.controlLayers.entities, + ...state.inpaintMasks.entities, + ...state.regions.entities, + ]; +} + +/** + * Selects the IP adapter for the specific Regional Guidance layer. + */ +export function selectRegionalGuidanceIPAdapter( + state: CanvasV2State, + entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>, + ipAdapterId: string +) { + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return undefined; + } + return entity.ipAdapters.find((ipAdapter) => ipAdapter.id === ipAdapterId); +} diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index b9ac58c8196..95eed0e25d0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,3 +1,4 @@ +import type { SerializableObject } from 'common/types'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { AspectRatioState } from 'features/parameters/components/DocumentSize/types'; @@ -692,7 +693,9 @@ export type CanvasEntityState = | CanvasRegionalGuidanceState | CanvasInpaintMaskState | CanvasIPAdapterState; -export type CanvasEntityIdentifier = Pick; + +export type CanvasEntityType = CanvasEntityState['type']; +export type CanvasEntityIdentifier = { id: string; type: T }; export type LoRA = { id: string; @@ -819,7 +822,17 @@ export type StageAttrs = { scale: number; }; -export type EntityIdentifierPayload = { entityIdentifier: CanvasEntityIdentifier } & T; +export type EntityIdentifierPayload< + T extends SerializableObject | void = void, + U extends CanvasEntityType = CanvasEntityType, +> = T extends void + ? { + entityIdentifier: CanvasEntityIdentifier; + } + : { + entityIdentifier: CanvasEntityIdentifier; + } & T; + export type EntityMovedPayload = EntityIdentifierPayload<{ position: Coordinate }>; export type EntityBrushLineAddedPayload = EntityIdentifierPayload<{ brushLine: CanvasBrushLineState }>; export type EntityEraserLineAddedPayload = EntityIdentifierPayload<{ eraserLine: CanvasEraserLineState }>; @@ -858,6 +871,8 @@ export function isDrawableEntity( return isDrawableEntityType(entity.type); } -export const getEntityIdentifier = (entity: CanvasEntityState): CanvasEntityIdentifier => { +export const getEntityIdentifier = ( + entity: Extract +): CanvasEntityIdentifier => { return { id: entity.id, type: entity.type }; }; diff --git a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx index 30f59aed1fc..c42d92736c7 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx +++ b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx @@ -1,7 +1,7 @@ import { ConfirmationAlertDialog, Divider, Flex, FormControl, FormLabel, Switch, Text } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions'; import { getImageUsage, selectImageUsage } from 'features/deleteImageModal/store/selectors'; import { diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts index c5491f8bc38..036c25b9c20 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts @@ -1,5 +1,5 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; import type { CanvasV2State } from 'features/controlLayers/store/types'; import { selectDeleteImageModalSlice } from 'features/deleteImageModal/store/slice'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx index 9ef21177da6..42ecf584d3a 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx @@ -13,7 +13,7 @@ import { import { skipToken } from '@reduxjs/toolkit/query'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; import ImageUsageMessage from 'features/deleteImageModal/components/ImageUsageMessage'; import { getImageUsage } from 'features/deleteImageModal/store/selectors'; import type { ImageUsage } from 'features/deleteImageModal/store/types'; diff --git a/invokeai/frontend/web/src/features/lora/components/LoRAList.tsx b/invokeai/frontend/web/src/features/lora/components/LoRAList.tsx index b5a2b1bba97..e96e38797d4 100644 --- a/invokeai/frontend/web/src/features/lora/components/LoRAList.tsx +++ b/invokeai/frontend/web/src/features/lora/components/LoRAList.tsx @@ -1,7 +1,7 @@ import { Flex } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; import { LoRACard } from 'features/lora/components/LoRACard'; import { memo } from 'react'; diff --git a/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx b/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx index 8296031418f..9d0a8164aea 100644 --- a/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx +++ b/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx @@ -4,7 +4,8 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; -import { loraAdded, selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { loraAdded } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useLoRAModels } from 'services/api/hooks/modelsByType'; diff --git a/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx b/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx index f0fd3b3afab..2afb05f1351 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx @@ -1,7 +1,7 @@ import { Flex } from '@invoke-ai/ui-library'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; import { ParamNegativePrompt } from 'features/parameters/components/Core/ParamNegativePrompt'; import { ParamPositivePrompt } from 'features/parameters/components/Core/ParamPositivePrompt'; import { ParamSDXLNegativeStylePrompt } from 'features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt'; diff --git a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx b/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx index fe44e550256..0d6c2163570 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx @@ -2,7 +2,7 @@ import { Divider, Flex, ListItem, Text, Tooltip, UnorderedList } from '@invoke-a import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { useIsReadyToEnqueue } from 'common/hooks/useIsReadyToEnqueue'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; import type { PropsWithChildren } from 'react'; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx index b58b5540fe3..3a67ffa41a1 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx @@ -3,7 +3,7 @@ import { Flex, FormControlGroup, StandaloneAccordion } from '@invoke-ai/ui-libra import { skipToken } from '@reduxjs/toolkit/query'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; import ParamCFGRescaleMultiplier from 'features/parameters/components/Advanced/ParamCFGRescaleMultiplier'; import ParamClipSkip from 'features/parameters/components/Advanced/ParamClipSkip'; import ParamSeamlessXAxis from 'features/parameters/components/Seamless/ParamSeamlessXAxis'; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx index 0fd4cdcdb06..2d694013699 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx @@ -3,7 +3,7 @@ import { Box, Expander, Flex, FormControlGroup, StandaloneAccordion } from '@inv import { EMPTY_ARRAY } from 'app/store/constants'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; import { LoRAList } from 'features/lora/components/LoRAList'; import LoRASelect from 'features/lora/components/LoRASelect'; import ParamCFGScale from 'features/parameters/components/Core/ParamCFGScale'; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx index 92c480aecbe..51dd3ed578b 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx @@ -2,7 +2,7 @@ import type { FormLabelProps } from '@invoke-ai/ui-library'; import { Expander, Flex, FormControlGroup, StandaloneAccordion } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; import { HrfSettings } from 'features/hrf/components/HrfSettings'; import { selectHrfSlice } from 'features/hrf/store/hrfSlice'; import ParamScaleBeforeProcessing from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing'; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion.tsx index 76f7ebe6841..c854c4960e1 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion.tsx @@ -2,7 +2,7 @@ import type { FormLabelProps } from '@invoke-ai/ui-library'; import { Flex, FormControlGroup, StandaloneAccordion, Text } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; import ParamSDXLRefinerCFGScale from 'features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale'; import ParamSDXLRefinerModelSelect from 'features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect'; import ParamSDXLRefinerNegativeAestheticScore from 'features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore'; From 9480691de5eb0326a63de72d91f3b551ce6c07cf Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 26 Aug 2024 19:14:56 +1000 Subject: [PATCH 496/678] feat(ui): move ephemeral state into canvas classes Things like `$lastCursorPos` are now created within the canvas drawing classes. Consumers in react access them via `useCanvasManager`. For example: ```tsx const canvasManager = useCanvasManager(); const lastCursorPos = useStore(canvasManager.stateApi.$lastCursorPos); ``` --- .../controlLayers/components/CanvasScale.tsx | 9 ++--- .../components/HeadsUpDisplay.tsx | 22 ++++------- .../StagingArea/StagingAreaToolbar.tsx | 9 +++-- .../konva/CanvasStateApiModule.ts | 38 +++++++++---------- .../controlLayers/store/canvasV2Slice.ts | 22 ----------- .../ParametersPanelTextToImage.tsx | 2 - 6 files changed, 34 insertions(+), 68 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasScale.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasScale.tsx index 5d8d2ee11be..05157b577b1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasScale.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasScale.tsx @@ -14,10 +14,9 @@ import { PopoverTrigger, } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; -import { $canvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/controlLayers/konva/constants'; import { snapToNearest } from 'features/controlLayers/konva/util'; -import { $stageAttrs } from 'features/controlLayers/store/canvasV2Slice'; import { clamp, round } from 'lodash-es'; import { computed } from 'nanostores'; import type { KeyboardEvent } from 'react'; @@ -72,12 +71,10 @@ const sliderDefaultValue = mapScaleToSliderValue(100); const snapCandidates = marks.slice(1, marks.length - 1); -const $scale = computed($stageAttrs, (attrs) => attrs.scale); - export const CanvasScale = memo(() => { const { t } = useTranslation(); - const canvasManager = useStore($canvasManager); - const scale = useStore($scale); + const canvasManager = useCanvasManager(); + const scale = useStore(computed(canvasManager.stateApi.$stageAttrs, (attrs) => attrs.scale)); const [localScale, setLocalScale] = useState(scale * 100); const onChangeSlider = useCallback( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx index 5f3bcab13cd..ec7bcdaa258 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx @@ -1,24 +1,18 @@ import { Box, Flex, Text } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; -import { - $isDrawing, - $isMouseDown, - $lastAddedPoint, - $lastCursorPos, - $lastMouseDownPos, - $stageAttrs, -} from 'features/controlLayers/store/canvasV2Slice'; +import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { round } from 'lodash-es'; import { memo } from 'react'; export const HeadsUpDisplay = memo(() => { - const stageAttrs = useStore($stageAttrs); - const cursorPos = useStore($lastCursorPos); - const isDrawing = useStore($isDrawing); - const isMouseDown = useStore($isMouseDown); - const lastMouseDownPos = useStore($lastMouseDownPos); - const lastAddedPoint = useStore($lastAddedPoint); + const canvasManager = useCanvasManager(); + const stageAttrs = useStore(canvasManager.stateApi.$stageAttrs); + const cursorPos = useStore(canvasManager.stateApi.$lastCursorPos); + const isDrawing = useStore(canvasManager.stateApi.$isDrawing); + const isMouseDown = useStore(canvasManager.stateApi.$isMouseDown); + const lastMouseDownPos = useStore(canvasManager.stateApi.$lastMouseDownPos); + const lastAddedPoint = useStore(canvasManager.stateApi.$lastAddedPoint); const bbox = useAppSelector((s) => s.canvasV2.bbox); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx index 45e51f59fc9..ed4bcb38b3d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx @@ -2,8 +2,8 @@ import { Button, ButtonGroup, IconButton } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { INTERACTION_SCOPES, useScopeOnMount } from 'common/hooks/interactionScopes'; +import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { - $shouldShowStagedImage, sessionNextStagedImageSelected, sessionPrevStagedImageSelected, sessionStagedImageDiscarded, @@ -28,7 +28,8 @@ import { useChangeImageIsIntermediateMutation } from 'services/api/endpoints/ima export const StagingAreaToolbar = memo(() => { const dispatch = useAppDispatch(); const session = useAppSelector((s) => s.canvasV2.session); - const shouldShowStagedImage = useStore($shouldShowStagedImage); + const canvasManager = useCanvasManager(); + const shouldShowStagedImage = useStore(canvasManager.stateApi.$shouldShowStagedImage); const images = useMemo(() => session.stagedImages, [session]); const selectedImage = useMemo(() => { return images[session.selectedStagedImageIndex] ?? null; @@ -70,8 +71,8 @@ export const StagingAreaToolbar = memo(() => { }, [dispatch]); const onToggleShouldShowStagedImage = useCallback(() => { - $shouldShowStagedImage.set(!shouldShowStagedImage); - }, [shouldShowStagedImage]); + canvasManager.stateApi.$shouldShowStagedImage.set(!shouldShowStagedImage); + }, [canvasManager.stateApi.$shouldShowStagedImage, shouldShowStagedImage]); const onSaveStagingImage = useCallback(() => { if (!selectedImage) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index 377c15cebb9..c9c20892ec1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -4,16 +4,6 @@ import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLaye import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; import { - $isDrawing, - $isMouseDown, - $isProcessingTransform, - $lastAddedPoint, - $lastCursorPos, - $lastMouseDownPos, - $shouldShowStagedImage, - $spaceKey, - $stageAttrs, - $transformingEntity, bboxChanged, brushWidthChanged, entityBrushLineAdded, @@ -36,6 +26,7 @@ import type { CanvasRasterLayerState, CanvasRegionalGuidanceState, CanvasV2State, + Coordinate, EntityBrushLineAddedPayload, EntityEraserLineAddedPayload, EntityIdentifierPayload, @@ -45,6 +36,7 @@ import type { Rect, RgbaColor, RgbColor, + StageAttrs, Tool, } from 'features/controlLayers/store/types'; import { RGBA_BLACK } from 'features/controlLayers/store/types'; @@ -243,8 +235,8 @@ export class CanvasStateApiModule { } }; - $transformingEntity = $transformingEntity; - $isProcessingTransform = $isProcessingTransform; + $transformingEntity = atom(null); + $isProcessingTransform = atom(false); $toolState: WritableAtom = atom(); $currentFill: WritableAtom = atom(); @@ -253,17 +245,23 @@ export class CanvasStateApiModule { $colorUnderCursor: WritableAtom = atom(RGBA_BLACK); // Read-write state, ephemeral interaction state - $isDrawing = $isDrawing; - $isMouseDown = $isMouseDown; - $lastAddedPoint = $lastAddedPoint; - $lastMouseDownPos = $lastMouseDownPos; - $lastCursorPos = $lastCursorPos; + $isDrawing = atom(false); + $isMouseDown = atom(false); + $lastAddedPoint = atom(null); + $lastMouseDownPos = atom(null); + $lastCursorPos = atom(null); $lastCanvasProgressEvent = $lastCanvasProgressEvent; - $spaceKey = $spaceKey; + $spaceKey = atom(false); $altKey = $alt; $ctrlKey = $ctrl; $metaKey = $meta; $shiftKey = $shift; - $shouldShowStagedImage = $shouldShowStagedImage; - $stageAttrs = $stageAttrs; + $shouldShowStagedImage = atom(true); + $stageAttrs = atom({ + x: 0, + y: 0, + width: 0, + height: 0, + scale: 0, + }); } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 611cf496220..81b688c950c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -22,20 +22,17 @@ import { simplifyFlatNumbersArray } from 'features/controlLayers/util/simplify'; import { initialAspectRatioState } from 'features/parameters/components/DocumentSize/constants'; import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; import { pick } from 'lodash-es'; -import { atom } from 'nanostores'; import { assert } from 'tsafe'; import type { CanvasEntityIdentifier, CanvasV2State, - Coordinate, EntityBrushLineAddedPayload, EntityEraserLineAddedPayload, EntityIdentifierPayload, EntityMovedPayload, EntityRasterizedPayload, EntityRectAddedPayload, - StageAttrs, } from './types'; import { getEntityIdentifier, isDrawableEntity } from './types'; @@ -564,25 +561,6 @@ const migrate = (state: any): any => { return state; }; -// Ephemeral state that does not need to be in redux -export const $isPreviewVisible = atom(true); -export const $stageAttrs = atom({ - x: 0, - y: 0, - width: 0, - height: 0, - scale: 0, -}); -export const $shouldShowStagedImage = atom(true); -export const $isDrawing = atom(false); -export const $isMouseDown = atom(false); -export const $lastAddedPoint = atom(null); -export const $lastMouseDownPos = atom(null); -export const $lastCursorPos = atom(null); -export const $spaceKey = atom(false); -export const $transformingEntity = atom(null); -export const $isProcessingTransform = atom(false); - export const canvasV2PersistConfig: PersistConfig = { name: canvasV2Slice.name, initialState, diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx index dad4fdc215b..85905142d3a 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx @@ -5,7 +5,6 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants'; import { CanvasEntityListMenuButton } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuButton'; import { CanvasPanelContent } from 'features/controlLayers/components/CanvasPanelContent'; -import { $isPreviewVisible } from 'features/controlLayers/store/canvasV2Slice'; import { selectEntityCount } from 'features/controlLayers/store/selectors'; import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; import { Prompts } from 'features/parameters/components/Prompts/Prompts'; @@ -56,7 +55,6 @@ const ParametersPanelTextToImage = () => { if (i === 1) { dispatch(isImageViewerOpenChanged(false)); } - $isPreviewVisible.set(i === 0); }, [dispatch] ); From 85eff566ddb64f82ee4dc9c92d6eb7a1e2e71077 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 26 Aug 2024 19:59:06 +1000 Subject: [PATCH 497/678] feat(ui): move selected tool and tool buffer out of redux This ephemeral state can live in the canvas classes. --- .../components/ControlLayersToolbar.tsx | 8 +- .../components/Tool/ToolBboxButton.tsx | 18 ++--- .../components/Tool/ToolBrushButton.tsx | 18 ++--- .../components/Tool/ToolColorPickerButton.tsx | 22 +++--- .../components/Tool/ToolEraserButton.tsx | 18 ++--- .../components/Tool/ToolMoveButton.tsx | 18 ++--- .../components/Tool/ToolRectButton.tsx | 18 ++--- .../components/Tool/ToolSettings.tsx | 19 +++++ .../components/Tool/ToolViewButton.tsx | 17 ++--- .../controlLayers/components/Tool/hooks.ts | 19 +++++ .../controlLayers/konva/CanvasBboxModule.ts | 31 ++++++-- .../controlLayers/konva/CanvasFilterModule.ts | 2 +- .../konva/CanvasObjectRenderer.ts | 9 ++- .../konva/CanvasRenderingModule.ts | 4 +- .../konva/CanvasStateApiModule.ts | 10 +-- .../controlLayers/konva/CanvasToolModule.ts | 76 +++++++++---------- .../controlLayers/konva/CanvasTransformer.ts | 25 ++---- .../controlLayers/store/canvasV2Slice.ts | 10 +-- .../controlLayers/store/sessionReducers.ts | 9 --- .../controlLayers/store/toolReducers.ts | 8 +- .../src/features/controlLayers/store/types.ts | 2 - 21 files changed, 173 insertions(+), 188 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolSettings.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Tool/hooks.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index 26df1cf1d86..885d47d7ff2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -1,14 +1,12 @@ /* eslint-disable i18next/no-literal-string */ import { Flex, Spacer } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; import { CanvasModeSwitcher } from 'features/controlLayers/components/CanvasModeSwitcher'; import { CanvasResetViewButton } from 'features/controlLayers/components/CanvasResetViewButton'; import { CanvasScale } from 'features/controlLayers/components/CanvasScale'; import { CanvasSettingsPopover } from 'features/controlLayers/components/Settings/CanvasSettingsPopover'; -import { ToolBrushWidth } from 'features/controlLayers/components/Tool/ToolBrushWidth'; import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser'; -import { ToolEraserWidth } from 'features/controlLayers/components/Tool/ToolEraserWidth'; import { ToolFillColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker'; +import { ToolSettings } from 'features/controlLayers/components/Tool/ToolSettings'; import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup'; import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton'; @@ -16,15 +14,13 @@ import { ViewerToggleMenu } from 'features/gallery/components/ImageViewer/Viewer import { memo } from 'react'; export const ControlLayersToolbar = memo(() => { - const tool = useAppSelector((s) => s.canvasV2.tool.selected); return ( - {tool === 'brush' && } - {tool === 'eraser' && } + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBboxButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBboxButton.tsx index 07841e9d062..5f676d5f989 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBboxButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBboxButton.tsx @@ -1,29 +1,25 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks'; import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering'; import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming'; -import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { memo, useCallback, useMemo } from 'react'; +import { memo, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiBoundingBoxBold } from 'react-icons/pi'; export const ToolBboxButton = memo(() => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); + const selectBbox = useSelectTool('bbox'); + const isSelected = useToolIsSelected('bbox'); const isFiltering = useIsFiltering(); const isTransforming = useIsTransforming(); const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); - const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'bbox'); const isDisabled = useMemo(() => { return isTransforming || isFiltering || isStaging; }, [isFiltering, isStaging, isTransforming]); - const onClick = useCallback(() => { - dispatch(toolChanged('bbox')); - }, [dispatch]); - - useHotkeys('q', onClick, { enabled: !isDisabled || isSelected }, [onClick, isSelected, isDisabled]); + useHotkeys('q', selectBbox, { enabled: !isDisabled || isSelected }, [selectBbox, isSelected, isDisabled]); return ( { icon={} colorScheme={isSelected ? 'invokeBlue' : 'base'} variant="outline" - onClick={onClick} + onClick={selectBbox} isDisabled={isDisabled} /> ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushButton.tsx index 666b110ef42..c508ab4d703 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushButton.tsx @@ -1,21 +1,21 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks'; import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering'; import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming'; -import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; import { isDrawableEntityType } from 'features/controlLayers/store/types'; -import { memo, useCallback, useMemo } from 'react'; +import { memo, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiPaintBrushBold } from 'react-icons/pi'; export const ToolBrushButton = memo(() => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); const isFiltering = useIsFiltering(); const isTransforming = useIsTransforming(); const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); - const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'brush'); + const selectBrush = useSelectTool('brush'); + const isSelected = useToolIsSelected('brush'); const isDrawingToolAllowed = useAppSelector((s) => { if (!s.canvasV2.selectedEntityIdentifier?.type) { return false; @@ -27,11 +27,7 @@ export const ToolBrushButton = memo(() => { return isTransforming || isFiltering || isStaging || !isDrawingToolAllowed; }, [isDrawingToolAllowed, isFiltering, isStaging, isTransforming]); - const onClick = useCallback(() => { - dispatch(toolChanged('brush')); - }, [dispatch]); - - useHotkeys('b', onClick, { enabled: !isDisabled || isSelected }, [isDisabled, isSelected, onClick]); + useHotkeys('b', selectBrush, { enabled: !isDisabled || isSelected }, [isDisabled, isSelected, selectBrush]); return ( { icon={} colorScheme={isSelected ? 'invokeBlue' : 'base'} variant="outline" - onClick={onClick} + onClick={selectBrush} isDisabled={isDisabled} /> ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolColorPickerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolColorPickerButton.tsx index 5e271e8b829..e4258170ac8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolColorPickerButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolColorPickerButton.tsx @@ -1,30 +1,30 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks'; import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering'; import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming'; -import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { memo, useCallback, useMemo } from 'react'; +import { memo, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiEyedropperBold } from 'react-icons/pi'; export const ToolColorPickerButton = memo(() => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); const isFiltering = useIsFiltering(); const isTransforming = useIsTransforming(); - const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'colorPicker'); + const selectColorPicker = useSelectTool('colorPicker'); + const isSelected = useToolIsSelected('colorPicker'); const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); const isDisabled = useMemo(() => { return isTransforming || isFiltering || isStaging; }, [isFiltering, isStaging, isTransforming]); - const onClick = useCallback(() => { - dispatch(toolChanged('colorPicker')); - }, [dispatch]); - - useHotkeys('i', onClick, { enabled: !isDisabled || isSelected }, [onClick, isSelected, isDisabled]); + useHotkeys('i', selectColorPicker, { enabled: !isDisabled || isSelected }, [ + selectColorPicker, + isSelected, + isDisabled, + ]); return ( { icon={} colorScheme={isSelected ? 'invokeBlue' : 'base'} variant="outline" - onClick={onClick} + onClick={selectColorPicker} isDisabled={isDisabled} /> ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserButton.tsx index eb10687d2d0..78b59421138 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserButton.tsx @@ -1,21 +1,21 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks'; import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering'; import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming'; -import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; import { isDrawableEntityType } from 'features/controlLayers/store/types'; -import { memo, useCallback, useMemo } from 'react'; +import { memo, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiEraserBold } from 'react-icons/pi'; export const ToolEraserButton = memo(() => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); const isFiltering = useIsFiltering(); const isTransforming = useIsTransforming(); const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); - const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'eraser'); + const selectEraser = useSelectTool('eraser'); + const isSelected = useToolIsSelected('eraser'); const isDrawingToolAllowed = useAppSelector((s) => { if (!s.canvasV2.selectedEntityIdentifier?.type) { return false; @@ -26,11 +26,7 @@ export const ToolEraserButton = memo(() => { return isTransforming || isFiltering || isStaging || !isDrawingToolAllowed; }, [isDrawingToolAllowed, isFiltering, isStaging, isTransforming]); - const onClick = useCallback(() => { - dispatch(toolChanged('eraser')); - }, [dispatch]); - - useHotkeys('e', onClick, { enabled: !isDisabled || isSelected }, [isDisabled, isSelected, onClick]); + useHotkeys('e', selectEraser, { enabled: !isDisabled || isSelected }, [isDisabled, isSelected, selectEraser]); return ( { icon={} colorScheme={isSelected ? 'invokeBlue' : 'base'} variant="outline" - onClick={onClick} + onClick={selectEraser} isDisabled={isDisabled} /> ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolMoveButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolMoveButton.tsx index 8a9b00ae414..91a8155a8e7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolMoveButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolMoveButton.tsx @@ -1,20 +1,20 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks'; import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering'; import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming'; -import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; import { isDrawableEntityType } from 'features/controlLayers/store/types'; -import { memo, useCallback, useMemo } from 'react'; +import { memo, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiCursorBold } from 'react-icons/pi'; export const ToolMoveButton = memo(() => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); const isFiltering = useIsFiltering(); const isTransforming = useIsTransforming(); - const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'move'); + const selectMove = useSelectTool('move'); + const isSelected = useToolIsSelected('move'); const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); const isDrawingToolAllowed = useAppSelector((s) => { if (!s.canvasV2.selectedEntityIdentifier?.type) { @@ -26,11 +26,7 @@ export const ToolMoveButton = memo(() => { return isTransforming || isFiltering || isStaging || !isDrawingToolAllowed; }, [isDrawingToolAllowed, isFiltering, isStaging, isTransforming]); - const onClick = useCallback(() => { - dispatch(toolChanged('move')); - }, [dispatch]); - - useHotkeys('v', onClick, { enabled: !isDisabled || isSelected }, [isDisabled, isSelected, onClick]); + useHotkeys('v', selectMove, { enabled: !isDisabled || isSelected }, [isDisabled, isSelected, selectMove]); return ( { icon={} colorScheme={isSelected ? 'invokeBlue' : 'base'} variant="outline" - onClick={onClick} + onClick={selectMove} isDisabled={isDisabled} /> ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolRectButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolRectButton.tsx index 9c908d16f85..3b5b1e338f2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolRectButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolRectButton.tsx @@ -1,18 +1,18 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks'; import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering'; import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming'; -import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; import { isDrawableEntityType } from 'features/controlLayers/store/types'; -import { memo, useCallback, useMemo } from 'react'; +import { memo, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiRectangleBold } from 'react-icons/pi'; export const ToolRectButton = memo(() => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'rect'); + const selectRect = useSelectTool('rect'); + const isSelected = useToolIsSelected('rect'); const isFiltering = useIsFiltering(); const isTransforming = useIsTransforming(); const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); @@ -27,11 +27,7 @@ export const ToolRectButton = memo(() => { return isTransforming || isFiltering || isStaging || !isDrawingToolAllowed; }, [isDrawingToolAllowed, isFiltering, isStaging, isTransforming]); - const onClick = useCallback(() => { - dispatch(toolChanged('rect')); - }, [dispatch]); - - useHotkeys('u', onClick, { enabled: !isDisabled || isSelected }, [isDisabled, isSelected, onClick]); + useHotkeys('u', selectRect, { enabled: !isDisabled || isSelected }, [isDisabled, isSelected, selectRect]); return ( { icon={} colorScheme={isSelected ? 'invokeBlue' : 'base'} variant="outline" - onClick={onClick} + onClick={selectRect} isDisabled={isDisabled} /> ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolSettings.tsx new file mode 100644 index 00000000000..aef9e5d2e3a --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolSettings.tsx @@ -0,0 +1,19 @@ +import { useStore } from '@nanostores/react'; +import { ToolBrushWidth } from 'features/controlLayers/components/Tool/ToolBrushWidth'; +import { ToolEraserWidth } from 'features/controlLayers/components/Tool/ToolEraserWidth'; +import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; +import { memo } from 'react'; + +export const ToolSettings = memo(() => { + const canvasManager = useCanvasManager(); + const tool = useStore(canvasManager.stateApi.$tool); + if (tool === 'brush') { + return ; + } + if (tool === 'eraser') { + return ; + } + return null; +}); + +ToolSettings.displayName = 'ToolSettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolViewButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolViewButton.tsx index be45717be67..6b94eaf0cce 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolViewButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolViewButton.tsx @@ -1,28 +1,25 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks'; import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering'; import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming'; -import { toolChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { memo, useCallback, useMemo } from 'react'; +import { memo, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiHandBold } from 'react-icons/pi'; export const ToolViewButton = memo(() => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); const isTransforming = useIsTransforming(); const isFiltering = useIsFiltering(); const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); - const isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'view'); + const selectView = useSelectTool('view'); + const isSelected = useToolIsSelected('view'); const isDisabled = useMemo(() => { return isTransforming || isFiltering || isStaging; }, [isFiltering, isStaging, isTransforming]); - const onClick = useCallback(() => { - dispatch(toolChanged('view')); - }, [dispatch]); - useHotkeys('h', onClick, { enabled: !isDisabled || isSelected }, [onClick, isSelected, isDisabled]); + useHotkeys('h', selectView, { enabled: !isDisabled || isSelected }, [selectView, isSelected, isDisabled]); return ( { icon={} colorScheme={isSelected ? 'invokeBlue' : 'base'} variant="outline" - onClick={onClick} + onClick={selectView} isDisabled={isDisabled} /> ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/hooks.ts b/invokeai/frontend/web/src/features/controlLayers/components/Tool/hooks.ts new file mode 100644 index 00000000000..1bd546e7439 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/hooks.ts @@ -0,0 +1,19 @@ +import { useStore } from '@nanostores/react'; +import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; +import type { Tool } from 'features/controlLayers/store/types'; +import { computed } from 'nanostores'; +import { useCallback } from 'react'; + +export const useToolIsSelected = (tool: Tool) => { + const canvasManager = useCanvasManager(); + const isSelected = useStore(computed(canvasManager.stateApi.$tool, (t) => t === tool)); + return isSelected; +}; + +export const useSelectTool = (tool: Tool) => { + const canvasManager = useCanvasManager(); + const setTool = useCallback(() => { + canvasManager.stateApi.$tool.set(tool); + }, [canvasManager.stateApi.$tool, tool]); + return setTool; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts index b7d19b8f662..4a2692752df 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts @@ -31,6 +31,11 @@ export class CanvasBboxModule { manager: CanvasManager; log: Logger; + /** + * A set of subscriptions that should be cleaned up when the transformer is destroyed. + */ + subscriptions: Set<() => void> = new Set(); + konva: { group: Konva.Group; rect: Konva.Rect; @@ -228,17 +233,19 @@ export class CanvasBboxModule { this.konva.transformer.nodes([this.konva.rect]); this.konva.group.add(this.konva.rect); this.konva.group.add(this.konva.transformer); + + this.subscriptions.add(this.manager.stateApi.$tool.listen(this.render)); } - render() { + render = () => { this.log.trace('Rendering generation bbox'); const bbox = this.manager.stateApi.getBbox(); - const toolState = this.manager.stateApi.getToolState(); + const tool = this.manager.stateApi.$tool.get(); this.konva.group.visible(true); - this.parent.getLayer().listening(toolState.selected === 'bbox'); - this.konva.group.listening(toolState.selected === 'bbox'); + this.parent.getLayer().listening(tool === 'bbox'); + this.konva.group.listening(tool === 'bbox'); this.konva.rect.setAttrs({ x: bbox.rect.x, y: bbox.rect.y, @@ -246,13 +253,21 @@ export class CanvasBboxModule { height: bbox.rect.height, scaleX: 1, scaleY: 1, - listening: toolState.selected === 'bbox', + listening: tool === 'bbox', }); this.konva.transformer.setAttrs({ - listening: toolState.selected === 'bbox', - enabledAnchors: toolState.selected === 'bbox' ? ALL_ANCHORS : NO_ANCHORS, + listening: tool === 'bbox', + enabledAnchors: tool === 'bbox' ? ALL_ANCHORS : NO_ANCHORS, }); - } + }; + + destroy = () => { + this.log.trace('Destroying generation bbox'); + for (const unsubscribe of this.subscriptions) { + unsubscribe(); + } + this.konva.group.destroy(); + }; getLoggingContext = (): SerializableObject => { return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts index b6e95a1d8b3..a32d7524655 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts @@ -47,7 +47,7 @@ export class CanvasFilterModule { return; } this.$adapter.set(entity.adapter); - this.manager.stateApi.setTool('view'); + this.manager.stateApi.$tool.set('view'); }; previewFilter = async () => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index f5b7b574046..c6c7194918b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -156,11 +156,12 @@ export class CanvasObjectRenderer { this.parent.konva.layer.add(this.konva.compositing.group); } + // When switching tool, commit the buffer. This is necessary to prevent the buffer from being lost when the + // user switches tool mid-drawing, for example by pressing space to pan the stage. It's easy to press space + // to pan _before_ releasing the mouse button, which would cause the buffer to be lost if we didn't commit it. this.subscriptions.add( - this.manager.stateApi.$toolState.listen((newVal, oldVal) => { - if (newVal.selected !== oldVal.selected) { - this.commitBuffer(); - } + this.manager.stateApi.$tool.listen(() => { + this.commitBuffer(); }) ); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts index bde46351f95..dee207c7920 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts @@ -145,7 +145,6 @@ export class CanvasRenderingModule { if ( !prevState || state.regions.entities !== prevState.regions.entities || - state.tool.selected !== prevState.tool.selected || state.selectedEntityIdentifier?.id !== prevState.selectedEntityIdentifier?.id ) { // Destroy the konva nodes for nonexistent entities @@ -184,7 +183,6 @@ export class CanvasRenderingModule { if ( !prevState || state.inpaintMasks.entities !== prevState.inpaintMasks.entities || - state.tool.selected !== prevState.tool.selected || state.selectedEntityIdentifier?.id !== prevState.selectedEntityIdentifier?.id ) { // Destroy the konva nodes for nonexistent entities @@ -212,7 +210,7 @@ export class CanvasRenderingModule { }; renderBbox = (state: CanvasV2State, prevState: CanvasV2State | null) => { - if (!prevState || state.bbox !== prevState.bbox || state.tool.selected !== prevState.tool.selected) { + if (!prevState || state.bbox !== prevState.bbox) { this.manager.preview.bbox.render(); } }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index c9c20892ec1..24d3c1bec10 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -15,8 +15,6 @@ import { entitySelected, eraserWidthChanged, fillChanged, - toolBufferChanged, - toolChanged, } from 'features/controlLayers/store/canvasV2Slice'; import { selectAllRenderableEntities } from 'features/controlLayers/store/selectors'; import type { @@ -115,12 +113,6 @@ export class CanvasStateApiModule { setEraserWidth = (width: number) => { this.store.dispatch(eraserWidthChanged(width)); }; - setTool = (tool: Tool) => { - this.store.dispatch(toolChanged(tool)); - }; - setToolBuffer = (toolBuffer: Tool | null) => { - this.store.dispatch(toolBufferChanged(toolBuffer)); - }; setFill = (fill: RgbaColor) => { return this.store.dispatch(fillChanged(fill)); }; @@ -245,6 +237,8 @@ export class CanvasStateApiModule { $colorUnderCursor: WritableAtom = atom(RGBA_BLACK); // Read-write state, ephemeral interaction state + $tool = atom('brush'); + $toolBuffer = atom(null); $isDrawing = atom(false); $isMouseDown = atom(false); $lastAddedPoint = atom(null); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts index 4123b9b190d..0bce3396ea8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts @@ -231,17 +231,9 @@ export class CanvasToolModule { ); this.konva.group.add(this.konva.colorPicker.group); - this.subscriptions.add( - this.manager.stateApi.$stageAttrs.listen(() => { - this.render(); - }) - ); - - this.subscriptions.add( - this.manager.stateApi.$toolState.listen(() => { - this.render(); - }) - ); + this.subscriptions.add(this.manager.stateApi.$stageAttrs.listen(this.render)); + this.subscriptions.add(this.manager.stateApi.$toolState.listen(this.render)); + this.subscriptions.add(this.manager.stateApi.$tool.listen(this.render)); const cleanupListeners = this.setEventListeners(); @@ -261,15 +253,14 @@ export class CanvasToolModule { this.konva.colorPicker.group.visible(tool === 'colorPicker'); }; - render() { + render = () => { const stage = this.manager.stage; const renderedEntityCount = this.manager.stateApi.getRenderedEntityCount(); const toolState = this.manager.stateApi.getToolState(); const selectedEntity = this.manager.stateApi.getSelectedEntity(); const cursorPos = this.manager.stateApi.$lastCursorPos.get(); const isMouseDown = this.manager.stateApi.$isMouseDown.get(); - - const tool = toolState.selected; + const tool = this.manager.stateApi.$tool.get(); const isDrawable = selectedEntity && selectedEntity.state.isEnabled && isDrawableEntity(selectedEntity.state); @@ -447,7 +438,7 @@ export class CanvasToolModule { this.setToolVisibility(tool); } - } + }; syncLastCursorPos = (): Coordinate | null => { const pos = getScaledCursorPosition(this.konva.stage); @@ -480,9 +471,9 @@ export class CanvasToolModule { return { r, g, b }; }; - getClip( + getClip = ( entity: CanvasRegionalGuidanceState | CanvasControlLayerState | CanvasRasterLayerState | CanvasInpaintMaskState - ) { + ) => { const settings = this.manager.stateApi.getSettings(); if (settings.clipToBbox) { @@ -504,7 +495,7 @@ export class CanvasToolModule { height: height / scale, }; } - } + }; setEventListeners = (): (() => void) => { this.konva.stage.on('mouseenter', this.onStageMouseEnter); @@ -537,10 +528,11 @@ export class CanvasToolModule { onStageMouseDown = async (e: KonvaEventObject) => { this.manager.stateApi.$isMouseDown.set(true); const toolState = this.manager.stateApi.getToolState(); + const tool = this.manager.stateApi.$tool.get(); const pos = this.syncLastCursorPos(); const selectedEntity = this.manager.stateApi.getSelectedEntity(); - if (toolState.selected === 'colorPicker') { + if (tool === 'colorPicker') { const color = this.getColorUnderCursor(); if (color) { this.manager.stateApi.$colorUnderCursor.set(color); @@ -555,7 +547,7 @@ export class CanvasToolModule { this.manager.stateApi.$lastMouseDownPos.set(pos); const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); - if (toolState.selected === 'brush') { + if (tool === 'brush') { const lastLinePoint = selectedEntity.adapter.getLastPointOfLastLine('brush_line'); const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); if (e.evt.shiftKey && lastLinePoint) { @@ -594,7 +586,7 @@ export class CanvasToolModule { this.manager.stateApi.$lastAddedPoint.set(alignedPoint); } - if (toolState.selected === 'eraser') { + if (tool === 'eraser') { const lastLinePoint = selectedEntity.adapter.getLastPointOfLastLine('eraser_line'); const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); if (e.evt.shiftKey && lastLinePoint) { @@ -630,7 +622,7 @@ export class CanvasToolModule { this.manager.stateApi.$lastAddedPoint.set(alignedPoint); } - if (toolState.selected === 'rect') { + if (tool === 'rect') { if (selectedEntity.adapter.renderer.bufferState) { selectedEntity.adapter.renderer.commitBuffer(); } @@ -650,11 +642,10 @@ export class CanvasToolModule { const pos = this.manager.stateApi.$lastCursorPos.get(); const selectedEntity = this.manager.stateApi.getSelectedEntity(); const isDrawable = selectedEntity?.state.isEnabled; + const tool = this.manager.stateApi.$tool.get(); if (pos && isDrawable && !this.manager.stateApi.$spaceKey.get()) { - const toolState = this.manager.stateApi.getToolState(); - - if (toolState.selected === 'brush') { + if (tool === 'brush') { const drawingBuffer = selectedEntity.adapter.renderer.bufferState; if (drawingBuffer?.type === 'brush_line') { selectedEntity.adapter.renderer.commitBuffer(); @@ -663,7 +654,7 @@ export class CanvasToolModule { } } - if (toolState.selected === 'eraser') { + if (tool === 'eraser') { const drawingBuffer = selectedEntity.adapter.renderer.bufferState; if (drawingBuffer?.type === 'eraser_line') { selectedEntity.adapter.renderer.commitBuffer(); @@ -672,7 +663,7 @@ export class CanvasToolModule { } } - if (toolState.selected === 'rect') { + if (tool === 'rect') { const drawingBuffer = selectedEntity.adapter.renderer.bufferState; if (drawingBuffer?.type === 'rect') { selectedEntity.adapter.renderer.commitBuffer(); @@ -690,8 +681,9 @@ export class CanvasToolModule { const toolState = this.manager.stateApi.getToolState(); const pos = this.syncLastCursorPos(); const selectedEntity = this.manager.stateApi.getSelectedEntity(); + const tool = this.manager.stateApi.$tool.get(); - if (toolState.selected === 'colorPicker') { + if (tool === 'colorPicker') { const color = this.getColorUnderCursor(); if (color) { this.manager.stateApi.$colorUnderCursor.set(color); @@ -699,7 +691,7 @@ export class CanvasToolModule { } else { const isDrawable = selectedEntity?.state.isEnabled; if (pos && isDrawable && !this.manager.stateApi.$spaceKey.get() && getIsPrimaryMouseDown(e)) { - if (toolState.selected === 'brush') { + if (tool === 'brush') { const drawingBuffer = selectedEntity.adapter.renderer.bufferState; if (drawingBuffer) { if (drawingBuffer.type === 'brush_line') { @@ -736,7 +728,7 @@ export class CanvasToolModule { } } - if (toolState.selected === 'eraser') { + if (tool === 'eraser') { const drawingBuffer = selectedEntity.adapter.renderer.bufferState; if (drawingBuffer) { if (drawingBuffer.type === 'eraser_line') { @@ -772,7 +764,7 @@ export class CanvasToolModule { } } - if (toolState.selected === 'rect') { + if (tool === 'rect') { const drawingBuffer = selectedEntity.adapter.renderer.bufferState; if (drawingBuffer) { if (drawingBuffer.type === 'rect') { @@ -798,21 +790,22 @@ export class CanvasToolModule { const selectedEntity = this.manager.stateApi.getSelectedEntity(); const toolState = this.manager.stateApi.getToolState(); const isDrawable = selectedEntity?.state.isEnabled; + const tool = this.manager.stateApi.$tool.get(); if (pos && isDrawable && !this.manager.stateApi.$spaceKey.get() && getIsPrimaryMouseDown(e)) { const drawingBuffer = selectedEntity.adapter.renderer.bufferState; const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); - if (toolState.selected === 'brush' && drawingBuffer?.type === 'brush_line') { + if (tool === 'brush' && drawingBuffer?.type === 'brush_line') { const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); selectedEntity.adapter.renderer.commitBuffer(); - } else if (toolState.selected === 'eraser' && drawingBuffer?.type === 'eraser_line') { + } else if (tool === 'eraser' && drawingBuffer?.type === 'eraser_line') { const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); selectedEntity.adapter.renderer.commitBuffer(); - } else if (toolState.selected === 'rect' && drawingBuffer?.type === 'rect') { + } else if (tool === 'rect' && drawingBuffer?.type === 'rect') { drawingBuffer.rect.width = Math.round(normalizedPoint.x - drawingBuffer.rect.x); drawingBuffer.rect.height = Math.round(normalizedPoint.y - drawingBuffer.rect.y); await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); @@ -831,6 +824,7 @@ export class CanvasToolModule { } const toolState = this.manager.stateApi.getToolState(); + const tool = this.manager.stateApi.$tool.get(); let delta = e.evt.deltaY; @@ -839,9 +833,9 @@ export class CanvasToolModule { } // Holding ctrl or meta while scrolling changes the brush size - if (toolState.selected === 'brush') { + if (tool === 'brush') { this.manager.stateApi.setBrushWidth(calculateNewBrushSizeFromWheelDelta(toolState.brush.width, delta)); - } else if (toolState.selected === 'eraser') { + } else if (tool === 'eraser') { this.manager.stateApi.setEraserWidth(calculateNewBrushSizeFromWheelDelta(toolState.eraser.width, delta)); } @@ -864,8 +858,8 @@ export class CanvasToolModule { } } else if (e.key === ' ') { // Select the view tool on space key down - this.manager.stateApi.setToolBuffer(this.manager.stateApi.getToolState().selected); - this.manager.stateApi.setTool('view'); + this.manager.stateApi.$toolBuffer.set(this.manager.stateApi.$tool.get()); + this.manager.stateApi.$tool.set('view'); this.manager.stateApi.$spaceKey.set(true); this.manager.stateApi.$lastCursorPos.set(null); this.manager.stateApi.$lastMouseDownPos.set(null); @@ -881,9 +875,9 @@ export class CanvasToolModule { } if (e.key === ' ') { // Revert the tool to the previous tool on space key up - const toolBuffer = this.manager.stateApi.getToolState().selectedBuffer; - this.manager.stateApi.setTool(toolBuffer ?? 'move'); - this.manager.stateApi.setToolBuffer(null); + const toolBuffer = this.manager.stateApi.$toolBuffer.get(); + this.manager.stateApi.$tool.set(toolBuffer ?? 'move'); + this.manager.stateApi.$toolBuffer.set(null); this.manager.stateApi.$spaceKey.set(false); } }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index 1df7aa7532b..f58105a25e5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -381,20 +381,10 @@ export class CanvasTransformer { ); // When the selected tool changes, we need to update the transformer's interaction state. - this.subscriptions.add( - this.manager.stateApi.$toolState.listen((newVal, oldVal) => { - if (newVal.selected !== oldVal.selected) { - this.syncInteractionState(); - } - }) - ); + this.subscriptions.add(this.manager.stateApi.$tool.listen(this.syncInteractionState)); // When the selected entity changes, we need to update the transformer's interaction state. - this.subscriptions.add( - this.manager.stateApi.$selectedEntityIdentifier.listen(() => { - this.syncInteractionState(); - }) - ); + this.subscriptions.add(this.manager.stateApi.$selectedEntityIdentifier.listen(this.syncInteractionState)); this.parent.konva.layer.add(this.konva.outlineRect); this.parent.konva.layer.add(this.konva.proxyRect); @@ -439,7 +429,7 @@ export class CanvasTransformer { return; } - const toolState = this.manager.stateApi.getToolState(); + const tool = this.manager.stateApi.$tool.get(); const isSelected = this.manager.stateApi.getIsSelected(this.parent.id); if (!this.parent.renderer.hasObjects()) { @@ -449,14 +439,14 @@ export class CanvasTransformer { return; } - if (isSelected && !this.isTransforming && toolState.selected === 'move') { + if (isSelected && !this.isTransforming && tool === 'move') { // We are moving this layer, it must be listening this.parent.konva.layer.listening(true); this.setInteractionMode('drag'); } else if (isSelected && this.isTransforming) { // When transforming, we want the stage to still be movable if the view tool is selected. If the transformer is // active, it will interrupt the stage drag events. So we should disable listening when the view tool is selected. - if (toolState.selected !== 'view') { + if (tool !== 'view') { this.parent.konva.layer.listening(true); this.setInteractionMode('all'); } else { @@ -493,11 +483,12 @@ export class CanvasTransformer { startTransform = () => { this.log.debug('Starting transform'); this.isTransforming = true; - this.manager.stateApi.setTool('move'); + this.manager.stateApi.$tool.set('move'); // When transforming, we want the stage to still be movable if the view tool is selected. If the transformer or // interaction rect are listening, it will interrupt the stage's drag events. So we should disable listening // when the view tool is selected - const shouldListen = this.manager.stateApi.getToolState().selected !== 'view'; + // TODO(psyche): We just set the tool to 'move', why would it be 'view'? Investigate and figure out if this is needed + const shouldListen = this.manager.stateApi.$tool.get() !== 'view'; this.parent.konva.layer.listening(shouldListen); this.setInteractionMode('all'); this.manager.stateApi.$transformingEntity.set(this.parent.getEntityIdentifier()); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 81b688c950c..9b3e8280281 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -58,8 +58,6 @@ const initialState: CanvasV2State = { loras: [], ipAdapters: { entities: [] }, tool: { - selected: 'view', - selectedBuffer: null, invertScroll: false, fill: { r: 31, g: 160, b: 224, a: 1 }, // invokeBlue.500 brush: { @@ -140,17 +138,19 @@ export const canvasV2Slice = createSlice({ name: 'canvasV2', initialState, reducers: { + // undoable canvas state ...rasterLayersReducers, ...controlLayersReducers, ...ipAdaptersReducers, ...regionsReducers, + ...inpaintMaskReducers, + ...bboxReducers, + // move out ...lorasReducers, ...paramsReducers, ...compositingReducers, ...settingsReducers, ...toolReducers, - ...bboxReducers, - ...inpaintMaskReducers, ...sessionReducers, entitySelected: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; @@ -424,8 +424,6 @@ export const { eraserWidthChanged, fillChanged, invertScrollChanged, - toolChanged, - toolBufferChanged, clipToBboxChanged, canvasReset, settingsDynamicGridToggled, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts index 8d2aeaeb11c..2e77f1220d3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts @@ -5,10 +5,6 @@ export const sessionReducers = { sessionStartedStaging: (state) => { state.session.isStaging = true; state.session.selectedStagedImageIndex = 0; - // When we start staging, the user should not be interacting with the stage except to move it around. Set the tool - // to view. - state.tool.selectedBuffer = state.tool.selected; - state.tool.selected = 'view'; }, sessionImageStaged: (state, action: PayloadAction<{ stagingAreaImage: StagingAreaImage }>) => { const { stagingAreaImage } = action.payload; @@ -39,11 +35,6 @@ export const sessionReducers = { state.session.isStaging = false; state.session.stagedImages = []; state.session.selectedStagedImageIndex = 0; - // When we finish staging, reset the tool back to the previous selection. - if (state.tool.selectedBuffer) { - state.tool.selected = state.tool.selectedBuffer; - state.tool.selectedBuffer = null; - } }, sessionModeChanged: (state, action: PayloadAction<{ mode: SessionMode }>) => { const { mode } = action.payload; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts index c1f14d7df40..74916f783b2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts @@ -1,5 +1,5 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import type { CanvasV2State, RgbaColor, Tool } from 'features/controlLayers/store/types'; +import type { CanvasV2State, RgbaColor } from 'features/controlLayers/store/types'; export const toolReducers = { brushWidthChanged: (state, action: PayloadAction) => { @@ -14,10 +14,4 @@ export const toolReducers = { invertScrollChanged: (state, action: PayloadAction) => { state.tool.invertScroll = action.payload; }, - toolChanged: (state, action: PayloadAction) => { - state.tool.selected = action.payload; - }, - toolBufferChanged: (state, action: PayloadAction) => { - state.tool.selectedBuffer = action.payload; - }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 95eed0e25d0..a093d892495 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -736,8 +736,6 @@ export type CanvasV2State = { }; loras: LoRA[]; tool: { - selected: Tool; - selectedBuffer: Tool | null; invertScroll: boolean; brush: { width: number }; eraser: { width: number }; From ff8bc93080e88576a4aeaf7f33b19f8bb27c232d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 26 Aug 2024 21:12:31 +1000 Subject: [PATCH 498/678] feat(ui): add CanvasModuleBase class to standardize canvas APIs I did this ages ago but undid it for some reason, not sure why. Caught a few issues related to subscriptions. --- .../components/StageComponent.tsx | 4 +- .../konva/CanvasBackgroundModule.ts | 37 +++++-- .../controlLayers/konva/CanvasBboxModule.ts | 35 ++++--- .../controlLayers/konva/CanvasBrushLine.ts | 22 +++-- .../controlLayers/konva/CanvasCacheModule.ts | 27 +++++- .../konva/CanvasCompositorModule.ts | 24 ++++- .../controlLayers/konva/CanvasEraserLine.ts | 22 +++-- .../controlLayers/konva/CanvasFilterModule.ts | 18 ++-- .../controlLayers/konva/CanvasImage.ts | 14 ++- .../controlLayers/konva/CanvasLayerAdapter.ts | 16 +++- .../controlLayers/konva/CanvasManager.ts | 96 ++++++++++++------- .../controlLayers/konva/CanvasMaskAdapter.ts | 17 +++- .../controlLayers/konva/CanvasModuleBase.ts | 20 ++++ .../konva/CanvasObjectRenderer.ts | 19 ++-- .../konva/CanvasPreviewModule.ts | 40 +++++++- .../konva/CanvasProgressImageModule.ts | 27 ++++-- .../controlLayers/konva/CanvasRect.ts | 27 +++--- .../konva/CanvasRenderingModule.ts | 22 ++++- .../controlLayers/konva/CanvasStageModule.ts | 35 +++++-- .../konva/CanvasStagingAreaModule.ts | 13 ++- .../konva/CanvasStateApiModule.ts | 40 +++++++- .../controlLayers/konva/CanvasToolModule.ts | 37 ++++--- .../controlLayers/konva/CanvasTransformer.ts | 16 ++-- .../controlLayers/konva/CanvasWorkerModule.ts | 29 +++++- 24 files changed, 470 insertions(+), 187 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasModuleBase.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index 1502491e73f..81af6c00d4b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -38,8 +38,8 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, } const manager = new CanvasManager(stage, container, store, socket); - const cleanup = manager.initialize(); - return cleanup; + manager.initialize(); + return manager.destroy; }, [asPreview, container, socket, stage, store]); useLayoutEffect(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts index 722d5a39e5e..b8f0bd6b2dd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts @@ -1,29 +1,35 @@ import { getArbitraryBaseColor } from '@invoke-ai/ui-library'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import Konva from 'konva'; +import type { Logger } from 'roarr'; -export class CanvasBackgroundModule { - readonly type = 'background_grid'; +export class CanvasBackgroundModule extends CanvasModuleBase { + readonly type = 'background'; static GRID_LINE_COLOR_COARSE = getArbitraryBaseColor(27); static GRID_LINE_COLOR_FINE = getArbitraryBaseColor(18); id: string; + path: string[]; manager: CanvasManager; + subscriptions = new Set<() => void>(); + log: Logger; konva: { layer: Konva.Layer; }; - /** - * A set of subscriptions that should be cleaned up when the transformer is destroyed. - */ - subscriptions: Set<() => void> = new Set(); - constructor(manager: CanvasManager) { + super(); this.id = getPrefixedId(this.type); this.manager = manager; + this.path = this.manager.path.concat(this.id); + this.log = this.manager.buildLogger(this.getLoggingContext); + + this.log.debug('Creating background module'); + this.konva = { layer: new Konva.Layer({ name: `${this.type}:layer`, listening: false }) }; this.subscriptions.add( @@ -116,9 +122,8 @@ export class CanvasBackgroundModule { } destroy = () => { - for (const cleanup of this.subscriptions) { - cleanup(); - } + this.log.trace('Destroying background module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); this.konva.layer.destroy(); }; @@ -145,4 +150,16 @@ export class CanvasBackgroundModule { } return 256; }; + + repr = () => { + return { + id: this.id, + path: this.path, + type: this.type, + }; + }; + + getLoggingContext = () => { + return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; + }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts index 4a2692752df..a84ba5c713e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts @@ -1,6 +1,7 @@ import type { SerializableObject } from 'common/types'; import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMultiple'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import type { CanvasPreviewModule } from 'features/controlLayers/konva/CanvasPreviewModule'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { Rect } from 'features/controlLayers/store/types'; @@ -22,20 +23,17 @@ const ALL_ANCHORS: string[] = [ const CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right']; const NO_ANCHORS: string[] = []; -export class CanvasBboxModule { - readonly type = 'generation_bbox'; +export class CanvasBboxModule extends CanvasModuleBase { + readonly type = 'bbox'; id: string; path: string[]; - parent: CanvasPreviewModule; manager: CanvasManager; log: Logger; - - /** - * A set of subscriptions that should be cleaned up when the transformer is destroyed. - */ subscriptions: Set<() => void> = new Set(); + parent: CanvasPreviewModule; + konva: { group: Konva.Group; rect: Konva.Rect; @@ -43,13 +41,14 @@ export class CanvasBboxModule { }; constructor(parent: CanvasPreviewModule) { + super(); this.id = getPrefixedId(this.type); this.parent = parent; this.manager = this.parent.manager; - this.path = this.manager.path.concat(this.id); + this.path = this.parent.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.trace('Creating bbox'); + this.log.debug('Creating bbox module'); // Create a stash to hold onto the last aspect ratio of the bbox - this allows for locking the aspect ratio when // transforming the bbox. @@ -238,7 +237,7 @@ export class CanvasBboxModule { } render = () => { - this.log.trace('Rendering generation bbox'); + this.log.trace('Rendering bbox module'); const bbox = this.manager.stateApi.getBbox(); const tool = this.manager.stateApi.$tool.get(); @@ -261,15 +260,21 @@ export class CanvasBboxModule { }); }; + repr = () => { + return { + id: this.id, + type: this.type, + path: this.path, + }; + }; + destroy = () => { - this.log.trace('Destroying generation bbox'); - for (const unsubscribe of this.subscriptions) { - unsubscribe(); - } + this.log.trace('Destroying bbox module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); this.konva.group.destroy(); }; getLoggingContext = (): SerializableObject => { - return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; + return { ...this.parent.getLoggingContext(), path: this.path.join('.') }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts index 07641dfca04..3dac0b77cc7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts @@ -1,13 +1,13 @@ -import type { SerializableObject } from 'common/types'; import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import type { CanvasBrushLineState } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; -export class CanvasBrushLineRenderer { +export class CanvasBrushLineRenderer extends CanvasModuleBase { readonly type = 'brush_line_renderer'; id: string; @@ -15,6 +15,7 @@ export class CanvasBrushLineRenderer { parent: CanvasObjectRenderer; manager: CanvasManager; log: Logger; + subscriptions = new Set<() => void>(); state: CanvasBrushLineState; konva: { @@ -23,6 +24,7 @@ export class CanvasBrushLineRenderer { }; constructor(state: CanvasBrushLineState, parent: CanvasObjectRenderer) { + super(); const { id, clip } = state; this.id = id; this.parent = parent; @@ -30,7 +32,7 @@ export class CanvasBrushLineRenderer { this.path = this.parent.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.trace({ state }, 'Creating brush line'); + this.log.debug({ state }, 'Creating brush line renderer module'); this.konva = { group: new Konva.Group({ @@ -69,26 +71,28 @@ export class CanvasBrushLineRenderer { return false; } - destroy() { - this.log.trace('Destroying brush line'); + destroy = () => { + this.log.debug('Destroying brush line renderer module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); this.konva.group.destroy(); - } + }; setVisibility(isVisible: boolean): void { this.log.trace({ isVisible }, 'Setting brush line visibility'); this.konva.group.visible(isVisible); } - repr() { + repr = () => { return { id: this.id, type: this.type, + path: this.path, parent: this.parent.id, state: deepClone(this.state), }; - } + }; - getLoggingContext = (): SerializableObject => { + getLoggingContext = () => { return { ...this.parent.getLoggingContext(), path: this.path.join('.') }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCacheModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCacheModule.ts index c81bde7fd4f..90feeed82b2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCacheModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCacheModule.ts @@ -1,26 +1,31 @@ -import type { SerializableObject } from 'common/types'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { GenerationMode } from 'features/controlLayers/store/types'; import { LRUCache } from 'lru-cache'; import type { Logger } from 'roarr'; -export class CanvasCacheModule { +export class CanvasCacheModule extends CanvasModuleBase { + readonly type = 'cache'; + id: string; path: string[]; log: Logger; manager: CanvasManager; + subscriptions = new Set<() => void>(); imageNameCache = new LRUCache({ max: 100 }); canvasElementCache = new LRUCache({ max: 32 }); generationModeCache = new LRUCache({ max: 100 }); constructor(manager: CanvasManager) { + super(); this.id = getPrefixedId('cache'); this.manager = manager; this.path = this.manager.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.debug('Creating canvas cache'); + + this.log.debug('Creating cache module'); } clearAll = () => { @@ -29,7 +34,21 @@ export class CanvasCacheModule { this.generationModeCache.clear(); }; - getLoggingContext = (): SerializableObject => { + repr = () => { + return { + id: this.id, + path: this.path, + type: this.type, + }; + }; + + destroy = () => { + this.log.debug('Destroying cache module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); + this.clearAll(); + }; + + getLoggingContext = () => { return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts index f2f20b0937a..2a93e79b145 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts @@ -1,5 +1,6 @@ import type { SerializableObject } from 'common/types'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { canvasToBlob, canvasToImageData, @@ -14,18 +15,22 @@ import type { ImageDTO } from 'services/api/types'; import stableHash from 'stable-hash'; import { assert } from 'tsafe'; -export class CanvasCompositorModule { +export class CanvasCompositorModule extends CanvasModuleBase { + readonly type = 'compositor'; + id: string; path: string[]; log: Logger; manager: CanvasManager; + subscriptions = new Set<() => void>(); constructor(manager: CanvasManager) { + super(); this.id = getPrefixedId('canvas_compositor'); this.manager = manager; this.path = this.manager.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.debug('Creating canvas compositor'); + this.log.debug('Creating compositor module'); } getCompositeRasterLayerEntityIds = (): string[] => { @@ -237,7 +242,20 @@ export class CanvasCompositorModule { return generationMode; } - getLoggingContext = (): SerializableObject => { + repr = () => { + return { + id: this.id, + type: this.type, + path: this.path, + }; + }; + + destroy = () => { + this.log.trace('Destroying compositor module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); + }; + + getLoggingContext = () => { return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts index b0b74fa51da..29b06ab9b6d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts @@ -1,12 +1,12 @@ -import type { SerializableObject } from 'common/types'; import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import type { CanvasEraserLineState } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; -export class CanvasEraserLineRenderer { +export class CanvasEraserLineRenderer extends CanvasModuleBase { readonly type = 'eraser_line_renderer'; id: string; @@ -14,6 +14,7 @@ export class CanvasEraserLineRenderer { parent: CanvasObjectRenderer; manager: CanvasManager; log: Logger; + subscriptions = new Set<() => void>(); state: CanvasEraserLineState; konva: { @@ -22,6 +23,7 @@ export class CanvasEraserLineRenderer { }; constructor(state: CanvasEraserLineState, parent: CanvasObjectRenderer) { + super(); const { id, clip } = state; this.id = id; this.parent = parent; @@ -29,7 +31,7 @@ export class CanvasEraserLineRenderer { this.path = this.parent.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.trace({ state }, 'Creating eraser line'); + this.log.debug({ state }, 'Creating eraser line renderer module'); this.konva = { group: new Konva.Group({ @@ -68,26 +70,28 @@ export class CanvasEraserLineRenderer { return false; } - destroy() { - this.log.trace('Destroying eraser line'); + destroy = () => { + this.log.debug('Destroying eraser line renderer module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); this.konva.group.destroy(); - } + }; setVisibility(isVisible: boolean): void { this.log.trace({ isVisible }, 'Setting brush line visibility'); this.konva.group.visible(isVisible); } - repr() { + repr = () => { return { id: this.id, type: this.type, + path: this.path, parent: this.parent.id, state: deepClone(this.state), }; - } + }; - getLoggingContext = (): SerializableObject => { + getLoggingContext = () => { return { ...this.parent.getLoggingContext(), path: this.path.join('.') }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts index a32d7524655..b0ce4268327 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts @@ -1,6 +1,7 @@ import type { SerializableObject } from 'common/types'; import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { CanvasEntityIdentifier, CanvasImageState, FilterConfig } from 'features/controlLayers/store/types'; import { IMAGE_FILTERS, imageDTOToImageObject } from 'features/controlLayers/store/types'; @@ -10,15 +11,14 @@ import { getImageDTO } from 'services/api/endpoints/images'; import type { BatchConfig, ImageDTO, S } from 'services/api/types'; import { assert } from 'tsafe'; -const TYPE = 'entity_filter_preview'; - -export class CanvasFilterModule { - readonly type = TYPE; +export class CanvasFilterModule extends CanvasModuleBase { + readonly type = 'canvas_filter'; id: string; path: string[]; manager: CanvasManager; log: Logger; + subscriptions = new Set<() => void>(); imageState: CanvasImageState | null = null; @@ -28,11 +28,13 @@ export class CanvasFilterModule { $config = atom(IMAGE_FILTERS.canny_image_processor.buildDefaults()); constructor(manager: CanvasManager) { + super(); this.id = getPrefixedId(this.type); this.manager = manager; this.path = this.manager.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.trace('Creating filter'); + + this.log.debug('Creating filter module'); } initialize = (entityIdentifier: CanvasEntityIdentifier) => { @@ -167,17 +169,19 @@ export class CanvasFilterModule { }; destroy = () => { - this.log.trace('Destroying filter'); + this.log.trace('Destroying filter module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); }; repr = () => { return { id: this.id, type: this.type, + path: this.path, }; }; - getLoggingContext = (): SerializableObject => { + getLoggingContext = () => { return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts index f1b1a3abb9e..6db8db5a7c7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -1,8 +1,8 @@ import { Mutex } from 'async-mutex'; -import type { SerializableObject } from 'common/types'; import { deepClone } from 'common/util/deepClone'; import type { CanvasFilterModule } from 'features/controlLayers/konva/CanvasFilterModule'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import type { CanvasStagingAreaModule } from 'features/controlLayers/konva/CanvasStagingAreaModule'; import { loadImage } from 'features/controlLayers/konva/util'; @@ -12,7 +12,7 @@ import Konva from 'konva'; import type { Logger } from 'roarr'; import { getImageDTO } from 'services/api/endpoints/images'; -export class CanvasImageRenderer { +export class CanvasImageRenderer extends CanvasModuleBase { readonly type = 'image_renderer'; id: string; @@ -20,6 +20,7 @@ export class CanvasImageRenderer { parent: CanvasObjectRenderer | CanvasStagingAreaModule | CanvasFilterModule; manager: CanvasManager; log: Logger; + subscriptions = new Set<() => void>(); state: CanvasImageState; konva: { @@ -33,6 +34,7 @@ export class CanvasImageRenderer { mutex = new Mutex(); constructor(state: CanvasImageState, parent: CanvasObjectRenderer | CanvasStagingAreaModule | CanvasFilterModule) { + super(); const { id, image } = state; const { width, height } = image; this.id = id; @@ -41,7 +43,7 @@ export class CanvasImageRenderer { this.path = this.parent.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.trace({ state }, 'Creating image'); + this.log.debug({ state }, 'Creating image renderer module'); this.konva = { group: new Konva.Group({ name: `${this.type}:group`, listening: false }), @@ -166,7 +168,8 @@ export class CanvasImageRenderer { }; destroy = () => { - this.log.trace('Destroying image'); + this.log.debug('Destroying image renderer module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); this.konva.group.destroy(); }; @@ -179,6 +182,7 @@ export class CanvasImageRenderer { return { id: this.id, type: this.type, + path: this.path, parent: this.parent.id, isLoading: this.isLoading, isError: this.isError, @@ -186,7 +190,7 @@ export class CanvasImageRenderer { }; }; - getLoggingContext = (): SerializableObject => { + getLoggingContext = () => { return { ...this.parent.getLoggingContext(), path: this.path.join('.') }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts index 30f3c4f6be3..47f803fbf94 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts @@ -1,6 +1,7 @@ import type { SerializableObject } from 'common/types'; import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; import { getLastPointOfLine } from 'features/controlLayers/konva/util'; @@ -22,12 +23,13 @@ import type { Logger } from 'roarr'; import stableHash from 'stable-hash'; import { assert } from 'tsafe'; -export class CanvasLayerAdapter { +export class CanvasLayerAdapter extends CanvasModuleBase { readonly type = 'layer_adapter'; id: string; path: string[]; manager: CanvasManager; + subscriptions = new Set<() => void>(); log: Logger; state: CanvasRasterLayerState | CanvasControlLayerState; @@ -41,11 +43,14 @@ export class CanvasLayerAdapter { isFirstRender: boolean = true; constructor(state: CanvasLayerAdapter['state'], manager: CanvasLayerAdapter['manager']) { + super(); this.id = state.id; this.manager = manager; this.path = this.manager.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.debug({ state }, 'Creating layer'); + + this.log.debug({ state }, 'Creating layer adapter module'); + this.state = state; this.konva = { @@ -71,10 +76,10 @@ export class CanvasLayerAdapter { }; destroy = (): void => { - this.log.debug('Destroying layer'); - // We need to call the destroy method on all children so they can do their own cleanup. - this.transformer.destroy(); + this.log.debug('Destroying layer adapter module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); this.renderer.destroy(); + this.transformer.destroy(); this.konva.layer.destroy(); }; @@ -144,6 +149,7 @@ export class CanvasLayerAdapter { return { id: this.id, type: this.type, + path: this.path, state: deepClone(this.state), transformer: this.transformer.repr(), renderer: this.renderer.repr(), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index b4131ddb052..b836eca30ab 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -6,6 +6,7 @@ import { SyncableMap } from 'common/util/SyncableMap/SyncableMap'; import { CanvasCacheModule } from 'features/controlLayers/konva/CanvasCacheModule'; import { CanvasCompositorModule } from 'features/controlLayers/konva/CanvasCompositorModule'; import { CanvasFilterModule } from 'features/controlLayers/konva/CanvasFilterModule'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { CanvasRenderingModule } from 'features/controlLayers/konva/CanvasRenderingModule'; import { CanvasStageModule } from 'features/controlLayers/konva/CanvasStageModule'; import { CanvasWorkerModule } from 'features/controlLayers/konva/CanvasWorkerModule.js'; @@ -21,17 +22,20 @@ import { CanvasPreviewModule } from './CanvasPreviewModule'; import { CanvasStateApiModule } from './CanvasStateApiModule'; export const $canvasManager = atom(null); -const TYPE = 'manager'; -export class CanvasManager { - readonly type = TYPE; +export class CanvasManager extends CanvasModuleBase { + readonly type = 'manager'; id: string; path: string[]; + manager: CanvasManager; + log: Logger; store: AppStore; socket: AppSocket; + subscriptions = new Set<() => void>(); + adapters = { rasterLayers: new SyncableMap(), controlLayers: new SyncableMap(), @@ -60,8 +64,21 @@ export class CanvasManager { _isDebugging: boolean = false; constructor(stage: Konva.Stage, container: HTMLDivElement, store: AppStore, socket: AppSocket) { + super(); this.id = getPrefixedId(this.type); this.path = [this.id]; + this.manager = this; + this.log = logger('canvas').child((message) => { + return { + ...message, + context: { + ...this.getLoggingContext(), + ...message.context, + }, + }; + }); + this.log.debug('Creating canvas manager module'); + this.store = store; this.socket = socket; @@ -80,16 +97,6 @@ export class CanvasManager { this.stage.addLayer(this.background.konva.layer); } - log = logger('canvas').child((message) => { - return { - ...message, - context: { - ...this.getLoggingContext(), - ...message.context, - }, - }; - }); - enableDebugging() { this._isDebugging = true; this.logDebugInfo(); @@ -100,7 +107,7 @@ export class CanvasManager { } initialize = () => { - this.log.debug('Initializing canvas manager'); + this.log.debug('Initializing canvas manager module'); // These atoms require the canvas manager to be set up before we can provide their initial values this.stateApi.$transformingEntity.set(null); @@ -109,26 +116,52 @@ export class CanvasManager { this.stateApi.$currentFill.set(this.stateApi.getCurrentFill()); this.stateApi.$selectedEntity.set(this.stateApi.getSelectedEntity()); - const cleanupStage = this.stage.initialize(); - const cleanupStore = this.store.subscribe(this.renderer.render); - - return () => { - this.log.debug('Cleaning up canvas manager'); - for (const adapter of this.adapters.getAll()) { - adapter.destroy(); - } - this.background.destroy(); - this.preview.destroy(); - cleanupStore(); - cleanupStage(); - }; + this.subscriptions.add(this.store.subscribe(this.renderer.render)); + this.stage.initialize(); + }; + + destroy = () => { + this.log.debug('Destroying canvas manager module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); + for (const adapter of this.adapters.getAll()) { + adapter.destroy(); + } + this.stateApi.destroy(); + this.preview.destroy(); + this.background.destroy(); + this.filter.destroy(); + this.worker.destroy(); + this.renderer.destroy(); + this.compositor.destroy(); + this.stage.destroy(); + $canvasManager.set(null); }; setCanvasManager = () => { - this.log.debug('Setting canvas manager'); + this.log.debug('Setting canvas manager global'); $canvasManager.set(this); }; + repr = () => { + return { + id: this.id, + type: this.type, + path: this.path, + rasterLayers: Array.from(this.adapters.rasterLayers.values()).map((adapter) => adapter.repr()), + controlLayers: Array.from(this.adapters.controlLayers.values()).map((adapter) => adapter.repr()), + inpaintMasks: Array.from(this.adapters.inpaintMasks.values()).map((adapter) => adapter.repr()), + regionMasks: Array.from(this.adapters.regionMasks.values()).map((adapter) => adapter.repr()), + stateApi: this.stateApi.repr(), + preview: this.preview.repr(), + background: this.background.repr(), + filter: this.filter.repr(), + worker: this.worker.repr(), + renderer: this.renderer.repr(), + compositor: this.compositor.repr(), + stage: this.stage.repr(), + }; + }; + getLoggingContext = (): SerializableObject => { return { path: this.path.join('.'), @@ -150,11 +183,6 @@ export class CanvasManager { logDebugInfo() { // eslint-disable-next-line no-console console.log('Canvas manager', this); - for (const adapter of this.adapters.getAll()) { - // eslint-disable-next-line no-console - console.log(adapter.id, adapter); - } + this.log.debug({ manager: this.repr() }, 'Canvas manager'); } - - getPrefixedId = getPrefixedId; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts index ec2a50501e8..a0aa213615e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts @@ -1,6 +1,7 @@ import type { SerializableObject } from 'common/types'; import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; import { getLastPointOfLine } from 'features/controlLayers/konva/util'; @@ -21,13 +22,14 @@ import { get, omit } from 'lodash-es'; import type { Logger } from 'roarr'; import stableHash from 'stable-hash'; -export class CanvasMaskAdapter { +export class CanvasMaskAdapter extends CanvasModuleBase { readonly type = 'mask_adapter'; id: string; path: string[]; manager: CanvasManager; log: Logger; + subscriptions = new Set<() => void>(); state: CanvasInpaintMaskState | CanvasRegionalGuidanceState; @@ -41,11 +43,14 @@ export class CanvasMaskAdapter { }; constructor(state: CanvasMaskAdapter['state'], manager: CanvasMaskAdapter['manager']) { + super(); this.id = state.id; this.manager = manager; this.path = this.manager.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.debug({ state }, 'Creating mask'); + + this.log.debug({ state }, 'Creating mask adapter module'); + this.state = state; this.konva = { @@ -71,8 +76,8 @@ export class CanvasMaskAdapter { }; destroy = (): void => { - this.log.debug('Destroying mask'); - // We need to call the destroy method on all children so they can do their own cleanup. + this.log.debug('Destroying mask adapter module'); + this.transformer.destroy(); this.renderer.destroy(); this.konva.layer.destroy(); @@ -157,6 +162,7 @@ export class CanvasMaskAdapter { return { id: this.id, type: this.type, + path: this.path, state: deepClone(this.state), }; }; @@ -182,7 +188,8 @@ export class CanvasMaskAdapter { const canvas = this.renderer.getCanvas(rect, attrs); return canvas; }; - getLoggingContext = (): SerializableObject => { + + getLoggingContext = () => { return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasModuleBase.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasModuleBase.ts new file mode 100644 index 00000000000..894c9622f7a --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasModuleBase.ts @@ -0,0 +1,20 @@ +import type { SerializableObject } from 'common/types'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import type { Logger } from 'roarr'; + +export abstract class CanvasModuleBase { + abstract id: string; + abstract type: string; + abstract path: string[]; + abstract manager: CanvasManager; + abstract log: Logger; + abstract subscriptions: Set<() => void>; + + abstract getLoggingContext: () => SerializableObject; + abstract destroy: () => void; + abstract repr: () => SerializableObject & { + id: string; + path: string[]; + type: string; + }; +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index c6c7194918b..9a3b32cec83 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -1,4 +1,3 @@ -import type { SerializableObject } from 'common/types'; import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine'; import { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine'; @@ -6,6 +5,7 @@ import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; import { getPatternSVG } from 'features/controlLayers/konva/patterns/getPatternSVG'; @@ -55,8 +55,8 @@ type AnyObjectState = CanvasBrushLineState | CanvasEraserLineState | CanvasImage /** * Handles rendering of objects for a canvas entity. */ -export class CanvasObjectRenderer { - readonly type = 'object_renderer'; +export class CanvasObjectRenderer extends CanvasModuleBase { + readonly type = 'entity_object_renderer'; id: string; path: string[]; @@ -123,12 +123,13 @@ export class CanvasObjectRenderer { $canvasCache = atom<{ canvas: HTMLCanvasElement; rect: Rect } | null>(null); constructor(parent: CanvasLayerAdapter | CanvasMaskAdapter) { + super(); this.id = getPrefixedId(this.type); this.parent = parent; this.path = this.parent.path.concat(this.id); this.manager = parent.manager; this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.trace('Creating object renderer'); + this.log.debug('Creating entity object renderer module'); this.konva = { objectGroup: new Konva.Group({ name: `${this.type}:object_group`, listening: false }), @@ -597,11 +598,8 @@ export class CanvasObjectRenderer { * Destroys this renderer and all of its object renderers. */ destroy = () => { - this.log.trace('Destroying object renderer'); - for (const cleanup of this.subscriptions) { - this.log.trace('Cleaning up listener'); - cleanup(); - } + this.log.debug('Destroying entity object renderer module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); for (const renderer of this.renderers.values()) { renderer.destroy(); } @@ -616,13 +614,14 @@ export class CanvasObjectRenderer { return { id: this.id, type: this.type, + path: this.path, parent: this.parent.id, renderers: Array.from(this.renderers.values()).map((renderer) => renderer.repr()), buffer: this.bufferRenderer?.repr(), }; }; - getLoggingContext = (): SerializableObject => { + getLoggingContext = () => { return { ...this.parent.getLoggingContext(), path: this.path.join('.') }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreviewModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreviewModule.ts index 6e2ef2bba73..0a3f47ee61f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreviewModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreviewModule.ts @@ -1,13 +1,22 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { CanvasProgressImageModule } from 'features/controlLayers/konva/CanvasProgressImageModule'; +import { getPrefixedId } from 'features/controlLayers/konva/util'; import Konva from 'konva'; +import type { Logger } from 'roarr'; import { CanvasBboxModule } from './CanvasBboxModule'; import { CanvasStagingAreaModule } from './CanvasStagingAreaModule'; import { CanvasToolModule } from './CanvasToolModule'; -export class CanvasPreviewModule { +export class CanvasPreviewModule extends CanvasModuleBase { + readonly type = 'preview'; + + id: string; + path: string[]; manager: CanvasManager; + log: Logger; + subscriptions = new Set<() => void>(); konva: { layer: Konva.Layer; @@ -19,7 +28,14 @@ export class CanvasPreviewModule { progressImage: CanvasProgressImageModule; constructor(manager: CanvasManager) { + super(); + this.id = getPrefixedId(this.type); this.manager = manager; + this.path = this.manager.path.concat(this.id); + this.log = this.manager.buildLogger(this.getLoggingContext); + + this.log.debug('Creating preview module'); + this.konva = { layer: new Konva.Layer({ listening: false, imageSmoothingEnabled: false }), }; @@ -41,11 +57,25 @@ export class CanvasPreviewModule { return this.konva.layer; }; - destroy() { - // this.stagingArea.destroy(); // TODO(psyche): implement destroy + repr = () => { + return { + id: this.id, + type: this.type, + path: this.path, + }; + }; + + destroy = () => { + this.log.debug('Destroying preview module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); + this.stagingArea.destroy(); this.progressImage.destroy(); - // this.bbox.destroy(); // TODO(psyche): implement destroy + this.bbox.destroy(); this.tool.destroy(); this.konva.layer.destroy(); - } + }; + + getLoggingContext = () => { + return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; + }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImageModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImageModule.ts index b2b8a6977d9..fb972aededb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImageModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImageModule.ts @@ -1,13 +1,13 @@ import { Mutex } from 'async-mutex'; -import type { SerializableObject } from 'common/types'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import type { CanvasPreviewModule } from 'features/controlLayers/konva/CanvasPreviewModule'; import { getPrefixedId, loadImage } from 'features/controlLayers/konva/util'; import Konva from 'konva'; import type { Logger } from 'roarr'; import type { S } from 'services/api/types'; -export class CanvasProgressImageModule { +export class CanvasProgressImageModule extends CanvasModuleBase { readonly type = 'progress_image'; id: string; @@ -35,13 +35,14 @@ export class CanvasProgressImageModule { mutex: Mutex = new Mutex(); constructor(parent: CanvasPreviewModule) { + super(); this.id = getPrefixedId(this.type); this.parent = parent; this.manager = parent.manager; - this.path = this.manager.path.concat(this.id); + this.path = this.parent.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.trace('Creating progress image'); + this.log.debug('Creating progress image module'); this.konva = { group: new Konva.Group({ name: `${this.type}:group`, listening: false }), @@ -105,15 +106,21 @@ export class CanvasProgressImageModule { } }; + repr = () => { + return { + id: this.id, + type: this.type, + path: this.path, + }; + }; + destroy = () => { - this.log.trace('Destroying progress image'); - for (const unsubscribe of this.subscriptions) { - unsubscribe(); - } + this.log.debug('Destroying progress image module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); this.konva.group.destroy(); }; - getLoggingContext = (): SerializableObject => { - return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; + getLoggingContext = () => { + return { ...this.parent.getLoggingContext(), path: this.path.join('.') }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts index fecaeb92b7b..3cb0525e3fb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts @@ -1,13 +1,13 @@ -import type { SerializableObject } from 'common/types'; import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import type { CanvasRectState } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; -export class CanvasRectRenderer { +export class CanvasRectRenderer extends CanvasModuleBase { readonly type = 'rect_renderer'; id: string; @@ -15,6 +15,7 @@ export class CanvasRectRenderer { parent: CanvasObjectRenderer; manager: CanvasManager; log: Logger; + subscriptions = new Set<() => void>(); state: CanvasRectState; konva: { @@ -24,13 +25,15 @@ export class CanvasRectRenderer { isFirstRender: boolean = false; constructor(state: CanvasRectState, parent: CanvasObjectRenderer) { + super(); const { id } = state; this.id = id; this.parent = parent; this.manager = parent.manager; this.path = this.parent.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.trace({ state }, 'Creating rect'); + + this.log.debug({ state }, 'Creating rect renderer module'); this.konva = { group: new Konva.Group({ name: `${this.type}:group`, listening: false }), @@ -60,27 +63,29 @@ export class CanvasRectRenderer { return false; } - destroy() { - this.log.trace('Destroying rect'); - this.konva.group.destroy(); - } - setVisibility(isVisible: boolean): void { this.log.trace({ isVisible }, 'Setting rect visibility'); this.konva.group.visible(isVisible); } - repr() { + destroy = () => { + this.log.debug('Destroying rect renderer module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); + this.konva.group.destroy(); + }; + + repr = () => { return { id: this.id, type: this.type, + path: this.path, parent: this.parent.id, isFirstRender: this.isFirstRender, state: deepClone(this.state), }; - } + }; - getLoggingContext = (): SerializableObject => { + getLoggingContext = () => { return { ...this.parent.getLoggingContext(), path: this.path.join('.') }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts index dee207c7920..69236633bb8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts @@ -2,24 +2,29 @@ import type { SerializableObject } from 'common/types'; import { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { CanvasV2State } from 'features/controlLayers/store/types'; import type { Logger } from 'roarr'; -export class CanvasRenderingModule { +export class CanvasRenderingModule extends CanvasModuleBase { + readonly type = 'canvas_renderer'; + id: string; path: string[]; log: Logger; manager: CanvasManager; + subscriptions = new Set<() => void>(); state: CanvasV2State | null = null; constructor(manager: CanvasManager) { + super(); this.id = getPrefixedId('canvas_renderer'); this.manager = manager; this.path = this.manager.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.debug('Creating canvas renderer'); + this.log.debug('Creating canvas renderer module'); } render = async () => { @@ -263,4 +268,17 @@ export class CanvasRenderingModule { this.manager.preview.getLayer().zIndex(++zIndex); } }; + + repr = () => { + return { + id: this.id, + path: this.path, + type: this.type, + }; + }; + + destroy = () => { + this.log.debug('Destroying canvas renderer module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); + }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts index 995751de10f..a23bfad9c71 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts @@ -1,5 +1,5 @@ -import type { SerializableObject } from 'common/types'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { CANVAS_SCALE_BY } from 'features/controlLayers/konva/constants'; import { getPrefixedId, getRectUnion } from 'features/controlLayers/konva/util'; import type { Coordinate, Dimensions, Rect } from 'features/controlLayers/store/types'; @@ -8,7 +8,9 @@ import type { KonvaEventObject } from 'konva/lib/Node'; import { clamp } from 'lodash-es'; import type { Logger } from 'roarr'; -export class CanvasStageModule { +export class CanvasStageModule extends CanvasModuleBase { + readonly type = 'stage'; + static MIN_CANVAS_SCALE = 0.1; static MAX_CANVAS_SCALE = 20; @@ -19,12 +21,17 @@ export class CanvasStageModule { container: HTMLDivElement; log: Logger; + subscriptions = new Set<() => void>(); + constructor(stage: Konva.Stage, container: HTMLDivElement, manager: CanvasManager) { + super(); this.id = getPrefixedId('stage'); this.manager = manager; this.path = this.manager.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); + this.log.debug('Creating stage module'); + this.container = container; this.konva = { stage }; } @@ -50,12 +57,10 @@ export class CanvasStageModule { this.fitLayersToStage(); const cleanupListeners = this.setEventListeners(); - return () => { - this.log.debug('Destroying stage'); + this.subscriptions.add(cleanupListeners); + this.subscriptions.add(() => { resizeObserver.disconnect(); - this.konva.stage.destroy(); - cleanupListeners(); - }; + }); }; fitStageToContainer = () => { @@ -276,7 +281,21 @@ export class CanvasStageModule { this.konva.stage.add(layer); }; - getLoggingContext = (): SerializableObject => { + repr = () => { + return { + id: this.id, + type: this.type, + path: this.path, + }; + }; + + destroy = () => { + this.log.debug('Destroying stage module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); + this.konva.stage.destroy(); + }; + + getLoggingContext = () => { return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts index 225e21eeb75..202b367edbc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts @@ -1,13 +1,14 @@ import type { SerializableObject } from 'common/types'; import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import type { CanvasPreviewModule } from 'features/controlLayers/konva/CanvasPreviewModule'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { imageDTOToImageWithDims, type StagingAreaImage } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; -export class CanvasStagingAreaModule { +export class CanvasStagingAreaModule extends CanvasModuleBase { readonly type = 'staging_area'; id: string; @@ -27,12 +28,14 @@ export class CanvasStagingAreaModule { subscriptions: Set<() => void> = new Set(); constructor(parent: CanvasPreviewModule) { + super(); this.id = getPrefixedId(this.type); this.parent = parent; this.manager = this.parent.manager; this.path = this.manager.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.debug('Creating staging area'); + + this.log.debug('Creating staging area module'); this.konva = { group: new Konva.Group({ name: `${this.type}:group`, listening: false }) }; this.image = null; @@ -85,12 +88,11 @@ export class CanvasStagingAreaModule { }; destroy = () => { + this.log.debug('Destroying staging area module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); if (this.image) { this.image.destroy(); } - for (const unsubscribe of this.subscriptions) { - unsubscribe(); - } for (const node of this.getNodes()) { node.destroy(); } @@ -100,6 +102,7 @@ export class CanvasStagingAreaModule { return { id: this.id, type: this.type, + path: this.path, selectedImage: this.selectedImage, }; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index 24d3c1bec10..60671a7f989 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -3,6 +3,8 @@ import type { AppStore } from 'app/store/store'; import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; +import { getPrefixedId } from 'features/controlLayers/konva/util'; import { bboxChanged, brushWidthChanged, @@ -40,6 +42,7 @@ import type { import { RGBA_BLACK } from 'features/controlLayers/store/types'; import type { WritableAtom } from 'nanostores'; import { atom } from 'nanostores'; +import type { Logger } from 'roarr'; import { queueApi } from 'services/api/endpoints/queue'; import type { BatchConfig } from 'services/api/types'; import { $lastCanvasProgressEvent } from 'services/events/setEventListeners'; @@ -70,13 +73,27 @@ type EntityStateAndAdapter = adapter: CanvasMaskAdapter; }; -export class CanvasStateApiModule { - store: AppStore; +export class CanvasStateApiModule extends CanvasModuleBase { + readonly type = 'state_api'; + + id: string; + path: string[]; manager: CanvasManager; + log: Logger; + subscriptions = new Set<() => void>(); + + store: AppStore; constructor(store: AppStore, manager: CanvasManager) { - this.store = store; + super(); + this.id = getPrefixedId(this.type); this.manager = manager; + this.path = this.manager.path.concat(this.id); + this.log = this.manager.buildLogger(this.getLoggingContext); + + this.log.debug('Creating state api module'); + + this.store = store; } // Reminder - use arrow functions to avoid binding issues @@ -258,4 +275,21 @@ export class CanvasStateApiModule { height: 0, scale: 0, }); + + destroy = () => { + this.log.debug('Destroying state api module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); + }; + + repr = () => { + return { + id: this.id, + type: this.type, + path: this.path, + }; + }; + + getLoggingContext = () => { + return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; + }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts index 0bce3396ea8..38bc0dd3ae7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts @@ -1,6 +1,6 @@ -import type { SerializableObject } from 'common/types'; import { rgbaColorToString, rgbColorToString } from 'common/util/colorCodeTransformers'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import type { CanvasPreviewModule } from 'features/controlLayers/konva/CanvasPreviewModule'; import { BRUSH_BORDER_INNER_COLOR, @@ -31,8 +31,8 @@ import Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; import type { Logger } from 'roarr'; -export class CanvasToolModule { - readonly type = 'tool_preview'; +export class CanvasToolModule extends CanvasModuleBase { + readonly type = 'tool'; static readonly COLOR_PICKER_RADIUS = 25; static readonly COLOR_PICKER_THICKNESS = 15; static readonly COLOR_PICKER_CROSSHAIR_SPACE = 5; @@ -84,11 +84,15 @@ export class CanvasToolModule { subscriptions: Set<() => void> = new Set(); constructor(parent: CanvasPreviewModule) { + super(); this.id = getPrefixedId(this.type); this.parent = parent; this.manager = this.parent.manager; - this.path = this.manager.path.concat(this.id); + this.path = this.parent.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); + + this.log.debug('Creating tool module'); + this.konva = { stage: this.manager.stage.konva.stage, group: new Konva.Group({ name: `${this.type}:group`, listening: false }), @@ -240,13 +244,6 @@ export class CanvasToolModule { this.subscriptions.add(cleanupListeners); } - destroy = () => { - for (const cleanup of this.subscriptions) { - cleanup(); - } - this.konva.group.destroy(); - }; - setToolVisibility = (tool: Tool) => { this.konva.brush.group.visible(tool === 'brush'); this.konva.eraser.group.visible(tool === 'eraser'); @@ -882,7 +879,21 @@ export class CanvasToolModule { } }; - getLoggingContext = (): SerializableObject => { - return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; + repr = () => { + return { + id: this.id, + type: this.type, + path: this.path, + }; + }; + + destroy = () => { + this.log.debug('Destroying tool module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); + this.konva.group.destroy(); + }; + + getLoggingContext = () => { + return { ...this.parent.getLoggingContext(), path: this.path.join('.') }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index f58105a25e5..9c8e3168516 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -1,7 +1,7 @@ -import type { SerializableObject } from 'common/types'; import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { canvasToImageData, getEmptyRect, getPrefixedId } from 'features/controlLayers/konva/util'; import type { Coordinate, Rect } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -18,7 +18,7 @@ import type { Logger } from 'roarr'; * * It renders an outline when dragging and resizing the entity, with transform anchors for resizing and rotation. */ -export class CanvasTransformer { +export class CanvasTransformer extends CanvasModuleBase { readonly type = 'entity_transformer'; static RECT_CALC_DEBOUNCE_MS = 300; @@ -99,11 +99,13 @@ export class CanvasTransformer { }; constructor(parent: CanvasLayerAdapter | CanvasMaskAdapter) { + super(); this.id = getPrefixedId(this.type); this.parent = parent; this.manager = parent.manager; this.path = this.parent.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); + this.log.debug('Creating entity transformer module'); this.konva = { outlineRect: new Konva.Rect({ @@ -721,6 +723,7 @@ export class CanvasTransformer { return { id: this.id, type: this.type, + path: this.path, mode: this.interactionMode, isTransformEnabled: this.isTransformEnabled, isDragEnabled: this.isDragEnabled, @@ -731,17 +734,14 @@ export class CanvasTransformer { * Destroys the transformer, cleaning up any subscriptions. */ destroy = () => { - this.log.trace('Destroying transformer'); - for (const cleanup of this.subscriptions) { - this.log.trace('Cleaning up listener'); - cleanup(); - } + this.log.debug('Destroying entity transformer module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); this.konva.outlineRect.destroy(); this.konva.transformer.destroy(); this.konva.proxyRect.destroy(); }; - getLoggingContext = (): SerializableObject => { + getLoggingContext = () => { return { ...this.parent.getLoggingContext(), path: this.path.join('.') }; }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasWorkerModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasWorkerModule.ts index 9d61518be82..422b1f5ef4c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasWorkerModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasWorkerModule.ts @@ -1,24 +1,29 @@ -import type { SerializableObject } from 'common/types'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { Extents, ExtentsResult, GetBboxTask, WorkerLogMessage } from 'features/controlLayers/konva/worker'; import type { Logger } from 'roarr'; -export class CanvasWorkerModule { +export class CanvasWorkerModule extends CanvasModuleBase { + readonly type = 'worker'; + id: string; path: string[]; log: Logger; manager: CanvasManager; + subscriptions = new Set<() => void>(); worker: Worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module', name: 'worker' }); tasks: Map void }> = new Map(); constructor(manager: CanvasManager) { + super(); this.id = getPrefixedId('worker'); this.manager = manager; this.path = this.manager.path.concat(this.id); this.log = this.manager.buildLogger(this.getLoggingContext); - this.log.debug('Creating canvas worker'); + + this.log.debug('Creating worker module'); this.worker.onmessage = (event: MessageEvent) => { const { type, data } = event.data; @@ -55,7 +60,23 @@ export class CanvasWorkerModule { this.worker.postMessage(task, [data.buffer]); } - getLoggingContext = (): SerializableObject => { + repr = () => { + return { + id: this.id, + type: this.type, + path: this.path, + tasks: Array.from(this.tasks.keys()), + }; + }; + + destroy = () => { + this.log.trace('Destroying worker module'); + this.subscriptions.forEach((unsubscribe) => unsubscribe()); + this.worker.terminate(); + this.tasks.clear(); + }; + + getLoggingContext = () => { return { ...this.manager.getLoggingContext(), path: this.path.join('.') }; }; } From 6b75ea3b017a279f12d7cde216d74371a73966ef Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 26 Aug 2024 21:41:47 +1000 Subject: [PATCH 499/678] feat(ui): split out params/compositing state from canvas rendering state First step to restoring undo/redo - the undoable state must be in its own slice. So params and settings must be isolated. --- .../frontend/web/.storybook/ReduxInit.tsx | 2 +- .../listeners/appConfigReceived.ts | 4 +- .../listeners/enqueueRequestedLinear.ts | 2 +- .../listeners/enqueueRequestedNodes.ts | 2 +- .../listeners/modelSelected.ts | 9 +- .../listeners/modelsLoaded.ts | 10 +- .../listeners/promptChanged.ts | 2 +- .../listeners/setDefaultSettings.ts | 7 +- invokeai/frontend/web/src/app/store/store.ts | 3 + .../common/hooks/useGroupedModelCombobox.ts | 2 +- .../src/common/hooks/useIsReadyToEnqueue.ts | 6 +- .../ControlLayerControlAdapterModel.tsx | 2 +- .../components/IPAdapter/IPAdapterModel.tsx | 2 +- .../hooks/useLayerControlAdapter.ts | 4 +- .../controlLayers/store/bboxReducers.ts | 11 +- .../controlLayers/store/canvasV2Slice.ts | 118 ++------ .../store/compositingReducers.ts | 30 -- .../controlLayers/store/paramsReducers.ts | 132 -------- .../controlLayers/store/paramsSlice.ts | 285 ++++++++++++++++++ .../features/controlLayers/store/selectors.ts | 5 +- .../src/features/controlLayers/store/types.ts | 63 +--- .../features/lora/components/LoRASelect.tsx | 2 +- .../src/features/metadata/util/handlers.ts | 2 +- .../src/features/metadata/util/recallers.ts | 4 +- .../src/features/metadata/util/validators.ts | 2 +- .../util/graph/buildLinearBatchConfig.ts | 2 +- .../graph/buildMultidiffusionUpscaleGraph.ts | 2 +- .../nodes/util/graph/generation/addInpaint.ts | 25 +- .../util/graph/generation/addOutpaint.ts | 27 +- .../util/graph/generation/addSDXLRefiner.ts | 2 +- .../util/graph/generation/addSeamless.ts | 2 +- .../util/graph/generation/buildSD1Graph.ts | 9 +- .../util/graph/generation/buildSDXLGraph.ts | 9 +- .../nodes/util/graph/graphBuilderUtils.ts | 8 +- .../Advanced/ParamCFGRescaleMultiplier.tsx | 4 +- .../components/Advanced/ParamClipSkip.tsx | 6 +- .../ParamCanvasCoherenceEdgeSize.tsx | 4 +- .../ParamCanvasCoherenceMinDenoise.tsx | 4 +- .../ParamCanvasCoherenceMode.tsx | 4 +- .../MaskAdjustment/ParamMaskBlur.tsx | 4 +- .../ParamInfillColorOptions.tsx | 6 +- .../InfillAndScaling/ParamInfillMethod.tsx | 4 +- .../InfillAndScaling/ParamInfillOptions.tsx | 2 +- .../ParamInfillPatchmatchDownscaleSize.tsx | 6 +- .../InfillAndScaling/ParamInfillTilesize.tsx | 6 +- .../Canvas/ParamImageToImageStrength.tsx | 4 +- .../components/Core/ParamCFGScale.tsx | 4 +- .../components/Core/ParamNegativePrompt.tsx | 4 +- .../components/Core/ParamPositivePrompt.tsx | 6 +- .../components/Core/ParamScheduler.tsx | 4 +- .../parameters/components/Core/ParamSteps.tsx | 4 +- .../MainModel/ParamMainModelSelect.tsx | 2 +- .../MainModel/UseDefaultSettingsButton.tsx | 2 +- .../parameters/components/Prompts/Prompts.tsx | 16 +- .../Seamless/ParamSeamlessXAxis.tsx | 4 +- .../Seamless/ParamSeamlessYAxis.tsx | 4 +- .../components/Seed/ParamSeedNumberInput.tsx | 6 +- .../components/Seed/ParamSeedRandomize.tsx | 4 +- .../components/Seed/ParamSeedShuffle.tsx | 4 +- .../VAEModel/ParamVAEModelSelect.tsx | 6 +- .../components/VAEModel/ParamVAEPrecision.tsx | 4 +- .../features/prompt/PromptTriggerSelect.tsx | 2 +- .../queue/components/QueueButtonTooltip.tsx | 8 +- .../components/QueueIterationsNumberInput.tsx | 4 +- .../ParamSDXLNegativeStylePrompt.tsx | 4 +- .../ParamSDXLPositiveStylePrompt.tsx | 4 +- .../SDXLPrompts/SDXLConcatButton.tsx | 4 +- .../SDXLRefiner/ParamSDXLRefinerCFGScale.tsx | 4 +- .../ParamSDXLRefinerModelSelect.tsx | 4 +- ...ParamSDXLRefinerNegativeAestheticScore.tsx | 4 +- ...ParamSDXLRefinerPositiveAestheticScore.tsx | 4 +- .../SDXLRefiner/ParamSDXLRefinerScheduler.tsx | 4 +- .../SDXLRefiner/ParamSDXLRefinerStart.tsx | 4 +- .../SDXLRefiner/ParamSDXLRefinerSteps.tsx | 4 +- .../AdvancedSettingsAccordion.tsx | 6 +- .../ImageSettingsAccordion.tsx | 42 +-- .../RefinerSettingsAccordion.tsx | 6 +- .../UpscaleWarning.tsx | 2 +- .../components/ActiveStylePreset.tsx | 2 +- .../hooks/usePresetModifiedPrompts.ts | 4 +- .../SettingsModal/SettingsModal.tsx | 4 +- .../ParametersPanelTextToImage.tsx | 2 +- .../api/hooks/useSelectedModelConfig.ts | 2 +- 83 files changed, 528 insertions(+), 521 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/store/compositingReducers.ts delete mode 100644 invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts diff --git a/invokeai/frontend/web/.storybook/ReduxInit.tsx b/invokeai/frontend/web/.storybook/ReduxInit.tsx index 6b1113c4be0..5359e5868dc 100644 --- a/invokeai/frontend/web/.storybook/ReduxInit.tsx +++ b/invokeai/frontend/web/.storybook/ReduxInit.tsx @@ -1,5 +1,5 @@ import { PropsWithChildren, memo, useEffect } from 'react'; -import { modelChanged } from '../src/features/controlLayers/store/canvasV2Slice'; +import { modelChanged } from '../src/features/controlLayers/store/paramsSlice'; import { useAppDispatch } from '../src/app/store/storeHooks'; import { useGlobalModifiersInit } from '@invoke-ai/ui-library'; /** diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appConfigReceived.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appConfigReceived.ts index 8104321ac60..d4d529b0de2 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appConfigReceived.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appConfigReceived.ts @@ -1,5 +1,5 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { setInfillMethod } from 'features/controlLayers/store/canvasV2Slice'; +import { setInfillMethod } from 'features/controlLayers/store/paramsSlice'; import { shouldUseNSFWCheckerChanged, shouldUseWatermarkerChanged } from 'features/system/store/systemSlice'; import { appInfoApi } from 'services/api/endpoints/appInfo'; @@ -8,7 +8,7 @@ export const addAppConfigReceivedListener = (startAppListening: AppStartListenin matcher: appInfoApi.endpoints.getAppConfig.matchFulfilled, effect: (action, { getState, dispatch }) => { const { infill_methods = [], nsfw_methods = [], watermarking_methods = [] } = action.payload; - const infillMethod = getState().canvasV2.compositing.infillMethod; + const infillMethod = getState().params.infillMethod; if (!infill_methods.includes(infillMethod)) { // if there is no infill method, set it to the first one diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index d2a065ec22a..63d2fdc7563 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -23,7 +23,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) enqueueRequested.match(action) && action.payload.tabName === 'generation', effect: async (action, { getState, dispatch }) => { const state = getState(); - const model = state.canvasV2.params.model; + const model = state.params.model; const { prepend } = action.payload; const manager = $canvasManager.get(); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts index 6d614c5f40b..88cb2b5f7b5 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts @@ -29,7 +29,7 @@ export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) = batch: { graph, workflow: builtWorkflow, - runs: state.canvasV2.params.iterations, + runs: state.params.iterations, origin: 'workflows', }, prepend: action.payload.prepend, diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts index d9c0bf6e674..41bd9d6712f 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts @@ -1,6 +1,7 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { loraDeleted, modelChanged, vaeSelected } from 'features/controlLayers/store/canvasV2Slice'; +import { loraDeleted } from 'features/controlLayers/store/canvasV2Slice'; +import { modelChanged, vaeSelected } from 'features/controlLayers/store/paramsSlice'; import { modelSelected } from 'features/parameters/store/actions'; import { zParameterModel } from 'features/parameters/types/parameterSchemas'; import { toast } from 'features/toast/toast'; @@ -23,7 +24,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = const newModel = result.data; const newBaseModel = newModel.base; - const didBaseModelChange = state.canvasV2.params.model?.base !== newBaseModel; + const didBaseModelChange = state.params.model?.base !== newBaseModel; if (didBaseModelChange) { // we may need to reset some incompatible submodels @@ -38,7 +39,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = }); // handle incompatible vae - const { vae } = state.canvasV2.params; + const { vae } = state.params; if (vae && vae.base !== newBaseModel) { dispatch(vaeSelected(null)); modelsCleared += 1; @@ -66,7 +67,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = } } - dispatch(modelChanged({ model: newModel, previousModel: state.canvasV2.params.model })); + dispatch(modelChanged({ model: newModel, previousModel: state.params.model })); }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts index f83e34b0369..f6b8818e139 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts @@ -8,11 +8,9 @@ import { controlLayerModelChanged, ipaModelChanged, loraDeleted, - modelChanged, - refinerModelChanged, rgIPAdapterModelChanged, - vaeSelected, } from 'features/controlLayers/store/canvasV2Slice'; +import { modelChanged, refinerModelChanged, vaeSelected } from 'features/controlLayers/store/paramsSlice'; import { getEntityIdentifier } from 'features/controlLayers/store/types'; import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; import { postProcessingModelChanged, upscaleModelChanged } from 'features/parameters/store/upscaleSlice'; @@ -63,7 +61,7 @@ type ModelHandler = ( ) => undefined; const handleMainModels: ModelHandler = (models, state, dispatch, log) => { - const currentModel = state.canvasV2.params.model; + const currentModel = state.params.model; const mainModels = models.filter(isNonRefinerMainModelConfig); if (mainModels.length === 0) { // No models loaded at all @@ -110,7 +108,7 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => { }; const handleRefinerModels: ModelHandler = (models, state, dispatch, _log) => { - const currentRefinerModel = state.canvasV2.params.refinerModel; + const currentRefinerModel = state.params.refinerModel; const refinerModels = models.filter(isRefinerMainModelModelConfig); if (models.length === 0) { // No models loaded at all @@ -129,7 +127,7 @@ const handleRefinerModels: ModelHandler = (models, state, dispatch, _log) => { }; const handleVAEModels: ModelHandler = (models, state, dispatch, log) => { - const currentVae = state.canvasV2.params.vae; + const currentVae = state.params.vae; if (currentVae === null) { // null is a valid VAE! it means "use the default with the main model" diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts index cce2b61cc6c..d8abe4c66c8 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts @@ -1,6 +1,6 @@ import { isAnyOf } from '@reduxjs/toolkit'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { positivePromptChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { positivePromptChanged } from 'features/controlLayers/store/paramsSlice'; import { combinatorialToggled, isErrorChanged, diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts index 29828c911af..e013b3d17f7 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts @@ -1,14 +1,13 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasV2Slice'; import { - bboxHeightChanged, - bboxWidthChanged, setCfgRescaleMultiplier, setCfgScale, setScheduler, setSteps, vaePrecisionChanged, vaeSelected, -} from 'features/controlLayers/store/canvasV2Slice'; +} from 'features/controlLayers/store/paramsSlice'; import { setDefaultSettings } from 'features/parameters/store/actions'; import { isParameterCFGRescaleMultiplier, @@ -31,7 +30,7 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni effect: async (action, { dispatch, getState }) => { const state = getState(); - const currentModel = state.canvasV2.params.model; + const currentModel = state.params.model; if (!currentModel) { return; diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 3b5defd9ab9..d507a838946 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -7,6 +7,7 @@ import type { SerializableObject } from 'common/types'; import { deepClone } from 'common/util/deepClone'; import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice'; import { canvasV2PersistConfig, canvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { paramsPersistConfig, paramsSlice } from 'features/controlLayers/store/paramsSlice'; import { deleteImageModalSlice } from 'features/deleteImageModal/store/slice'; import { dynamicPromptsPersistConfig, dynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/gallerySlice'; @@ -57,6 +58,7 @@ const allReducers = { [workflowSettingsSlice.name]: workflowSettingsSlice.reducer, [upscaleSlice.name]: upscaleSlice.reducer, [stylePresetSlice.name]: stylePresetSlice.reducer, + [paramsSlice.name]: paramsSlice.reducer, }; const rootReducer = combineReducers(allReducers); @@ -98,6 +100,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = { [workflowSettingsPersistConfig.name]: workflowSettingsPersistConfig, [upscalePersistConfig.name]: upscalePersistConfig, [stylePresetPersistConfig.name]: stylePresetPersistConfig, + [paramsPersistConfig.name]: paramsPersistConfig, }; const unserialize: UnserializeFunction = (data, key) => { diff --git a/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts b/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts index d620c72eaf2..a06979b0346 100644 --- a/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts +++ b/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts @@ -32,7 +32,7 @@ export const useGroupedModelCombobox = ( arg: UseGroupedModelComboboxArg ): UseGroupedModelComboboxReturn => { const { t } = useTranslation(); - const base_model = useAppSelector((s) => s.canvasV2.params.model?.base ?? 'sdxl'); + const base_model = useAppSelector((s) => s.params.model?.base ?? 'sdxl'); const { modelConfigs, selectedModel, getIsDisabled, onChange, isLoading, groupByType = false } = arg; const options = useMemo[]>(() => { if (!modelConfigs) { diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index cf17a945ed1..f060a0e6c27 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -2,6 +2,7 @@ import { useStore } from '@nanostores/react'; import { $isConnected } from 'app/hooks/useSocketIO'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; +import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; @@ -34,13 +35,14 @@ const createSelector = (templates: Templates, isConnected: boolean) => selectWorkflowSettingsSlice, selectDynamicPromptsSlice, selectCanvasV2Slice, + selectParamsSlice, selectUpscalelice, selectConfigSlice, selectActiveTab, ], - (system, nodes, workflowSettings, dynamicPrompts, canvasV2, upscale, config, activeTabName) => { + (system, nodes, workflowSettings, dynamicPrompts, canvasV2, params, upscale, config, activeTabName) => { const { bbox } = canvasV2; - const { model, positivePrompt } = canvasV2.params; + const { model, positivePrompt } = params; const reasons: { prefix?: string; content: string }[] = []; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx index cf88a8ae2dd..139f0a3af59 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx @@ -18,7 +18,7 @@ export const ControlLayerControlAdapterModel = memo(({ modelKey, onChange: onCha const { t } = useTranslation(); const entityIdentifier = useEntityIdentifierContext(); const canvasManager = useCanvasManager(); - const currentBaseModel = useAppSelector((s) => s.canvasV2.params.model?.base); + const currentBaseModel = useAppSelector((s) => s.params.model?.base); const [modelConfigs, { isLoading }] = useControlNetAndT2IAdapterModels(); const selectedModel = useMemo(() => modelConfigs.find((m) => m.key === modelKey), [modelConfigs, modelKey]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterModel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterModel.tsx index 047f19d6bee..b14b761951a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterModel.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterModel.tsx @@ -24,7 +24,7 @@ type Props = { export const IPAdapterModel = memo(({ modelKey, onChangeModel, clipVisionModel, onChangeCLIPVisionModel }: Props) => { const { t } = useTranslation(); - const currentBaseModel = useAppSelector((s) => s.canvasV2.params.model?.base); + const currentBaseModel = useAppSelector((s) => s.params.model?.base); const [modelConfigs, { isLoading }] = useIPAdapterModels(); const selectedModel = useMemo(() => modelConfigs.find((m) => m.key === modelKey), [modelConfigs, modelKey]); diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts index 93157c3564b..3c2223ecf8b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts @@ -30,7 +30,7 @@ export const useControlLayerControlAdapter = (entityIdentifier: CanvasEntityIden export const useDefaultControlAdapter = (): ControlNetConfig | T2IAdapterConfig => { const [modelConfigs] = useControlNetAndT2IAdapterModels(); - const baseModel = useAppSelector((s) => s.canvasV2.params.model?.base); + const baseModel = useAppSelector((s) => s.params.model?.base); const defaultControlAdapter = useMemo(() => { const compatibleModels = modelConfigs.filter((m) => (baseModel ? m.base === baseModel : true)); @@ -51,7 +51,7 @@ export const useDefaultControlAdapter = (): ControlNetConfig | T2IAdapterConfig export const useDefaultIPAdapter = (): IPAdapterConfig => { const [modelConfigs] = useIPAdapterModels(); - const baseModel = useAppSelector((s) => s.canvasV2.params.model?.base); + const baseModel = useAppSelector((s) => s.params.model?.base); const defaultControlAdapter = useMemo(() => { const compatibleModels = modelConfigs.filter((m) => (baseModel ? m.base === baseModel : true)); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts index e6a5fb356e6..038e448fd5d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts @@ -6,14 +6,12 @@ import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getS import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; import { ASPECT_RATIO_MAP, initialAspectRatioState } from 'features/parameters/components/DocumentSize/constants'; import type { AspectRatioID } from 'features/parameters/components/DocumentSize/types'; -import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; import type { IRect } from 'konva/lib/types'; const syncScaledSize = (state: CanvasV2State) => { if (state.bbox.scaleMethod === 'auto') { - const optimalDimension = getOptimalDimension(state.params.model); const { width, height } = state.bbox.rect; - state.bbox.scaledSize = getScaledBoundingBoxDimensions({ width, height }, optimalDimension); + state.bbox.scaledSize = getScaledBoundingBoxDimensions({ width, height }, state.bbox.optimalDimension); } }; @@ -106,15 +104,14 @@ export const bboxReducers = { syncScaledSize(state); }, bboxSizeOptimized: (state) => { - const optimalDimension = getOptimalDimension(state.params.model); if (state.bbox.aspectRatio.isLocked) { - const { width, height } = calculateNewSize(state.bbox.aspectRatio.value, optimalDimension ** 2); + const { width, height } = calculateNewSize(state.bbox.aspectRatio.value, state.bbox.optimalDimension ** 2); state.bbox.rect.width = width; state.bbox.rect.height = height; } else { state.bbox.aspectRatio = deepClone(initialAspectRatioState); - state.bbox.rect.width = optimalDimension; - state.bbox.rect.height = optimalDimension; + state.bbox.rect.width = state.bbox.optimalDimension; + state.bbox.rect.height = state.bbox.optimalDimension; } syncScaledSize(state); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 9b3e8280281..fb765db6b9d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -5,12 +5,11 @@ import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/uti import { deepClone } from 'common/util/deepClone'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { bboxReducers } from 'features/controlLayers/store/bboxReducers'; -import { compositingReducers } from 'features/controlLayers/store/compositingReducers'; import { controlLayersReducers } from 'features/controlLayers/store/controlLayersReducers'; import { inpaintMaskReducers } from 'features/controlLayers/store/inpaintMaskReducers'; import { ipAdaptersReducers } from 'features/controlLayers/store/ipAdaptersReducers'; import { lorasReducers } from 'features/controlLayers/store/lorasReducers'; -import { paramsReducers } from 'features/controlLayers/store/paramsReducers'; +import { modelChanged } from 'features/controlLayers/store/paramsSlice'; import { rasterLayersReducers } from 'features/controlLayers/store/rasterLayersReducers'; import { regionsReducers } from 'features/controlLayers/store/regionsReducers'; import { selectAllEntities, selectAllEntitiesOfType, selectEntity } from 'features/controlLayers/store/selectors'; @@ -19,8 +18,9 @@ import { settingsReducers } from 'features/controlLayers/store/settingsReducers' import { toolReducers } from 'features/controlLayers/store/toolReducers'; import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; import { simplifyFlatNumbersArray } from 'features/controlLayers/util/simplify'; +import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; import { initialAspectRatioState } from 'features/parameters/components/DocumentSize/constants'; -import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; +import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension'; import { pick } from 'lodash-es'; import { assert } from 'tsafe'; @@ -69,6 +69,7 @@ const initialState: CanvasV2State = { }, bbox: { rect: { x: 0, y: 0, width: 512, height: 512 }, + optimalDimension: 512, aspectRatio: deepClone(initialAspectRatioState), scaleMethod: 'auto', scaledSize: { @@ -86,46 +87,6 @@ const initialState: CanvasV2State = { cropToBboxOnSave: false, dynamicGrid: false, }, - compositing: { - maskBlur: 16, - maskBlurMethod: 'box', - canvasCoherenceMode: 'Gaussian Blur', - canvasCoherenceMinDenoise: 0, - canvasCoherenceEdgeSize: 16, - infillMethod: 'patchmatch', - infillTileSize: 32, - infillPatchmatchDownscaleSize: 1, - infillColorValue: { r: 0, g: 0, b: 0, a: 1 }, - }, - params: { - cfgScale: 7.5, - cfgRescaleMultiplier: 0, - img2imgStrength: 0.75, - iterations: 1, - scheduler: 'euler', - seed: 0, - shouldRandomizeSeed: true, - steps: 50, - model: null, - vae: null, - vaePrecision: 'fp32', - seamlessXAxis: false, - seamlessYAxis: false, - clipSkip: 0, - shouldUseCpuNoise: true, - positivePrompt: '', - negativePrompt: '', - positivePrompt2: '', - negativePrompt2: '', - shouldConcatPrompts: true, - refinerModel: null, - refinerSteps: 20, - refinerCFGScale: 7.5, - refinerScheduler: 'euler', - refinerPositiveAestheticScore: 6, - refinerNegativeAestheticScore: 2.5, - refinerStart: 0.8, - }, session: { mode: 'generate', isStaging: false, @@ -147,8 +108,6 @@ export const canvasV2Slice = createSlice({ ...bboxReducers, // move out ...lorasReducers, - ...paramsReducers, - ...compositingReducers, ...settingsReducers, ...toolReducers, ...sessionReducers, @@ -400,11 +359,10 @@ export const canvasV2Slice = createSlice({ }, canvasReset: (state) => { state.bbox = deepClone(initialState.bbox); - const optimalDimension = getOptimalDimension(state.params.model); - state.bbox.rect.width = optimalDimension; - state.bbox.rect.height = optimalDimension; + state.bbox.rect.width = state.bbox.optimalDimension; + state.bbox.rect.height = state.bbox.optimalDimension; const size = pick(state.bbox.rect, 'width', 'height'); - state.bbox.scaledSize = getScaledBoundingBoxDimensions(size, optimalDimension); + state.bbox.scaledSize = getScaledBoundingBoxDimensions(size, state.bbox.optimalDimension); state.session = deepClone(initialState.session); state.tool = deepClone(initialState.tool); @@ -417,6 +375,31 @@ export const canvasV2Slice = createSlice({ state.selectedEntityIdentifier = deepClone(initialState.selectedEntityIdentifier); }, }, + extraReducers(builder) { + builder.addCase(modelChanged, (state, action) => { + const { model, previousModel } = action.payload; + + // If the model base changes (e.g. SD1.5 -> SDXL), we need to change a few things + if (model === null || previousModel?.base === model.base) { + return; + } + + // Update the bbox size to match the new model's optimal size + const optimalDimension = getOptimalDimension(model); + + state.bbox.optimalDimension = optimalDimension; + + if (!getIsSizeOptimal(state.bbox.rect.width, state.bbox.rect.height, optimalDimension)) { + const bboxDims = calculateNewSize(state.bbox.aspectRatio.value, optimalDimension * optimalDimension); + state.bbox.rect.width = bboxDims.width; + state.bbox.rect.height = bboxDims.height; + + if (state.bbox.scaleMethod === 'auto') { + state.bbox.scaledSize = getScaledBoundingBoxDimensions(bboxDims, optimalDimension); + } + } + }); + }, }); export const { @@ -495,43 +478,6 @@ export const { rgIPAdapterMethodChanged, rgIPAdapterModelChanged, rgIPAdapterCLIPVisionModelChanged, - // Compositing - setInfillMethod, - setInfillTileSize, - setInfillPatchmatchDownscaleSize, - setInfillColorValue, - setMaskBlur, - setCanvasCoherenceMode, - setCanvasCoherenceEdgeSize, - setCanvasCoherenceMinDenoise, - // Parameters - setIterations, - setSteps, - setCfgScale, - setCfgRescaleMultiplier, - setScheduler, - setSeed, - setImg2imgStrength, - setSeamlessXAxis, - setSeamlessYAxis, - setShouldRandomizeSeed, - vaeSelected, - vaePrecisionChanged, - setClipSkip, - shouldUseCpuNoiseChanged, - positivePromptChanged, - negativePromptChanged, - positivePrompt2Changed, - negativePrompt2Changed, - shouldConcatPromptsChanged, - refinerModelChanged, - setRefinerSteps, - setRefinerCFGScale, - setRefinerScheduler, - setRefinerPositiveAestheticScore, - setRefinerNegativeAestheticScore, - setRefinerStart, - modelChanged, // LoRAs loraAdded, loraRecalled, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/compositingReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/compositingReducers.ts deleted file mode 100644 index 03bb81ce04e..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/store/compositingReducers.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import type { CanvasV2State, RgbaColor } from 'features/controlLayers/store/types'; -import type { ParameterCanvasCoherenceMode } from 'features/parameters/types/parameterSchemas'; - -export const compositingReducers = { - setInfillMethod: (state, action: PayloadAction) => { - state.compositing.infillMethod = action.payload; - }, - setInfillTileSize: (state, action: PayloadAction) => { - state.compositing.infillTileSize = action.payload; - }, - setInfillPatchmatchDownscaleSize: (state, action: PayloadAction) => { - state.compositing.infillPatchmatchDownscaleSize = action.payload; - }, - setInfillColorValue: (state, action: PayloadAction) => { - state.compositing.infillColorValue = action.payload; - }, - setMaskBlur: (state, action: PayloadAction) => { - state.compositing.maskBlur = action.payload; - }, - setCanvasCoherenceMode: (state, action: PayloadAction) => { - state.compositing.canvasCoherenceMode = action.payload; - }, - setCanvasCoherenceEdgeSize: (state, action: PayloadAction) => { - state.compositing.canvasCoherenceEdgeSize = action.payload; - }, - setCanvasCoherenceMinDenoise: (state, action: PayloadAction) => { - state.compositing.canvasCoherenceMinDenoise = action.payload; - }, -} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts deleted file mode 100644 index 5e8a2b60ae6..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/store/paramsReducers.ts +++ /dev/null @@ -1,132 +0,0 @@ -import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import type { CanvasV2State } from 'features/controlLayers/store/types'; -import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; -import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; -import { CLIP_SKIP_MAP } from 'features/parameters/types/constants'; -import type { - ParameterCFGRescaleMultiplier, - ParameterCFGScale, - ParameterModel, - ParameterPrecision, - ParameterScheduler, - ParameterSDXLRefinerModel, - ParameterVAEModel, -} from 'features/parameters/types/parameterSchemas'; -import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension'; -import { clamp } from 'lodash-es'; - -export const paramsReducers = { - setIterations: (state, action: PayloadAction) => { - state.params.iterations = action.payload; - }, - setSteps: (state, action: PayloadAction) => { - state.params.steps = action.payload; - }, - setCfgScale: (state, action: PayloadAction) => { - state.params.cfgScale = action.payload; - }, - setCfgRescaleMultiplier: (state, action: PayloadAction) => { - state.params.cfgRescaleMultiplier = action.payload; - }, - setScheduler: (state, action: PayloadAction) => { - state.params.scheduler = action.payload; - }, - setSeed: (state, action: PayloadAction) => { - state.params.seed = action.payload; - state.params.shouldRandomizeSeed = false; - }, - setImg2imgStrength: (state, action: PayloadAction) => { - state.params.img2imgStrength = action.payload; - }, - setSeamlessXAxis: (state, action: PayloadAction) => { - state.params.seamlessXAxis = action.payload; - }, - setSeamlessYAxis: (state, action: PayloadAction) => { - state.params.seamlessYAxis = action.payload; - }, - setShouldRandomizeSeed: (state, action: PayloadAction) => { - state.params.shouldRandomizeSeed = action.payload; - }, - modelChanged: ( - state, - action: PayloadAction<{ model: ParameterModel | null; previousModel?: ParameterModel | null }> - ) => { - const { model, previousModel } = action.payload; - state.params.model = model; - - // If the model base changes (e.g. SD1.5 -> SDXL), we need to change a few things - if (model === null || previousModel?.base === model.base) { - return; - } - - // Update the bbox size to match the new model's optimal size - const optimalDimension = getOptimalDimension(model); - if (!getIsSizeOptimal(state.bbox.rect.width, state.bbox.rect.height, optimalDimension)) { - const bboxDims = calculateNewSize(state.bbox.aspectRatio.value, optimalDimension * optimalDimension); - state.bbox.rect.width = bboxDims.width; - state.bbox.rect.height = bboxDims.height; - - if (state.bbox.scaleMethod === 'auto') { - state.bbox.scaledSize = getScaledBoundingBoxDimensions(bboxDims, optimalDimension); - } - } - - // Clamp CLIP skip layer count to the bounds of the new model - if (model.base === 'sdxl') { - // We don't support user-defined CLIP skip for SDXL because it doesn't do anything useful - state.params.clipSkip = 0; - } else { - const { maxClip } = CLIP_SKIP_MAP[model.base]; - state.params.clipSkip = clamp(state.params.clipSkip, 0, maxClip); - } - }, - vaeSelected: (state, action: PayloadAction) => { - // null is a valid VAE! - state.params.vae = action.payload; - }, - vaePrecisionChanged: (state, action: PayloadAction) => { - state.params.vaePrecision = action.payload; - }, - setClipSkip: (state, action: PayloadAction) => { - state.params.clipSkip = action.payload; - }, - shouldUseCpuNoiseChanged: (state, action: PayloadAction) => { - state.params.shouldUseCpuNoise = action.payload; - }, - positivePromptChanged: (state, action: PayloadAction) => { - state.params.positivePrompt = action.payload; - }, - negativePromptChanged: (state, action: PayloadAction) => { - state.params.negativePrompt = action.payload; - }, - positivePrompt2Changed: (state, action: PayloadAction) => { - state.params.positivePrompt2 = action.payload; - }, - negativePrompt2Changed: (state, action: PayloadAction) => { - state.params.negativePrompt2 = action.payload; - }, - shouldConcatPromptsChanged: (state, action: PayloadAction) => { - state.params.shouldConcatPrompts = action.payload; - }, - refinerModelChanged: (state, action: PayloadAction) => { - state.params.refinerModel = action.payload; - }, - setRefinerSteps: (state, action: PayloadAction) => { - state.params.refinerSteps = action.payload; - }, - setRefinerCFGScale: (state, action: PayloadAction) => { - state.params.refinerCFGScale = action.payload; - }, - setRefinerScheduler: (state, action: PayloadAction) => { - state.params.refinerScheduler = action.payload; - }, - setRefinerPositiveAestheticScore: (state, action: PayloadAction) => { - state.params.refinerPositiveAestheticScore = action.payload; - }, - setRefinerNegativeAestheticScore: (state, action: PayloadAction) => { - state.params.refinerNegativeAestheticScore = action.payload; - }, - setRefinerStart: (state, action: PayloadAction) => { - state.params.refinerStart = action.payload; - }, -} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts new file mode 100644 index 00000000000..e20b51686ba --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts @@ -0,0 +1,285 @@ +import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; +import type { PersistConfig, RootState } from 'app/store/store'; +import type { RgbaColor } from 'features/controlLayers/store/types'; +import { CLIP_SKIP_MAP } from 'features/parameters/types/constants'; +import type { + ParameterCanvasCoherenceMode, + ParameterCFGRescaleMultiplier, + ParameterCFGScale, + ParameterMaskBlurMethod, + ParameterModel, + ParameterNegativePrompt, + ParameterNegativeStylePromptSDXL, + ParameterPositivePrompt, + ParameterPositiveStylePromptSDXL, + ParameterPrecision, + ParameterScheduler, + ParameterSDXLRefinerModel, + ParameterSeed, + ParameterSteps, + ParameterStrength, + ParameterVAEModel, +} from 'features/parameters/types/parameterSchemas'; +import { clamp } from 'lodash-es'; + +export type ParamsState = { + maskBlur: number; + maskBlurMethod: ParameterMaskBlurMethod; + canvasCoherenceMode: ParameterCanvasCoherenceMode; + canvasCoherenceMinDenoise: ParameterStrength; + canvasCoherenceEdgeSize: number; + infillMethod: string; + infillTileSize: number; + infillPatchmatchDownscaleSize: number; + infillColorValue: RgbaColor; + cfgScale: ParameterCFGScale; + cfgRescaleMultiplier: ParameterCFGRescaleMultiplier; + img2imgStrength: ParameterStrength; + iterations: number; + scheduler: ParameterScheduler; + seed: ParameterSeed; + shouldRandomizeSeed: boolean; + steps: ParameterSteps; + model: ParameterModel | null; + vae: ParameterVAEModel | null; + vaePrecision: ParameterPrecision; + seamlessXAxis: boolean; + seamlessYAxis: boolean; + clipSkip: number; + shouldUseCpuNoise: boolean; + positivePrompt: ParameterPositivePrompt; + negativePrompt: ParameterNegativePrompt; + positivePrompt2: ParameterPositiveStylePromptSDXL; + negativePrompt2: ParameterNegativeStylePromptSDXL; + shouldConcatPrompts: boolean; + refinerModel: ParameterSDXLRefinerModel | null; + refinerSteps: number; + refinerCFGScale: number; + refinerScheduler: ParameterScheduler; + refinerPositiveAestheticScore: number; + refinerNegativeAestheticScore: number; + refinerStart: number; +}; + +const initialState: ParamsState = { + maskBlur: 16, + maskBlurMethod: 'box', + canvasCoherenceMode: 'Gaussian Blur', + canvasCoherenceMinDenoise: 0, + canvasCoherenceEdgeSize: 16, + infillMethod: 'patchmatch', + infillTileSize: 32, + infillPatchmatchDownscaleSize: 1, + infillColorValue: { r: 0, g: 0, b: 0, a: 1 }, + cfgScale: 7.5, + cfgRescaleMultiplier: 0, + img2imgStrength: 0.75, + iterations: 1, + scheduler: 'euler', + seed: 0, + shouldRandomizeSeed: true, + steps: 50, + model: null, + vae: null, + vaePrecision: 'fp32', + seamlessXAxis: false, + seamlessYAxis: false, + clipSkip: 0, + shouldUseCpuNoise: true, + positivePrompt: '', + negativePrompt: '', + positivePrompt2: '', + negativePrompt2: '', + shouldConcatPrompts: true, + refinerModel: null, + refinerSteps: 20, + refinerCFGScale: 7.5, + refinerScheduler: 'euler', + refinerPositiveAestheticScore: 6, + refinerNegativeAestheticScore: 2.5, + refinerStart: 0.8, +}; + +export const paramsSlice = createSlice({ + name: 'params', + initialState, + reducers: { + setIterations: (state, action: PayloadAction) => { + state.iterations = action.payload; + }, + setSteps: (state, action: PayloadAction) => { + state.steps = action.payload; + }, + setCfgScale: (state, action: PayloadAction) => { + state.cfgScale = action.payload; + }, + setCfgRescaleMultiplier: (state, action: PayloadAction) => { + state.cfgRescaleMultiplier = action.payload; + }, + setScheduler: (state, action: PayloadAction) => { + state.scheduler = action.payload; + }, + setSeed: (state, action: PayloadAction) => { + state.seed = action.payload; + state.shouldRandomizeSeed = false; + }, + setImg2imgStrength: (state, action: PayloadAction) => { + state.img2imgStrength = action.payload; + }, + setSeamlessXAxis: (state, action: PayloadAction) => { + state.seamlessXAxis = action.payload; + }, + setSeamlessYAxis: (state, action: PayloadAction) => { + state.seamlessYAxis = action.payload; + }, + setShouldRandomizeSeed: (state, action: PayloadAction) => { + state.shouldRandomizeSeed = action.payload; + }, + modelChanged: ( + state, + action: PayloadAction<{ model: ParameterModel | null; previousModel?: ParameterModel | null }> + ) => { + const { model, previousModel } = action.payload; + state.model = model; + + // If the model base changes (e.g. SD1.5 -> SDXL), we need to change a few things + if (model === null || previousModel?.base === model.base) { + return; + } + + // Clamp CLIP skip layer count to the bounds of the new model + if (model.base === 'sdxl') { + // We don't support user-defined CLIP skip for SDXL because it doesn't do anything useful + state.clipSkip = 0; + } else { + const { maxClip } = CLIP_SKIP_MAP[model.base]; + state.clipSkip = clamp(state.clipSkip, 0, maxClip); + } + }, + vaeSelected: (state, action: PayloadAction) => { + // null is a valid VAE! + state.vae = action.payload; + }, + vaePrecisionChanged: (state, action: PayloadAction) => { + state.vaePrecision = action.payload; + }, + setClipSkip: (state, action: PayloadAction) => { + state.clipSkip = action.payload; + }, + shouldUseCpuNoiseChanged: (state, action: PayloadAction) => { + state.shouldUseCpuNoise = action.payload; + }, + positivePromptChanged: (state, action: PayloadAction) => { + state.positivePrompt = action.payload; + }, + negativePromptChanged: (state, action: PayloadAction) => { + state.negativePrompt = action.payload; + }, + positivePrompt2Changed: (state, action: PayloadAction) => { + state.positivePrompt2 = action.payload; + }, + negativePrompt2Changed: (state, action: PayloadAction) => { + state.negativePrompt2 = action.payload; + }, + shouldConcatPromptsChanged: (state, action: PayloadAction) => { + state.shouldConcatPrompts = action.payload; + }, + refinerModelChanged: (state, action: PayloadAction) => { + state.refinerModel = action.payload; + }, + setRefinerSteps: (state, action: PayloadAction) => { + state.refinerSteps = action.payload; + }, + setRefinerCFGScale: (state, action: PayloadAction) => { + state.refinerCFGScale = action.payload; + }, + setRefinerScheduler: (state, action: PayloadAction) => { + state.refinerScheduler = action.payload; + }, + setRefinerPositiveAestheticScore: (state, action: PayloadAction) => { + state.refinerPositiveAestheticScore = action.payload; + }, + setRefinerNegativeAestheticScore: (state, action: PayloadAction) => { + state.refinerNegativeAestheticScore = action.payload; + }, + setRefinerStart: (state, action: PayloadAction) => { + state.refinerStart = action.payload; + }, + setInfillMethod: (state, action: PayloadAction) => { + state.infillMethod = action.payload; + }, + setInfillTileSize: (state, action: PayloadAction) => { + state.infillTileSize = action.payload; + }, + setInfillPatchmatchDownscaleSize: (state, action: PayloadAction) => { + state.infillPatchmatchDownscaleSize = action.payload; + }, + setInfillColorValue: (state, action: PayloadAction) => { + state.infillColorValue = action.payload; + }, + setMaskBlur: (state, action: PayloadAction) => { + state.maskBlur = action.payload; + }, + setCanvasCoherenceMode: (state, action: PayloadAction) => { + state.canvasCoherenceMode = action.payload; + }, + setCanvasCoherenceEdgeSize: (state, action: PayloadAction) => { + state.canvasCoherenceEdgeSize = action.payload; + }, + setCanvasCoherenceMinDenoise: (state, action: PayloadAction) => { + state.canvasCoherenceMinDenoise = action.payload; + }, + }, +}); + +export const { + setInfillMethod, + setInfillTileSize, + setInfillPatchmatchDownscaleSize, + setInfillColorValue, + setMaskBlur, + setCanvasCoherenceMode, + setCanvasCoherenceEdgeSize, + setCanvasCoherenceMinDenoise, + setIterations, + setSteps, + setCfgScale, + setCfgRescaleMultiplier, + setScheduler, + setSeed, + setImg2imgStrength, + setSeamlessXAxis, + setSeamlessYAxis, + setShouldRandomizeSeed, + vaeSelected, + vaePrecisionChanged, + setClipSkip, + shouldUseCpuNoiseChanged, + positivePromptChanged, + negativePromptChanged, + positivePrompt2Changed, + negativePrompt2Changed, + shouldConcatPromptsChanged, + refinerModelChanged, + setRefinerSteps, + setRefinerCFGScale, + setRefinerScheduler, + setRefinerPositiveAestheticScore, + setRefinerNegativeAestheticScore, + setRefinerStart, + modelChanged, +} = paramsSlice.actions; + +export const selectParamsSlice = (state: RootState) => state.params; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +const migrate = (state: any): any => { + return state; +}; + +export const paramsPersistConfig: PersistConfig = { + name: paramsSlice.name, + initialState, + migrate, + persistDenylist: [], +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts index 93e837ee2c2..ffbd30867d0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts @@ -1,5 +1,6 @@ import { createSelector } from '@reduxjs/toolkit'; import type { RootState } from 'app/store/store'; +import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; import type { CanvasControlLayerState, CanvasEntityIdentifier, @@ -40,8 +41,8 @@ export const selectEntityCount = createSelector(selectCanvasV2Slice, (canvasV2) /** * Selects the optimal dimension for the canvas based on the currently-model */ -export const selectOptimalDimension = createSelector(selectCanvasV2Slice, (canvasV2) => { - return getOptimalDimension(canvasV2.params.model); +export const selectOptimalDimension = createSelector(selectParamsSlice, (params) => { + return getOptimalDimension(params.model); }); /** diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index a093d892495..628bda4c806 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -2,27 +2,7 @@ import type { SerializableObject } from 'common/types'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { AspectRatioState } from 'features/parameters/components/DocumentSize/types'; -import type { - ParameterCanvasCoherenceMode, - ParameterCFGRescaleMultiplier, - ParameterCFGScale, - ParameterHeight, - ParameterLoRAModel, - ParameterMaskBlurMethod, - ParameterModel, - ParameterNegativePrompt, - ParameterNegativeStylePromptSDXL, - ParameterPositivePrompt, - ParameterPositiveStylePromptSDXL, - ParameterPrecision, - ParameterScheduler, - ParameterSDXLRefinerModel, - ParameterSeed, - ParameterSteps, - ParameterStrength, - ParameterVAEModel, - ParameterWidth, -} from 'features/parameters/types/parameterSchemas'; +import type { ParameterHeight, ParameterLoRAModel, ParameterWidth } from 'features/parameters/types/parameterSchemas'; import { zParameterNegativePrompt, zParameterPositivePrompt } from 'features/parameters/types/parameterSchemas'; import type { AnyInvocation, BaseModelType, ImageDTO, S } from 'services/api/types'; import { z } from 'zod'; @@ -763,46 +743,7 @@ export type CanvasV2State = { height: ParameterHeight; }; scaleMethod: BoundingBoxScaleMethod; - }; - compositing: { - maskBlur: number; - maskBlurMethod: ParameterMaskBlurMethod; - canvasCoherenceMode: ParameterCanvasCoherenceMode; - canvasCoherenceMinDenoise: ParameterStrength; - canvasCoherenceEdgeSize: number; - infillMethod: string; - infillTileSize: number; - infillPatchmatchDownscaleSize: number; - infillColorValue: RgbaColor; - }; - params: { - cfgScale: ParameterCFGScale; - cfgRescaleMultiplier: ParameterCFGRescaleMultiplier; - img2imgStrength: ParameterStrength; - iterations: number; - scheduler: ParameterScheduler; - seed: ParameterSeed; - shouldRandomizeSeed: boolean; - steps: ParameterSteps; - model: ParameterModel | null; - vae: ParameterVAEModel | null; - vaePrecision: ParameterPrecision; - seamlessXAxis: boolean; - seamlessYAxis: boolean; - clipSkip: number; - shouldUseCpuNoise: boolean; - positivePrompt: ParameterPositivePrompt; - negativePrompt: ParameterNegativePrompt; - positivePrompt2: ParameterPositiveStylePromptSDXL; - negativePrompt2: ParameterNegativeStylePromptSDXL; - shouldConcatPrompts: boolean; - refinerModel: ParameterSDXLRefinerModel | null; - refinerSteps: number; - refinerCFGScale: number; - refinerScheduler: ParameterScheduler; - refinerPositiveAestheticScore: number; - refinerNegativeAestheticScore: number; - refinerStart: number; + optimalDimension: number; }; session: { mode: SessionMode; diff --git a/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx b/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx index 9d0a8164aea..2f552ba8634 100644 --- a/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx +++ b/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx @@ -18,7 +18,7 @@ const LoRASelect = () => { const [modelConfigs, { isLoading }] = useLoRAModels(); const { t } = useTranslation(); const addedLoRAs = useAppSelector(selectLoRAs); - const currentBaseModel = useAppSelector((s) => s.canvasV2.params.model?.base); + const currentBaseModel = useAppSelector((s) => s.params.model?.base); const getIsDisabled = (model: LoRAModelConfig): boolean => { const isCompatible = currentBaseModel === model.base; diff --git a/invokeai/frontend/web/src/features/metadata/util/handlers.ts b/invokeai/frontend/web/src/features/metadata/util/handlers.ts index 760a738df43..6f8d742c34b 100644 --- a/invokeai/frontend/web/src/features/metadata/util/handlers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/handlers.ts @@ -1,7 +1,7 @@ import { getStore } from 'app/store/nanostores/store'; import { deepClone } from 'common/util/deepClone'; import { objectKeys } from 'common/util/objectKeys'; -import { shouldConcatPromptsChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { shouldConcatPromptsChanged } from 'features/controlLayers/store/paramsSlice'; import type { LoRA } from 'features/controlLayers/store/types'; import type { BuildMetadataHandlers, diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index 41d092d0c5e..6333167768f 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -4,6 +4,8 @@ import { bboxWidthChanged, loraAllDeleted, loraRecalled, +} from 'features/controlLayers/store/canvasV2Slice'; +import { negativePrompt2Changed, negativePromptChanged, positivePrompt2Changed, @@ -22,7 +24,7 @@ import { setSeed, setSteps, vaeSelected, -} from 'features/controlLayers/store/canvasV2Slice'; +} from 'features/controlLayers/store/paramsSlice'; import type { LoRA } from 'features/controlLayers/store/types'; import { setHrfEnabled, setHrfMethod, setHrfStrength } from 'features/hrf/store/hrfSlice'; import type { MetadataRecallFunc } from 'features/metadata/types'; diff --git a/invokeai/frontend/web/src/features/metadata/util/validators.ts b/invokeai/frontend/web/src/features/metadata/util/validators.ts index 2defbbfb0e5..9b9ebadcd40 100644 --- a/invokeai/frontend/web/src/features/metadata/util/validators.ts +++ b/invokeai/frontend/web/src/features/metadata/util/validators.ts @@ -20,7 +20,7 @@ const validateBaseCompatibility = (base?: BaseModelType, message?: string) => { if (!base) { throw new InvalidModelConfigError(message || 'Missing base'); } - const currentBase = getStore().getState().canvasV2.params.model?.base; + const currentBase = getStore().getState().params.model?.base; if (currentBase && base !== currentBase) { throw new InvalidModelConfigError(message || `Incompatible base models: ${base} and ${currentBase}`); } diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts index c38d9e252a4..42622989515 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts @@ -12,7 +12,7 @@ export const prepareLinearUIBatch = ( noise: Invocation<'noise'>, posCond: Invocation<'compel' | 'sdxl_compel_prompt'> ): BatchConfig => { - const { iterations, model, shouldRandomizeSeed, seed, shouldConcatPrompts } = state.canvasV2.params; + const { iterations, model, shouldRandomizeSeed, seed, shouldConcatPrompts } = state.params; const { prompts, seedBehaviour } = state.dynamicPrompts; const data: Batch['data'] = []; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildMultidiffusionUpscaleGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildMultidiffusionUpscaleGraph.ts index d9c29d92f5b..a5db54ebb77 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildMultidiffusionUpscaleGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildMultidiffusionUpscaleGraph.ts @@ -13,7 +13,7 @@ import { getBoardField, getPresetModifiedPrompts } from './graphBuilderUtils'; export const buildMultidiffusionUpscaleGraph = async ( state: RootState ): Promise<{ g: Graph; noise: Invocation<'noise'>; posCond: Invocation<'compel' | 'sdxl_compel_prompt'> }> => { - const { model, cfgScale: cfg_scale, scheduler, steps, vaePrecision, seed, vae } = state.canvasV2.params; + const { model, cfgScale: cfg_scale, scheduler, steps, vaePrecision, seed, vae } = state.params; const { upscaleModel, upscaleInitialImage, structure, creativity, tileControlnetModel, scale } = state.upscale; assert(model, 'No model found in state'); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts index 5092deb171d..438f76d28d7 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts @@ -1,7 +1,7 @@ import type { RootState } from 'app/store/store'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import type { CanvasV2State, Dimensions } from 'features/controlLayers/store/types'; +import type { Dimensions } from 'features/controlLayers/store/types'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { isEqual } from 'lodash-es'; import type { Invocation } from 'services/api/types'; @@ -16,14 +16,15 @@ export const addInpaint = async ( modelLoader: Invocation<'main_model_loader' | 'sdxl_model_loader'>, originalSize: Dimensions, scaledSize: Dimensions, - bbox: CanvasV2State['bbox'], - compositing: CanvasV2State['compositing'], denoising_start: number, fp32: boolean ): Promise> => { denoise.denoising_start = denoising_start; - const mode = state.canvasV2.session.mode; + const { params, canvasV2 } = state; + const { bbox, session } = canvasV2; + const { mode } = session; + const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect); const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect); @@ -60,15 +61,15 @@ export const addInpaint = async ( const createGradientMask = g.addNode({ id: getPrefixedId('create_gradient_mask'), type: 'create_gradient_mask', - coherence_mode: compositing.canvasCoherenceMode, - minimum_denoise: compositing.canvasCoherenceMinDenoise, - edge_radius: compositing.canvasCoherenceEdgeSize, + coherence_mode: params.canvasCoherenceMode, + minimum_denoise: params.canvasCoherenceMinDenoise, + edge_radius: params.canvasCoherenceEdgeSize, fp32, }); const canvasPasteBack = g.addNode({ id: getPrefixedId('canvas_v2_mask_and_crop'), type: 'canvas_v2_mask_and_crop', - mask_blur: compositing.maskBlur, + mask_blur: params.maskBlur, }); // Resize initial image and mask to scaled size, feed into to gradient mask @@ -114,16 +115,16 @@ export const addInpaint = async ( const createGradientMask = g.addNode({ id: getPrefixedId('create_gradient_mask'), type: 'create_gradient_mask', - coherence_mode: compositing.canvasCoherenceMode, - minimum_denoise: compositing.canvasCoherenceMinDenoise, - edge_radius: compositing.canvasCoherenceEdgeSize, + coherence_mode: params.canvasCoherenceMode, + minimum_denoise: params.canvasCoherenceMinDenoise, + edge_radius: params.canvasCoherenceEdgeSize, fp32, image: { image_name: initialImage.image_name }, }); const canvasPasteBack = g.addNode({ id: getPrefixedId('canvas_v2_mask_and_crop'), type: 'canvas_v2_mask_and_crop', - mask_blur: compositing.maskBlur, + mask_blur: params.maskBlur, }); g.addEdge(alphaToMask, 'image', createGradientMask, 'mask'); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index ef999ef2cfe..7a2b95da97f 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts @@ -1,7 +1,7 @@ import type { RootState } from 'app/store/store'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import type { CanvasV2State, Dimensions } from 'features/controlLayers/store/types'; +import type { Dimensions } from 'features/controlLayers/store/types'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { getInfill } from 'features/nodes/util/graph/graphBuilderUtils'; import { isEqual } from 'lodash-es'; @@ -17,17 +17,18 @@ export const addOutpaint = async ( modelLoader: Invocation<'main_model_loader' | 'sdxl_model_loader'>, originalSize: Dimensions, scaledSize: Dimensions, - bbox: CanvasV2State['bbox'], - compositing: CanvasV2State['compositing'], denoising_start: number, fp32: boolean ): Promise> => { denoise.denoising_start = denoising_start; - const mode = state.canvasV2.session.mode; + const { params, canvasV2 } = state; + const { bbox, session } = canvasV2; + const { mode } = session; + const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect); const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect); - const infill = getInfill(g, compositing); + const infill = getInfill(g, params); if (!isEqual(scaledSize, originalSize)) { // Scale before processing requires some resizing @@ -72,9 +73,9 @@ export const addOutpaint = async ( const createGradientMask = g.addNode({ id: getPrefixedId('create_gradient_mask'), type: 'create_gradient_mask', - coherence_mode: compositing.canvasCoherenceMode, - minimum_denoise: compositing.canvasCoherenceMinDenoise, - edge_radius: compositing.canvasCoherenceEdgeSize, + coherence_mode: params.canvasCoherenceMode, + minimum_denoise: params.canvasCoherenceMinDenoise, + edge_radius: params.canvasCoherenceEdgeSize, fp32, }); g.addEdge(infill, 'image', createGradientMask, 'image'); @@ -103,7 +104,7 @@ export const addOutpaint = async ( const canvasPasteBack = g.addNode({ id: getPrefixedId('canvas_v2_mask_and_crop'), type: 'canvas_v2_mask_and_crop', - mask_blur: compositing.maskBlur, + mask_blur: params.maskBlur, }); // Resize initial image and mask to scaled size, feed into to gradient mask @@ -143,16 +144,16 @@ export const addOutpaint = async ( const createGradientMask = g.addNode({ id: getPrefixedId('create_gradient_mask'), type: 'create_gradient_mask', - coherence_mode: compositing.canvasCoherenceMode, - minimum_denoise: compositing.canvasCoherenceMinDenoise, - edge_radius: compositing.canvasCoherenceEdgeSize, + coherence_mode: params.canvasCoherenceMode, + minimum_denoise: params.canvasCoherenceMinDenoise, + edge_radius: params.canvasCoherenceEdgeSize, fp32, image: { image_name: initialImage.image_name }, }); const canvasPasteBack = g.addNode({ id: getPrefixedId('canvas_v2_mask_and_crop'), type: 'canvas_v2_mask_and_crop', - mask_blur: compositing.maskBlur, + mask_blur: params.maskBlur, }); g.addEdge(maskAlphaToMask, 'image', maskCombine, 'mask1'); g.addEdge(initialImageAlphaToMask, 'image', maskCombine, 'mask2'); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLRefiner.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLRefiner.ts index 09152d6659f..5485834db13 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLRefiner.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLRefiner.ts @@ -23,7 +23,7 @@ export const addSDXLRefiner = async ( refinerScheduler, refinerCFGScale, refinerStart, - } = state.canvasV2.params; + } = state.params; assert(refinerModel, 'No refiner model found in state'); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSeamless.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSeamless.ts index 8a48a6e9fd4..5424f05b0ba 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSeamless.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSeamless.ts @@ -21,7 +21,7 @@ export const addSeamless = ( modelLoader: Invocation<'main_model_loader'> | Invocation<'sdxl_model_loader'>, vaeLoader: Invocation<'vae_loader'> | null ): Invocation<'seamless'> | null => { - const { seamlessXAxis: seamless_x, seamlessYAxis: seamless_y } = state.canvasV2.params; + const { seamlessXAxis: seamless_x, seamlessYAxis: seamless_y } = state.params; if (!seamless_x && !seamless_y) { return null; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index 6182de8ee02..0eaa86c9e7a 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -31,7 +31,8 @@ export const buildSD1Graph = async ( const generationMode = manager.compositor.getGenerationMode(); log.debug({ generationMode }, 'Building SD1/SD2 graph'); - const { bbox, params, session, settings } = state.canvasV2; + const { canvasV2, params } = state; + const { bbox, session, settings } = canvasV2; const { model, @@ -172,7 +173,6 @@ export const buildSD1Graph = async ( vaePrecision === 'fp32' ); } else if (generationMode === 'inpaint') { - const { compositing } = state.canvasV2; canvasOutput = await addInpaint( state, g, @@ -183,13 +183,10 @@ export const buildSD1Graph = async ( modelLoader, originalSize, scaledSize, - bbox, - compositing, 1 - params.img2imgStrength, vaePrecision === 'fp32' ); } else if (generationMode === 'outpaint') { - const { compositing } = state.canvasV2; canvasOutput = await addOutpaint( state, g, @@ -200,8 +197,6 @@ export const buildSD1Graph = async ( modelLoader, originalSize, scaledSize, - bbox, - compositing, 1 - params.img2imgStrength, fp32 ); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index 184be31fd6c..35b7e401735 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -31,7 +31,8 @@ export const buildSDXLGraph = async ( const generationMode = manager.compositor.getGenerationMode(); log.debug({ generationMode }, 'Building SDXL graph'); - const { bbox, params, session, settings } = state.canvasV2; + const { params, canvasV2 } = state; + const { bbox, session, settings } = canvasV2; const { model, @@ -175,7 +176,6 @@ export const buildSDXLGraph = async ( fp32 ); } else if (generationMode === 'inpaint') { - const { compositing } = state.canvasV2; canvasOutput = await addInpaint( state, g, @@ -186,13 +186,10 @@ export const buildSDXLGraph = async ( modelLoader, originalSize, scaledSize, - bbox, - compositing, refinerModel ? Math.min(refinerStart, 1 - params.img2imgStrength) : 1 - params.img2imgStrength, fp32 ); } else if (generationMode === 'outpaint') { - const { compositing } = state.canvasV2; canvasOutput = await addOutpaint( state, g, @@ -203,8 +200,6 @@ export const buildSDXLGraph = async ( modelLoader, originalSize, scaledSize, - bbox, - compositing, refinerModel ? Math.min(refinerStart, 1 - params.img2imgStrength) : 1 - params.img2imgStrength, fp32 ); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts index 5b4748cb2e9..91fa2270558 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts @@ -1,4 +1,5 @@ import type { RootState } from 'app/store/store'; +import type { ParamsState } from 'features/controlLayers/store/paramsSlice'; import type { CanvasV2State } from 'features/controlLayers/store/types'; import type { BoardField } from 'features/nodes/types/common'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; @@ -25,8 +26,7 @@ export const getBoardField = (state: RootState): BoardField | undefined => { export const getPresetModifiedPrompts = ( state: RootState ): { positivePrompt: string; negativePrompt: string; positiveStylePrompt?: string; negativeStylePrompt?: string } => { - const { positivePrompt, negativePrompt, positivePrompt2, negativePrompt2, shouldConcatPrompts } = - state.canvasV2.params; + const { positivePrompt, negativePrompt, positivePrompt2, negativePrompt2, shouldConcatPrompts } = state.params; const { activeStylePresetId } = state.stylePreset; if (activeStylePresetId) { @@ -70,9 +70,9 @@ export const getSizes = (bboxState: CanvasV2State['bbox']) => { export const getInfill = ( g: Graph, - compositing: CanvasV2State['compositing'] + params: ParamsState ): Invocation<'infill_patchmatch' | 'infill_cv2' | 'infill_lama' | 'infill_rgba' | 'infill_tile'> => { - const { infillMethod, infillColorValue, infillPatchmatchDownscaleSize, infillTileSize } = compositing; + const { infillMethod, infillColorValue, infillPatchmatchDownscaleSize, infillTileSize } = params; // Add Infill Nodes if (infillMethod === 'patchmatch') { diff --git a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCFGRescaleMultiplier.tsx b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCFGRescaleMultiplier.tsx index bb6ff6bfc70..c81f518b20e 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCFGRescaleMultiplier.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCFGRescaleMultiplier.tsx @@ -1,12 +1,12 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setCfgRescaleMultiplier } from 'features/controlLayers/store/canvasV2Slice'; +import { setCfgRescaleMultiplier } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamCFGRescaleMultiplier = () => { - const cfgRescaleMultiplier = useAppSelector((s) => s.canvasV2.params.cfgRescaleMultiplier); + const cfgRescaleMultiplier = useAppSelector((s) => s.params.cfgRescaleMultiplier); const initial = useAppSelector((s) => s.config.sd.cfgRescaleMultiplier.initial); const sliderMin = useAppSelector((s) => s.config.sd.cfgRescaleMultiplier.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.cfgRescaleMultiplier.sliderMax); diff --git a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamClipSkip.tsx b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamClipSkip.tsx index 25649581334..5605b81d866 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamClipSkip.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamClipSkip.tsx @@ -1,19 +1,19 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setClipSkip } from 'features/controlLayers/store/canvasV2Slice'; +import { setClipSkip } from 'features/controlLayers/store/paramsSlice'; import { CLIP_SKIP_MAP } from 'features/parameters/types/constants'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamClipSkip = () => { - const clipSkip = useAppSelector((s) => s.canvasV2.params.clipSkip); + const clipSkip = useAppSelector((s) => s.params.clipSkip); const initial = useAppSelector((s) => s.config.sd.clipSkip.initial); const sliderMin = useAppSelector((s) => s.config.sd.clipSkip.sliderMin); const numberInputMin = useAppSelector((s) => s.config.sd.clipSkip.numberInputMin); const coarseStep = useAppSelector((s) => s.config.sd.clipSkip.coarseStep); const fineStep = useAppSelector((s) => s.config.sd.clipSkip.fineStep); - const model = useAppSelector((s) => s.canvasV2.params.model); + const model = useAppSelector((s) => s.params.model); const dispatch = useAppDispatch(); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceEdgeSize.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceEdgeSize.tsx index 63826ca71cf..998a8fe05a8 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceEdgeSize.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceEdgeSize.tsx @@ -1,13 +1,13 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setCanvasCoherenceEdgeSize } from 'features/controlLayers/store/canvasV2Slice'; +import { setCanvasCoherenceEdgeSize } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamCanvasCoherenceEdgeSize = () => { const dispatch = useAppDispatch(); - const canvasCoherenceEdgeSize = useAppSelector((s) => s.canvasV2.compositing.canvasCoherenceEdgeSize); + const canvasCoherenceEdgeSize = useAppSelector((s) => s.params.canvasCoherenceEdgeSize); const initial = useAppSelector((s) => s.config.sd.canvasCoherenceEdgeSize.initial); const sliderMin = useAppSelector((s) => s.config.sd.canvasCoherenceEdgeSize.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.canvasCoherenceEdgeSize.sliderMax); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMinDenoise.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMinDenoise.tsx index 841ac727baa..0a55596ad54 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMinDenoise.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMinDenoise.tsx @@ -1,13 +1,13 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setCanvasCoherenceMinDenoise } from 'features/controlLayers/store/canvasV2Slice'; +import { setCanvasCoherenceMinDenoise } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamCanvasCoherenceMinDenoise = () => { const dispatch = useAppDispatch(); - const canvasCoherenceMinDenoise = useAppSelector((s) => s.canvasV2.compositing.canvasCoherenceMinDenoise); + const canvasCoherenceMinDenoise = useAppSelector((s) => s.params.canvasCoherenceMinDenoise); const { t } = useTranslation(); const handleChange = useCallback( diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMode.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMode.tsx index 376756285fe..266798a6c9c 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMode.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMode.tsx @@ -2,14 +2,14 @@ import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setCanvasCoherenceMode } from 'features/controlLayers/store/canvasV2Slice'; +import { setCanvasCoherenceMode } from 'features/controlLayers/store/paramsSlice'; import { isParameterCanvasCoherenceMode } from 'features/parameters/types/parameterSchemas'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamCanvasCoherenceMode = () => { const dispatch = useAppDispatch(); - const canvasCoherenceMode = useAppSelector((s) => s.canvasV2.compositing.canvasCoherenceMode); + const canvasCoherenceMode = useAppSelector((s) => s.params.canvasCoherenceMode); const { t } = useTranslation(); const options = useMemo( diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/MaskAdjustment/ParamMaskBlur.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/MaskAdjustment/ParamMaskBlur.tsx index 9bc7150d0f2..911b0847646 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/MaskAdjustment/ParamMaskBlur.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/MaskAdjustment/ParamMaskBlur.tsx @@ -1,14 +1,14 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setMaskBlur } from 'features/controlLayers/store/canvasV2Slice'; +import { setMaskBlur } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamMaskBlur = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const maskBlur = useAppSelector((s) => s.canvasV2.compositing.maskBlur); + const maskBlur = useAppSelector((s) => s.params.maskBlur); const initial = useAppSelector((s) => s.config.sd.maskBlur.initial); const sliderMin = useAppSelector((s) => s.config.sd.maskBlur.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.maskBlur.sliderMax); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillColorOptions.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillColorOptions.tsx index 0e65ceb06f9..90b802d5b60 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillColorOptions.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillColorOptions.tsx @@ -1,7 +1,7 @@ import { Box, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIColorPicker from 'common/components/IAIColorPicker'; -import { setInfillColorValue } from 'features/controlLayers/store/canvasV2Slice'; +import { setInfillColorValue } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import type { RgbaColor } from 'react-colorful'; import { useTranslation } from 'react-i18next'; @@ -9,8 +9,8 @@ import { useTranslation } from 'react-i18next'; const ParamInfillColorOptions = () => { const dispatch = useAppDispatch(); - const infillColor = useAppSelector((s) => s.canvasV2.compositing.infillColorValue); - const infillMethod = useAppSelector((s) => s.canvasV2.compositing.infillMethod); + const infillColor = useAppSelector((s) => s.params.infillColorValue); + const infillMethod = useAppSelector((s) => s.params.infillMethod); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillMethod.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillMethod.tsx index f66ae697184..070a3e224a8 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillMethod.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillMethod.tsx @@ -2,7 +2,7 @@ import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setInfillMethod } from 'features/controlLayers/store/canvasV2Slice'; +import { setInfillMethod } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useGetAppConfigQuery } from 'services/api/endpoints/appInfo'; @@ -10,7 +10,7 @@ import { useGetAppConfigQuery } from 'services/api/endpoints/appInfo'; const ParamInfillMethod = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const infillMethod = useAppSelector((s) => s.canvasV2.compositing.infillMethod); + const infillMethod = useAppSelector((s) => s.params.infillMethod); const { data: appConfigData } = useGetAppConfigQuery(); const options = useMemo( () => diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillOptions.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillOptions.tsx index 9da4b0df1ae..1a5cb1e82a2 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillOptions.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillOptions.tsx @@ -6,7 +6,7 @@ import ParamInfillPatchmatchDownscaleSize from './ParamInfillPatchmatchDownscale import ParamInfillTilesize from './ParamInfillTilesize'; const ParamInfillOptions = () => { - const infillMethod = useAppSelector((s) => s.canvasV2.compositing.infillMethod); + const infillMethod = useAppSelector((s) => s.params.infillMethod); if (infillMethod === 'tile') { return ; } diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillPatchmatchDownscaleSize.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillPatchmatchDownscaleSize.tsx index 6589a72a37b..c227b6f954f 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillPatchmatchDownscaleSize.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillPatchmatchDownscaleSize.tsx @@ -1,14 +1,14 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setInfillPatchmatchDownscaleSize } from 'features/controlLayers/store/canvasV2Slice'; +import { setInfillPatchmatchDownscaleSize } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamInfillPatchmatchDownscaleSize = () => { const dispatch = useAppDispatch(); - const infillMethod = useAppSelector((s) => s.canvasV2.compositing.infillMethod); - const infillPatchmatchDownscaleSize = useAppSelector((s) => s.canvasV2.compositing.infillPatchmatchDownscaleSize); + const infillMethod = useAppSelector((s) => s.params.infillMethod); + const infillPatchmatchDownscaleSize = useAppSelector((s) => s.params.infillPatchmatchDownscaleSize); const initial = useAppSelector((s) => s.config.sd.infillPatchmatchDownscaleSize.initial); const sliderMin = useAppSelector((s) => s.config.sd.infillPatchmatchDownscaleSize.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.infillPatchmatchDownscaleSize.sliderMax); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillTilesize.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillTilesize.tsx index ffcfa2be236..3590bab284f 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillTilesize.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillTilesize.tsx @@ -1,12 +1,12 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { setInfillTileSize } from 'features/controlLayers/store/canvasV2Slice'; +import { setInfillTileSize } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamInfillTileSize = () => { const dispatch = useAppDispatch(); - const infillTileSize = useAppSelector((s) => s.canvasV2.compositing.infillTileSize); + const infillTileSize = useAppSelector((s) => s.params.infillTileSize); const initial = useAppSelector((s) => s.config.sd.infillTileSize.initial); const sliderMin = useAppSelector((s) => s.config.sd.infillTileSize.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.infillTileSize.sliderMax); @@ -15,7 +15,7 @@ const ParamInfillTileSize = () => { const coarseStep = useAppSelector((s) => s.config.sd.infillTileSize.coarseStep); const fineStep = useAppSelector((s) => s.config.sd.infillTileSize.fineStep); - const infillMethod = useAppSelector((s) => s.canvasV2.compositing.infillMethod); + const infillMethod = useAppSelector((s) => s.params.infillMethod); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/ParamImageToImageStrength.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/ParamImageToImageStrength.tsx index 128aec2ddd0..f09e62d1ea9 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/ParamImageToImageStrength.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/ParamImageToImageStrength.tsx @@ -1,10 +1,10 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { setImg2imgStrength } from 'features/controlLayers/store/canvasV2Slice'; +import { setImg2imgStrength } from 'features/controlLayers/store/paramsSlice'; import ImageToImageStrength from 'features/parameters/components/ImageToImage/ImageToImageStrength'; import { memo, useCallback } from 'react'; const ParamImageToImageStrength = () => { - const img2imgStrength = useAppSelector((s) => s.canvasV2.params.img2imgStrength); + const img2imgStrength = useAppSelector((s) => s.params.img2imgStrength); const dispatch = useAppDispatch(); const onChange = useCallback( diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamCFGScale.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamCFGScale.tsx index 2b774e7e39a..8465d94cd53 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamCFGScale.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamCFGScale.tsx @@ -1,12 +1,12 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setCfgScale } from 'features/controlLayers/store/canvasV2Slice'; +import { setCfgScale } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamCFGScale = () => { - const cfgScale = useAppSelector((s) => s.canvasV2.params.cfgScale); + const cfgScale = useAppSelector((s) => s.params.cfgScale); const sliderMin = useAppSelector((s) => s.config.sd.guidance.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.guidance.sliderMax); const numberInputMin = useAppSelector((s) => s.config.sd.guidance.numberInputMin); diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx index ff157dfa53a..0117383a7b3 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx @@ -1,6 +1,6 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { negativePromptChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { negativePromptChanged } from 'features/controlLayers/store/paramsSlice'; import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { ViewModePrompt } from 'features/parameters/components/Prompts/ViewModePrompt'; @@ -13,7 +13,7 @@ import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets'; export const ParamNegativePrompt = memo(() => { const dispatch = useAppDispatch(); - const prompt = useAppSelector((s) => s.canvasV2.params.negativePrompt); + const prompt = useAppSelector((s) => s.params.negativePrompt); const viewMode = useAppSelector((s) => s.stylePreset.viewMode); const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId); diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx index de19dfd29bf..b5659e19bf5 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx @@ -1,6 +1,6 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { positivePromptChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { positivePromptChanged } from 'features/controlLayers/store/paramsSlice'; import { ShowDynamicPromptsPreviewButton } from 'features/dynamicPrompts/components/ShowDynamicPromptsPreviewButton'; import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; @@ -17,8 +17,8 @@ import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets'; export const ParamPositivePrompt = memo(() => { const dispatch = useAppDispatch(); - const prompt = useAppSelector((s) => s.canvasV2.params.positivePrompt); - const baseModel = useAppSelector((s) => s.canvasV2.params.model)?.base; + const prompt = useAppSelector((s) => s.params.positivePrompt); + const baseModel = useAppSelector((s) => s.params.model)?.base; const viewMode = useAppSelector((s) => s.stylePreset.viewMode); const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId); diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamScheduler.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamScheduler.tsx index 1f9b12357be..3fa62e67350 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamScheduler.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamScheduler.tsx @@ -2,7 +2,7 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setScheduler } from 'features/controlLayers/store/canvasV2Slice'; +import { setScheduler } from 'features/controlLayers/store/paramsSlice'; import { SCHEDULER_OPTIONS } from 'features/parameters/types/constants'; import { isParameterScheduler } from 'features/parameters/types/parameterSchemas'; import { memo, useCallback, useMemo } from 'react'; @@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next'; const ParamScheduler = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const scheduler = useAppSelector((s) => s.canvasV2.params.scheduler); + const scheduler = useAppSelector((s) => s.params.scheduler); const onChange = useCallback( (v) => { diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx index 2b9806d8cfa..582f80e37c4 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx @@ -1,12 +1,12 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setSteps } from 'features/controlLayers/store/canvasV2Slice'; +import { setSteps } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSteps = () => { - const steps = useAppSelector((s) => s.canvasV2.params.steps); + const steps = useAppSelector((s) => s.params.steps); const initial = useAppSelector((s) => s.config.sd.steps.initial); const sliderMin = useAppSelector((s) => s.config.sd.steps.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.steps.sliderMax); diff --git a/invokeai/frontend/web/src/features/parameters/components/MainModel/ParamMainModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/MainModel/ParamMainModelSelect.tsx index 07ad047d7ab..bcc56d2e40d 100644 --- a/invokeai/frontend/web/src/features/parameters/components/MainModel/ParamMainModelSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/MainModel/ParamMainModelSelect.tsx @@ -12,7 +12,7 @@ import type { MainModelConfig } from 'services/api/types'; const ParamMainModelSelect = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const selectedModel = useAppSelector((s) => s.canvasV2.params.model); + const selectedModel = useAppSelector((s) => s.params.model); const [modelConfigs, { isLoading }] = useSDMainModels(); const tooltipLabel = useMemo(() => { if (!modelConfigs.length || !selectedModel) { diff --git a/invokeai/frontend/web/src/features/parameters/components/MainModel/UseDefaultSettingsButton.tsx b/invokeai/frontend/web/src/features/parameters/components/MainModel/UseDefaultSettingsButton.tsx index 600a27bb7e0..5eb3cfb2d03 100644 --- a/invokeai/frontend/web/src/features/parameters/components/MainModel/UseDefaultSettingsButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/MainModel/UseDefaultSettingsButton.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'; import { RiSparklingFill } from 'react-icons/ri'; export const UseDefaultSettingsButton = () => { - const model = useAppSelector((s) => s.canvasV2.params.model); + const model = useAppSelector((s) => s.params.model); const { t } = useTranslation(); const dispatch = useAppDispatch(); diff --git a/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx b/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx index 2afb05f1351..3bec31c64c0 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx @@ -1,25 +1,23 @@ import { Flex } from '@invoke-ai/ui-library'; -import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; import { ParamNegativePrompt } from 'features/parameters/components/Core/ParamNegativePrompt'; import { ParamPositivePrompt } from 'features/parameters/components/Core/ParamPositivePrompt'; import { ParamSDXLNegativeStylePrompt } from 'features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt'; import { ParamSDXLPositiveStylePrompt } from 'features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt'; import { memo } from 'react'; -const concatPromptsSelector = createSelector([selectCanvasV2Slice], (canvasV2) => { - return canvasV2.params.model?.base !== 'sdxl' || canvasV2.params.shouldConcatPrompts; -}); - export const Prompts = memo(() => { - const shouldConcatPrompts = useAppSelector(concatPromptsSelector); + const withStylePrompts = useAppSelector((s) => { + const isSDXL = s.params.model?.base === 'sdxl'; + const shouldConcatPrompts = s.params.shouldConcatPrompts; + return isSDXL && !shouldConcatPrompts; + }); return ( - {!shouldConcatPrompts && } + {withStylePrompts && } - {!shouldConcatPrompts && } + {withStylePrompts && } ); }); diff --git a/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessXAxis.tsx b/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessXAxis.tsx index b99bb637a1a..338feaa1e47 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessXAxis.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessXAxis.tsx @@ -1,14 +1,14 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setSeamlessXAxis } from 'features/controlLayers/store/canvasV2Slice'; +import { setSeamlessXAxis } from 'features/controlLayers/store/paramsSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSeamlessXAxis = () => { const { t } = useTranslation(); - const seamlessXAxis = useAppSelector((s) => s.canvasV2.params.seamlessXAxis); + const seamlessXAxis = useAppSelector((s) => s.params.seamlessXAxis); const dispatch = useAppDispatch(); diff --git a/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessYAxis.tsx b/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessYAxis.tsx index c5ab0f68c82..9b75f76798c 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessYAxis.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessYAxis.tsx @@ -1,14 +1,14 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setSeamlessYAxis } from 'features/controlLayers/store/canvasV2Slice'; +import { setSeamlessYAxis } from 'features/controlLayers/store/paramsSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSeamlessYAxis = () => { const { t } = useTranslation(); - const seamlessYAxis = useAppSelector((s) => s.canvasV2.params.seamlessYAxis); + const seamlessYAxis = useAppSelector((s) => s.params.seamlessYAxis); const dispatch = useAppDispatch(); const handleChange = useCallback( (e: ChangeEvent) => { diff --git a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedNumberInput.tsx b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedNumberInput.tsx index 6f2b57259f7..149e5135ae4 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedNumberInput.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedNumberInput.tsx @@ -2,13 +2,13 @@ import { CompositeNumberInput, FormControl, FormLabel } from '@invoke-ai/ui-libr import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from 'app/constants'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setSeed } from 'features/controlLayers/store/canvasV2Slice'; +import { setSeed } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; export const ParamSeedNumberInput = memo(() => { - const seed = useAppSelector((s) => s.canvasV2.params.seed); - const shouldRandomizeSeed = useAppSelector((s) => s.canvasV2.params.shouldRandomizeSeed); + const seed = useAppSelector((s) => s.params.seed); + const shouldRandomizeSeed = useAppSelector((s) => s.params.shouldRandomizeSeed); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedRandomize.tsx b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedRandomize.tsx index 0cbff60b59b..c25f5c0fe9e 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedRandomize.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedRandomize.tsx @@ -1,6 +1,6 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { setShouldRandomizeSeed } from 'features/controlLayers/store/canvasV2Slice'; +import { setShouldRandomizeSeed } from 'features/controlLayers/store/paramsSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,7 +9,7 @@ export const ParamSeedRandomize = memo(() => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const shouldRandomizeSeed = useAppSelector((s) => s.canvasV2.params.shouldRandomizeSeed); + const shouldRandomizeSeed = useAppSelector((s) => s.params.shouldRandomizeSeed); const handleChangeShouldRandomizeSeed = useCallback( (e: ChangeEvent) => dispatch(setShouldRandomizeSeed(e.target.checked)), diff --git a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedShuffle.tsx b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedShuffle.tsx index 2ca2ec5d197..24820bb6149 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedShuffle.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedShuffle.tsx @@ -2,14 +2,14 @@ import { Button } from '@invoke-ai/ui-library'; import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from 'app/constants'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import randomInt from 'common/util/randomInt'; -import { setSeed } from 'features/controlLayers/store/canvasV2Slice'; +import { setSeed } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiShuffleBold } from 'react-icons/pi'; export const ParamSeedShuffle = memo(() => { const dispatch = useAppDispatch(); - const shouldRandomizeSeed = useAppSelector((s) => s.canvasV2.params.shouldRandomizeSeed); + const shouldRandomizeSeed = useAppSelector((s) => s.params.shouldRandomizeSeed); const { t } = useTranslation(); const handleClickRandomizeSeed = useCallback( diff --git a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx index efcddf0ac3f..cf7008a3ea0 100644 --- a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx @@ -2,7 +2,7 @@ import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; -import { vaeSelected } from 'features/controlLayers/store/canvasV2Slice'; +import { vaeSelected } from 'features/controlLayers/store/paramsSlice'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -12,8 +12,8 @@ import type { VAEModelConfig } from 'services/api/types'; const ParamVAEModelSelect = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const model = useAppSelector((s) => s.canvasV2.params.model); - const vae = useAppSelector((s) => s.canvasV2.params.vae); + const model = useAppSelector((s) => s.params.model); + const vae = useAppSelector((s) => s.params.vae); const [modelConfigs, { isLoading }] = useVAEModels(); const getIsDisabled = useCallback( (vae: VAEModelConfig): boolean => { diff --git a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEPrecision.tsx b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEPrecision.tsx index ae1cac2152a..4677b97e0fa 100644 --- a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEPrecision.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEPrecision.tsx @@ -2,7 +2,7 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { vaePrecisionChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { vaePrecisionChanged } from 'features/controlLayers/store/paramsSlice'; import { isParameterPrecision } from 'features/parameters/types/parameterSchemas'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -15,7 +15,7 @@ const options = [ const ParamVAEModelSelect = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const vaePrecision = useAppSelector((s) => s.canvasV2.params.vaePrecision); + const vaePrecision = useAppSelector((s) => s.params.vaePrecision); const onChange = useCallback( (v) => { diff --git a/invokeai/frontend/web/src/features/prompt/PromptTriggerSelect.tsx b/invokeai/frontend/web/src/features/prompt/PromptTriggerSelect.tsx index 493979f5047..ad89f6872d9 100644 --- a/invokeai/frontend/web/src/features/prompt/PromptTriggerSelect.tsx +++ b/invokeai/frontend/web/src/features/prompt/PromptTriggerSelect.tsx @@ -17,7 +17,7 @@ const noOptionsMessage = () => t('prompt.noMatchingTriggers'); export const PromptTriggerSelect = memo(({ onSelect, onClose }: PromptTriggerSelectProps) => { const { t } = useTranslation(); - const mainModel = useAppSelector((s) => s.canvasV2.params.model); + const mainModel = useAppSelector((s) => s.params.model); const addedLoRAs = useAppSelector((s) => s.canvasV2.loras); const { data: mainModelConfig, isLoading: isLoadingMainModelConfig } = useGetModelConfigQuery( mainModel?.key ?? skipToken diff --git a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx b/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx index 0d6c2163570..7fbd97f6d5a 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx @@ -2,7 +2,7 @@ import { Divider, Flex, ListItem, Text, Tooltip, UnorderedList } from '@invoke-a import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { useIsReadyToEnqueue } from 'common/hooks/useIsReadyToEnqueue'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; +import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; import type { PropsWithChildren } from 'react'; @@ -11,8 +11,8 @@ import { useTranslation } from 'react-i18next'; import { useEnqueueBatchMutation } from 'services/api/endpoints/queue'; import { useBoardName } from 'services/api/hooks/useBoardName'; -const selectPromptsCount = createSelector(selectCanvasV2Slice, selectDynamicPromptsSlice, (canvasV2, dynamicPrompts) => - getShouldProcessPrompt(canvasV2.params.positivePrompt) ? dynamicPrompts.prompts.length : 1 +const selectPromptsCount = createSelector(selectParamsSlice, selectDynamicPromptsSlice, (params, dynamicPrompts) => + getShouldProcessPrompt(params.positivePrompt) ? dynamicPrompts.prompts.length : 1 ); type Props = { @@ -32,7 +32,7 @@ const TooltipContent = memo(({ prepend = false }: Props) => { const { isReady, reasons } = useIsReadyToEnqueue(); const isLoadingDynamicPrompts = useAppSelector((s) => s.dynamicPrompts.isLoading); const promptsCount = useAppSelector(selectPromptsCount); - const iterationsCount = useAppSelector((s) => s.canvasV2.params.iterations); + const iterationsCount = useAppSelector((s) => s.params.iterations); const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId); const autoAddBoardName = useBoardName(autoAddBoardId); const [_, { isLoading }] = useEnqueueBatchMutation({ diff --git a/invokeai/frontend/web/src/features/queue/components/QueueIterationsNumberInput.tsx b/invokeai/frontend/web/src/features/queue/components/QueueIterationsNumberInput.tsx index 61278d9a2c2..a9a0a055a1e 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueIterationsNumberInput.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueIterationsNumberInput.tsx @@ -1,11 +1,11 @@ import { CompositeNumberInput } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setIterations } from 'features/controlLayers/store/canvasV2Slice'; +import { setIterations } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; export const QueueIterationsNumberInput = memo(() => { - const iterations = useAppSelector((s) => s.canvasV2.params.iterations); + const iterations = useAppSelector((s) => s.params.iterations); const coarseStep = useAppSelector((s) => s.config.sd.iterations.coarseStep); const fineStep = useAppSelector((s) => s.config.sd.iterations.fineStep); const dispatch = useAppDispatch(); diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt.tsx index ba1918d05f8..0bd542e0dba 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt.tsx @@ -1,6 +1,6 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { negativePrompt2Changed } from 'features/controlLayers/store/canvasV2Slice'; +import { negativePrompt2Changed } from 'features/controlLayers/store/paramsSlice'; import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; @@ -12,7 +12,7 @@ import { useTranslation } from 'react-i18next'; export const ParamSDXLNegativeStylePrompt = memo(() => { const dispatch = useAppDispatch(); - const prompt = useAppSelector((s) => s.canvasV2.params.negativePrompt2); + const prompt = useAppSelector((s) => s.params.negativePrompt2); const textareaRef = useRef(null); const { t } = useTranslation(); const handleChange = useCallback( diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt.tsx index ee8b2419652..a73bf76d90e 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt.tsx @@ -1,6 +1,6 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { positivePrompt2Changed } from 'features/controlLayers/store/canvasV2Slice'; +import { positivePrompt2Changed } from 'features/controlLayers/store/paramsSlice'; import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; @@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next'; export const ParamSDXLPositiveStylePrompt = memo(() => { const dispatch = useAppDispatch(); - const prompt = useAppSelector((s) => s.canvasV2.params.positivePrompt2); + const prompt = useAppSelector((s) => s.params.positivePrompt2); const textareaRef = useRef(null); const { t } = useTranslation(); const handleChange = useCallback( diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/SDXLConcatButton.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/SDXLConcatButton.tsx index 31da96b197c..3cde1edc64f 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/SDXLConcatButton.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/SDXLConcatButton.tsx @@ -1,12 +1,12 @@ import { IconButton, Tooltip } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { shouldConcatPromptsChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { shouldConcatPromptsChanged } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiLinkSimpleBold, PiLinkSimpleBreakBold } from 'react-icons/pi'; export const SDXLConcatButton = memo(() => { - const shouldConcatPrompts = useAppSelector((s) => s.canvasV2.params.shouldConcatPrompts); + const shouldConcatPrompts = useAppSelector((s) => s.params.shouldConcatPrompts); const dispatch = useAppDispatch(); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale.tsx index e110a770e31..af4ebdabb80 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale.tsx @@ -1,14 +1,14 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setRefinerCFGScale } from 'features/controlLayers/store/canvasV2Slice'; +import { setRefinerCFGScale } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerCFGScale = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const refinerCFGScale = useAppSelector((s) => s.canvasV2.params.refinerCFGScale); + const refinerCFGScale = useAppSelector((s) => s.params.refinerCFGScale); const sliderMin = useAppSelector((s) => s.config.sd.guidance.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.guidance.sliderMax); const numberInputMin = useAppSelector((s) => s.config.sd.guidance.numberInputMin); diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx index 6e175b17623..e54a8329b96 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx @@ -2,7 +2,7 @@ import { Box, Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { useModelCombobox } from 'common/hooks/useModelCombobox'; -import { refinerModelChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { refinerModelChanged } from 'features/controlLayers/store/paramsSlice'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -13,7 +13,7 @@ const optionsFilter = (model: MainModelConfig) => model.base === 'sdxl-refiner'; const ParamSDXLRefinerModelSelect = () => { const dispatch = useAppDispatch(); - const model = useAppSelector((s) => s.canvasV2.params.refinerModel); + const model = useAppSelector((s) => s.params.refinerModel); const { t } = useTranslation(); const [modelConfigs, { isLoading }] = useRefinerModels(); const _onChange = useCallback( diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore.tsx index a98eae78e73..f378acd9421 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore.tsx @@ -1,12 +1,12 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setRefinerNegativeAestheticScore } from 'features/controlLayers/store/canvasV2Slice'; +import { setRefinerNegativeAestheticScore } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerNegativeAestheticScore = () => { - const refinerNegativeAestheticScore = useAppSelector((s) => s.canvasV2.params.refinerNegativeAestheticScore); + const refinerNegativeAestheticScore = useAppSelector((s) => s.params.refinerNegativeAestheticScore); const dispatch = useAppDispatch(); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerPositiveAestheticScore.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerPositiveAestheticScore.tsx index 61ff4de9256..bc92f5a1788 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerPositiveAestheticScore.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerPositiveAestheticScore.tsx @@ -1,12 +1,12 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setRefinerPositiveAestheticScore } from 'features/controlLayers/store/canvasV2Slice'; +import { setRefinerPositiveAestheticScore } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerPositiveAestheticScore = () => { - const refinerPositiveAestheticScore = useAppSelector((s) => s.canvasV2.params.refinerPositiveAestheticScore); + const refinerPositiveAestheticScore = useAppSelector((s) => s.params.refinerPositiveAestheticScore); const dispatch = useAppDispatch(); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerScheduler.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerScheduler.tsx index 7e0f0acfd5d..f2b63f65424 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerScheduler.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerScheduler.tsx @@ -2,7 +2,7 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setRefinerScheduler } from 'features/controlLayers/store/canvasV2Slice'; +import { setRefinerScheduler } from 'features/controlLayers/store/paramsSlice'; import { SCHEDULER_OPTIONS } from 'features/parameters/types/constants'; import { isParameterScheduler } from 'features/parameters/types/parameterSchemas'; import { memo, useCallback, useMemo } from 'react'; @@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerScheduler = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const refinerScheduler = useAppSelector((s) => s.canvasV2.params.refinerScheduler); + const refinerScheduler = useAppSelector((s) => s.params.refinerScheduler); const onChange = useCallback( (v) => { diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart.tsx index fc364ab1a8a..c5024d5ecac 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart.tsx @@ -1,12 +1,12 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setRefinerStart } from 'features/controlLayers/store/canvasV2Slice'; +import { setRefinerStart } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerStart = () => { - const refinerStart = useAppSelector((s) => s.canvasV2.params.refinerStart); + const refinerStart = useAppSelector((s) => s.params.refinerStart); const dispatch = useAppDispatch(); const handleChange = useCallback((v: number) => dispatch(setRefinerStart(v)), [dispatch]); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerSteps.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerSteps.tsx index 49018932ed1..f24e896a800 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerSteps.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerSteps.tsx @@ -1,14 +1,14 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setRefinerSteps } from 'features/controlLayers/store/canvasV2Slice'; +import { setRefinerSteps } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerSteps = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const refinerSteps = useAppSelector((s) => s.canvasV2.params.refinerSteps); + const refinerSteps = useAppSelector((s) => s.params.refinerSteps); const initial = useAppSelector((s) => s.config.sd.steps.initial); const sliderMin = useAppSelector((s) => s.config.sd.steps.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.steps.sliderMax); diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx index 3a67ffa41a1..2c7616118e4 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx @@ -3,7 +3,7 @@ import { Flex, FormControlGroup, StandaloneAccordion } from '@invoke-ai/ui-libra import { skipToken } from '@reduxjs/toolkit/query'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; +import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; import ParamCFGRescaleMultiplier from 'features/parameters/components/Advanced/ParamCFGRescaleMultiplier'; import ParamClipSkip from 'features/parameters/components/Advanced/ParamClipSkip'; import ParamSeamlessXAxis from 'features/parameters/components/Seamless/ParamSeamlessXAxis'; @@ -28,13 +28,13 @@ const formLabelProps2: FormLabelProps = { }; export const AdvancedSettingsAccordion = memo(() => { - const vaeKey = useAppSelector((state) => state.canvasV2.params.vae?.key); + const vaeKey = useAppSelector((state) => state.params.vae?.key); const { currentData: vaeConfig } = useGetModelConfigQuery(vaeKey ?? skipToken); const activeTabName = useAppSelector(selectActiveTab); const selectBadges = useMemo( () => - createMemoizedSelector(selectCanvasV2Slice, ({ params }) => { + createMemoizedSelector(selectParamsSlice, (params) => { const badges: (string | number)[] = []; if (vaeConfig) { let vaeBadge = vaeConfig.name; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx index 51dd3ed578b..a0b9e018299 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx @@ -2,6 +2,7 @@ import type { FormLabelProps } from '@invoke-ai/ui-library'; import { Expander, Flex, FormControlGroup, StandaloneAccordion } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; +import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; import { HrfSettings } from 'features/hrf/components/HrfSettings'; import { selectHrfSlice } from 'features/hrf/store/hrfSlice'; @@ -18,31 +19,34 @@ import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/ import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -const selector = createMemoizedSelector([selectHrfSlice, selectCanvasV2Slice], (hrf, canvasV2) => { - const { shouldRandomizeSeed, model } = canvasV2.params; - const { hrfEnabled } = hrf; - const badges: string[] = []; - const isSDXL = model?.base === 'sdxl'; +const selector = createMemoizedSelector( + [selectHrfSlice, selectCanvasV2Slice, selectParamsSlice], + (hrf, canvasV2, params) => { + const { shouldRandomizeSeed, model } = params; + const { hrfEnabled } = hrf; + const badges: string[] = []; + const isSDXL = model?.base === 'sdxl'; - const { aspectRatio } = canvasV2.bbox; - const { width, height } = canvasV2.bbox.rect; + const { aspectRatio } = canvasV2.bbox; + const { width, height } = canvasV2.bbox.rect; - badges.push(`${width}×${height}`); - badges.push(aspectRatio.id); + badges.push(`${width}×${height}`); + badges.push(aspectRatio.id); - if (aspectRatio.isLocked) { - badges.push('locked'); - } + if (aspectRatio.isLocked) { + badges.push('locked'); + } - if (!shouldRandomizeSeed) { - badges.push('Manual Seed'); - } + if (!shouldRandomizeSeed) { + badges.push('Manual Seed'); + } - if (hrfEnabled && !isSDXL) { - badges.push('HiRes Fix'); + if (hrfEnabled && !isSDXL) { + badges.push('HiRes Fix'); + } + return { badges, isSDXL }; } - return { badges, isSDXL }; -}); +); const scalingLabelProps: FormLabelProps = { minW: '4.5rem', diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion.tsx index c854c4960e1..bb8c5627bff 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion.tsx @@ -2,7 +2,7 @@ import type { FormLabelProps } from '@invoke-ai/ui-library'; import { Flex, FormControlGroup, StandaloneAccordion, Text } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; +import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; import ParamSDXLRefinerCFGScale from 'features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale'; import ParamSDXLRefinerModelSelect from 'features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect'; import ParamSDXLRefinerNegativeAestheticScore from 'features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore'; @@ -24,7 +24,7 @@ const stepsScaleLabelProps: FormLabelProps = { minW: '5rem', }; -const selectBadges = createMemoizedSelector(selectCanvasV2Slice, ({ params }) => +const selectBadges = createMemoizedSelector(selectParamsSlice, (params) => params.refinerModel ? ['Enabled'] : undefined ); @@ -60,7 +60,7 @@ const RefinerSettingsAccordionNoRefiner: React.FC = memo(() => { RefinerSettingsAccordionNoRefiner.displayName = 'RefinerSettingsAccordionNoRefiner'; const RefinerSettingsAccordionContent: React.FC = memo(() => { - const isRefinerModelSelected = useAppSelector((state) => !isNil(state.canvasV2.params.refinerModel)); + const isRefinerModelSelected = useAppSelector((state) => !isNil(state.params.refinerModel)); return ( diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleWarning.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleWarning.tsx index 0992ac865a3..8a791032b61 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleWarning.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleWarning.tsx @@ -10,7 +10,7 @@ import { useControlNetModels } from 'services/api/hooks/modelsByType'; export const UpscaleWarning = () => { const { t } = useTranslation(); - const model = useAppSelector((s) => s.canvasV2.params.model); + const model = useAppSelector((s) => s.params.model); const upscaleModel = useAppSelector((s) => s.upscale.upscaleModel); const tileControlnetModel = useAppSelector((s) => s.upscale.tileControlnetModel); const upscaleInitialImage = useAppSelector((s) => s.upscale.upscaleInitialImage); diff --git a/invokeai/frontend/web/src/features/stylePresets/components/ActiveStylePreset.tsx b/invokeai/frontend/web/src/features/stylePresets/components/ActiveStylePreset.tsx index 39f1dd0d3f2..60d14bde221 100644 --- a/invokeai/frontend/web/src/features/stylePresets/components/ActiveStylePreset.tsx +++ b/invokeai/frontend/web/src/features/stylePresets/components/ActiveStylePreset.tsx @@ -1,6 +1,6 @@ import { Badge, Flex, IconButton, Spacer, Text, Tooltip } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { negativePromptChanged, positivePromptChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { negativePromptChanged, positivePromptChanged } from 'features/controlLayers/store/paramsSlice'; import { usePresetModifiedPrompts } from 'features/stylePresets/hooks/usePresetModifiedPrompts'; import { activeStylePresetIdChanged, viewModeChanged } from 'features/stylePresets/store/stylePresetSlice'; import type { MouseEventHandler } from 'react'; diff --git a/invokeai/frontend/web/src/features/stylePresets/hooks/usePresetModifiedPrompts.ts b/invokeai/frontend/web/src/features/stylePresets/hooks/usePresetModifiedPrompts.ts index 661a6972315..516121a0390 100644 --- a/invokeai/frontend/web/src/features/stylePresets/hooks/usePresetModifiedPrompts.ts +++ b/invokeai/frontend/web/src/features/stylePresets/hooks/usePresetModifiedPrompts.ts @@ -10,8 +10,8 @@ export const buildPresetModifiedPrompt = (presetPrompt: string, currentPrompt: s }; export const usePresetModifiedPrompts = () => { - const positivePrompt = useAppSelector((s) => s.canvasV2.params.positivePrompt); - const negativePrompt = useAppSelector((s) => s.canvasV2.params.negativePrompt); + const positivePrompt = useAppSelector((s) => s.params.positivePrompt); + const negativePrompt = useAppSelector((s) => s.params.negativePrompt); const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId); diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx index 7f72eb37a71..9b19616fa1e 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx @@ -19,7 +19,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import { useClearStorage } from 'common/hooks/useClearStorage'; -import { shouldUseCpuNoiseChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { shouldUseCpuNoiseChanged } from 'features/controlLayers/store/paramsSlice'; import { SettingsDeveloperLogIsEnabled } from 'features/system/components/SettingsModal/SettingsDeveloperLogIsEnabled'; import { SettingsDeveloperLogLevel } from 'features/system/components/SettingsModal/SettingsDeveloperLogLevel'; import { SettingsDeveloperLogNamespaces } from 'features/system/components/SettingsModal/SettingsDeveloperLogNamespaces'; @@ -89,7 +89,7 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => { const { isOpen: isRefreshModalOpen, onOpen: onRefreshModalOpen, onClose: onRefreshModalClose } = useDisclosure(); - const shouldUseCpuNoise = useAppSelector((s) => s.canvasV2.params.shouldUseCpuNoise); + const shouldUseCpuNoise = useAppSelector((s) => s.params.shouldUseCpuNoise); const shouldConfirmOnDelete = useAppSelector((s) => s.system.shouldConfirmOnDelete); const shouldShowProgressInViewer = useAppSelector((s) => s.ui.shouldShowProgressInViewer); const shouldAntialiasProgressImage = useAppSelector((s) => s.system.shouldAntialiasProgressImage); diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx index 85905142d3a..0c574654045 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx @@ -49,7 +49,7 @@ const ParametersPanelTextToImage = () => { } return `${t('controlLayers.controlLayers')} (${controlLayersCount})`; }, [controlLayersCount, t]); - const isSDXL = useAppSelector((s) => s.canvasV2.params.model?.base === 'sdxl'); + const isSDXL = useAppSelector((s) => s.params.model?.base === 'sdxl'); const onChangeTabs = useCallback( (i: number) => { if (i === 1) { diff --git a/invokeai/frontend/web/src/services/api/hooks/useSelectedModelConfig.ts b/invokeai/frontend/web/src/services/api/hooks/useSelectedModelConfig.ts index 3c43101d4c9..d4dbdd37fd1 100644 --- a/invokeai/frontend/web/src/services/api/hooks/useSelectedModelConfig.ts +++ b/invokeai/frontend/web/src/services/api/hooks/useSelectedModelConfig.ts @@ -3,7 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { useGetModelConfigQuery } from 'services/api/endpoints/models'; export const useSelectedModelConfig = () => { - const key = useAppSelector((s) => s.canvasV2.params.model?.key); + const key = useAppSelector((s) => s.params.model?.key); const { currentData: modelConfig } = useGetModelConfigQuery(key ?? skipToken); return modelConfig; From 2b1e930cdfcc35226799d8b34f2f332b92c0d628 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 26 Aug 2024 21:52:43 +1000 Subject: [PATCH 500/678] feat(ui): split out tool state from canvas rendering state --- invokeai/frontend/web/src/app/store/store.ts | 3 ++ .../CanvasSettingsInvertScrollCheckbox.tsx | 4 +- .../components/Tool/ToolBrushWidth.tsx | 4 +- .../components/Tool/ToolEraserWidth.tsx | 4 +- .../components/Tool/ToolFillColorPicker.tsx | 4 +- .../controlLayers/konva/CanvasLayerAdapter.ts | 7 +-- .../controlLayers/konva/CanvasManager.ts | 2 +- .../controlLayers/konva/CanvasMaskAdapter.ts | 7 +-- .../konva/CanvasRenderingModule.ts | 28 +++------- .../konva/CanvasStateApiModule.ts | 44 +++++++-------- .../controlLayers/store/canvasV2Slice.ts | 17 ------ .../controlLayers/store/toolReducers.ts | 17 ------ .../features/controlLayers/store/toolSlice.ts | 54 +++++++++++++++++++ .../src/features/controlLayers/store/types.ts | 6 --- 14 files changed, 96 insertions(+), 105 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/toolSlice.ts diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index d507a838946..72be76bf1c6 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -8,6 +8,7 @@ import { deepClone } from 'common/util/deepClone'; import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice'; import { canvasV2PersistConfig, canvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { paramsPersistConfig, paramsSlice } from 'features/controlLayers/store/paramsSlice'; +import { toolPersistConfig, toolSlice } from 'features/controlLayers/store/toolSlice'; import { deleteImageModalSlice } from 'features/deleteImageModal/store/slice'; import { dynamicPromptsPersistConfig, dynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/gallerySlice'; @@ -59,6 +60,7 @@ const allReducers = { [upscaleSlice.name]: upscaleSlice.reducer, [stylePresetSlice.name]: stylePresetSlice.reducer, [paramsSlice.name]: paramsSlice.reducer, + [toolSlice.name]: toolSlice.reducer, }; const rootReducer = combineReducers(allReducers); @@ -101,6 +103,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = { [upscalePersistConfig.name]: upscalePersistConfig, [stylePresetPersistConfig.name]: stylePresetPersistConfig, [paramsPersistConfig.name]: paramsPersistConfig, + [toolPersistConfig.name]: toolPersistConfig, }; const unserialize: UnserializeFunction = (data, key) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox.tsx index 1c9a4f174ce..e4a8abd1b0a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox.tsx @@ -1,6 +1,6 @@ import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { invertScrollChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { invertScrollChanged } from 'features/controlLayers/store/toolSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'; export const CanvasSettingsInvertScrollCheckbox = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const invertScroll = useAppSelector((s) => s.canvasV2.tool.invertScroll); + const invertScroll = useAppSelector((s) => s.tool.invertScroll); const onChange = useCallback( (e: ChangeEvent) => dispatch(invertScrollChanged(e.target.checked)), [dispatch] diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushWidth.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushWidth.tsx index 20a51155315..0b91faf3beb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushWidth.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushWidth.tsx @@ -10,7 +10,7 @@ import { PopoverTrigger, } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { brushWidthChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { brushWidthChanged } from 'features/controlLayers/store/toolSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -20,7 +20,7 @@ const formatPx = (v: number | string) => `${v} px`; export const ToolBrushWidth = memo(() => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const width = useAppSelector((s) => s.canvasV2.tool.brush.width); + const width = useAppSelector((s) => s.tool.brush.width); const onChange = useCallback( (v: number) => { dispatch(brushWidthChanged(Math.round(v))); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserWidth.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserWidth.tsx index e227e0bedd1..8f7eb2ad4fb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserWidth.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserWidth.tsx @@ -10,7 +10,7 @@ import { PopoverTrigger, } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { eraserWidthChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { eraserWidthChanged } from 'features/controlLayers/store/toolSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -20,7 +20,7 @@ const formatPx = (v: number | string) => `${v} px`; export const ToolEraserWidth = memo(() => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const width = useAppSelector((s) => s.canvasV2.tool.eraser.width); + const width = useAppSelector((s) => s.tool.eraser.width); const onChange = useCallback( (v: number) => { dispatch(eraserWidthChanged(Math.round(v))); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolFillColorPicker.tsx index 477e23bc594..69f2afec91f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolFillColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolFillColorPicker.tsx @@ -2,14 +2,14 @@ import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@inv import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIColorPicker from 'common/components/IAIColorPicker'; import { rgbaColorToString } from 'common/util/colorCodeTransformers'; -import { fillChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { fillChanged } from 'features/controlLayers/store/toolSlice'; import type { RgbaColor } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; export const ToolFillColorPicker = memo(() => { const { t } = useTranslation(); - const fill = useAppSelector((s) => s.canvasV2.tool.fill); + const fill = useAppSelector((s) => s.tool.fill); const dispatch = useAppDispatch(); const onChange = useCallback( (color: RgbaColor) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts index 47f803fbf94..50ef8faa47b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts @@ -11,7 +11,6 @@ import type { CanvasEntityIdentifier, CanvasEraserLineState, CanvasRasterLayerState, - CanvasV2State, Coordinate, Rect, } from 'features/controlLayers/store/types'; @@ -83,11 +82,7 @@ export class CanvasLayerAdapter extends CanvasModuleBase { this.konva.layer.destroy(); }; - update = async (arg?: { - state: CanvasLayerAdapter['state']; - toolState: CanvasV2State['tool']; - isSelected: boolean; - }) => { + update = async (arg?: { state: CanvasLayerAdapter['state'] }) => { const state = get(arg, 'state', this.state); if (!this.isFirstRender && state === this.state) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index b836eca30ab..072a7a5d122 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -112,7 +112,7 @@ export class CanvasManager extends CanvasModuleBase { // These atoms require the canvas manager to be set up before we can provide their initial values this.stateApi.$transformingEntity.set(null); this.stateApi.$toolState.set(this.stateApi.getToolState()); - this.stateApi.$selectedEntityIdentifier.set(this.stateApi.getState().selectedEntityIdentifier); + this.stateApi.$selectedEntityIdentifier.set(this.stateApi.getCanvasState().selectedEntityIdentifier); this.stateApi.$currentFill.set(this.stateApi.getCurrentFill()); this.stateApi.$selectedEntity.set(this.stateApi.getSelectedEntity()); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts index a0aa213615e..cd0f16d73cf 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts @@ -11,7 +11,6 @@ import type { CanvasEraserLineState, CanvasInpaintMaskState, CanvasRegionalGuidanceState, - CanvasV2State, Coordinate, Rect, } from 'features/controlLayers/store/types'; @@ -83,11 +82,7 @@ export class CanvasMaskAdapter extends CanvasModuleBase { this.konva.layer.destroy(); }; - update = async (arg?: { - state: CanvasMaskAdapter['state']; - toolState: CanvasV2State['tool']; - isSelected: boolean; - }) => { + update = async (arg?: { state: CanvasMaskAdapter['state'] }) => { const state = get(arg, 'state', this.state); if (!this.isFirstRender && state === this.state && state.fill === this.state.fill) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts index 69236633bb8..3c25a795a30 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts @@ -28,7 +28,7 @@ export class CanvasRenderingModule extends CanvasModuleBase { } render = async () => { - const state = this.manager.stateApi.getState(); + const state = this.manager.stateApi.getCanvasState(); if (!this.state) { this.log.trace('First render'); @@ -51,7 +51,7 @@ export class CanvasRenderingModule extends CanvasModuleBase { await this.renderStagingArea(state, prevState); this.arrangeEntities(state, prevState); - this.manager.stateApi.$toolState.set(state.tool); + this.manager.stateApi.$toolState.set(this.manager.stateApi.getToolState()); this.manager.stateApi.$selectedEntityIdentifier.set(state.selectedEntityIdentifier); this.manager.stateApi.$selectedEntity.set(this.manager.stateApi.getSelectedEntity()); this.manager.stateApi.$currentFill.set(this.manager.stateApi.getCurrentFill()); @@ -96,11 +96,7 @@ export class CanvasRenderingModule extends CanvasModuleBase { adapterMap.set(adapter.id, adapter); this.manager.stage.addLayer(adapter.konva.layer); } - await adapter.update({ - state: entityState, - toolState: state.tool, - isSelected: state.selectedEntityIdentifier?.id === entityState.id, - }); + await adapter.update({ state: entityState }); } } }; @@ -129,11 +125,7 @@ export class CanvasRenderingModule extends CanvasModuleBase { adapterMap.set(adapter.id, adapter); this.manager.stage.addLayer(adapter.konva.layer); } - await adapter.update({ - state: entityState, - toolState: state.tool, - isSelected: state.selectedEntityIdentifier?.id === entityState.id, - }); + await adapter.update({ state: entityState }); } } }; @@ -167,11 +159,7 @@ export class CanvasRenderingModule extends CanvasModuleBase { adapterMap.set(adapter.id, adapter); this.manager.stage.addLayer(adapter.konva.layer); } - await adapter.update({ - state: entityState, - toolState: state.tool, - isSelected: state.selectedEntityIdentifier?.id === entityState.id, - }); + await adapter.update({ state: entityState }); } } }; @@ -205,11 +193,7 @@ export class CanvasRenderingModule extends CanvasModuleBase { adapterMap.set(adapter.id, adapter); this.manager.stage.addLayer(adapter.konva.layer); } - await adapter.update({ - state: entityState, - toolState: state.tool, - isSelected: state.selectedEntityIdentifier?.id === entityState.id, - }); + await adapter.update({ state: entityState }); } } }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index 60671a7f989..edb650f5041 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -7,7 +7,6 @@ import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase' import { getPrefixedId } from 'features/controlLayers/konva/util'; import { bboxChanged, - brushWidthChanged, entityBrushLineAdded, entityEraserLineAdded, entityMoved, @@ -15,17 +14,20 @@ import { entityRectAdded, entityReset, entitySelected, - eraserWidthChanged, - fillChanged, } from 'features/controlLayers/store/canvasV2Slice'; import { selectAllRenderableEntities } from 'features/controlLayers/store/selectors'; +import { + brushWidthChanged, + eraserWidthChanged, + fillChanged, + type ToolState, +} from 'features/controlLayers/store/toolSlice'; import type { CanvasControlLayerState, CanvasEntityIdentifier, CanvasInpaintMaskState, CanvasRasterLayerState, CanvasRegionalGuidanceState, - CanvasV2State, Coordinate, EntityBrushLineAddedPayload, EntityEraserLineAddedPayload, @@ -97,7 +99,7 @@ export class CanvasStateApiModule extends CanvasModuleBase { } // Reminder - use arrow functions to avoid binding issues - getState = () => { + getCanvasState = () => { return this.store.getState().canvasV2; }; resetEntity = (arg: EntityIdentifierPayload) => { @@ -141,36 +143,36 @@ export class CanvasStateApiModule extends CanvasModuleBase { ); }; getBbox = () => { - return this.getState().bbox; + return this.getCanvasState().bbox; }; getToolState = () => { - return this.getState().tool; + return this.store.getState().tool; }; getSettings = () => { - return this.getState().settings; + return this.getCanvasState().settings; }; getRegionsState = () => { - return this.getState().regions; + return this.getCanvasState().regions; }; getRasterLayersState = () => { - return this.getState().rasterLayers; + return this.getCanvasState().rasterLayers; }; getControlLayersState = () => { - return this.getState().controlLayers; + return this.getCanvasState().controlLayers; }; getInpaintMasksState = () => { - return this.getState().inpaintMasks; + return this.getCanvasState().inpaintMasks; }; getSession = () => { - return this.getState().session; + return this.getCanvasState().session; }; getIsSelected = (id: string) => { - return this.getState().selectedEntityIdentifier?.id === id; + return this.getCanvasState().selectedEntityIdentifier?.id === id; }; getEntity(identifier: CanvasEntityIdentifier): EntityStateAndAdapter | null { - const state = this.getState(); + const state = this.getCanvasState(); let entityState: EntityStateAndAdapter['state'] | null = null; let entityAdapter: EntityStateAndAdapter['adapter'] | null = null; @@ -202,7 +204,7 @@ export class CanvasStateApiModule extends CanvasModuleBase { } getRenderedEntityCount = () => { - const renderableEntities = selectAllRenderableEntities(this.getState()); + const renderableEntities = selectAllRenderableEntities(this.getCanvasState()); let count = 0; for (const entity of renderableEntities) { if (entity.isEnabled) { @@ -213,7 +215,7 @@ export class CanvasStateApiModule extends CanvasModuleBase { }; getSelectedEntity = () => { - const state = this.getState(); + const state = this.getCanvasState(); if (state.selectedEntityIdentifier) { return this.getEntity(state.selectedEntityIdentifier); } @@ -221,8 +223,7 @@ export class CanvasStateApiModule extends CanvasModuleBase { }; getCurrentFill = () => { - const state = this.getState(); - let currentFill: RgbaColor = state.tool.fill; + let currentFill: RgbaColor = this.getToolState().fill; const selectedEntity = this.getSelectedEntity(); if (selectedEntity) { // These two entity types use a compositing rect for opacity. Their fill is always a solid color. @@ -239,15 +240,14 @@ export class CanvasStateApiModule extends CanvasModuleBase { // The brush should use the mask opacity for these enktity types return { ...selectedEntity.state.fill.color, a: 1 }; } else { - const state = this.getState(); - return state.tool.fill; + return this.getToolState().fill; } }; $transformingEntity = atom(null); $isProcessingTransform = atom(false); - $toolState: WritableAtom = atom(); + $toolState: WritableAtom = atom(); $currentFill: WritableAtom = atom(); $selectedEntity: WritableAtom = atom(); $selectedEntityIdentifier: WritableAtom = atom(); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index fb765db6b9d..5134b1bf50c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -15,7 +15,6 @@ import { regionsReducers } from 'features/controlLayers/store/regionsReducers'; import { selectAllEntities, selectAllEntitiesOfType, selectEntity } from 'features/controlLayers/store/selectors'; import { sessionReducers } from 'features/controlLayers/store/sessionReducers'; import { settingsReducers } from 'features/controlLayers/store/settingsReducers'; -import { toolReducers } from 'features/controlLayers/store/toolReducers'; import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; import { simplifyFlatNumbersArray } from 'features/controlLayers/util/simplify'; import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; @@ -57,16 +56,6 @@ const initialState: CanvasV2State = { }, loras: [], ipAdapters: { entities: [] }, - tool: { - invertScroll: false, - fill: { r: 31, g: 160, b: 224, a: 1 }, // invokeBlue.500 - brush: { - width: 50, - }, - eraser: { - width: 50, - }, - }, bbox: { rect: { x: 0, y: 0, width: 512, height: 512 }, optimalDimension: 512, @@ -109,7 +98,6 @@ export const canvasV2Slice = createSlice({ // move out ...lorasReducers, ...settingsReducers, - ...toolReducers, ...sessionReducers, entitySelected: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; @@ -364,7 +352,6 @@ export const canvasV2Slice = createSlice({ const size = pick(state.bbox.rect, 'width', 'height'); state.bbox.scaledSize = getScaledBoundingBoxDimensions(size, state.bbox.optimalDimension); state.session = deepClone(initialState.session); - state.tool = deepClone(initialState.tool); state.ipAdapters = deepClone(initialState.ipAdapters); state.rasterLayers = deepClone(initialState.rasterLayers); @@ -403,10 +390,6 @@ export const canvasV2Slice = createSlice({ }); export const { - brushWidthChanged, - eraserWidthChanged, - fillChanged, - invertScrollChanged, clipToBboxChanged, canvasReset, settingsDynamicGridToggled, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts deleted file mode 100644 index 74916f783b2..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import type { CanvasV2State, RgbaColor } from 'features/controlLayers/store/types'; - -export const toolReducers = { - brushWidthChanged: (state, action: PayloadAction) => { - state.tool.brush.width = Math.round(action.payload); - }, - eraserWidthChanged: (state, action: PayloadAction) => { - state.tool.eraser.width = Math.round(action.payload); - }, - fillChanged: (state, action: PayloadAction) => { - state.tool.fill = action.payload; - }, - invertScrollChanged: (state, action: PayloadAction) => { - state.tool.invertScroll = action.payload; - }, -} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/toolSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/toolSlice.ts new file mode 100644 index 00000000000..bbe70d3bca7 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/toolSlice.ts @@ -0,0 +1,54 @@ +import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; +import type { PersistConfig } from 'app/store/store'; +import type { RgbaColor } from 'features/controlLayers/store/types'; + +export type ToolState = { + invertScroll: boolean; + brush: { width: number }; + eraser: { width: number }; + fill: RgbaColor; +}; + +const initialState: ToolState = { + invertScroll: false, + fill: { r: 31, g: 160, b: 224, a: 1 }, // invokeBlue.500 + brush: { + width: 50, + }, + eraser: { + width: 50, + }, +}; + +export const toolSlice = createSlice({ + name: 'tool', + initialState, + reducers: { + brushWidthChanged: (state, action: PayloadAction) => { + state.brush.width = Math.round(action.payload); + }, + eraserWidthChanged: (state, action: PayloadAction) => { + state.eraser.width = Math.round(action.payload); + }, + fillChanged: (state, action: PayloadAction) => { + state.fill = action.payload; + }, + invertScrollChanged: (state, action: PayloadAction) => { + state.invertScroll = action.payload; + }, + }, +}); + +export const { brushWidthChanged, eraserWidthChanged, fillChanged, invertScrollChanged } = toolSlice.actions; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +const migrate = (state: any): any => { + return state; +}; + +export const toolPersistConfig: PersistConfig = { + name: toolSlice.name, + initialState, + migrate, + persistDenylist: [], +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 628bda4c806..766cdf8b3ac 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -715,12 +715,6 @@ export type CanvasV2State = { entities: CanvasIPAdapterState[]; }; loras: LoRA[]; - tool: { - invertScroll: boolean; - brush: { width: number }; - eraser: { width: number }; - fill: RgbaColor; - }; settings: { imageSmoothing: boolean; showHUD: boolean; From a07346b3647b60af80bee0b2182a0a161c7f4675 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 26 Aug 2024 22:02:56 +1000 Subject: [PATCH 501/678] feat(ui): split out settings state from canvas rendering state --- invokeai/frontend/web/src/app/store/store.ts | 3 ++ .../CanvasSettingsAutoSaveCheckbox.tsx | 4 +- .../CanvasSettingsClipToBboxCheckbox.tsx | 4 +- .../CanvasSettingsDynamicGridSwitch.tsx | 4 +- .../components/StageComponent.tsx | 2 +- .../konva/CanvasRenderingModule.ts | 18 ++++--- .../konva/CanvasStateApiModule.ts | 2 +- .../store/canvasSettingsSlice.ts | 53 +++++++++++++++++++ .../controlLayers/store/canvasV2Slice.ts | 15 ------ .../controlLayers/store/settingsReducers.ts | 14 ----- .../src/features/controlLayers/store/types.ts | 9 ---- .../util/graph/generation/buildSD1Graph.ts | 6 +-- .../util/graph/generation/buildSDXLGraph.ts | 6 +-- 13 files changed, 82 insertions(+), 58 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts delete mode 100644 invokeai/frontend/web/src/features/controlLayers/store/settingsReducers.ts diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 72be76bf1c6..f7aee74dbc7 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -6,6 +6,7 @@ import { errorHandler } from 'app/store/enhancers/reduxRemember/errors'; import type { SerializableObject } from 'common/types'; import { deepClone } from 'common/util/deepClone'; import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice'; +import { canvasSettingsPersistConfig, canvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice'; import { canvasV2PersistConfig, canvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { paramsPersistConfig, paramsSlice } from 'features/controlLayers/store/paramsSlice'; import { toolPersistConfig, toolSlice } from 'features/controlLayers/store/toolSlice'; @@ -61,6 +62,7 @@ const allReducers = { [stylePresetSlice.name]: stylePresetSlice.reducer, [paramsSlice.name]: paramsSlice.reducer, [toolSlice.name]: toolSlice.reducer, + [canvasSettingsSlice.name]: canvasSettingsSlice.reducer, }; const rootReducer = combineReducers(allReducers); @@ -104,6 +106,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = { [stylePresetPersistConfig.name]: stylePresetPersistConfig, [paramsPersistConfig.name]: paramsPersistConfig, [toolPersistConfig.name]: toolPersistConfig, + [canvasSettingsPersistConfig.name]: canvasSettingsPersistConfig, }; const unserialize: UnserializeFunction = (data, key) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsAutoSaveCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsAutoSaveCheckbox.tsx index 0dfcae290a0..ddfcfe8f3a2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsAutoSaveCheckbox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsAutoSaveCheckbox.tsx @@ -1,13 +1,13 @@ import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { settingsAutoSaveToggled } from 'features/controlLayers/store/canvasV2Slice'; +import { settingsAutoSaveToggled } from 'features/controlLayers/store/canvasSettingsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; export const CanvasSettingsAutoSaveCheckbox = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const autoSave = useAppSelector((s) => s.canvasV2.settings.autoSave); + const autoSave = useAppSelector((s) => s.canvasSettings.autoSave); const onChange = useCallback(() => dispatch(settingsAutoSaveToggled()), [dispatch]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox.tsx index 205c36070cd..60b8cf4ff90 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox.tsx @@ -1,6 +1,6 @@ import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { clipToBboxChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { clipToBboxChanged } from 'features/controlLayers/store/canvasSettingsSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'; export const CanvasSettingsClipToBboxCheckbox = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const clipToBbox = useAppSelector((s) => s.canvasV2.settings.clipToBbox); + const clipToBbox = useAppSelector((s) => s.canvasSettings.clipToBbox); const onChange = useCallback( (e: ChangeEvent) => dispatch(clipToBboxChanged(e.target.checked)), [dispatch] diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsDynamicGridSwitch.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsDynamicGridSwitch.tsx index 71ffe1e729c..f29b5ac16f2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsDynamicGridSwitch.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsDynamicGridSwitch.tsx @@ -1,13 +1,13 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { settingsDynamicGridToggled } from 'features/controlLayers/store/canvasV2Slice'; +import { settingsDynamicGridToggled } from 'features/controlLayers/store/canvasSettingsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; export const CanvasSettingsDynamicGridSwitch = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const dynamicGrid = useAppSelector((s) => s.canvasV2.settings.dynamicGrid); + const dynamicGrid = useAppSelector((s) => s.canvasSettings.dynamicGrid); const onChange = useCallback(() => { dispatch(settingsDynamicGridToggled()); }, [dispatch]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index 81af6c00d4b..f7679346c02 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -52,7 +52,7 @@ type Props = { }; export const StageComponent = memo(({ asPreview = false }: Props) => { - const dynamicGrid = useAppSelector((s) => s.canvasV2.settings.dynamicGrid); + const dynamicGrid = useAppSelector((s) => s.canvasSettings.dynamicGrid); const [stage] = useState( () => diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts index 3c25a795a30..f62b275eb80 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts @@ -4,6 +4,7 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { getPrefixedId } from 'features/controlLayers/konva/util'; +import type { CanvasSettingsState } from 'features/controlLayers/store/canvasSettingsSlice'; import type { CanvasV2State } from 'features/controlLayers/store/types'; import type { Logger } from 'roarr'; @@ -17,6 +18,7 @@ export class CanvasRenderingModule extends CanvasModuleBase { subscriptions = new Set<() => void>(); state: CanvasV2State | null = null; + settings: CanvasSettingsState | null = null; constructor(manager: CanvasManager) { super(); @@ -29,20 +31,24 @@ export class CanvasRenderingModule extends CanvasModuleBase { render = async () => { const state = this.manager.stateApi.getCanvasState(); + const settings = this.manager.stateApi.getSettings(); - if (!this.state) { + if (!this.state || !this.settings) { this.log.trace('First render'); } const prevState = this.state; this.state = state; - if (prevState === state) { + const prevSettings = this.settings; + this.settings = settings; + + if (prevState === state && prevSettings === settings) { // No changes to state - no need to render return; } - this.renderBackground(state, prevState); + this.renderBackground(settings, prevSettings); await this.renderRasterLayers(state, prevState); await this.renderControlLayers(prevState, state); await this.renderRegionalGuidance(prevState, state); @@ -57,7 +63,7 @@ export class CanvasRenderingModule extends CanvasModuleBase { this.manager.stateApi.$currentFill.set(this.manager.stateApi.getCurrentFill()); // We have no prev state for the first render - if (!prevState) { + if (!prevState && !prevSettings) { this.manager.setCanvasManager(); } }; @@ -66,8 +72,8 @@ export class CanvasRenderingModule extends CanvasModuleBase { return { ...this.manager.getLoggingContext(), path: this.manager.path.join('.') }; }; - renderBackground = (state: CanvasV2State, prevState: CanvasV2State | null) => { - if (!prevState || state.settings.dynamicGrid !== prevState.settings.dynamicGrid) { + renderBackground = (settings: CanvasSettingsState, prevSettings: CanvasSettingsState | null) => { + if (!prevSettings || settings.dynamicGrid !== prevSettings.dynamicGrid) { this.manager.background.render(); } }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index edb650f5041..1d5d28ef8c5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -150,7 +150,7 @@ export class CanvasStateApiModule extends CanvasModuleBase { return this.store.getState().tool; }; getSettings = () => { - return this.getCanvasState().settings; + return this.store.getState().canvasSettings; }; getRegionsState = () => { return this.getCanvasState().regions; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts new file mode 100644 index 00000000000..18e8340d33c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts @@ -0,0 +1,53 @@ +import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; +import type { PersistConfig } from 'app/store/store'; + +export type CanvasSettingsState = { + imageSmoothing: boolean; + showHUD: boolean; + autoSave: boolean; + preserveMaskedArea: boolean; + cropToBboxOnSave: boolean; + clipToBbox: boolean; + dynamicGrid: boolean; +}; + +const initialState: CanvasSettingsState = { + // TODO(psyche): These are copied from old canvas state, need to be implemented + autoSave: false, + imageSmoothing: true, + preserveMaskedArea: false, + showHUD: true, + clipToBbox: false, + cropToBboxOnSave: false, + dynamicGrid: false, +}; + +export const canvasSettingsSlice = createSlice({ + name: 'canvasSettings', + initialState, + reducers: { + clipToBboxChanged: (state, action: PayloadAction) => { + state.clipToBbox = action.payload; + }, + settingsDynamicGridToggled: (state) => { + state.dynamicGrid = !state.dynamicGrid; + }, + settingsAutoSaveToggled: (state) => { + state.autoSave = !state.autoSave; + }, + }, +}); + +export const { clipToBboxChanged, settingsAutoSaveToggled, settingsDynamicGridToggled } = canvasSettingsSlice.actions; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +const migrate = (state: any): any => { + return state; +}; + +export const canvasSettingsPersistConfig: PersistConfig = { + name: canvasSettingsSlice.name, + initialState, + migrate, + persistDenylist: [], +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 5134b1bf50c..b7667b58eb7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -14,7 +14,6 @@ import { rasterLayersReducers } from 'features/controlLayers/store/rasterLayersR import { regionsReducers } from 'features/controlLayers/store/regionsReducers'; import { selectAllEntities, selectAllEntitiesOfType, selectEntity } from 'features/controlLayers/store/selectors'; import { sessionReducers } from 'features/controlLayers/store/sessionReducers'; -import { settingsReducers } from 'features/controlLayers/store/settingsReducers'; import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; import { simplifyFlatNumbersArray } from 'features/controlLayers/util/simplify'; import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; @@ -66,16 +65,6 @@ const initialState: CanvasV2State = { height: 512, }, }, - settings: { - // TODO(psyche): These are copied from old canvas state, need to be implemented - autoSave: false, - imageSmoothing: true, - preserveMaskedArea: false, - showHUD: true, - clipToBbox: false, - cropToBboxOnSave: false, - dynamicGrid: false, - }, session: { mode: 'generate', isStaging: false, @@ -97,7 +86,6 @@ export const canvasV2Slice = createSlice({ ...bboxReducers, // move out ...lorasReducers, - ...settingsReducers, ...sessionReducers, entitySelected: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; @@ -390,10 +378,7 @@ export const canvasV2Slice = createSlice({ }); export const { - clipToBboxChanged, canvasReset, - settingsDynamicGridToggled, - settingsAutoSaveToggled, // All entities entitySelected, entityNameChanged, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/settingsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/settingsReducers.ts deleted file mode 100644 index 324077f8eb4..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/store/settingsReducers.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import type { CanvasV2State } from 'features/controlLayers/store/types'; - -export const settingsReducers = { - clipToBboxChanged: (state, action: PayloadAction) => { - state.settings.clipToBbox = action.payload; - }, - settingsDynamicGridToggled: (state) => { - state.settings.dynamicGrid = !state.settings.dynamicGrid; - }, - settingsAutoSaveToggled: (state) => { - state.settings.autoSave = !state.settings.autoSave; - }, -} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 766cdf8b3ac..7f20ee06f36 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -715,15 +715,6 @@ export type CanvasV2State = { entities: CanvasIPAdapterState[]; }; loras: LoRA[]; - settings: { - imageSmoothing: boolean; - showHUD: boolean; - autoSave: boolean; - preserveMaskedArea: boolean; - cropToBboxOnSave: boolean; - clipToBbox: boolean; - dynamicGrid: boolean; - }; bbox: { rect: { x: number; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index 0eaa86c9e7a..cf724b227eb 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -31,8 +31,8 @@ export const buildSD1Graph = async ( const generationMode = manager.compositor.getGenerationMode(); log.debug({ generationMode }, 'Building SD1/SD2 graph'); - const { canvasV2, params } = state; - const { bbox, session, settings } = canvasV2; + const { canvasV2, params, canvasSettings } = state; + const { bbox, session } = canvasV2; const { model, @@ -274,7 +274,7 @@ export const buildSD1Graph = async ( canvasOutput = addWatermarker(g, canvasOutput); } - const shouldSaveToGallery = session.mode === 'generate' || settings.autoSave; + const shouldSaveToGallery = session.mode === 'generate' || canvasSettings.autoSave; g.updateNode(canvasOutput, { id: getPrefixedId('canvas_output'), diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index 35b7e401735..976e59527ca 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -31,8 +31,8 @@ export const buildSDXLGraph = async ( const generationMode = manager.compositor.getGenerationMode(); log.debug({ generationMode }, 'Building SDXL graph'); - const { params, canvasV2 } = state; - const { bbox, session, settings } = canvasV2; + const { params, canvasV2, canvasSettings } = state; + const { bbox, session } = canvasV2; const { model, @@ -277,7 +277,7 @@ export const buildSDXLGraph = async ( canvasOutput = addWatermarker(g, canvasOutput); } - const shouldSaveToGallery = session.mode === 'generate' || settings.autoSave; + const shouldSaveToGallery = session.mode === 'generate' || canvasSettings.autoSave; g.updateNode(canvasOutput, { id: getPrefixedId('canvas_output'), From 4670f82e65248ea005291e943e46663a5bee8586 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 26 Aug 2024 22:20:04 +1000 Subject: [PATCH 502/678] feat(ui): split out session state from canvas rendering state --- .../addCommitStagingAreaImageListener.ts | 6 +- .../listeners/enqueueRequestedLinear.ts | 6 +- invokeai/frontend/web/src/app/store/store.ts | 3 + .../components/CanvasModeSwitcher.tsx | 4 +- .../components/ControlLayersEditor.tsx | 8 +- .../StagingArea/StagingAreaIsStagingGate.tsx | 2 +- .../StagingArea/StagingAreaToolbar.tsx | 4 +- .../components/Tool/ToolBboxButton.tsx | 2 +- .../components/Tool/ToolBrushButton.tsx | 2 +- .../components/Tool/ToolColorPickerButton.tsx | 2 +- .../components/Tool/ToolEraserButton.tsx | 2 +- .../components/Tool/ToolMoveButton.tsx | 2 +- .../components/Tool/ToolRectButton.tsx | 2 +- .../components/Tool/ToolViewButton.tsx | 2 +- .../hooks/useCanvasDeleteLayerHotkey.ts | 2 +- .../konva/CanvasRenderingModule.ts | 71 ++++++++++++---- .../konva/CanvasStateApiModule.ts | 2 +- .../controlLayers/store/canvasSessionSlice.ts | 83 +++++++++++++++++++ .../controlLayers/store/canvasV2Slice.ts | 23 +---- .../controlLayers/store/sessionReducers.ts | 43 ---------- .../src/features/controlLayers/store/types.ts | 6 -- .../nodes/util/graph/generation/addInpaint.ts | 6 +- .../util/graph/generation/addOutpaint.ts | 6 +- .../util/graph/generation/buildSD1Graph.ts | 6 +- .../util/graph/generation/buildSDXLGraph.ts | 6 +- .../services/events/onInvocationComplete.ts | 4 +- 26 files changed, 182 insertions(+), 123 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/canvasSessionSlice.ts delete mode 100644 invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts index 70540b13c5c..7c533717e7e 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts @@ -1,10 +1,10 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { - rasterLayerAdded, sessionStagingAreaImageAccepted, sessionStagingAreaReset, -} from 'features/controlLayers/store/canvasV2Slice'; +} from 'features/controlLayers/store/canvasSessionSlice'; +import { rasterLayerAdded } from 'features/controlLayers/store/canvasV2Slice'; import type { CanvasRasterLayerState } from 'features/controlLayers/store/types'; import { imageDTOToImageObject } from 'features/controlLayers/store/types'; import { toast } from 'features/toast/toast'; @@ -55,7 +55,7 @@ export const addStagingListeners = (startAppListening: AppStartListening) => { effect: (action, api) => { const { index } = action.payload; const state = api.getState(); - const stagingAreaImage = state.canvasV2.session.stagedImages[index]; + const stagingAreaImage = state.canvasSession.stagedImages[index]; assert(stagingAreaImage, 'No staged image found to accept'); const { x, y } = state.canvasV2.bbox.rect; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index 63d2fdc7563..89777f5081e 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -5,7 +5,7 @@ import type { SerializableObject } from 'common/types'; import type { Result } from 'common/util/result'; import { isErr, withResult, withResultAsync } from 'common/util/result'; import { $canvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { sessionStagingAreaReset, sessionStartedStaging } from 'features/controlLayers/store/canvasV2Slice'; +import { sessionStagingAreaReset, sessionStartedStaging } from 'features/controlLayers/store/canvasSessionSlice'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph'; import { buildSDXLGraph } from 'features/nodes/util/graph/generation/buildSDXLGraph'; @@ -31,13 +31,13 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) let didStartStaging = false; - if (!state.canvasV2.session.isStaging && state.canvasV2.session.mode === 'compose') { + if (!state.canvasSession.isStaging && state.canvasSession.mode === 'compose') { dispatch(sessionStartedStaging()); didStartStaging = true; } const abortStaging = () => { - if (didStartStaging && getState().canvasV2.session.isStaging) { + if (didStartStaging && getState().canvasSession.isStaging) { dispatch(sessionStagingAreaReset()); } }; diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index f7aee74dbc7..aabc77829a8 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -6,6 +6,7 @@ import { errorHandler } from 'app/store/enhancers/reduxRemember/errors'; import type { SerializableObject } from 'common/types'; import { deepClone } from 'common/util/deepClone'; import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice'; +import { canvasSessionPersistConfig, canvasSessionSlice } from 'features/controlLayers/store/canvasSessionSlice'; import { canvasSettingsPersistConfig, canvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice'; import { canvasV2PersistConfig, canvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { paramsPersistConfig, paramsSlice } from 'features/controlLayers/store/paramsSlice'; @@ -63,6 +64,7 @@ const allReducers = { [paramsSlice.name]: paramsSlice.reducer, [toolSlice.name]: toolSlice.reducer, [canvasSettingsSlice.name]: canvasSettingsSlice.reducer, + [canvasSessionSlice.name]: canvasSessionSlice.reducer, }; const rootReducer = combineReducers(allReducers); @@ -107,6 +109,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = { [paramsPersistConfig.name]: paramsPersistConfig, [toolPersistConfig.name]: toolPersistConfig, [canvasSettingsPersistConfig.name]: canvasSettingsPersistConfig, + [canvasSessionPersistConfig.name]: canvasSessionPersistConfig, }; const unserialize: UnserializeFunction = (data, key) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasModeSwitcher.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasModeSwitcher.tsx index 81b91141533..7613782e6fc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasModeSwitcher.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasModeSwitcher.tsx @@ -1,13 +1,13 @@ import { Button, ButtonGroup } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { sessionModeChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { sessionModeChanged } from 'features/controlLayers/store/canvasSessionSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; export const CanvasModeSwitcher = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const mode = useAppSelector((s) => s.canvasV2.session.mode); + const mode = useAppSelector((s) => s.canvasSession.mode); const onClickGenerate = useCallback(() => dispatch(sessionModeChanged({ mode: 'generate' })), [dispatch]); const onClickCompose = useCallback(() => dispatch(sessionModeChanged({ mode: 'compose' })), [dispatch]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx index 682e7570769..fabc93604c3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx @@ -33,9 +33,11 @@ export const CanvasEditor = memo(() => { - - - + + + + + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaIsStagingGate.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaIsStagingGate.tsx index 51fc64c96fa..c5cbe43755b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaIsStagingGate.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaIsStagingGate.tsx @@ -3,7 +3,7 @@ import type { PropsWithChildren } from 'react'; import { memo } from 'react'; export const StagingAreaIsStagingGate = memo((props: PropsWithChildren) => { - const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); + const isStaging = useAppSelector((s) => s.canvasSession.isStaging); if (!isStaging) { return null; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx index ed4bcb38b3d..12d56517f5d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx @@ -9,7 +9,7 @@ import { sessionStagedImageDiscarded, sessionStagingAreaImageAccepted, sessionStagingAreaReset, -} from 'features/controlLayers/store/canvasV2Slice'; +} from 'features/controlLayers/store/canvasSessionSlice'; import { memo, useCallback, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; @@ -27,7 +27,7 @@ import { useChangeImageIsIntermediateMutation } from 'services/api/endpoints/ima export const StagingAreaToolbar = memo(() => { const dispatch = useAppDispatch(); - const session = useAppSelector((s) => s.canvasV2.session); + const session = useAppSelector((s) => s.canvasSession); const canvasManager = useCanvasManager(); const shouldShowStagedImage = useStore(canvasManager.stateApi.$shouldShowStagedImage); const images = useMemo(() => session.stagedImages, [session]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBboxButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBboxButton.tsx index 5f676d5f989..cde84309da4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBboxButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBboxButton.tsx @@ -14,7 +14,7 @@ export const ToolBboxButton = memo(() => { const isSelected = useToolIsSelected('bbox'); const isFiltering = useIsFiltering(); const isTransforming = useIsTransforming(); - const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); + const isStaging = useAppSelector((s) => s.canvasSession.isStaging); const isDisabled = useMemo(() => { return isTransforming || isFiltering || isStaging; }, [isFiltering, isStaging, isTransforming]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushButton.tsx index c508ab4d703..170416619d5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushButton.tsx @@ -13,7 +13,7 @@ export const ToolBrushButton = memo(() => { const { t } = useTranslation(); const isFiltering = useIsFiltering(); const isTransforming = useIsTransforming(); - const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); + const isStaging = useAppSelector((s) => s.canvasSession.isStaging); const selectBrush = useSelectTool('brush'); const isSelected = useToolIsSelected('brush'); const isDrawingToolAllowed = useAppSelector((s) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolColorPickerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolColorPickerButton.tsx index e4258170ac8..8d1bbddc68b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolColorPickerButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolColorPickerButton.tsx @@ -14,7 +14,7 @@ export const ToolColorPickerButton = memo(() => { const isTransforming = useIsTransforming(); const selectColorPicker = useSelectTool('colorPicker'); const isSelected = useToolIsSelected('colorPicker'); - const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); + const isStaging = useAppSelector((s) => s.canvasSession.isStaging); const isDisabled = useMemo(() => { return isTransforming || isFiltering || isStaging; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserButton.tsx index 78b59421138..2ac15df287a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserButton.tsx @@ -13,7 +13,7 @@ export const ToolEraserButton = memo(() => { const { t } = useTranslation(); const isFiltering = useIsFiltering(); const isTransforming = useIsTransforming(); - const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); + const isStaging = useAppSelector((s) => s.canvasSession.isStaging); const selectEraser = useSelectTool('eraser'); const isSelected = useToolIsSelected('eraser'); const isDrawingToolAllowed = useAppSelector((s) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolMoveButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolMoveButton.tsx index 91a8155a8e7..f1422e93118 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolMoveButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolMoveButton.tsx @@ -15,7 +15,7 @@ export const ToolMoveButton = memo(() => { const isTransforming = useIsTransforming(); const selectMove = useSelectTool('move'); const isSelected = useToolIsSelected('move'); - const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); + const isStaging = useAppSelector((s) => s.canvasSession.isStaging); const isDrawingToolAllowed = useAppSelector((s) => { if (!s.canvasV2.selectedEntityIdentifier?.type) { return false; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolRectButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolRectButton.tsx index 3b5b1e338f2..8e0bfa083f3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolRectButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolRectButton.tsx @@ -15,7 +15,7 @@ export const ToolRectButton = memo(() => { const isSelected = useToolIsSelected('rect'); const isFiltering = useIsFiltering(); const isTransforming = useIsTransforming(); - const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); + const isStaging = useAppSelector((s) => s.canvasSession.isStaging); const isDrawingToolAllowed = useAppSelector((s) => { if (!s.canvasV2.selectedEntityIdentifier?.type) { return false; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolViewButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolViewButton.tsx index 6b94eaf0cce..0a82651302c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolViewButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolViewButton.tsx @@ -12,7 +12,7 @@ export const ToolViewButton = memo(() => { const { t } = useTranslation(); const isTransforming = useIsTransforming(); const isFiltering = useIsFiltering(); - const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); + const isStaging = useAppSelector((s) => s.canvasSession.isStaging); const selectView = useSelectTool('view'); const isSelected = useToolIsSelected('view'); const isDisabled = useMemo(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts index 88f444401c9..9f2261c8878 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts @@ -15,7 +15,7 @@ export function useCanvasDeleteLayerHotkey() { useAssertSingleton(useCanvasDeleteLayerHotkey.name); const dispatch = useAppDispatch(); const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); - const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging); + const isStaging = useAppSelector((s) => s.canvasSession.isStaging); const deleteSelectedLayer = useCallback(() => { if (selectedEntityIdentifier === null) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts index f62b275eb80..6be2f8d2489 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts @@ -4,6 +4,7 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { getPrefixedId } from 'features/controlLayers/konva/util'; +import type { CanvasSessionState } from 'features/controlLayers/store/canvasSessionSlice'; import type { CanvasSettingsState } from 'features/controlLayers/store/canvasSettingsSlice'; import type { CanvasV2State } from 'features/controlLayers/store/types'; import type { Logger } from 'roarr'; @@ -19,6 +20,9 @@ export class CanvasRenderingModule extends CanvasModuleBase { state: CanvasV2State | null = null; settings: CanvasSettingsState | null = null; + session: CanvasSessionState | null = null; + + isFirstRender = true; constructor(manager: CanvasManager) { super(); @@ -30,42 +34,79 @@ export class CanvasRenderingModule extends CanvasModuleBase { } render = async () => { - const state = this.manager.stateApi.getCanvasState(); - const settings = this.manager.stateApi.getSettings(); - - if (!this.state || !this.settings) { + if (!this.state || !this.settings || !this.session) { this.log.trace('First render'); } + await this.renderCanvas(); + this.renderSettings(); + await this.renderSession(); + + // We have no prev state for the first render + if (this.isFirstRender) { + this.isFirstRender = false; + this.manager.setCanvasManager(); + } + }; + + renderCanvas = async () => { + const state = this.manager.stateApi.getCanvasState(); + const prevState = this.state; this.state = state; - const prevSettings = this.settings; - this.settings = settings; - - if (prevState === state && prevSettings === settings) { + if (prevState === state) { // No changes to state - no need to render return; } - this.renderBackground(settings, prevSettings); await this.renderRasterLayers(state, prevState); await this.renderControlLayers(prevState, state); await this.renderRegionalGuidance(prevState, state); await this.renderInpaintMasks(state, prevState); await this.renderBbox(state, prevState); - await this.renderStagingArea(state, prevState); this.arrangeEntities(state, prevState); this.manager.stateApi.$toolState.set(this.manager.stateApi.getToolState()); this.manager.stateApi.$selectedEntityIdentifier.set(state.selectedEntityIdentifier); this.manager.stateApi.$selectedEntity.set(this.manager.stateApi.getSelectedEntity()); this.manager.stateApi.$currentFill.set(this.manager.stateApi.getCurrentFill()); + }; - // We have no prev state for the first render - if (!prevState && !prevSettings) { - this.manager.setCanvasManager(); + renderSettings = () => { + const settings = this.manager.stateApi.getSettings(); + + if (!this.settings) { + this.log.trace('First settings render'); + } + + const prevSettings = this.settings; + this.settings = settings; + + if (prevSettings === settings) { + // No changes to state - no need to render + return; + } + + this.renderBackground(settings, prevSettings); + }; + + renderSession = async () => { + const session = this.manager.stateApi.getSession(); + + if (!this.session) { + this.log.trace('First session render'); + } + + const prevSession = this.session; + this.session = session; + + if (prevSession === session) { + // No changes to state - no need to render + return; } + + await this.renderStagingArea(session, prevSession); }; getLoggingContext = (): SerializableObject => { @@ -210,8 +251,8 @@ export class CanvasRenderingModule extends CanvasModuleBase { } }; - renderStagingArea = async (state: CanvasV2State, prevState: CanvasV2State | null) => { - if (!prevState || state.session !== prevState.session) { + renderStagingArea = async (session: CanvasSessionState, prevSession: CanvasSessionState | null) => { + if (!prevSession || session !== prevSession) { await this.manager.preview.stagingArea.render(); } }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index 1d5d28ef8c5..eff144968c9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -165,7 +165,7 @@ export class CanvasStateApiModule extends CanvasModuleBase { return this.getCanvasState().inpaintMasks; }; getSession = () => { - return this.getCanvasState().session; + return this.store.getState().canvasSession; }; getIsSelected = (id: string) => { return this.getCanvasState().selectedEntityIdentifier?.id === id; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSessionSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSessionSlice.ts new file mode 100644 index 00000000000..f8d70511d02 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSessionSlice.ts @@ -0,0 +1,83 @@ +import { createAction, createSlice, type PayloadAction } from '@reduxjs/toolkit'; +import type { PersistConfig } from 'app/store/store'; +import { canvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import type { SessionMode, StagingAreaImage } from 'features/controlLayers/store/types'; + +export type CanvasSessionState = { + mode: SessionMode; + isStaging: boolean; + stagedImages: StagingAreaImage[]; + selectedStagedImageIndex: number; +}; + +const initialState: CanvasSessionState = { + mode: 'generate', + isStaging: false, + stagedImages: [], + selectedStagedImageIndex: 0, +}; + +export const canvasSessionSlice = createSlice({ + name: 'canvasSession', + initialState, + reducers: { + sessionStartedStaging: (state) => { + state.isStaging = true; + state.selectedStagedImageIndex = 0; + }, + sessionImageStaged: (state, action: PayloadAction<{ stagingAreaImage: StagingAreaImage }>) => { + const { stagingAreaImage } = action.payload; + state.stagedImages.push(stagingAreaImage); + state.selectedStagedImageIndex = state.stagedImages.length - 1; + }, + sessionNextStagedImageSelected: (state) => { + state.selectedStagedImageIndex = (state.selectedStagedImageIndex + 1) % state.stagedImages.length; + }, + sessionPrevStagedImageSelected: (state) => { + state.selectedStagedImageIndex = + (state.selectedStagedImageIndex - 1 + state.stagedImages.length) % state.stagedImages.length; + }, + sessionStagedImageDiscarded: (state, action: PayloadAction<{ index: number }>) => { + const { index } = action.payload; + state.stagedImages.splice(index, 1); + state.selectedStagedImageIndex = Math.min(state.selectedStagedImageIndex, state.stagedImages.length - 1); + if (state.stagedImages.length === 0) { + state.isStaging = false; + } + }, + sessionStagingAreaReset: (state) => { + state.isStaging = false; + state.stagedImages = []; + state.selectedStagedImageIndex = 0; + }, + sessionModeChanged: (state, action: PayloadAction<{ mode: SessionMode }>) => { + const { mode } = action.payload; + state.mode = mode; + }, + }, +}); + +export const { + sessionStartedStaging, + sessionImageStaged, + sessionStagedImageDiscarded, + sessionStagingAreaReset, + sessionNextStagedImageSelected, + sessionPrevStagedImageSelected, + sessionModeChanged, +} = canvasSessionSlice.actions; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +const migrate = (state: any): any => { + return state; +}; + +export const canvasSessionPersistConfig: PersistConfig = { + name: canvasSessionSlice.name, + initialState, + migrate, + persistDenylist: [], +}; +export const sessionStagingAreaImageAccepted = createAction<{ index: number }>( + `${canvasV2Slice.name}/sessionStagingAreaImageAccepted` +); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index b7667b58eb7..8951797f777 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -1,5 +1,5 @@ import type { PayloadAction } from '@reduxjs/toolkit'; -import { createAction, createSlice } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; import type { PersistConfig } from 'app/store/store'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { deepClone } from 'common/util/deepClone'; @@ -13,7 +13,6 @@ import { modelChanged } from 'features/controlLayers/store/paramsSlice'; import { rasterLayersReducers } from 'features/controlLayers/store/rasterLayersReducers'; import { regionsReducers } from 'features/controlLayers/store/regionsReducers'; import { selectAllEntities, selectAllEntitiesOfType, selectEntity } from 'features/controlLayers/store/selectors'; -import { sessionReducers } from 'features/controlLayers/store/sessionReducers'; import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; import { simplifyFlatNumbersArray } from 'features/controlLayers/util/simplify'; import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; @@ -65,12 +64,6 @@ const initialState: CanvasV2State = { height: 512, }, }, - session: { - mode: 'generate', - isStaging: false, - stagedImages: [], - selectedStagedImageIndex: 0, - }, }; export const canvasV2Slice = createSlice({ @@ -86,7 +79,6 @@ export const canvasV2Slice = createSlice({ ...bboxReducers, // move out ...lorasReducers, - ...sessionReducers, entitySelected: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; state.selectedEntityIdentifier = entityIdentifier; @@ -339,7 +331,6 @@ export const canvasV2Slice = createSlice({ state.bbox.rect.height = state.bbox.optimalDimension; const size = pick(state.bbox.rect, 'width', 'height'); state.bbox.scaledSize = getScaledBoundingBoxDimensions(size, state.bbox.optimalDimension); - state.session = deepClone(initialState.session); state.ipAdapters = deepClone(initialState.ipAdapters); state.rasterLayers = deepClone(initialState.rasterLayers); @@ -458,14 +449,6 @@ export const { // inpaintMaskRecalled, inpaintMaskFillColorChanged, inpaintMaskFillStyleChanged, - // Staging - sessionStartedStaging, - sessionImageStaged, - sessionStagedImageDiscarded, - sessionStagingAreaReset, - sessionNextStagedImageSelected, - sessionPrevStagedImageSelected, - sessionModeChanged, } = canvasV2Slice.actions; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ @@ -479,7 +462,3 @@ export const canvasV2PersistConfig: PersistConfig = { migrate, persistDenylist: [], }; - -export const sessionStagingAreaImageAccepted = createAction<{ index: number }>( - `${canvasV2Slice.name}/sessionStagingAreaImageAccepted` -); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts deleted file mode 100644 index 2e77f1220d3..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import type { CanvasV2State, SessionMode, StagingAreaImage } from 'features/controlLayers/store/types'; - -export const sessionReducers = { - sessionStartedStaging: (state) => { - state.session.isStaging = true; - state.session.selectedStagedImageIndex = 0; - }, - sessionImageStaged: (state, action: PayloadAction<{ stagingAreaImage: StagingAreaImage }>) => { - const { stagingAreaImage } = action.payload; - state.session.stagedImages.push(stagingAreaImage); - state.session.selectedStagedImageIndex = state.session.stagedImages.length - 1; - }, - sessionNextStagedImageSelected: (state) => { - state.session.selectedStagedImageIndex = - (state.session.selectedStagedImageIndex + 1) % state.session.stagedImages.length; - }, - sessionPrevStagedImageSelected: (state) => { - state.session.selectedStagedImageIndex = - (state.session.selectedStagedImageIndex - 1 + state.session.stagedImages.length) % - state.session.stagedImages.length; - }, - sessionStagedImageDiscarded: (state, action: PayloadAction<{ index: number }>) => { - const { index } = action.payload; - state.session.stagedImages.splice(index, 1); - state.session.selectedStagedImageIndex = Math.min( - state.session.selectedStagedImageIndex, - state.session.stagedImages.length - 1 - ); - if (state.session.stagedImages.length === 0) { - state.session.isStaging = false; - } - }, - sessionStagingAreaReset: (state) => { - state.session.isStaging = false; - state.session.stagedImages = []; - state.session.selectedStagedImageIndex = 0; - }, - sessionModeChanged: (state, action: PayloadAction<{ mode: SessionMode }>) => { - const { mode } = action.payload; - state.session.mode = mode; - }, -} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 7f20ee06f36..74231cdb9f9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -730,12 +730,6 @@ export type CanvasV2State = { scaleMethod: BoundingBoxScaleMethod; optimalDimension: number; }; - session: { - mode: SessionMode; - isStaging: boolean; - stagedImages: StagingAreaImage[]; - selectedStagedImageIndex: number; - }; }; export type StageAttrs = { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts index 438f76d28d7..3f8c4fbb02b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts @@ -21,9 +21,9 @@ export const addInpaint = async ( ): Promise> => { denoise.denoising_start = denoising_start; - const { params, canvasV2 } = state; - const { bbox, session } = canvasV2; - const { mode } = session; + const { params, canvasV2, canvasSession } = state; + const { bbox } = canvasV2; + const { mode } = canvasSession; const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect); const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index 7a2b95da97f..c0f298bfbd2 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts @@ -22,9 +22,9 @@ export const addOutpaint = async ( ): Promise> => { denoise.denoising_start = denoising_start; - const { params, canvasV2 } = state; - const { bbox, session } = canvasV2; - const { mode } = session; + const { params, canvasV2, canvasSession } = state; + const { bbox } = canvasV2; + const { mode } = canvasSession; const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect); const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index cf724b227eb..73d3c9e85ad 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -31,8 +31,8 @@ export const buildSD1Graph = async ( const generationMode = manager.compositor.getGenerationMode(); log.debug({ generationMode }, 'Building SD1/SD2 graph'); - const { canvasV2, params, canvasSettings } = state; - const { bbox, session } = canvasV2; + const { canvasV2, params, canvasSettings, canvasSession } = state; + const { bbox } = canvasV2; const { model, @@ -274,7 +274,7 @@ export const buildSD1Graph = async ( canvasOutput = addWatermarker(g, canvasOutput); } - const shouldSaveToGallery = session.mode === 'generate' || canvasSettings.autoSave; + const shouldSaveToGallery = canvasSession.mode === 'generate' || canvasSettings.autoSave; g.updateNode(canvasOutput, { id: getPrefixedId('canvas_output'), diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index 976e59527ca..a85bd57a66b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -31,8 +31,8 @@ export const buildSDXLGraph = async ( const generationMode = manager.compositor.getGenerationMode(); log.debug({ generationMode }, 'Building SDXL graph'); - const { params, canvasV2, canvasSettings } = state; - const { bbox, session } = canvasV2; + const { params, canvasV2, canvasSettings, canvasSession } = state; + const { bbox } = canvasV2; const { model, @@ -277,7 +277,7 @@ export const buildSDXLGraph = async ( canvasOutput = addWatermarker(g, canvasOutput); } - const shouldSaveToGallery = session.mode === 'generate' || canvasSettings.autoSave; + const shouldSaveToGallery = canvasSession.mode === 'generate' || canvasSettings.autoSave; g.updateNode(canvasOutput, { id: getPrefixedId('canvas_output'), diff --git a/invokeai/frontend/web/src/services/events/onInvocationComplete.ts b/invokeai/frontend/web/src/services/events/onInvocationComplete.ts index 5b737b41ff4..09cd840bd16 100644 --- a/invokeai/frontend/web/src/services/events/onInvocationComplete.ts +++ b/invokeai/frontend/web/src/services/events/onInvocationComplete.ts @@ -2,7 +2,7 @@ import { logger } from 'app/logging/logger'; import type { AppDispatch, RootState } from 'app/store/store'; import type { SerializableObject } from 'common/types'; import { deepClone } from 'common/util/deepClone'; -import { sessionImageStaged } from 'features/controlLayers/store/canvasV2Slice'; +import { sessionImageStaged } from 'features/controlLayers/store/canvasSessionSlice'; import { boardIdSelected, galleryViewChanged, imageSelected, offsetChanged } from 'features/gallery/store/gallerySlice'; import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; import { zNodeStatus } from 'features/nodes/types/invocation'; @@ -114,7 +114,7 @@ export const buildOnInvocationComplete = ( }; const handleOriginCanvas = async (data: S['InvocationCompleteEvent']) => { - const session = getState().canvasV2.session; + const session = getState().canvasSession; const imageDTO = await getResultImageDTO(data); From a33d1b979de131a5c5871533c3e747defb6ff583 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 26 Aug 2024 22:29:28 +1000 Subject: [PATCH 503/678] feat(ui): split out loras state from canvas rendering state --- .../listeners/modelSelected.ts | 4 +- .../listeners/modelsLoaded.ts | 4 +- invokeai/frontend/web/src/app/store/store.ts | 3 + .../controlLayers/store/canvasV2Slice.ts | 11 --- .../controlLayers/store/lorasReducers.ts | 50 ------------ .../controlLayers/store/lorasSlice.ts | 80 +++++++++++++++++++ .../src/features/controlLayers/store/types.ts | 1 - .../src/features/lora/components/LoRACard.tsx | 2 +- .../src/features/lora/components/LoRAList.tsx | 4 +- .../features/lora/components/LoRASelect.tsx | 5 +- .../web/src/features/metadata/util/parsers.ts | 2 +- .../src/features/metadata/util/recallers.ts | 8 +- .../nodes/util/graph/generation/addLoRAs.ts | 2 +- .../util/graph/generation/addSDXLLoRAs.ts | 2 +- .../features/prompt/PromptTriggerSelect.tsx | 2 +- .../GenerationSettingsAccordion.tsx | 6 +- 16 files changed, 101 insertions(+), 85 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/store/lorasReducers.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts index 41bd9d6712f..fefd88800eb 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts @@ -1,6 +1,6 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { loraDeleted } from 'features/controlLayers/store/canvasV2Slice'; +import { loraDeleted } from 'features/controlLayers/store/lorasSlice'; import { modelChanged, vaeSelected } from 'features/controlLayers/store/paramsSlice'; import { modelSelected } from 'features/parameters/store/actions'; import { zParameterModel } from 'features/parameters/types/parameterSchemas'; @@ -31,7 +31,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = let modelsCleared = 0; // handle incompatible loras - state.canvasV2.loras.forEach((lora) => { + state.loras.loras.forEach((lora) => { if (lora.model.base !== newBaseModel) { dispatch(loraDeleted({ id: lora.id })); modelsCleared += 1; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts index f6b8818e139..cc452a2152d 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts @@ -7,9 +7,9 @@ import { bboxWidthChanged, controlLayerModelChanged, ipaModelChanged, - loraDeleted, rgIPAdapterModelChanged, } from 'features/controlLayers/store/canvasV2Slice'; +import { loraDeleted } from 'features/controlLayers/store/lorasSlice'; import { modelChanged, refinerModelChanged, vaeSelected } from 'features/controlLayers/store/paramsSlice'; import { getEntityIdentifier } from 'features/controlLayers/store/types'; import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; @@ -161,7 +161,7 @@ const handleVAEModels: ModelHandler = (models, state, dispatch, log) => { const handleLoRAModels: ModelHandler = (models, state, dispatch, _log) => { const loraModels = models.filter(isLoRAModelConfig); - state.canvasV2.loras.forEach((lora) => { + state.loras.loras.forEach((lora) => { const isLoRAAvailable = loraModels.some((m) => m.key === lora.model.key); if (isLoRAAvailable) { return; diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index aabc77829a8..8c8dbb9681f 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -9,6 +9,7 @@ import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice'; import { canvasSessionPersistConfig, canvasSessionSlice } from 'features/controlLayers/store/canvasSessionSlice'; import { canvasSettingsPersistConfig, canvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice'; import { canvasV2PersistConfig, canvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { lorasPersistConfig, lorasSlice } from 'features/controlLayers/store/lorasSlice'; import { paramsPersistConfig, paramsSlice } from 'features/controlLayers/store/paramsSlice'; import { toolPersistConfig, toolSlice } from 'features/controlLayers/store/toolSlice'; import { deleteImageModalSlice } from 'features/deleteImageModal/store/slice'; @@ -65,6 +66,7 @@ const allReducers = { [toolSlice.name]: toolSlice.reducer, [canvasSettingsSlice.name]: canvasSettingsSlice.reducer, [canvasSessionSlice.name]: canvasSessionSlice.reducer, + [lorasSlice.name]: lorasSlice.reducer, }; const rootReducer = combineReducers(allReducers); @@ -110,6 +112,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = { [toolPersistConfig.name]: toolPersistConfig, [canvasSettingsPersistConfig.name]: canvasSettingsPersistConfig, [canvasSessionPersistConfig.name]: canvasSessionPersistConfig, + [lorasPersistConfig.name]: lorasPersistConfig, }; const unserialize: UnserializeFunction = (data, key) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 8951797f777..92ac8162d23 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -8,7 +8,6 @@ import { bboxReducers } from 'features/controlLayers/store/bboxReducers'; import { controlLayersReducers } from 'features/controlLayers/store/controlLayersReducers'; import { inpaintMaskReducers } from 'features/controlLayers/store/inpaintMaskReducers'; import { ipAdaptersReducers } from 'features/controlLayers/store/ipAdaptersReducers'; -import { lorasReducers } from 'features/controlLayers/store/lorasReducers'; import { modelChanged } from 'features/controlLayers/store/paramsSlice'; import { rasterLayersReducers } from 'features/controlLayers/store/rasterLayersReducers'; import { regionsReducers } from 'features/controlLayers/store/regionsReducers'; @@ -52,7 +51,6 @@ const initialState: CanvasV2State = { isHidden: false, entities: [], }, - loras: [], ipAdapters: { entities: [] }, bbox: { rect: { x: 0, y: 0, width: 512, height: 512 }, @@ -77,8 +75,6 @@ export const canvasV2Slice = createSlice({ ...regionsReducers, ...inpaintMaskReducers, ...bboxReducers, - // move out - ...lorasReducers, entitySelected: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; state.selectedEntityIdentifier = entityIdentifier; @@ -437,13 +433,6 @@ export const { rgIPAdapterMethodChanged, rgIPAdapterModelChanged, rgIPAdapterCLIPVisionModelChanged, - // LoRAs - loraAdded, - loraRecalled, - loraDeleted, - loraWeightChanged, - loraIsEnabledChanged, - loraAllDeleted, // Inpaint mask inpaintMaskAdded, // inpaintMaskRecalled, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/lorasReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/lorasReducers.ts deleted file mode 100644 index d43f608346b..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/store/lorasReducers.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import type { CanvasV2State, LoRA } from 'features/controlLayers/store/types'; -import { zModelIdentifierField } from 'features/nodes/types/common'; -import type { LoRAModelConfig } from 'services/api/types'; -import { v4 as uuidv4 } from 'uuid'; - -export const defaultLoRAConfig: Pick = { - weight: 0.75, - isEnabled: true, -}; - -const selectLoRA = (state: CanvasV2State, id: string) => state.loras.find((lora) => lora.id === id); - -export const lorasReducers = { - loraAdded: { - reducer: (state, action: PayloadAction<{ model: LoRAModelConfig; id: string }>) => { - const { model, id } = action.payload; - const parsedModel = zModelIdentifierField.parse(model); - state.loras.push({ ...defaultLoRAConfig, model: parsedModel, id }); - }, - prepare: (payload: { model: LoRAModelConfig }) => ({ payload: { ...payload, id: uuidv4() } }), - }, - loraRecalled: (state, action: PayloadAction<{ lora: LoRA }>) => { - const { lora } = action.payload; - state.loras.push(lora); - }, - loraDeleted: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - state.loras = state.loras.filter((lora) => lora.id !== id); - }, - loraWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { - const { id, weight } = action.payload; - const lora = selectLoRA(state, id); - if (!lora) { - return; - } - lora.weight = weight; - }, - loraIsEnabledChanged: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => { - const { id, isEnabled } = action.payload; - const lora = selectLoRA(state, id); - if (!lora) { - return; - } - lora.isEnabled = isEnabled; - }, - loraAllDeleted: (state) => { - state.loras = []; - }, -} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts new file mode 100644 index 00000000000..66d5521113a --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts @@ -0,0 +1,80 @@ +import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; +import type { PersistConfig, RootState } from 'app/store/store'; +import type { LoRA } from 'features/controlLayers/store/types'; +import { zModelIdentifierField } from 'features/nodes/types/common'; +import type { LoRAModelConfig } from 'services/api/types'; +import { v4 as uuidv4 } from 'uuid'; + +type LoRAsState = { + loras: LoRA[]; +}; + +export const defaultLoRAConfig: Pick = { + weight: 0.75, + isEnabled: true, +}; + +const initialState: LoRAsState = { + loras: [], +}; + +const selectLoRA = (state: LoRAsState, id: string) => state.loras.find((lora) => lora.id === id); + +export const lorasSlice = createSlice({ + name: 'loras', + initialState, + reducers: { + loraAdded: { + reducer: (state, action: PayloadAction<{ model: LoRAModelConfig; id: string }>) => { + const { model, id } = action.payload; + const parsedModel = zModelIdentifierField.parse(model); + state.loras.push({ ...defaultLoRAConfig, model: parsedModel, id }); + }, + prepare: (payload: { model: LoRAModelConfig }) => ({ payload: { ...payload, id: uuidv4() } }), + }, + loraRecalled: (state, action: PayloadAction<{ lora: LoRA }>) => { + const { lora } = action.payload; + state.loras.push(lora); + }, + loraDeleted: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + state.loras = state.loras.filter((lora) => lora.id !== id); + }, + loraWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { + const { id, weight } = action.payload; + const lora = selectLoRA(state, id); + if (!lora) { + return; + } + lora.weight = weight; + }, + loraIsEnabledChanged: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => { + const { id, isEnabled } = action.payload; + const lora = selectLoRA(state, id); + if (!lora) { + return; + } + lora.isEnabled = isEnabled; + }, + loraAllDeleted: (state) => { + state.loras = []; + }, + }, +}); + +export const { loraAdded, loraRecalled, loraDeleted, loraWeightChanged, loraIsEnabledChanged, loraAllDeleted } = + lorasSlice.actions; + +export const selectLoRAsSlice = (state: RootState) => state.loras; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +const migrate = (state: any): any => { + return state; +}; + +export const lorasPersistConfig: PersistConfig = { + name: lorasSlice.name, + initialState, + migrate, + persistDenylist: [], +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 74231cdb9f9..cb504d7768b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -714,7 +714,6 @@ export type CanvasV2State = { ipAdapters: { entities: CanvasIPAdapterState[]; }; - loras: LoRA[]; bbox: { rect: { x: number; diff --git a/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx b/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx index 54ad2cf9879..28c0ea8198b 100644 --- a/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx +++ b/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx @@ -11,7 +11,7 @@ import { } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { loraDeleted, loraIsEnabledChanged, loraWeightChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { loraDeleted, loraIsEnabledChanged, loraWeightChanged } from 'features/controlLayers/store/lorasSlice'; import type { LoRA } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { PiTrashSimpleBold } from 'react-icons/pi'; diff --git a/invokeai/frontend/web/src/features/lora/components/LoRAList.tsx b/invokeai/frontend/web/src/features/lora/components/LoRAList.tsx index e96e38797d4..6d05f1ea6fe 100644 --- a/invokeai/frontend/web/src/features/lora/components/LoRAList.tsx +++ b/invokeai/frontend/web/src/features/lora/components/LoRAList.tsx @@ -1,11 +1,11 @@ import { Flex } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; +import { selectLoRAsSlice } from 'features/controlLayers/store/lorasSlice'; import { LoRACard } from 'features/lora/components/LoRACard'; import { memo } from 'react'; -const selectLoRAsArray = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => canvasV2.loras); +const selectLoRAsArray = createMemoizedSelector(selectLoRAsSlice, (loras) => loras.loras); export const LoRAList = memo(() => { const lorasArray = useAppSelector(selectLoRAsArray); diff --git a/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx b/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx index 2f552ba8634..8786e6c1b3a 100644 --- a/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx +++ b/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx @@ -4,14 +4,13 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; -import { loraAdded } from 'features/controlLayers/store/canvasV2Slice'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; +import { loraAdded, selectLoRAsSlice } from 'features/controlLayers/store/lorasSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useLoRAModels } from 'services/api/hooks/modelsByType'; import type { LoRAModelConfig } from 'services/api/types'; -const selectLoRAs = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => canvasV2.loras); +const selectLoRAs = createMemoizedSelector(selectLoRAsSlice, (loras) => loras.loras); const LoRASelect = () => { const dispatch = useAppDispatch(); diff --git a/invokeai/frontend/web/src/features/metadata/util/parsers.ts b/invokeai/frontend/web/src/features/metadata/util/parsers.ts index 60f9eea8333..efcfffd0914 100644 --- a/invokeai/frontend/web/src/features/metadata/util/parsers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/parsers.ts @@ -1,5 +1,5 @@ import { getPrefixedId } from 'features/controlLayers/konva/util'; -import { defaultLoRAConfig } from 'features/controlLayers/store/lorasReducers'; +import { defaultLoRAConfig } from 'features/controlLayers/store/lorasSlice'; import type { CanvasControlLayerState, CanvasInpaintMaskState, diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index 6333167768f..71278e0d9c4 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -1,10 +1,6 @@ import { getStore } from 'app/store/nanostores/store'; -import { - bboxHeightChanged, - bboxWidthChanged, - loraAllDeleted, - loraRecalled, -} from 'features/controlLayers/store/canvasV2Slice'; +import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { loraAllDeleted, loraRecalled } from 'features/controlLayers/store/lorasSlice'; import { negativePrompt2Changed, negativePromptChanged, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLoRAs.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLoRAs.ts index 92bf0cbeaaf..79a8521efba 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLoRAs.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLoRAs.ts @@ -14,7 +14,7 @@ export const addLoRAs = ( posCond: Invocation<'compel'>, negCond: Invocation<'compel'> ): void => { - const enabledLoRAs = state.canvasV2.loras.filter( + const enabledLoRAs = state.loras.loras.filter( (l) => l.isEnabled && (l.model.base === 'sd-1' || l.model.base === 'sd-2') ); const loraCount = enabledLoRAs.length; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts index ffb5268520f..a38c9757cea 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts @@ -13,7 +13,7 @@ export const addSDXLLoRAs = ( posCond: Invocation<'sdxl_compel_prompt'>, negCond: Invocation<'sdxl_compel_prompt'> ): void => { - const enabledLoRAs = state.canvasV2.loras.filter((l) => l.isEnabled && l.model.base === 'sdxl'); + const enabledLoRAs = state.loras.loras.filter((l) => l.isEnabled && l.model.base === 'sdxl'); const loraCount = enabledLoRAs.length; if (loraCount === 0) { diff --git a/invokeai/frontend/web/src/features/prompt/PromptTriggerSelect.tsx b/invokeai/frontend/web/src/features/prompt/PromptTriggerSelect.tsx index ad89f6872d9..b4b14f4e52b 100644 --- a/invokeai/frontend/web/src/features/prompt/PromptTriggerSelect.tsx +++ b/invokeai/frontend/web/src/features/prompt/PromptTriggerSelect.tsx @@ -18,7 +18,7 @@ export const PromptTriggerSelect = memo(({ onSelect, onClose }: PromptTriggerSel const { t } = useTranslation(); const mainModel = useAppSelector((s) => s.params.model); - const addedLoRAs = useAppSelector((s) => s.canvasV2.loras); + const addedLoRAs = useAppSelector((s) => s.loras.loras); const { data: mainModelConfig, isLoading: isLoadingMainModelConfig } = useGetModelConfigQuery( mainModel?.key ?? skipToken ); diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx index 2d694013699..9edab01c371 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx @@ -3,7 +3,7 @@ import { Box, Expander, Flex, FormControlGroup, StandaloneAccordion } from '@inv import { EMPTY_ARRAY } from 'app/store/constants'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; +import { selectLoRAsSlice } from 'features/controlLayers/store/lorasSlice'; import { LoRAList } from 'features/lora/components/LoRAList'; import LoRASelect from 'features/lora/components/LoRASelect'; import ParamCFGScale from 'features/parameters/components/Core/ParamCFGScale'; @@ -29,8 +29,8 @@ export const GenerationSettingsAccordion = memo(() => { const activeTabName = useAppSelector(selectActiveTab); const selectBadges = useMemo( () => - createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { - const enabledLoRAsCount = canvasV2.loras.filter((l) => l.isEnabled).length; + createMemoizedSelector(selectLoRAsSlice, (loras) => { + const enabledLoRAsCount = loras.loras.filter((l) => l.isEnabled).length; const loraTabBadges = enabledLoRAsCount ? [`${enabledLoRAsCount} ${t('models.concepts')}`] : EMPTY_ARRAY; const accordionBadges = modelConfig ? [modelConfig.name, modelConfig.base] : EMPTY_ARRAY; return { loraTabBadges, accordionBadges }; From 9056f446bbc7b1cba0c27338454b75e70576c802 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 26 Aug 2024 22:44:47 +1000 Subject: [PATCH 504/678] fix(ui): handle error from internal konva method We are dipping into konva's private API for preview images and it appears to be unsafe (got an error once). Wrapped in a try/catch. --- .../konva/CanvasObjectRenderer.ts | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index 9a3b32cec83..b586976f1a6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -30,6 +30,7 @@ import type { GroupConfig } from 'konva/lib/Group'; import { debounce } from 'lodash-es'; import { atom } from 'nanostores'; import type { Logger } from 'roarr'; +import { serializeError } from 'serialize-error'; import { getImageDTO, uploadImage } from 'services/api/endpoints/images'; import type { ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; @@ -550,17 +551,22 @@ export class CanvasObjectRenderer extends CanvasModuleBase { if (this.parent.transformer.pixelRect.width === 0 || this.parent.transformer.pixelRect.height === 0) { return; } - const canvas = this.konva.objectGroup._getCachedSceneCanvas()._canvas as HTMLCanvasElement | undefined | null; - if (canvas) { - const nodeRect = this.parent.transformer.nodeRect; - const pixelRect = this.parent.transformer.pixelRect; - const rect = { - x: pixelRect.x - nodeRect.x, - y: pixelRect.y - nodeRect.y, - width: pixelRect.width, - height: pixelRect.height, - }; - this.$canvasCache.set({ rect, canvas }); + try { + const canvas = this.konva.objectGroup._getCachedSceneCanvas()._canvas as HTMLCanvasElement | undefined | null; + if (canvas) { + const nodeRect = this.parent.transformer.nodeRect; + const pixelRect = this.parent.transformer.pixelRect; + const rect = { + x: pixelRect.x - nodeRect.x, + y: pixelRect.y - nodeRect.y, + width: pixelRect.width, + height: pixelRect.height, + }; + this.$canvasCache.set({ rect, canvas }); + } + } catch (error) { + // We are using an internal Konva method, so we need to catch any errors that may occur. + this.log.warn({ error: serializeError(error) }, 'Failed to update preview canvas'); } }, 300); From 36604f752e0b4b65c6c76dcdee7307a64e735433 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 26 Aug 2024 22:46:07 +1000 Subject: [PATCH 505/678] chore: release v4.2.9.dev4 Canvas dev build. --- invokeai/version/invokeai_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/version/invokeai_version.py b/invokeai/version/invokeai_version.py index b91bbf2bf67..421b13861d8 100644 --- a/invokeai/version/invokeai_version.py +++ b/invokeai/version/invokeai_version.py @@ -1 +1 @@ -__version__ = "4.2.9.dev3" +__version__ = "4.2.9.dev4" From 04d41085a3dcaed6cd74dad3dedf2c21f9deb476 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 27 Aug 2024 12:05:45 +1000 Subject: [PATCH 506/678] feat(ui): rough out undo/redo on canvas --- .../addCommitStagingAreaImageListener.ts | 5 +- .../listeners/boardAndImagesDeleted.ts | 9 ++- .../listeners/imageDeletionListeners.ts | 9 +-- .../listeners/imageDropped.ts | 7 +- .../listeners/imageUploaded.ts | 2 +- .../listeners/modelSelected.ts | 2 +- .../listeners/modelsLoaded.ts | 18 +++-- .../listeners/setDefaultSettings.ts | 2 +- invokeai/frontend/web/src/app/store/store.ts | 6 +- .../src/common/hooks/useIsReadyToEnqueue.ts | 16 ++--- .../components/CanvasAddEntityButtons.tsx | 2 +- .../CanvasEntityListMenuItems.tsx | 2 +- .../ControlLayer/ControlLayerBadges.tsx | 4 +- .../ControlLayerControlAdapter.tsx | 2 +- .../ControlLayer/ControlLayerEntityList.tsx | 8 +-- .../ControlLayerMenuItemsControlToRaster.tsx | 2 +- ...ontrolLayerMenuItemsTransparencyEffect.tsx | 8 +-- .../components/HeadsUpDisplay.tsx | 6 +- .../IPAdapter/IPAdapterImagePreview.tsx | 2 +- .../components/IPAdapter/IPAdapterList.tsx | 12 ++-- .../IPAdapter/IPAdapterSettings.tsx | 11 +++- .../InpaintMask/InpaintMaskList.tsx | 13 ++-- .../InpaintMaskMaskFillColorPicker.tsx | 13 ++-- .../RasterLayer/RasterLayerEntityList.tsx | 12 ++-- .../RasterLayerMenuItemsRasterToControl.tsx | 2 +- ...onalGuidanceAddPromptsIPAdapterButtons.tsx | 8 +-- .../RegionalGuidanceBadges.tsx | 11 +++- .../RegionalGuidanceEntityList.tsx | 12 ++-- .../RegionalGuidanceIPAdapterSettings.tsx | 20 ++++-- .../RegionalGuidanceIPAdapters.tsx | 6 +- .../RegionalGuidanceMaskFillColorPicker.tsx | 13 ++-- ...uidanceMenuItemsAddPromptsAndIPAdapter.tsx | 8 +-- .../RegionalGuidanceMenuItemsAutoNegative.tsx | 13 ++-- .../RegionalGuidanceNegativePrompt.tsx | 14 ++-- .../RegionalGuidancePositivePrompt.tsx | 14 ++-- .../RegionalGuidanceSettings.tsx | 37 +++++++---- .../Settings/CanvasSettingsResetButton.tsx | 2 +- .../components/Tool/ToolBrushButton.tsx | 12 ++-- .../components/Tool/ToolEraserButton.tsx | 12 ++-- .../components/Tool/ToolMoveButton.tsx | 12 ++-- .../components/Tool/ToolRectButton.tsx | 12 ++-- .../components/UndoRedoButtonGroup.tsx | 15 +++-- .../common/CanvasEntityContainer.tsx | 2 +- .../common/CanvasEntityEnabledToggle.tsx | 2 +- .../common/CanvasEntityMenuItemsArrange.tsx | 32 ++++----- .../common/CanvasEntityMenuItemsDelete.tsx | 2 +- .../common/CanvasEntityMenuItemsDuplicate.tsx | 2 +- .../components/common/CanvasEntityOpacity.tsx | 15 +++-- .../common/CanvasEntityPreviewImage.tsx | 4 +- .../common/CanvasEntityTitleEdit.tsx | 2 +- .../common/CanvasEntityTypeIsHiddenToggle.tsx | 2 +- .../hooks/useCanvasDeleteLayerHotkey.ts | 8 +-- .../hooks/useCanvasResetLayerHotkey.ts | 8 +-- .../controlLayers/hooks/useEntityIsEnabled.ts | 6 +- .../hooks/useEntityIsSelected.ts | 14 ++-- .../hooks/useEntityObjectCount.ts | 6 +- .../hooks/useEntitySelectionColor.ts | 6 +- .../controlLayers/hooks/useEntityTitle.ts | 6 +- .../controlLayers/hooks/useEntityTypeCount.ts | 14 ++-- .../hooks/useEntityTypeIsHidden.ts | 12 ++-- .../hooks/useLayerControlAdapter.ts | 6 +- .../konva/CanvasRenderingModule.ts | 16 ++--- .../konva/CanvasStateApiModule.ts | 6 +- .../controlLayers/store/bboxReducers.ts | 6 +- .../controlLayers/store/canvasSessionSlice.ts | 12 ++-- .../store/canvasSettingsSlice.ts | 3 +- .../{canvasV2Slice.ts => canvasSlice.ts} | 16 ++--- .../store/controlLayersReducers.ts | 4 +- .../store/inpaintMaskReducers.ts | 4 +- .../controlLayers/store/ipAdaptersReducers.ts | 4 +- .../store/rasterLayersReducers.ts | 4 +- .../controlLayers/store/regionsReducers.ts | 6 +- .../features/controlLayers/store/selectors.ts | 65 ++++++++++++------- .../src/features/controlLayers/store/types.ts | 2 +- .../components/DeleteImageModal.tsx | 8 +-- .../deleteImageModal/store/selectors.ts | 14 ++-- .../components/Boards/DeleteBoardModal.tsx | 6 +- .../src/features/metadata/util/recallers.ts | 2 +- .../util/graph/generation/addImageToImage.ts | 4 +- .../nodes/util/graph/generation/addInpaint.ts | 10 ++- .../util/graph/generation/addOutpaint.ts | 10 ++- .../util/graph/generation/buildSD1Graph.ts | 26 +++++--- .../util/graph/generation/buildSDXLGraph.ts | 26 +++++--- .../nodes/util/graph/graphBuilderUtils.ts | 4 +- .../ParamScaleBeforeProcessing.tsx | 8 ++- .../InfillAndScaling/ParamScaledHeight.tsx | 44 +++++++------ .../InfillAndScaling/ParamScaledWidth.tsx | 44 +++++++------ .../components/Core/ParamHeight.tsx | 39 ++++++----- .../parameters/components/Core/ParamWidth.tsx | 39 ++++++----- .../DocumentSize/AspectRatioSelect.tsx | 8 ++- .../DocumentSize/LockAspectRatioButton.tsx | 8 ++- .../DocumentSize/SetOptimalSizeButton.tsx | 12 ++-- .../DocumentSize/SwapDimensionsButton.tsx | 2 +- .../ImageSettingsAccordion.tsx | 10 +-- 94 files changed, 586 insertions(+), 431 deletions(-) rename invokeai/frontend/web/src/features/controlLayers/store/{canvasV2Slice.ts => canvasSlice.ts} (97%) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts index 7c533717e7e..2b5949e4145 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts @@ -4,7 +4,8 @@ import { sessionStagingAreaImageAccepted, sessionStagingAreaReset, } from 'features/controlLayers/store/canvasSessionSlice'; -import { rasterLayerAdded } from 'features/controlLayers/store/canvasV2Slice'; +import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice'; +import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; import type { CanvasRasterLayerState } from 'features/controlLayers/store/types'; import { imageDTOToImageObject } from 'features/controlLayers/store/types'; import { toast } from 'features/toast/toast'; @@ -58,7 +59,7 @@ export const addStagingListeners = (startAppListening: AppStartListening) => { const stagingAreaImage = state.canvasSession.stagedImages[index]; assert(stagingAreaImage, 'No staged image found to accept'); - const { x, y } = state.canvasV2.bbox.rect; + const { x, y } = selectCanvasSlice(state).bbox.rect; const { imageDTO, offsetX, offsetY } = stagingAreaImage; const imageObject = imageDTOToImageObject(imageDTO); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts index be0d9ab195e..0e30802328f 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts @@ -1,6 +1,7 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; import { getImageUsage } from 'features/deleteImageModal/store/selectors'; -import { nodeEditorReset } from 'features/nodes/store/nodesSlice'; +import { nodeEditorReset, selectNodesSlice } from 'features/nodes/store/nodesSlice'; import { imagesApi } from 'services/api/endpoints/images'; export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppStartListening) => { @@ -13,10 +14,12 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS let wasNodeEditorReset = false; - const { nodes, canvasV2 } = getState(); + const state = getState(); + const nodes = selectNodesSlice(state); + const canvas = selectCanvasSlice(state); deleted_images.forEach((image_name) => { - const imageUsage = getImageUsage(nodes.present, canvasV2, image_name); + const imageUsage = getImageUsage(nodes, canvas, image_name); if (imageUsage.isNodesImage && !wasNodeEditorReset) { dispatch(nodeEditorReset()); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts index cf1fc0ff309..258ddf46208 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts @@ -1,7 +1,8 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import type { AppDispatch, RootState } from 'app/store/store'; -import { entityDeleted, ipaImageChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { entityDeleted, ipaImageChanged } from 'features/controlLayers/store/canvasSlice'; +import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; import { getEntityIdentifier } from 'features/controlLayers/store/types'; import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions'; import { isModalOpenChanged } from 'features/deleteImageModal/store/slice'; @@ -40,7 +41,7 @@ const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: Im }; // const deleteControlAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { -// state.canvasV2.controlAdapters.entities.forEach(({ id, imageObject, processedImageObject }) => { +// state.canvas.present.controlAdapters.entities.forEach(({ id, imageObject, processedImageObject }) => { // if ( // imageObject?.image.image_name === imageDTO.image_name || // processedImageObject?.image.image_name === imageDTO.image_name @@ -52,7 +53,7 @@ const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: Im // }; const deleteIPAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { - state.canvasV2.ipAdapters.entities.forEach((entity) => { + selectCanvasSlice(state).ipAdapters.entities.forEach((entity) => { if (entity.ipAdapter.image?.image_name === imageDTO.image_name) { dispatch(ipaImageChanged({ entityIdentifier: getEntityIdentifier(entity), imageDTO: null })); } @@ -60,7 +61,7 @@ const deleteIPAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO }; const deleteLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { - state.canvasV2.rasterLayers.entities.forEach(({ id, objects }) => { + selectCanvasSlice(state).rasterLayers.entities.forEach(({ id, objects }) => { let shouldDelete = false; for (const obj of objects) { if (obj.type === 'image' && obj.image.image_name === imageDTO.image_name) { diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index 1d6bdacaa69..996050c3e24 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -6,7 +6,8 @@ import { ipaImageChanged, rasterLayerAdded, rgIPAdapterImageChanged, -} from 'features/controlLayers/store/canvasV2Slice'; +} from 'features/controlLayers/store/canvasSlice'; +import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; import type { CanvasControlLayerState, CanvasRasterLayerState } from 'features/controlLayers/store/types'; import { imageDTOToImageObject } from 'features/controlLayers/store/types'; import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; @@ -85,7 +86,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => activeData.payload.imageDTO ) { const imageObject = imageDTOToImageObject(activeData.payload.imageDTO); - const { x, y } = getState().canvasV2.bbox.rect; + const { x, y } = selectCanvasSlice(getState()).bbox.rect; const overrides: Partial = { objects: [imageObject], position: { x, y }, @@ -103,7 +104,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => activeData.payload.imageDTO ) { const imageObject = imageDTOToImageObject(activeData.payload.imageDTO); - const { x, y } = getState().canvasV2.bbox.rect; + const { x, y } = selectCanvasSlice(getState()).bbox.rect; const overrides: Partial = { objects: [imageObject], position: { x, y }, diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts index c10fc60a4dc..0a5e18a160f 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts @@ -1,6 +1,6 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { ipaImageChanged, rgIPAdapterImageChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { ipaImageChanged, rgIPAdapterImageChanged } from 'features/controlLayers/store/canvasSlice'; import { selectListBoardsQueryArgs } from 'features/gallery/store/gallerySelectors'; import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice'; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts index fefd88800eb..13a256ad4e8 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts @@ -46,7 +46,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = } // handle incompatible controlnets - // state.canvasV2.controlAdapters.entities.forEach((ca) => { + // state.canvas.present.controlAdapters.entities.forEach((ca) => { // if (ca.model?.base !== newBaseModel) { // modelsCleared += 1; // if (ca.isEnabled) { diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts index cc452a2152d..f5a3fce8651 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts @@ -8,9 +8,10 @@ import { controlLayerModelChanged, ipaModelChanged, rgIPAdapterModelChanged, -} from 'features/controlLayers/store/canvasV2Slice'; +} from 'features/controlLayers/store/canvasSlice'; import { loraDeleted } from 'features/controlLayers/store/lorasSlice'; import { modelChanged, refinerModelChanged, vaeSelected } from 'features/controlLayers/store/paramsSlice'; +import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; import { getEntityIdentifier } from 'features/controlLayers/store/types'; import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; import { postProcessingModelChanged, upscaleModelChanged } from 'features/parameters/store/upscaleSlice'; @@ -81,15 +82,12 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => { const result = zParameterModel.safeParse(defaultModelInList); if (result.success) { dispatch(modelChanged({ model: defaultModelInList, previousModel: currentModel })); - + const { bbox } = selectCanvasSlice(state); const optimalDimension = getOptimalDimension(defaultModelInList); - if (getIsSizeOptimal(state.canvasV2.bbox.rect.width, state.canvasV2.bbox.rect.height, optimalDimension)) { + if (getIsSizeOptimal(bbox.rect.width, bbox.rect.height, optimalDimension)) { return; } - const { width, height } = calculateNewSize( - state.canvasV2.bbox.aspectRatio.value, - optimalDimension * optimalDimension - ); + const { width, height } = calculateNewSize(bbox.aspectRatio.value, optimalDimension * optimalDimension); dispatch(bboxWidthChanged({ width })); dispatch(bboxHeightChanged({ height })); @@ -172,7 +170,7 @@ const handleLoRAModels: ModelHandler = (models, state, dispatch, _log) => { const handleControlAdapterModels: ModelHandler = (models, state, dispatch, _log) => { const caModels = models.filter(isControlNetOrT2IAdapterModelConfig); - state.canvasV2.controlLayers.entities.forEach((entity) => { + selectCanvasSlice(state).controlLayers.entities.forEach((entity) => { const isModelAvailable = caModels.some((m) => m.key === entity.controlAdapter.model?.key); if (isModelAvailable) { return; @@ -183,7 +181,7 @@ const handleControlAdapterModels: ModelHandler = (models, state, dispatch, _log) const handleIPAdapterModels: ModelHandler = (models, state, dispatch, _log) => { const ipaModels = models.filter(isIPAdapterModelConfig); - state.canvasV2.ipAdapters.entities.forEach((entity) => { + selectCanvasSlice(state).ipAdapters.entities.forEach((entity) => { const isModelAvailable = ipaModels.some((m) => m.key === entity.ipAdapter.model?.key); if (isModelAvailable) { return; @@ -191,7 +189,7 @@ const handleIPAdapterModels: ModelHandler = (models, state, dispatch, _log) => { dispatch(ipaModelChanged({ entityIdentifier: getEntityIdentifier(entity), modelConfig: null })); }); - state.canvasV2.regions.entities.forEach((entity) => { + selectCanvasSlice(state).regions.entities.forEach((entity) => { entity.ipAdapters.forEach(({ id: ipAdapterId, model }) => { const isModelAvailable = ipaModels.some((m) => m.key === model?.key); if (isModelAvailable) { diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts index e013b3d17f7..42e17b938b3 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts @@ -1,5 +1,5 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasSlice'; import { setCfgRescaleMultiplier, setCfgScale, diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 8c8dbb9681f..00bdde8ff58 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -8,7 +8,7 @@ import { deepClone } from 'common/util/deepClone'; import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice'; import { canvasSessionPersistConfig, canvasSessionSlice } from 'features/controlLayers/store/canvasSessionSlice'; import { canvasSettingsPersistConfig, canvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice'; -import { canvasV2PersistConfig, canvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { canvasPersistConfig, canvasSlice } from 'features/controlLayers/store/canvasSlice'; import { lorasPersistConfig, lorasSlice } from 'features/controlLayers/store/lorasSlice'; import { paramsPersistConfig, paramsSlice } from 'features/controlLayers/store/paramsSlice'; import { toolPersistConfig, toolSlice } from 'features/controlLayers/store/toolSlice'; @@ -58,7 +58,7 @@ const allReducers = { [queueSlice.name]: queueSlice.reducer, [workflowSlice.name]: workflowSlice.reducer, [hrfSlice.name]: hrfSlice.reducer, - [canvasV2Slice.name]: canvasV2Slice.reducer, + [canvasSlice.name]: undoable(canvasSlice.reducer), [workflowSettingsSlice.name]: workflowSettingsSlice.reducer, [upscaleSlice.name]: upscaleSlice.reducer, [stylePresetSlice.name]: stylePresetSlice.reducer, @@ -104,7 +104,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = { [dynamicPromptsPersistConfig.name]: dynamicPromptsPersistConfig, [modelManagerV2PersistConfig.name]: modelManagerV2PersistConfig, [hrfPersistConfig.name]: hrfPersistConfig, - [canvasV2PersistConfig.name]: canvasV2PersistConfig, + [canvasPersistConfig.name]: canvasPersistConfig, [workflowSettingsPersistConfig.name]: workflowSettingsPersistConfig, [upscalePersistConfig.name]: upscalePersistConfig, [stylePresetPersistConfig.name]: stylePresetPersistConfig, diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index f060a0e6c27..c3946a2f0ab 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -3,7 +3,7 @@ import { $isConnected } from 'app/hooks/useSocketIO'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; +import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; @@ -34,14 +34,14 @@ const createSelector = (templates: Templates, isConnected: boolean) => selectNodesSlice, selectWorkflowSettingsSlice, selectDynamicPromptsSlice, - selectCanvasV2Slice, + selectCanvasSlice, selectParamsSlice, selectUpscalelice, selectConfigSlice, selectActiveTab, ], - (system, nodes, workflowSettings, dynamicPrompts, canvasV2, params, upscale, config, activeTabName) => { - const { bbox } = canvasV2; + (system, nodes, workflowSettings, dynamicPrompts, canvas, params, upscale, config, activeTabName) => { + const { bbox } = canvas; const { model, positivePrompt } = params; const reasons: { prefix?: string; content: string }[] = []; @@ -124,7 +124,7 @@ const createSelector = (templates: Templates, isConnected: boolean) => reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') }); } - canvasV2.controlLayers.entities + canvas.controlLayers.entities .filter((controlLayer) => controlLayer.isEnabled) .forEach((controlLayer, i) => { const layerLiteral = i18n.t('controlLayers.layers_one'); @@ -154,7 +154,7 @@ const createSelector = (templates: Templates, isConnected: boolean) => } }); - canvasV2.ipAdapters.entities + canvas.ipAdapters.entities .filter((entity) => entity.isEnabled) .forEach((entity, i) => { const layerLiteral = i18n.t('controlLayers.layers_one'); @@ -182,7 +182,7 @@ const createSelector = (templates: Templates, isConnected: boolean) => } }); - canvasV2.regions.entities + canvas.regions.entities .filter((entity) => entity.isEnabled) .forEach((entity, i) => { const layerLiteral = i18n.t('controlLayers.layers_one'); @@ -219,7 +219,7 @@ const createSelector = (templates: Templates, isConnected: boolean) => } }); - canvasV2.rasterLayers.entities + canvas.rasterLayers.entities .filter((entity) => entity.isEnabled) .forEach((entity, i) => { const layerLiteral = i18n.t('controlLayers.layers_one'); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx index abb689983af..7b507211859 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx @@ -6,7 +6,7 @@ import { ipaAdded, rasterLayerAdded, rgAdded, -} from 'features/controlLayers/store/canvasV2Slice'; +} from 'features/controlLayers/store/canvasSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPlusBold } from 'react-icons/pi'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuItems.tsx index 38be1d477da..82ceb609296 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuItems.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuItems.tsx @@ -7,7 +7,7 @@ import { ipaAdded, rasterLayerAdded, rgAdded, -} from 'features/controlLayers/store/canvasV2Slice'; +} from 'features/controlLayers/store/canvasSlice'; import { selectEntityCount } from 'features/controlLayers/store/selectors'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerBadges.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerBadges.tsx index ec68367b3a9..126d8fabbbf 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerBadges.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerBadges.tsx @@ -1,7 +1,7 @@ import { Badge } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { selectEntityOrThrow } from 'features/controlLayers/store/selectors'; +import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,7 +9,7 @@ export const ControlLayerBadges = memo(() => { const entityIdentifier = useEntityIdentifierContext('control_layer'); const { t } = useTranslation(); const withTransparencyEffect = useAppSelector( - (s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).withTransparencyEffect + (s) => selectEntityOrThrow(selectCanvasSlice(s), entityIdentifier).withTransparencyEffect ); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx index 3b9e6d1a312..549c8fe7b67 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx @@ -11,7 +11,7 @@ import { controlLayerControlModeChanged, controlLayerModelChanged, controlLayerWeightChanged, -} from 'features/controlLayers/store/canvasV2Slice'; +} from 'features/controlLayers/store/canvasSlice'; import type { ControlModeV2 } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import type { ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx index 8f94481f867..42c66ddd52b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx @@ -3,15 +3,15 @@ import { useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList'; import { ControlLayer } from 'features/controlLayers/components/ControlLayer/ControlLayer'; import { mapId } from 'features/controlLayers/konva/util'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; +import { selectCanvasSlice, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; import { memo } from 'react'; -const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { - return canvasV2.controlLayers.entities.map(mapId).reverse(); +const selectEntityIds = createMemoizedSelector(selectCanvasSlice, (canvas) => { + return canvas.controlLayers.entities.map(mapId).reverse(); }); export const ControlLayerEntityList = memo(() => { - const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'control_layer')); + const isSelected = useAppSelector((s) => selectSelectedEntityIdentifier(s)?.type === 'control_layer'); const layerIds = useAppSelector(selectEntityIds); if (layerIds.length === 0) { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsControlToRaster.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsControlToRaster.tsx index 924122fe240..5f1797a0a72 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsControlToRaster.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsControlToRaster.tsx @@ -1,7 +1,7 @@ import { MenuItem } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { controlLayerConvertedToRasterLayer } from 'features/controlLayers/store/canvasV2Slice'; +import { controlLayerConvertedToRasterLayer } from 'features/controlLayers/store/canvasSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiLightningBold } from 'react-icons/pi'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsTransparencyEffect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsTransparencyEffect.tsx index 5342e975bae..ce48a6332fc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsTransparencyEffect.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsTransparencyEffect.tsx @@ -2,8 +2,8 @@ import { MenuItem } from '@invoke-ai/ui-library'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { controlLayerWithTransparencyEffectToggled } from 'features/controlLayers/store/canvasV2Slice'; -import { selectCanvasV2Slice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; +import { controlLayerWithTransparencyEffectToggled } from 'features/controlLayers/store/canvasSlice'; +import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiDropHalfBold } from 'react-icons/pi'; @@ -14,8 +14,8 @@ export const ControlLayerMenuItemsTransparencyEffect = memo(() => { const entityIdentifier = useEntityIdentifierContext('control_layer'); const selectWithTransparencyEffect = useMemo( () => - createSelector(selectCanvasV2Slice, (canvasV2) => { - const entity = selectEntityOrThrow(canvasV2, entityIdentifier); + createSelector(selectCanvasSlice, (canvas) => { + const entity = selectEntityOrThrow(canvas, entityIdentifier); return entity.withTransparencyEffect; }), [entityIdentifier] diff --git a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx index ec7bcdaa258..28fef1854a7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx @@ -1,10 +1,14 @@ import { Box, Flex, Text } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; +import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; import { round } from 'lodash-es'; import { memo } from 'react'; +const selectBbox = createSelector(selectCanvasSlice, (canvas) => canvas.bbox); + export const HeadsUpDisplay = memo(() => { const canvasManager = useCanvasManager(); const stageAttrs = useStore(canvasManager.stateApi.$stageAttrs); @@ -13,7 +17,7 @@ export const HeadsUpDisplay = memo(() => { const isMouseDown = useStore(canvasManager.stateApi.$isMouseDown); const lastMouseDownPos = useStore(canvasManager.stateApi.$lastMouseDownPos); const lastAddedPoint = useStore(canvasManager.stateApi.$lastAddedPoint); - const bbox = useAppSelector((s) => s.canvasV2.bbox); + const bbox = useAppSelector(selectBbox); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterImagePreview.tsx index e1f6b078570..9849c9578fa 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterImagePreview.tsx @@ -5,7 +5,7 @@ import { $isConnected } from 'app/hooks/useSocketIO'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; -import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasSlice'; import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import type { ImageWithDims } from 'features/controlLayers/store/types'; import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterList.tsx index cdfcc897b55..c2cc874a4aa 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterList.tsx @@ -1,18 +1,22 @@ /* eslint-disable i18next/no-literal-string */ +import { createSelector } from '@reduxjs/toolkit'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList'; import { IPAdapter } from 'features/controlLayers/components/IPAdapter/IPAdapter'; import { mapId } from 'features/controlLayers/konva/util'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; +import { selectCanvasSlice, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; import { memo } from 'react'; -const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { - return canvasV2.ipAdapters.entities.map(mapId).reverse(); +const selectEntityIds = createMemoizedSelector(selectCanvasSlice, (canvas) => { + return canvas.ipAdapters.entities.map(mapId).reverse(); +}); +const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selectedEntityIdentifier) => { + return selectedEntityIdentifier?.type === 'ip_adapter'; }); export const IPAdapterList = memo(() => { - const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'ip_adapter')); + const isSelected = useAppSelector(selectIsSelected); const ipaIds = useAppSelector(selectEntityIds); if (ipaIds.length === 0) { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx index 0f8152fd9ff..44b708a2389 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx @@ -1,4 +1,5 @@ import { Box, Flex } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct'; import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper'; @@ -12,8 +13,8 @@ import { ipaMethodChanged, ipaModelChanged, ipaWeightChanged, -} from 'features/controlLayers/store/canvasV2Slice'; -import { selectEntityOrThrow } from 'features/controlLayers/store/selectors'; +} from 'features/controlLayers/store/canvasSlice'; +import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types'; import type { IPAImageDropData } from 'features/dnd/types'; import { memo, useCallback, useMemo } from 'react'; @@ -25,7 +26,11 @@ import { IPAdapterModel } from './IPAdapterModel'; export const IPAdapterSettings = memo(() => { const dispatch = useAppDispatch(); const entityIdentifier = useEntityIdentifierContext('ip_adapter'); - const ipAdapter = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).ipAdapter); + const selectIPAdapter = useMemo( + () => createSelector(selectCanvasSlice, (s) => selectEntityOrThrow(s, entityIdentifier).ipAdapter), + [entityIdentifier] + ); + const ipAdapter = useAppSelector(selectIPAdapter); const onChangeBeginEndStepPct = useCallback( (beginEndStepPct: [number, number]) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskList.tsx index b4f92759e47..6b04ff511a9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskList.tsx @@ -1,17 +1,22 @@ +import { createSelector } from '@reduxjs/toolkit'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList'; import { InpaintMask } from 'features/controlLayers/components/InpaintMask/InpaintMask'; import { mapId } from 'features/controlLayers/konva/util'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; +import { selectCanvasSlice, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; import { memo } from 'react'; -const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { - return canvasV2.inpaintMasks.entities.map(mapId).reverse(); +const selectEntityIds = createMemoizedSelector(selectCanvasSlice, (canvas) => { + return canvas.inpaintMasks.entities.map(mapId).reverse(); +}); + +const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selectedEntityIdentifier) => { + return selectedEntityIdentifier?.type === 'inpaint_mask'; }); export const InpaintMaskList = memo(() => { - const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'inpaint_mask')); + const isSelected = useAppSelector(selectIsSelected); const entityIds = useAppSelector(selectEntityIds); if (entityIds.length === 0) { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx index 426d5261f55..6d2879eac60 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx @@ -1,20 +1,25 @@ import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import RgbColorPicker from 'common/components/RgbColorPicker'; import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { MaskFillStyle } from 'features/controlLayers/components/common/MaskFillStyle'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { inpaintMaskFillColorChanged, inpaintMaskFillStyleChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { selectEntityOrThrow } from 'features/controlLayers/store/selectors'; +import { inpaintMaskFillColorChanged, inpaintMaskFillStyleChanged } from 'features/controlLayers/store/canvasSlice'; +import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import type { FillStyle, RgbColor } from 'features/controlLayers/store/types'; -import { memo, useCallback } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; export const InpaintMaskMaskFillColorPicker = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const entityIdentifier = useEntityIdentifierContext('inpaint_mask'); - const fill = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).fill); + const selectFill = useMemo( + () => createSelector(selectCanvasSlice, (canvas) => selectEntityOrThrow(canvas, entityIdentifier).fill), + [entityIdentifier] + ); + const fill = useAppSelector(selectFill); const onChangeFillColor = useCallback( (color: RgbColor) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerEntityList.tsx index 82bf728d0b8..4e2cbd581c7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerEntityList.tsx @@ -1,17 +1,21 @@ +import { createSelector } from '@reduxjs/toolkit'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList'; import { RasterLayer } from 'features/controlLayers/components/RasterLayer/RasterLayer'; import { mapId } from 'features/controlLayers/konva/util'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; +import { selectCanvasSlice, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; import { memo } from 'react'; -const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { - return canvasV2.rasterLayers.entities.map(mapId).reverse(); +const selectEntityIds = createMemoizedSelector(selectCanvasSlice, (canvas) => { + return canvas.rasterLayers.entities.map(mapId).reverse(); +}); +const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selectedEntityIdentifier) => { + return selectedEntityIdentifier?.type === 'raster_layer'; }); export const RasterLayerEntityList = memo(() => { - const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'raster_layer')); + const isSelected = useAppSelector(selectIsSelected); const layerIds = useAppSelector(selectEntityIds); if (layerIds.length === 0) { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl.tsx index f844f3aa389..7ca76922769 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl.tsx @@ -1,7 +1,7 @@ import { MenuItem } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { rasterLayerConvertedToControlLayer } from 'features/controlLayers/store/canvasV2Slice'; +import { rasterLayerConvertedToControlLayer } from 'features/controlLayers/store/canvasSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiLightningBold } from 'react-icons/pi'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons.tsx index 4e1a302233e..e6458e44c83 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons.tsx @@ -6,8 +6,8 @@ import { rgIPAdapterAdded, rgNegativePromptChanged, rgPositivePromptChanged, -} from 'features/controlLayers/store/canvasV2Slice'; -import { selectCanvasV2Slice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; +} from 'features/controlLayers/store/canvasSlice'; +import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPlusBold } from 'react-icons/pi'; @@ -18,8 +18,8 @@ export const RegionalGuidanceAddPromptsIPAdapterButtons = () => { const dispatch = useAppDispatch(); const selectValidActions = useMemo( () => - createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { - const entity = selectEntityOrThrow(canvasV2, entityIdentifier); + createMemoizedSelector(selectCanvasSlice, (canvas) => { + const entity = selectEntityOrThrow(canvas, entityIdentifier); return { canAddPositivePrompt: entity?.positivePrompt === null, canAddNegativePrompt: entity?.negativePrompt === null, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges.tsx index be928090f1d..8e77de7218e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges.tsx @@ -1,14 +1,19 @@ import { Badge } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { selectEntityOrThrow } from 'features/controlLayers/store/selectors'; -import { memo } from 'react'; +import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; +import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; export const RegionalGuidanceBadges = memo(() => { const entityIdentifier = useEntityIdentifierContext('regional_guidance'); const { t } = useTranslation(); - const autoNegative = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).autoNegative); + const selectAutoNegative = useMemo( + () => createSelector(selectCanvasSlice, (canvas) => selectEntityOrThrow(canvas, entityIdentifier).autoNegative), + [entityIdentifier] + ); + const autoNegative = useAppSelector(selectAutoNegative); return ( <> diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList.tsx index ef6faa51b19..a7271d10dbc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList.tsx @@ -1,17 +1,21 @@ +import { createSelector } from '@reduxjs/toolkit'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList'; import { RegionalGuidance } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidance'; import { mapId } from 'features/controlLayers/konva/util'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; +import { selectCanvasSlice, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; import { memo } from 'react'; -const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { - return canvasV2.regions.entities.map(mapId).reverse(); +const selectEntityIds = createMemoizedSelector(selectCanvasSlice, (canvas) => { + return canvas.regions.entities.map(mapId).reverse(); +}); +const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selectedEntityIdentifier) => { + return selectedEntityIdentifier?.type === 'raster_layer'; }); export const RegionalGuidanceEntityList = memo(() => { - const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'regional_guidance')); + const isSelected = useAppSelector(selectIsSelected); const rgIds = useAppSelector(selectEntityIds); if (rgIds.length === 0) { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx index cc60a8a023d..b11cc933bd9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx @@ -1,4 +1,5 @@ import { Box, Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct'; import { Weight } from 'features/controlLayers/components/common/Weight'; @@ -14,8 +15,8 @@ import { rgIPAdapterMethodChanged, rgIPAdapterModelChanged, rgIPAdapterWeightChanged, -} from 'features/controlLayers/store/canvasV2Slice'; -import { selectRegionalGuidanceIPAdapter } from 'features/controlLayers/store/selectors'; +} from 'features/controlLayers/store/canvasSlice'; +import { selectCanvasSlice, selectRegionalGuidanceIPAdapter } from 'features/controlLayers/store/selectors'; import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types'; import type { RGIPAdapterImageDropData } from 'features/dnd/types'; import { memo, useCallback, useMemo } from 'react'; @@ -34,11 +35,16 @@ export const RegionalGuidanceIPAdapterSettings = memo(({ ipAdapterId, ipAdapterN const onDeleteIPAdapter = useCallback(() => { dispatch(rgIPAdapterDeleted({ entityIdentifier, ipAdapterId })); }, [dispatch, entityIdentifier, ipAdapterId]); - const ipAdapter = useAppSelector((s) => { - const ipa = selectRegionalGuidanceIPAdapter(s.canvasV2, entityIdentifier, ipAdapterId); - assert(ipa, `Regional GuidanceIP Adapter with id ${ipAdapterId} not found`); - return ipa; - }); + const selectIPAdapter = useMemo( + () => + createSelector(selectCanvasSlice, (canvas) => { + const ipAdapter = selectRegionalGuidanceIPAdapter(canvas, entityIdentifier, ipAdapterId); + assert(ipAdapter, `Regional GuidanceIP Adapter with id ${ipAdapterId} not found`); + return ipAdapter; + }), + [entityIdentifier, ipAdapterId] + ); + const ipAdapter = useAppSelector(selectIPAdapter); const onChangeBeginEndStepPct = useCallback( (beginEndStepPct: [number, number]) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapters.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapters.tsx index ae47edb3e4f..b9c9b0bcc02 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapters.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapters.tsx @@ -4,7 +4,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { RegionalGuidanceIPAdapterSettings } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { selectCanvasV2Slice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; +import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import { Fragment, memo, useMemo } from 'react'; export const RegionalGuidanceIPAdapters = memo(() => { @@ -12,8 +12,8 @@ export const RegionalGuidanceIPAdapters = memo(() => { const selectIPAdapterIds = useMemo( () => - createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { - const ipAdapterIds = selectEntityOrThrow(canvasV2, entityIdentifier).ipAdapters.map(({ id }) => id); + createMemoizedSelector(selectCanvasSlice, (canvas) => { + const ipAdapterIds = selectEntityOrThrow(canvas, entityIdentifier).ipAdapters.map(({ id }) => id); if (ipAdapterIds.length === 0) { return EMPTY_ARRAY; } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx index 331401b7051..b950cf99956 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx @@ -1,20 +1,25 @@ import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import RgbColorPicker from 'common/components/RgbColorPicker'; import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { MaskFillStyle } from 'features/controlLayers/components/common/MaskFillStyle'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { rgFillColorChanged, rgFillStyleChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { selectEntityOrThrow } from 'features/controlLayers/store/selectors'; +import { rgFillColorChanged, rgFillStyleChanged } from 'features/controlLayers/store/canvasSlice'; +import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import type { FillStyle, RgbColor } from 'features/controlLayers/store/types'; -import { memo, useCallback } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; export const RegionalGuidanceMaskFillColorPicker = memo(() => { const entityIdentifier = useEntityIdentifierContext('regional_guidance'); const { t } = useTranslation(); const dispatch = useAppDispatch(); - const fill = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).fill); + const selectFill = useMemo( + () => createSelector(selectCanvasSlice, (canvas) => selectEntityOrThrow(canvas, entityIdentifier).fill), + [entityIdentifier] + ); + const fill = useAppSelector(selectFill); const onChangeFillColor = useCallback( (color: RgbColor) => { dispatch(rgFillColorChanged({ entityIdentifier, color })); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx index d47b6cc0f93..256f46086de 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx @@ -6,8 +6,8 @@ import { rgIPAdapterAdded, rgNegativePromptChanged, rgPositivePromptChanged, -} from 'features/controlLayers/store/canvasV2Slice'; -import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/selectors'; +} from 'features/controlLayers/store/canvasSlice'; +import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -17,8 +17,8 @@ export const RegionalGuidanceMenuItemsAddPromptsAndIPAdapter = memo(() => { const dispatch = useAppDispatch(); const selectValidActions = useMemo( () => - createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { - const entity = selectEntity(canvasV2, entityIdentifier); + createMemoizedSelector(selectCanvasSlice, (canvas) => { + const entity = selectEntity(canvas, entityIdentifier); return { canAddPositivePrompt: entity?.positivePrompt === null, canAddNegativePrompt: entity?.negativePrompt === null, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAutoNegative.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAutoNegative.tsx index 955cea7d66e..0cd3480fd93 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAutoNegative.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAutoNegative.tsx @@ -1,9 +1,10 @@ import { MenuItem } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { rgAutoNegativeToggled } from 'features/controlLayers/store/canvasV2Slice'; -import { selectEntityOrThrow } from 'features/controlLayers/store/selectors'; -import { memo, useCallback } from 'react'; +import { rgAutoNegativeToggled } from 'features/controlLayers/store/canvasSlice'; +import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; +import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiSelectionInverseBold } from 'react-icons/pi'; @@ -11,7 +12,11 @@ export const RegionalGuidanceMenuItemsAutoNegative = memo(() => { const entityIdentifier = useEntityIdentifierContext('regional_guidance'); const { t } = useTranslation(); const dispatch = useAppDispatch(); - const autoNegative = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).autoNegative); + const selectAutoNegative = useMemo( + () => createSelector(selectCanvasSlice, (canvas) => selectEntityOrThrow(canvas, entityIdentifier).autoNegative), + [entityIdentifier] + ); + const autoNegative = useAppSelector(selectAutoNegative); const onClick = useCallback(() => { dispatch(rgAutoNegativeToggled({ entityIdentifier })); }, [dispatch, entityIdentifier]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceNegativePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceNegativePrompt.tsx index b83539fbc8d..be2039abdad 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceNegativePrompt.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceNegativePrompt.tsx @@ -1,19 +1,25 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { RegionalGuidanceDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceDeletePromptButton'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { rgNegativePromptChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { selectEntityOrThrow } from 'features/controlLayers/store/selectors'; +import { rgNegativePromptChanged } from 'features/controlLayers/store/canvasSlice'; +import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; import { usePrompt } from 'features/prompt/usePrompt'; -import { memo, useCallback, useRef } from 'react'; +import { memo, useCallback, useMemo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; export const RegionalGuidanceNegativePrompt = memo(() => { const entityIdentifier = useEntityIdentifierContext('regional_guidance'); - const prompt = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).negativePrompt ?? ''); + const selectPrompt = useMemo( + () => + createSelector(selectCanvasSlice, (canvas) => selectEntityOrThrow(canvas, entityIdentifier).negativePrompt ?? ''), + [entityIdentifier] + ); + const prompt = useAppSelector(selectPrompt); const dispatch = useAppDispatch(); const textareaRef = useRef(null); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidancePositivePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidancePositivePrompt.tsx index e699dcf8b2c..bf72f6f7c44 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidancePositivePrompt.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidancePositivePrompt.tsx @@ -1,19 +1,25 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { RegionalGuidanceDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceDeletePromptButton'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { rgPositivePromptChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { selectEntityOrThrow } from 'features/controlLayers/store/selectors'; +import { rgPositivePromptChanged } from 'features/controlLayers/store/canvasSlice'; +import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; import { usePrompt } from 'features/prompt/usePrompt'; -import { memo, useCallback, useRef } from 'react'; +import { memo, useCallback, useMemo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; export const RegionalGuidancePositivePrompt = memo(() => { const entityIdentifier = useEntityIdentifierContext('regional_guidance'); - const prompt = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).positivePrompt ?? ''); + const selectPrompt = useMemo( + () => + createSelector(selectCanvasSlice, (canvas) => selectEntityOrThrow(canvas, entityIdentifier).positivePrompt ?? ''), + [entityIdentifier] + ); + const prompt = useAppSelector(selectPrompt); const dispatch = useAppDispatch(); const textareaRef = useRef(null); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx index b2c1e3db988..e2be6ecff68 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx @@ -1,10 +1,11 @@ import { Divider } from '@invoke-ai/ui-library'; +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper'; import { RegionalGuidanceAddPromptsIPAdapterButtons } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { selectEntityOrThrow } from 'features/controlLayers/store/selectors'; -import { memo } from 'react'; +import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; +import { memo, useMemo } from 'react'; import { RegionalGuidanceIPAdapters } from './RegionalGuidanceIPAdapters'; import { RegionalGuidanceNegativePrompt } from './RegionalGuidanceNegativePrompt'; @@ -12,30 +13,38 @@ import { RegionalGuidancePositivePrompt } from './RegionalGuidancePositivePrompt export const RegionalGuidanceSettings = memo(() => { const entityIdentifier = useEntityIdentifierContext('regional_guidance'); - const hasPositivePrompt = useAppSelector( - (s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).positivePrompt !== null + const selectFlags = useMemo( + () => + createMemoizedSelector(selectCanvasSlice, (canvas) => { + const entity = selectEntityOrThrow(canvas, entityIdentifier); + return { + hasPositivePrompt: entity.positivePrompt !== null, + hasNegativePrompt: entity.negativePrompt !== null, + hasIPAdapters: entity.ipAdapters.length > 0, + }; + }), + [entityIdentifier] ); - const hasNegativePrompt = useAppSelector( - (s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).negativePrompt !== null - ); - const hasIPAdapters = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).ipAdapters.length > 0); + const flags = useAppSelector(selectFlags); return ( - {!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && } - {hasPositivePrompt && ( + {!flags.hasPositivePrompt && !flags.hasNegativePrompt && !flags.hasIPAdapters && ( + + )} + {flags.hasPositivePrompt && ( <> - {(hasNegativePrompt || hasIPAdapters) && } + {(flags.hasNegativePrompt || flags.hasIPAdapters) && } )} - {hasNegativePrompt && ( + {flags.hasNegativePrompt && ( <> - {hasIPAdapters && } + {flags.hasIPAdapters && } )} - {hasIPAdapters && } + {flags.hasIPAdapters && } ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsResetButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsResetButton.tsx index a40a3f3aae8..47c5a380844 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsResetButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsResetButton.tsx @@ -1,6 +1,6 @@ import { Button } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; -import { canvasReset } from 'features/controlLayers/store/canvasV2Slice'; +import { canvasReset } from 'features/controlLayers/store/canvasSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushButton.tsx index 170416619d5..1a998fd3b88 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushButton.tsx @@ -3,7 +3,8 @@ import { useAppSelector } from 'app/store/storeHooks'; import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks'; import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering'; import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming'; -import { isDrawableEntityType } from 'features/controlLayers/store/types'; +import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice'; +import { selectIsSelectedEntityDrawable } from 'features/controlLayers/store/selectors'; import { memo, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; @@ -13,15 +14,10 @@ export const ToolBrushButton = memo(() => { const { t } = useTranslation(); const isFiltering = useIsFiltering(); const isTransforming = useIsTransforming(); - const isStaging = useAppSelector((s) => s.canvasSession.isStaging); + const isStaging = useAppSelector(selectIsStaging); const selectBrush = useSelectTool('brush'); const isSelected = useToolIsSelected('brush'); - const isDrawingToolAllowed = useAppSelector((s) => { - if (!s.canvasV2.selectedEntityIdentifier?.type) { - return false; - } - return isDrawableEntityType(s.canvasV2.selectedEntityIdentifier.type); - }); + const isDrawingToolAllowed = useAppSelector(selectIsSelectedEntityDrawable); const isDisabled = useMemo(() => { return isTransforming || isFiltering || isStaging || !isDrawingToolAllowed; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserButton.tsx index 2ac15df287a..be3004e7b50 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserButton.tsx @@ -3,7 +3,8 @@ import { useAppSelector } from 'app/store/storeHooks'; import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks'; import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering'; import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming'; -import { isDrawableEntityType } from 'features/controlLayers/store/types'; +import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice'; +import { selectIsSelectedEntityDrawable } from 'features/controlLayers/store/selectors'; import { memo, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; @@ -13,15 +14,10 @@ export const ToolEraserButton = memo(() => { const { t } = useTranslation(); const isFiltering = useIsFiltering(); const isTransforming = useIsTransforming(); - const isStaging = useAppSelector((s) => s.canvasSession.isStaging); + const isStaging = useAppSelector(selectIsStaging); const selectEraser = useSelectTool('eraser'); const isSelected = useToolIsSelected('eraser'); - const isDrawingToolAllowed = useAppSelector((s) => { - if (!s.canvasV2.selectedEntityIdentifier?.type) { - return false; - } - return isDrawableEntityType(s.canvasV2.selectedEntityIdentifier.type); - }); + const isDrawingToolAllowed = useAppSelector(selectIsSelectedEntityDrawable); const isDisabled = useMemo(() => { return isTransforming || isFiltering || isStaging || !isDrawingToolAllowed; }, [isDrawingToolAllowed, isFiltering, isStaging, isTransforming]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolMoveButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolMoveButton.tsx index f1422e93118..4c1340e15f6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolMoveButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolMoveButton.tsx @@ -3,7 +3,8 @@ import { useAppSelector } from 'app/store/storeHooks'; import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks'; import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering'; import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming'; -import { isDrawableEntityType } from 'features/controlLayers/store/types'; +import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice'; +import { selectIsSelectedEntityDrawable } from 'features/controlLayers/store/selectors'; import { memo, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; @@ -15,13 +16,8 @@ export const ToolMoveButton = memo(() => { const isTransforming = useIsTransforming(); const selectMove = useSelectTool('move'); const isSelected = useToolIsSelected('move'); - const isStaging = useAppSelector((s) => s.canvasSession.isStaging); - const isDrawingToolAllowed = useAppSelector((s) => { - if (!s.canvasV2.selectedEntityIdentifier?.type) { - return false; - } - return isDrawableEntityType(s.canvasV2.selectedEntityIdentifier.type); - }); + const isStaging = useAppSelector(selectIsStaging); + const isDrawingToolAllowed = useAppSelector(selectIsSelectedEntityDrawable); const isDisabled = useMemo(() => { return isTransforming || isFiltering || isStaging || !isDrawingToolAllowed; }, [isDrawingToolAllowed, isFiltering, isStaging, isTransforming]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolRectButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolRectButton.tsx index 8e0bfa083f3..801d603b40e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolRectButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolRectButton.tsx @@ -3,7 +3,8 @@ import { useAppSelector } from 'app/store/storeHooks'; import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks'; import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering'; import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming'; -import { isDrawableEntityType } from 'features/controlLayers/store/types'; +import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice'; +import { selectIsSelectedEntityDrawable } from 'features/controlLayers/store/selectors'; import { memo, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; @@ -15,13 +16,8 @@ export const ToolRectButton = memo(() => { const isSelected = useToolIsSelected('rect'); const isFiltering = useIsFiltering(); const isTransforming = useIsTransforming(); - const isStaging = useAppSelector((s) => s.canvasSession.isStaging); - const isDrawingToolAllowed = useAppSelector((s) => { - if (!s.canvasV2.selectedEntityIdentifier?.type) { - return false; - } - return isDrawableEntityType(s.canvasV2.selectedEntityIdentifier.type); - }); + const isStaging = useAppSelector(selectIsStaging); + const isDrawingToolAllowed = useAppSelector(selectIsSelectedEntityDrawable); const isDisabled = useMemo(() => { return isTransforming || isFiltering || isStaging || !isDrawingToolAllowed; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/UndoRedoButtonGroup.tsx b/invokeai/frontend/web/src/features/controlLayers/components/UndoRedoButtonGroup.tsx index 0d0b8651c10..df90d86496a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/UndoRedoButtonGroup.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/UndoRedoButtonGroup.tsx @@ -5,22 +5,25 @@ import { memo, useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiArrowClockwiseBold, PiArrowCounterClockwiseBold } from 'react-icons/pi'; +import { useDispatch } from 'react-redux'; +import { ActionCreators } from 'redux-undo'; export const UndoRedoButtonGroup = memo(() => { const { t } = useTranslation(); + const dispatch = useDispatch(); - const mayUndo = useAppSelector(() => false); + const mayUndo = useAppSelector(() => true); const handleUndo = useCallback(() => { // TODO(psyche): Implement undo - // dispatch(undo()); - }, []); + dispatch(ActionCreators.undo()); + }, [dispatch]); useHotkeys(['meta+z', 'ctrl+z'], handleUndo, { enabled: mayUndo, preventDefault: true }, [mayUndo, handleUndo]); - const mayRedo = useAppSelector(() => false); + const mayRedo = useAppSelector(() => true); const handleRedo = useCallback(() => { // TODO(psyche): Implement redo - // dispatch(redo()); - }, []); + dispatch(ActionCreators.redo()); + }, [dispatch]); useHotkeys(['meta+shift+z', 'ctrl+shift+z'], handleRedo, { enabled: mayRedo, preventDefault: true }, [ mayRedo, handleRedo, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx index f55283c1c0f..559eec70cbc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx @@ -3,7 +3,7 @@ import { useAppDispatch } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { useEntityIsSelected } from 'features/controlLayers/hooks/useEntityIsSelected'; import { useEntitySelectionColor } from 'features/controlLayers/hooks/useEntitySelectionColor'; -import { entitySelected } from 'features/controlLayers/store/canvasV2Slice'; +import { entitySelected } from 'features/controlLayers/store/canvasSlice'; import type { PropsWithChildren } from 'react'; import { memo, useCallback } from 'react'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx index bd743044186..77ccc94812a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx @@ -3,7 +3,7 @@ import { useAppDispatch } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { useEntityIsEnabled } from 'features/controlLayers/hooks/useEntityIsEnabled'; -import { entityIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice'; +import { entityIsEnabledToggled } from 'features/controlLayers/store/canvasSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiCheckBold } from 'react-icons/pi'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx index 1582cce92d1..8555b65e647 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx @@ -7,41 +7,41 @@ import { entityArrangedForwardOne, entityArrangedToBack, entityArrangedToFront, -} from 'features/controlLayers/store/canvasV2Slice'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; -import type { CanvasEntityIdentifier, CanvasV2State } from 'features/controlLayers/store/types'; +} from 'features/controlLayers/store/canvasSlice'; +import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; +import type { CanvasEntityIdentifier, CanvasState } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowDownBold, PiArrowLineDownBold, PiArrowLineUpBold, PiArrowUpBold } from 'react-icons/pi'; const getIndexAndCount = ( - canvasV2: CanvasV2State, + canvas: CanvasState, { id, type }: CanvasEntityIdentifier ): { index: number; count: number } => { if (type === 'raster_layer') { return { - index: canvasV2.rasterLayers.entities.findIndex((entity) => entity.id === id), - count: canvasV2.rasterLayers.entities.length, + index: canvas.rasterLayers.entities.findIndex((entity) => entity.id === id), + count: canvas.rasterLayers.entities.length, }; } else if (type === 'control_layer') { return { - index: canvasV2.controlLayers.entities.findIndex((entity) => entity.id === id), - count: canvasV2.controlLayers.entities.length, + index: canvas.controlLayers.entities.findIndex((entity) => entity.id === id), + count: canvas.controlLayers.entities.length, }; } else if (type === 'regional_guidance') { return { - index: canvasV2.regions.entities.findIndex((entity) => entity.id === id), - count: canvasV2.regions.entities.length, + index: canvas.regions.entities.findIndex((entity) => entity.id === id), + count: canvas.regions.entities.length, }; } else if (type === 'inpaint_mask') { return { - index: canvasV2.inpaintMasks.entities.findIndex((entity) => entity.id === id), - count: canvasV2.inpaintMasks.entities.length, + index: canvas.inpaintMasks.entities.findIndex((entity) => entity.id === id), + count: canvas.inpaintMasks.entities.length, }; } else if (type === 'ip_adapter') { return { - index: canvasV2.ipAdapters.entities.findIndex((entity) => entity.id === id), - count: canvasV2.ipAdapters.entities.length, + index: canvas.ipAdapters.entities.findIndex((entity) => entity.id === id), + count: canvas.ipAdapters.entities.length, }; } else { return { @@ -57,8 +57,8 @@ export const CanvasEntityMenuItemsArrange = memo(() => { const entityIdentifier = useEntityIdentifierContext(); const selectValidActions = useMemo( () => - createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { - const { index, count } = getIndexAndCount(canvasV2, entityIdentifier); + createMemoizedSelector(selectCanvasSlice, (canvas) => { + const { index, count } = getIndexAndCount(canvas, entityIdentifier); return { canMoveForwardOne: index < count - 1, canMoveBackwardOne: index > 0, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDelete.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDelete.tsx index 24d9a326829..29b6ec5c2f9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDelete.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDelete.tsx @@ -1,7 +1,7 @@ import { MenuItem } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { entityDeleted } from 'features/controlLayers/store/canvasV2Slice'; +import { entityDeleted } from 'features/controlLayers/store/canvasSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiTrashSimpleBold } from 'react-icons/pi'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate.tsx index dd84a84f428..36a4f18fd55 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate.tsx @@ -1,7 +1,7 @@ import { MenuItem } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { entityDuplicated } from 'features/controlLayers/store/canvasV2Slice'; +import { entityDuplicated } from 'features/controlLayers/store/canvasSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiCopyFill } from 'react-icons/pi'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityOpacity.tsx index dc5c5895102..8bac01f75b5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityOpacity.tsx @@ -15,8 +15,12 @@ import { } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { snapToNearest } from 'features/controlLayers/konva/util'; -import { entityOpacityChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { selectEntity } from 'features/controlLayers/store/selectors'; +import { entityOpacityChanged } from 'features/controlLayers/store/canvasSlice'; +import { + selectCanvasSlice, + selectEntity, + selectSelectedEntityIdentifier, +} from 'features/controlLayers/store/selectors'; import { isDrawableEntity } from 'features/controlLayers/store/types'; import { clamp, round } from 'lodash-es'; import type { KeyboardEvent } from 'react'; @@ -59,13 +63,14 @@ const snapCandidates = marks.slice(1, marks.length - 1); export const CanvasEntityOpacity = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const selectedEntityIdentifier = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier); + const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); const opacity = useAppSelector((s) => { - const selectedEntityIdentifier = s.canvasV2.selectedEntityIdentifier; + const selectedEntityIdentifier = selectSelectedEntityIdentifier(s); if (!selectedEntityIdentifier) { return null; } - const selectedEntity = selectEntity(s.canvasV2, selectedEntityIdentifier); + const canvas = selectCanvasSlice(s); + const selectedEntity = selectEntity(canvas, selectedEntityIdentifier); if (!selectedEntity) { return null; } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityPreviewImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityPreviewImage.tsx index 2a7630c1a8a..31dccfd2b62 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityPreviewImage.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityPreviewImage.tsx @@ -5,7 +5,7 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { useEntityAdapter } from 'features/controlLayers/contexts/EntityAdapterContext'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants'; -import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/selectors'; +import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors'; import { memo, useEffect, useMemo, useRef } from 'react'; import { useSelector } from 'react-redux'; @@ -18,7 +18,7 @@ export const CanvasEntityPreviewImage = memo(() => { const adapter = useEntityAdapter(); const selectMaskColor = useMemo( () => - createSelector(selectCanvasV2Slice, (state) => { + createSelector(selectCanvasSlice, (state) => { const entity = selectEntity(state, entityIdentifier); if (!entity) { return null; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitleEdit.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitleEdit.tsx index 91ae91e2b83..8883b6615fa 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitleEdit.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitleEdit.tsx @@ -4,7 +4,7 @@ import { useBoolean } from 'common/hooks/useBoolean'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { useEntityTitle } from 'features/controlLayers/hooks/useEntityTitle'; -import { entityNameChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { entityNameChanged } from 'features/controlLayers/store/canvasSlice'; import type { ChangeEvent, KeyboardEvent } from 'react'; import { memo, useCallback, useEffect, useRef, useState } from 'react'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTypeIsHiddenToggle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTypeIsHiddenToggle.tsx index 1e13042d3ce..25ab3b9a426 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTypeIsHiddenToggle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTypeIsHiddenToggle.tsx @@ -2,7 +2,7 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { useEntityTypeIsHidden } from 'features/controlLayers/hooks/useEntityTypeIsHidden'; import { useEntityTypeString } from 'features/controlLayers/hooks/useEntityTypeString'; -import { allEntitiesOfTypeIsHiddenToggled } from 'features/controlLayers/store/canvasV2Slice'; +import { allEntitiesOfTypeIsHiddenToggled } from 'features/controlLayers/store/canvasSlice'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts index 9f2261c8878..3b75e2696e7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts @@ -1,14 +1,14 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; -import { entityDeleted } from 'features/controlLayers/store/canvasV2Slice'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; +import { entityDeleted } from 'features/controlLayers/store/canvasSlice'; +import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; import { useCallback, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; const selectSelectedEntityIdentifier = createMemoizedSelector( - selectCanvasV2Slice, - (canvasV2State) => canvasV2State.selectedEntityIdentifier + selectCanvasSlice, + (canvasState) => canvasState.selectedEntityIdentifier ); export function useCanvasDeleteLayerHotkey() { diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts index c752a7348cf..5418e0c2ec7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts @@ -1,14 +1,14 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; -import { entityReset } from 'features/controlLayers/store/canvasV2Slice'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; +import { entityReset } from 'features/controlLayers/store/canvasSlice'; +import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; import { useCallback, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; const selectSelectedEntityIdentifier = createMemoizedSelector( - selectCanvasV2Slice, - (canvasV2State) => canvasV2State.selectedEntityIdentifier + selectCanvasSlice, + (canvasState) => canvasState.selectedEntityIdentifier ); export function useCanvasResetLayerHotkey() { diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsEnabled.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsEnabled.ts index 022cbeb2121..938364d91d7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsEnabled.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsEnabled.ts @@ -1,14 +1,14 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/selectors'; +import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; export const useEntityIsEnabled = (entityIdentifier: CanvasEntityIdentifier) => { const selectIsEnabled = useMemo( () => - createSelector(selectCanvasV2Slice, (canvasV2) => { - const entity = selectEntity(canvasV2, entityIdentifier); + createSelector(selectCanvasSlice, (canvas) => { + const entity = selectEntity(canvas, entityIdentifier); if (!entity) { return false; } else { diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsSelected.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsSelected.ts index 80fe4a61b47..b07bce98a50 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsSelected.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsSelected.ts @@ -1,12 +1,18 @@ +import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; +import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; export const useEntityIsSelected = (entityIdentifier: CanvasEntityIdentifier) => { - const selectedEntityIdentifier = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier); - const isSelected = useMemo(() => { - return selectedEntityIdentifier?.id === entityIdentifier.id; - }, [selectedEntityIdentifier, entityIdentifier.id]); + const selectIsSelected = useMemo( + () => + createSelector(selectSelectedEntityIdentifier, (selectedEntityIdentifier) => { + return selectedEntityIdentifier?.id === entityIdentifier.id; + }), + [entityIdentifier.id] + ); + const isSelected = useAppSelector(selectIsSelected); return isSelected; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityObjectCount.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityObjectCount.ts index 62f3a8e8d00..fe9fbedbd16 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityObjectCount.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityObjectCount.ts @@ -1,14 +1,14 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/selectors'; +import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors'; import { type CanvasEntityIdentifier, isDrawableEntity } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; export const useEntityObjectCount = (entityIdentifier: CanvasEntityIdentifier) => { const selectObjectCount = useMemo( () => - createSelector(selectCanvasV2Slice, (canvasV2) => { - const entity = selectEntity(canvasV2, entityIdentifier); + createSelector(selectCanvasSlice, (canvas) => { + const entity = selectEntity(canvas, entityIdentifier); if (!entity) { return 0; } else if (isDrawableEntity(entity)) { diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntitySelectionColor.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntitySelectionColor.ts index 15061d28b86..9e758d00469 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntitySelectionColor.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntitySelectionColor.ts @@ -1,15 +1,15 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { rgbColorToString } from 'common/util/colorCodeTransformers'; -import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/selectors'; +import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; export const useEntitySelectionColor = (entityIdentifier: CanvasEntityIdentifier) => { const selectSelectionColor = useMemo( () => - createSelector(selectCanvasV2Slice, (canvasV2) => { - const entity = selectEntity(canvasV2, entityIdentifier); + createSelector(selectCanvasSlice, (canvas) => { + const entity = selectEntity(canvas, entityIdentifier); if (!entity) { return 'base.400'; } else if (entity.type === 'inpaint_mask') { diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts index c5649963d87..d054083e162 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts @@ -1,15 +1,15 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { useEntityObjectCount } from 'features/controlLayers/hooks/useEntityObjectCount'; -import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/selectors'; +import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { assert } from 'tsafe'; const createSelectName = (entityIdentifier: CanvasEntityIdentifier) => - createSelector(selectCanvasV2Slice, (canvasV2) => { - const entity = selectEntity(canvasV2, entityIdentifier); + createSelector(selectCanvasSlice, (canvas) => { + const entity = selectEntity(canvas, entityIdentifier); if (!entity) { return null; } diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeCount.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeCount.ts index e770f220fbc..e8bb5167b80 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeCount.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeCount.ts @@ -1,24 +1,24 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; +import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; export const useEntityTypeCount = (type: CanvasEntityIdentifier['type']): number => { const selectEntityCount = useMemo( () => - createSelector(selectCanvasV2Slice, (canvasV2) => { + createSelector(selectCanvasSlice, (canvas) => { switch (type) { case 'control_layer': - return canvasV2.controlLayers.entities.length; + return canvas.controlLayers.entities.length; case 'raster_layer': - return canvasV2.rasterLayers.entities.length; + return canvas.rasterLayers.entities.length; case 'inpaint_mask': - return canvasV2.inpaintMasks.entities.length; + return canvas.inpaintMasks.entities.length; case 'regional_guidance': - return canvasV2.regions.entities.length; + return canvas.regions.entities.length; case 'ip_adapter': - return canvasV2.ipAdapters.entities.length; + return canvas.ipAdapters.entities.length; default: return 0; } diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeIsHidden.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeIsHidden.ts index 0e867be2be4..04ed438b628 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeIsHidden.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeIsHidden.ts @@ -1,22 +1,22 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; +import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; export const useEntityTypeIsHidden = (type: CanvasEntityIdentifier['type']): boolean => { const selectIsHidden = useMemo( () => - createSelector(selectCanvasV2Slice, (canvasV2) => { + createSelector(selectCanvasSlice, (canvas) => { switch (type) { case 'control_layer': - return canvasV2.controlLayers.isHidden; + return canvas.controlLayers.isHidden; case 'raster_layer': - return canvasV2.rasterLayers.isHidden; + return canvas.rasterLayers.isHidden; case 'inpaint_mask': - return canvasV2.inpaintMasks.isHidden; + return canvas.inpaintMasks.isHidden; case 'regional_guidance': - return canvasV2.regions.isHidden; + return canvas.regions.isHidden; case 'ip_adapter': default: return false; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts index 3c2223ecf8b..8f59935d470 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts @@ -1,7 +1,7 @@ import { createMemoizedAppSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { deepClone } from 'common/util/deepClone'; -import { selectCanvasV2Slice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; +import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier, ControlNetConfig, @@ -16,8 +16,8 @@ import { useControlNetAndT2IAdapterModels, useIPAdapterModels } from 'services/a export const useControlLayerControlAdapter = (entityIdentifier: CanvasEntityIdentifier<'control_layer'>) => { const selectControlAdapter = useMemo( () => - createMemoizedAppSelector(selectCanvasV2Slice, (canvasV2) => { - const layer = selectEntityOrThrow(canvasV2, entityIdentifier); + createMemoizedAppSelector(selectCanvasSlice, (canvas) => { + const layer = selectEntityOrThrow(canvas, entityIdentifier); return layer.controlAdapter; }), [entityIdentifier] diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts index 6be2f8d2489..ce994085b74 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts @@ -6,7 +6,7 @@ import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase' import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { CanvasSessionState } from 'features/controlLayers/store/canvasSessionSlice'; import type { CanvasSettingsState } from 'features/controlLayers/store/canvasSettingsSlice'; -import type { CanvasV2State } from 'features/controlLayers/store/types'; +import type { CanvasState } from 'features/controlLayers/store/types'; import type { Logger } from 'roarr'; export class CanvasRenderingModule extends CanvasModuleBase { @@ -18,7 +18,7 @@ export class CanvasRenderingModule extends CanvasModuleBase { manager: CanvasManager; subscriptions = new Set<() => void>(); - state: CanvasV2State | null = null; + state: CanvasState | null = null; settings: CanvasSettingsState | null = null; session: CanvasSessionState | null = null; @@ -119,7 +119,7 @@ export class CanvasRenderingModule extends CanvasModuleBase { } }; - renderRasterLayers = async (state: CanvasV2State, prevState: CanvasV2State | null) => { + renderRasterLayers = async (state: CanvasState, prevState: CanvasState | null) => { const adapterMap = this.manager.adapters.rasterLayers; if (!prevState || state.rasterLayers.isHidden !== prevState.rasterLayers.isHidden) { @@ -148,7 +148,7 @@ export class CanvasRenderingModule extends CanvasModuleBase { } }; - renderControlLayers = async (prevState: CanvasV2State | null, state: CanvasV2State) => { + renderControlLayers = async (prevState: CanvasState | null, state: CanvasState) => { const adapterMap = this.manager.adapters.controlLayers; if (!prevState || state.controlLayers.isHidden !== prevState.controlLayers.isHidden) { @@ -177,7 +177,7 @@ export class CanvasRenderingModule extends CanvasModuleBase { } }; - renderRegionalGuidance = async (prevState: CanvasV2State | null, state: CanvasV2State) => { + renderRegionalGuidance = async (prevState: CanvasState | null, state: CanvasState) => { const adapterMap = this.manager.adapters.regionMasks; if (!prevState || state.regions.isHidden !== prevState.regions.isHidden) { @@ -211,7 +211,7 @@ export class CanvasRenderingModule extends CanvasModuleBase { } }; - renderInpaintMasks = async (state: CanvasV2State, prevState: CanvasV2State | null) => { + renderInpaintMasks = async (state: CanvasState, prevState: CanvasState | null) => { const adapterMap = this.manager.adapters.inpaintMasks; if (!prevState || state.inpaintMasks.isHidden !== prevState.inpaintMasks.isHidden) { @@ -245,7 +245,7 @@ export class CanvasRenderingModule extends CanvasModuleBase { } }; - renderBbox = (state: CanvasV2State, prevState: CanvasV2State | null) => { + renderBbox = (state: CanvasState, prevState: CanvasState | null) => { if (!prevState || state.bbox !== prevState.bbox) { this.manager.preview.bbox.render(); } @@ -257,7 +257,7 @@ export class CanvasRenderingModule extends CanvasModuleBase { } }; - arrangeEntities = (state: CanvasV2State, prevState: CanvasV2State | null) => { + arrangeEntities = (state: CanvasState, prevState: CanvasState | null) => { if ( !prevState || state.rasterLayers.entities !== prevState.rasterLayers.entities || diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index eff144968c9..aa3be585f2d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -14,8 +14,8 @@ import { entityRectAdded, entityReset, entitySelected, -} from 'features/controlLayers/store/canvasV2Slice'; -import { selectAllRenderableEntities } from 'features/controlLayers/store/selectors'; +} from 'features/controlLayers/store/canvasSlice'; +import { selectAllRenderableEntities, selectCanvasSlice } from 'features/controlLayers/store/selectors'; import { brushWidthChanged, eraserWidthChanged, @@ -100,7 +100,7 @@ export class CanvasStateApiModule extends CanvasModuleBase { // Reminder - use arrow functions to avoid binding issues getCanvasState = () => { - return this.store.getState().canvasV2; + return selectCanvasSlice(this.store.getState()); }; resetEntity = (arg: EntityIdentifierPayload) => { this.store.dispatch(entityReset(arg)); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts index 038e448fd5d..af328d944d8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/bboxReducers.ts @@ -1,14 +1,14 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { deepClone } from 'common/util/deepClone'; import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple'; -import type { BoundingBoxScaleMethod, CanvasV2State, Dimensions } from 'features/controlLayers/store/types'; +import type { BoundingBoxScaleMethod, CanvasState, Dimensions } from 'features/controlLayers/store/types'; import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; import { ASPECT_RATIO_MAP, initialAspectRatioState } from 'features/parameters/components/DocumentSize/constants'; import type { AspectRatioID } from 'features/parameters/components/DocumentSize/types'; import type { IRect } from 'konva/lib/types'; -const syncScaledSize = (state: CanvasV2State) => { +const syncScaledSize = (state: CanvasState) => { if (state.bbox.scaleMethod === 'auto') { const { width, height } = state.bbox.rect; state.bbox.scaledSize = getScaledBoundingBoxDimensions({ width, height }, state.bbox.optimalDimension); @@ -116,4 +116,4 @@ export const bboxReducers = { syncScaledSize(state); }, -} satisfies SliceCaseReducers; +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSessionSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSessionSlice.ts index f8d70511d02..6947ffd2df5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSessionSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSessionSlice.ts @@ -1,6 +1,6 @@ -import { createAction, createSlice, type PayloadAction } from '@reduxjs/toolkit'; -import type { PersistConfig } from 'app/store/store'; -import { canvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; +import { createAction, createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit'; +import type { PersistConfig, RootState } from 'app/store/store'; +import { canvasSlice } from 'features/controlLayers/store/canvasSlice'; import type { SessionMode, StagingAreaImage } from 'features/controlLayers/store/types'; export type CanvasSessionState = { @@ -79,5 +79,9 @@ export const canvasSessionPersistConfig: PersistConfig = { persistDenylist: [], }; export const sessionStagingAreaImageAccepted = createAction<{ index: number }>( - `${canvasV2Slice.name}/sessionStagingAreaImageAccepted` + `${canvasSlice.name}/sessionStagingAreaImageAccepted` ); + +export const selectCanvasSessionSlice = (s: RootState) => s.canvasSession; + +export const selectIsStaging = createSelector(selectCanvasSessionSlice, (canvasSession) => canvasSession.isStaging); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts index 18e8340d33c..627f1f28d32 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts @@ -1,5 +1,5 @@ import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; -import type { PersistConfig } from 'app/store/store'; +import type { PersistConfig, RootState } from 'app/store/store'; export type CanvasSettingsState = { imageSmoothing: boolean; @@ -51,3 +51,4 @@ export const canvasSettingsPersistConfig: PersistConfig = { migrate, persistDenylist: [], }; +export const selectCanvasSettingsSlice = (s: RootState) => s.canvasSettings; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts similarity index 97% rename from invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts rename to invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index 92ac8162d23..189f6e32c3c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -22,7 +22,7 @@ import { assert } from 'tsafe'; import type { CanvasEntityIdentifier, - CanvasV2State, + CanvasState, EntityBrushLineAddedPayload, EntityEraserLineAddedPayload, EntityIdentifierPayload, @@ -32,7 +32,7 @@ import type { } from './types'; import { getEntityIdentifier, isDrawableEntity } from './types'; -const initialState: CanvasV2State = { +const initialState: CanvasState = { _version: 3, selectedEntityIdentifier: null, rasterLayers: { @@ -64,8 +64,8 @@ const initialState: CanvasV2State = { }, }; -export const canvasV2Slice = createSlice({ - name: 'canvasV2', +export const canvasSlice = createSlice({ + name: 'canvas', initialState, reducers: { // undoable canvas state @@ -217,7 +217,7 @@ export const canvasV2Slice = createSlice({ entityDeleted: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; - let selectedEntityIdentifier: CanvasV2State['selectedEntityIdentifier'] = null; + let selectedEntityIdentifier: CanvasState['selectedEntityIdentifier'] = null; const allEntities = selectAllEntities(state); const index = allEntities.findIndex((entity) => entity.id === entityIdentifier.id); const nextIndex = allEntities.length > 1 ? (index + 1) % allEntities.length : -1; @@ -438,15 +438,15 @@ export const { // inpaintMaskRecalled, inpaintMaskFillColorChanged, inpaintMaskFillStyleChanged, -} = canvasV2Slice.actions; +} = canvasSlice.actions; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const migrate = (state: any): any => { return state; }; -export const canvasV2PersistConfig: PersistConfig = { - name: canvasV2Slice.name, +export const canvasPersistConfig: PersistConfig = { + name: canvasSlice.name, initialState, migrate, persistDenylist: [], diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts index 7ed4a341cee..3563f7e5466 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts @@ -9,7 +9,7 @@ import type { ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/ import type { CanvasControlLayerState, CanvasRasterLayerState, - CanvasV2State, + CanvasState, ControlModeV2, ControlNetConfig, EntityIdentifierPayload, @@ -159,4 +159,4 @@ export const controlLayersReducers = { } layer.withTransparencyEffect = !layer.withTransparencyEffect; }, -} satisfies SliceCaseReducers; +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts index 9056f082a60..6004ccfc368 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts @@ -3,7 +3,7 @@ import { getPrefixedId } from 'features/controlLayers/konva/util'; import { selectEntity } from 'features/controlLayers/store/selectors'; import type { CanvasInpaintMaskState, - CanvasV2State, + CanvasState, EntityIdentifierPayload, FillStyle, RgbColor, @@ -68,4 +68,4 @@ export const inpaintMaskReducers = { } entity.fill.style = style; }, -} satisfies SliceCaseReducers; +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts index d3bb67c9ce3..fcadb3480e5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts @@ -8,7 +8,7 @@ import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; import type { CanvasIPAdapterState, - CanvasV2State, + CanvasState, CLIPVisionModelV2, EntityIdentifierPayload, IPMethodV2, @@ -104,4 +104,4 @@ export const ipAdaptersReducers = { } entity.ipAdapter.beginEndStepPct = beginEndStepPct; }, -} satisfies SliceCaseReducers; +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts index 0db696b84e0..25ea31f9cca 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts @@ -4,7 +4,7 @@ import { getPrefixedId } from 'features/controlLayers/konva/util'; import { selectEntity } from 'features/controlLayers/store/selectors'; import { merge } from 'lodash-es'; -import type { CanvasControlLayerState, CanvasRasterLayerState, CanvasV2State, EntityIdentifierPayload } from './types'; +import type { CanvasControlLayerState, CanvasRasterLayerState, CanvasState, EntityIdentifierPayload } from './types'; import { getEntityIdentifier, initialControlNet } from './types'; export const rasterLayersReducers = { @@ -67,4 +67,4 @@ export const rasterLayersReducers = { payload: { ...payload, newId: getPrefixedId('control_layer') }, }), }, -} satisfies SliceCaseReducers; +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index ff2729fcc55..87687fb3959 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -3,7 +3,7 @@ import { deepClone } from 'common/util/deepClone'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { selectEntity, selectRegionalGuidanceIPAdapter } from 'features/controlLayers/store/selectors'; import type { - CanvasV2State, + CanvasState, CLIPVisionModelV2, EntityIdentifierPayload, FillStyle, @@ -29,7 +29,7 @@ const DEFAULT_MASK_COLORS: RgbColor[] = [ { r: 161, g: 120, b: 214 }, // rgb(161, 120, 214) ]; -const getRGMaskFill = (state: CanvasV2State): RgbColor => { +const getRGMaskFill = (state: CanvasState): RgbColor => { const lastFill = state.regions.entities.slice(-1)[0]?.fill.color; let i = DEFAULT_MASK_COLORS.findIndex((c) => isEqual(c, lastFill)); if (i === -1) { @@ -249,4 +249,4 @@ export const regionsReducers = { } ipAdapter.clipVisionModel = clipVisionModel; }, -} satisfies SliceCaseReducers; +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts index ffbd30867d0..abd7df40e87 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts @@ -1,22 +1,23 @@ import { createSelector } from '@reduxjs/toolkit'; import type { RootState } from 'app/store/store'; import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; -import type { - CanvasControlLayerState, - CanvasEntityIdentifier, - CanvasEntityState, - CanvasInpaintMaskState, - CanvasRasterLayerState, - CanvasRegionalGuidanceState, - CanvasV2State, +import { + type CanvasControlLayerState, + type CanvasEntityIdentifier, + type CanvasEntityState, + type CanvasInpaintMaskState, + type CanvasRasterLayerState, + type CanvasRegionalGuidanceState, + type CanvasState, + isDrawableEntityType, } from 'features/controlLayers/store/types'; import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; import { assert } from 'tsafe'; /** - * Selects the canvasV2 slice from the root state + * Selects the canvas slice from the root state */ -export const selectCanvasV2Slice = (state: RootState) => state.canvasV2; +export const selectCanvasSlice = (state: RootState) => state.canvas.present; /** * Selects the total canvas entity count: @@ -28,13 +29,13 @@ export const selectCanvasV2Slice = (state: RootState) => state.canvasV2; * * It does not check for validity of the entities. */ -export const selectEntityCount = createSelector(selectCanvasV2Slice, (canvasV2) => { +export const selectEntityCount = createSelector(selectCanvasSlice, (canvas) => { return ( - canvasV2.regions.entities.length + - canvasV2.ipAdapters.entities.length + - canvasV2.rasterLayers.entities.length + - canvasV2.controlLayers.entities.length + - canvasV2.inpaintMasks.entities.length + canvas.regions.entities.length + + canvas.ipAdapters.entities.length + + canvas.rasterLayers.entities.length + + canvas.controlLayers.entities.length + + canvas.inpaintMasks.entities.length ); }); @@ -46,11 +47,11 @@ export const selectOptimalDimension = createSelector(selectParamsSlice, (params) }); /** - * Selects a single entity from the canvasV2 slice. If the entity identifier is narrowed to a specific type, the + * Selects a single entity from the canvas slice. If the entity identifier is narrowed to a specific type, the * return type will be narrowed as well. */ export function selectEntity( - state: CanvasV2State, + state: CanvasState, entityIdentifier: T ): Extract | undefined { const { id, type } = entityIdentifier; @@ -80,11 +81,11 @@ export function selectEntity( } /** - * Selected an entity from the canvasV2 slice. If the entity is not found, an error is thrown. + * Selected an entity from the canvas slice. If the entity is not found, an error is thrown. * Wrapper around {@link selectEntity}. */ export function selectEntityOrThrow( - state: CanvasV2State, + state: CanvasState, entityIdentifier: T ): Extract { const entity = selectEntity(state, entityIdentifier); @@ -96,7 +97,7 @@ export function selectEntityOrThrow( * Selects all entities of the given type. */ export function selectAllEntitiesOfType( - state: CanvasV2State, + state: CanvasState, type: T ): Extract[] { let entities: CanvasEntityState[] = []; @@ -126,7 +127,7 @@ export function selectAllEntitiesOfType( /** * Selects all entities, in the order they are displayed in the list. */ -export function selectAllEntities(state: CanvasV2State): CanvasEntityState[] { +export function selectAllEntities(state: CanvasState): CanvasEntityState[] { // These are in the same order as they are displayed in the list! return [ ...state.inpaintMasks.entities.toReversed(), @@ -145,7 +146,7 @@ export function selectAllEntities(state: CanvasV2State): CanvasEntityState[] { * - Regional guidance */ export function selectAllRenderableEntities( - state: CanvasV2State + state: CanvasState ): (CanvasRasterLayerState | CanvasControlLayerState | CanvasInpaintMaskState | CanvasRegionalGuidanceState)[] { return [ ...state.rasterLayers.entities, @@ -159,7 +160,7 @@ export function selectAllRenderableEntities( * Selects the IP adapter for the specific Regional Guidance layer. */ export function selectRegionalGuidanceIPAdapter( - state: CanvasV2State, + state: CanvasState, entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>, ipAdapterId: string ) { @@ -169,3 +170,19 @@ export function selectRegionalGuidanceIPAdapter( } return entity.ipAdapters.find((ipAdapter) => ipAdapter.id === ipAdapterId); } + +export const selectSelectedEntityIdentifier = createSelector( + selectCanvasSlice, + (canvas) => canvas.selectedEntityIdentifier +); + +export const selectIsSelectedEntityDrawable = createSelector( + selectSelectedEntityIdentifier, + (selectedEntityIdentifier) => { + if (!selectedEntityIdentifier) { + return false; + } + return isDrawableEntityType(selectedEntityIdentifier.type); + } +); + diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index cb504d7768b..35437810075 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -692,7 +692,7 @@ export type StagingAreaImage = { export type SessionMode = 'generate' | 'compose'; -export type CanvasV2State = { +export type CanvasState = { _version: 3; selectedEntityIdentifier: CanvasEntityIdentifier | null; inpaintMasks: { diff --git a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx index c42d92736c7..afe2acff90c 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx +++ b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx @@ -1,7 +1,7 @@ import { ConfirmationAlertDialog, Divider, Flex, FormControl, FormLabel, Switch, Text } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; +import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions'; import { getImageUsage, selectImageUsage } from 'features/deleteImageModal/store/selectors'; import { @@ -20,11 +20,11 @@ import { useTranslation } from 'react-i18next'; import ImageUsageMessage from './ImageUsageMessage'; const selectImageUsages = createMemoizedSelector( - [selectDeleteImageModalSlice, selectNodesSlice, selectCanvasV2Slice, selectImageUsage], - (deleteImageModal, nodes, canvasV2, imagesUsage) => { + [selectDeleteImageModalSlice, selectNodesSlice, selectCanvasSlice, selectImageUsage], + (deleteImageModal, nodes, canvas, imagesUsage) => { const { imagesToDelete } = deleteImageModal; - const allImageUsage = (imagesToDelete ?? []).map(({ image_name }) => getImageUsage(nodes, canvasV2, image_name)); + const allImageUsage = (imagesToDelete ?? []).map(({ image_name }) => getImageUsage(nodes, canvas, image_name)); const imageUsageSummary: ImageUsage = { isLayerImage: some(allImageUsage, (i) => i.isLayerImage), diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts index 036c25b9c20..7aa602b522f 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts @@ -1,6 +1,6 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; -import type { CanvasV2State } from 'features/controlLayers/store/types'; +import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; +import type { CanvasState } from 'features/controlLayers/store/types'; import { selectDeleteImageModalSlice } from 'features/deleteImageModal/store/slice'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; import type { NodesState } from 'features/nodes/store/types'; @@ -10,14 +10,14 @@ import { some } from 'lodash-es'; import type { ImageUsage } from './types'; // TODO(psyche): handle image deletion (canvas sessions?) -export const getImageUsage = (nodes: NodesState, canvasV2: CanvasV2State, image_name: string) => { +export const getImageUsage = (nodes: NodesState, canvas: CanvasState, image_name: string) => { const isNodesImage = nodes.nodes .filter(isInvocationNode) .some((node) => some(node.data.inputs, (input) => isImageFieldInputInstance(input) && input.value?.image_name === image_name) ); - const isIPAdapterImage = canvasV2.ipAdapters.entities.some( + const isIPAdapterImage = canvas.ipAdapters.entities.some( ({ ipAdapter }) => ipAdapter.image?.image_name === image_name ); @@ -34,15 +34,15 @@ export const getImageUsage = (nodes: NodesState, canvasV2: CanvasV2State, image_ export const selectImageUsage = createMemoizedSelector( selectDeleteImageModalSlice, selectNodesSlice, - selectCanvasV2Slice, - (deleteImageModal, nodes, canvasV2) => { + selectCanvasSlice, + (deleteImageModal, nodes, canvas) => { const { imagesToDelete } = deleteImageModal; if (!imagesToDelete.length) { return []; } - const imagesUsage = imagesToDelete.map((i) => getImageUsage(nodes, canvasV2, i.image_name)); + const imagesUsage = imagesToDelete.map((i) => getImageUsage(nodes, canvas, i.image_name)); return imagesUsage; } diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx index 42ecf584d3a..458f2fd78cb 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx @@ -13,7 +13,7 @@ import { import { skipToken } from '@reduxjs/toolkit/query'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; +import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; import ImageUsageMessage from 'features/deleteImageModal/components/ImageUsageMessage'; import { getImageUsage } from 'features/deleteImageModal/store/selectors'; import type { ImageUsage } from 'features/deleteImageModal/store/types'; @@ -39,8 +39,8 @@ const DeleteBoardModal = (props: Props) => { const selectImageUsageSummary = useMemo( () => - createMemoizedSelector([selectNodesSlice, selectCanvasV2Slice], (nodes, canvasV2) => { - const allImageUsage = (boardImageNames ?? []).map((imageName) => getImageUsage(nodes, canvasV2, imageName)); + createMemoizedSelector([selectNodesSlice, selectCanvasSlice], (nodes, canvas) => { + const allImageUsage = (boardImageNames ?? []).map((imageName) => getImageUsage(nodes, canvas, imageName)); const imageUsageSummary: ImageUsage = { isLayerImage: some(allImageUsage, (i) => i.isLayerImage), diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index 71278e0d9c4..a88bc54ee8c 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -1,5 +1,5 @@ import { getStore } from 'app/store/nanostores/store'; -import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasSlice'; import { loraAllDeleted, loraRecalled } from 'features/controlLayers/store/lorasSlice'; import { negativePrompt2Changed, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts index 5c72b548b07..a4d9804dccf 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts @@ -1,6 +1,6 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import type { CanvasV2State, Dimensions } from 'features/controlLayers/store/types'; +import type { CanvasState, Dimensions } from 'features/controlLayers/store/types'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { isEqual } from 'lodash-es'; import type { Invocation } from 'services/api/types'; @@ -13,7 +13,7 @@ export const addImageToImage = async ( vaeSource: Invocation<'main_model_loader' | 'sdxl_model_loader' | 'seamless' | 'vae_loader'>, originalSize: Dimensions, scaledSize: Dimensions, - bbox: CanvasV2State['bbox'], + bbox: CanvasState['bbox'], denoising_start: number, fp32: boolean ): Promise> => { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts index 3f8c4fbb02b..f9fa00c5d6c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts @@ -1,6 +1,9 @@ import type { RootState } from 'app/store/store'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; +import { selectCanvasSessionSlice } from 'features/controlLayers/store/canvasSessionSlice'; +import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; +import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; import type { Dimensions } from 'features/controlLayers/store/types'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { isEqual } from 'lodash-es'; @@ -21,8 +24,11 @@ export const addInpaint = async ( ): Promise> => { denoise.denoising_start = denoising_start; - const { params, canvasV2, canvasSession } = state; - const { bbox } = canvasV2; + const params = selectParamsSlice(state); + const canvasSession = selectCanvasSessionSlice(state); + const canvas = selectCanvasSlice(state); + + const { bbox } = canvas; const { mode } = canvasSession; const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index c0f298bfbd2..ecbc09f9162 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts @@ -1,6 +1,9 @@ import type { RootState } from 'app/store/store'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; +import { selectCanvasSessionSlice } from 'features/controlLayers/store/canvasSessionSlice'; +import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; +import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; import type { Dimensions } from 'features/controlLayers/store/types'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { getInfill } from 'features/nodes/util/graph/graphBuilderUtils'; @@ -22,8 +25,11 @@ export const addOutpaint = async ( ): Promise> => { denoise.denoising_start = denoising_start; - const { params, canvasV2, canvasSession } = state; - const { bbox } = canvasV2; + const params = selectParamsSlice(state); + const canvasSession = selectCanvasSessionSlice(state); + const canvas = selectCanvasSlice(state); + + const { bbox } = canvas; const { mode } = canvasSession; const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index 73d3c9e85ad..112fb20a60c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -2,6 +2,10 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; +import { selectCanvasSessionSlice } from 'features/controlLayers/store/canvasSessionSlice'; +import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice'; +import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; +import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { addControlNets, addT2IAdapters } from 'features/nodes/util/graph/generation/addControlAdapters'; import { addImageToImage } from 'features/nodes/util/graph/generation/addImageToImage'; @@ -31,8 +35,12 @@ export const buildSD1Graph = async ( const generationMode = manager.compositor.getGenerationMode(); log.debug({ generationMode }, 'Building SD1/SD2 graph'); - const { canvasV2, params, canvasSettings, canvasSession } = state; - const { bbox } = canvasV2; + const params = selectParamsSlice(state); + const canvasSession = selectCanvasSessionSlice(state); + const canvasSettings = selectCanvasSettingsSlice(state); + const canvas = selectCanvasSlice(state); + + const { bbox } = canvas; const { model, @@ -208,9 +216,9 @@ export const buildSD1Graph = async ( }); const controlNetResult = await addControlNets( manager, - state.canvasV2.controlLayers.entities, + canvas.controlLayers.entities, g, - state.canvasV2.bbox.rect, + canvas.bbox.rect, controlNetCollector, modelConfig.base ); @@ -226,9 +234,9 @@ export const buildSD1Graph = async ( }); const t2iAdapterResult = await addT2IAdapters( manager, - state.canvasV2.controlLayers.entities, + canvas.controlLayers.entities, g, - state.canvasV2.bbox.rect, + canvas.bbox.rect, t2iAdapterCollector, modelConfig.base ); @@ -242,13 +250,13 @@ export const buildSD1Graph = async ( type: 'collect', id: getPrefixedId('ip_adapter_collector'), }); - const ipAdapterResult = addIPAdapters(state.canvasV2.ipAdapters.entities, g, ipAdapterCollector, modelConfig.base); + const ipAdapterResult = addIPAdapters(canvas.ipAdapters.entities, g, ipAdapterCollector, modelConfig.base); const regionsResult = await addRegions( manager, - state.canvasV2.regions.entities, + canvas.regions.entities, g, - state.canvasV2.bbox.rect, + canvas.bbox.rect, modelConfig.base, denoise, posCond, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index a85bd57a66b..ddf8da0dcd8 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -2,6 +2,10 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; +import { selectCanvasSessionSlice } from 'features/controlLayers/store/canvasSessionSlice'; +import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice'; +import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; +import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { addControlNets, addT2IAdapters } from 'features/nodes/util/graph/generation/addControlAdapters'; import { addImageToImage } from 'features/nodes/util/graph/generation/addImageToImage'; @@ -31,8 +35,12 @@ export const buildSDXLGraph = async ( const generationMode = manager.compositor.getGenerationMode(); log.debug({ generationMode }, 'Building SDXL graph'); - const { params, canvasV2, canvasSettings, canvasSession } = state; - const { bbox } = canvasV2; + const params = selectParamsSlice(state); + const canvasSession = selectCanvasSessionSlice(state); + const canvasSettings = selectCanvasSettingsSlice(state); + const canvas = selectCanvasSlice(state); + + const { bbox } = canvas; const { model, @@ -211,9 +219,9 @@ export const buildSDXLGraph = async ( }); const controlNetResult = await addControlNets( manager, - state.canvasV2.controlLayers.entities, + canvas.controlLayers.entities, g, - state.canvasV2.bbox.rect, + canvas.bbox.rect, controlNetCollector, modelConfig.base ); @@ -229,9 +237,9 @@ export const buildSDXLGraph = async ( }); const t2iAdapterResult = await addT2IAdapters( manager, - state.canvasV2.controlLayers.entities, + canvas.controlLayers.entities, g, - state.canvasV2.bbox.rect, + canvas.bbox.rect, t2iAdapterCollector, modelConfig.base ); @@ -245,13 +253,13 @@ export const buildSDXLGraph = async ( type: 'collect', id: getPrefixedId('ip_adapter_collector'), }); - const ipAdapterResult = addIPAdapters(state.canvasV2.ipAdapters.entities, g, ipAdapterCollector, modelConfig.base); + const ipAdapterResult = addIPAdapters(canvas.ipAdapters.entities, g, ipAdapterCollector, modelConfig.base); const regionsResult = await addRegions( manager, - state.canvasV2.regions.entities, + canvas.regions.entities, g, - state.canvasV2.bbox.rect, + canvas.bbox.rect, modelConfig.base, denoise, posCond, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts index 91fa2270558..f3fa234430b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts @@ -1,6 +1,6 @@ import type { RootState } from 'app/store/store'; import type { ParamsState } from 'features/controlLayers/store/paramsSlice'; -import type { CanvasV2State } from 'features/controlLayers/store/types'; +import type { CanvasState } from 'features/controlLayers/store/types'; import type { BoardField } from 'features/nodes/types/common'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { buildPresetModifiedPrompt } from 'features/stylePresets/hooks/usePresetModifiedPrompts'; @@ -62,7 +62,7 @@ export const getPresetModifiedPrompts = ( }; }; -export const getSizes = (bboxState: CanvasV2State['bbox']) => { +export const getSizes = (bboxState: CanvasState['bbox']) => { const originalSize = pick(bboxState.rect, 'width', 'height'); const scaledSize = ['auto', 'manual'].includes(bboxState.scaleMethod) ? bboxState.scaledSize : originalSize; return { originalSize, scaledSize }; diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing.tsx index 1a0d95d57ad..46fd222ac9f 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing.tsx @@ -1,16 +1,20 @@ import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { bboxScaleMethodChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { bboxScaleMethodChanged } from 'features/controlLayers/store/canvasSlice'; +import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; import { isBoundingBoxScaleMethod } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +const selectScaleMethod = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.scaleMethod); + const ParamScaleBeforeProcessing = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const scaleMethod = useAppSelector((s) => s.canvasV2.bbox.scaleMethod); + const scaleMethod = useAppSelector(selectScaleMethod); const OPTIONS: ComboboxOption[] = useMemo( () => [ diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx index 5d35387fd87..01569a83b05 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx @@ -1,22 +1,26 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { bboxScaledSizeChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; +import { bboxScaledSizeChanged } from 'features/controlLayers/store/canvasSlice'; +import { selectCanvasSlice, selectOptimalDimension } from 'features/controlLayers/store/selectors'; +import { selectConfigSlice } from 'features/system/store/configSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; +const selectIsManual = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.scaleMethod === 'manual'); +const selectScaledHeight = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.scaledSize.height); +const selectScaledBoundingBoxHeightConfig = createSelector( + selectConfigSlice, + (config) => config.sd.scaledBoundingBoxHeight +); + const ParamScaledHeight = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const optimalDimension = useAppSelector(selectOptimalDimension); - const isManual = useAppSelector((s) => s.canvasV2.bbox.scaleMethod === 'manual'); - const height = useAppSelector((s) => s.canvasV2.bbox.scaledSize.height); - const sliderMin = useAppSelector((s) => s.config.sd.scaledBoundingBoxHeight.sliderMin); - const sliderMax = useAppSelector((s) => s.config.sd.scaledBoundingBoxHeight.sliderMax); - const numberInputMin = useAppSelector((s) => s.config.sd.scaledBoundingBoxHeight.numberInputMin); - const numberInputMax = useAppSelector((s) => s.config.sd.scaledBoundingBoxHeight.numberInputMax); - const coarseStep = useAppSelector((s) => s.config.sd.scaledBoundingBoxHeight.coarseStep); - const fineStep = useAppSelector((s) => s.config.sd.scaledBoundingBoxHeight.fineStep); + const isManual = useAppSelector(selectIsManual); + const scaledHeight = useAppSelector(selectScaledHeight); + const config = useAppSelector(selectScaledBoundingBoxHeightConfig); const onChange = useCallback( (height: number) => { @@ -29,21 +33,21 @@ const ParamScaledHeight = () => { {t('parameters.scaledHeight')} diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx index 7c92e2a7dc7..5eb2430b1da 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx @@ -1,22 +1,26 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { bboxScaledSizeChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; +import { bboxScaledSizeChanged } from 'features/controlLayers/store/canvasSlice'; +import { selectCanvasSlice, selectOptimalDimension } from 'features/controlLayers/store/selectors'; +import { selectConfigSlice } from 'features/system/store/configSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; +const selectIsManual = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.scaleMethod === 'manual'); +const selectScaledWidth = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.scaledSize.width); +const selectScaledBoundingBoxWidthConfig = createSelector( + selectConfigSlice, + (config) => config.sd.scaledBoundingBoxWidth +); + const ParamScaledWidth = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const optimalDimension = useAppSelector(selectOptimalDimension); - const isManual = useAppSelector((s) => s.canvasV2.bbox.scaleMethod === 'manual'); - const width = useAppSelector((s) => s.canvasV2.bbox.scaledSize.width); - const sliderMin = useAppSelector((s) => s.config.sd.scaledBoundingBoxWidth.sliderMin); - const sliderMax = useAppSelector((s) => s.config.sd.scaledBoundingBoxWidth.sliderMax); - const numberInputMin = useAppSelector((s) => s.config.sd.scaledBoundingBoxWidth.numberInputMin); - const numberInputMax = useAppSelector((s) => s.config.sd.scaledBoundingBoxWidth.numberInputMax); - const coarseStep = useAppSelector((s) => s.config.sd.scaledBoundingBoxWidth.coarseStep); - const fineStep = useAppSelector((s) => s.config.sd.scaledBoundingBoxWidth.fineStep); + const isManual = useAppSelector(selectIsManual); + const scaledWidth = useAppSelector(selectScaledWidth); + const config = useAppSelector(selectScaledBoundingBoxWidthConfig); const onChange = useCallback( (width: number) => { dispatch(bboxScaledSizeChanged({ width })); @@ -28,21 +32,21 @@ const ParamScaledWidth = () => { {t('parameters.scaledWidth')} diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx index 93fa1319b64..4db731c7ee1 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx @@ -1,22 +1,22 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { bboxHeightChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; +import { bboxHeightChanged } from 'features/controlLayers/store/canvasSlice'; +import { selectCanvasSlice, selectOptimalDimension } from 'features/controlLayers/store/selectors'; +import { selectConfigSlice } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +const selectHeight = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.rect.height); +const selectHeightConfig = createSelector(selectConfigSlice, (config) => config.sd.height); + export const ParamHeight = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const optimalDimension = useAppSelector(selectOptimalDimension); - const height = useAppSelector((s) => s.canvasV2.bbox.rect.height); - const sliderMin = useAppSelector((s) => s.config.sd.height.sliderMin); - const sliderMax = useAppSelector((s) => s.config.sd.height.sliderMax); - const numberInputMin = useAppSelector((s) => s.config.sd.height.numberInputMin); - const numberInputMax = useAppSelector((s) => s.config.sd.height.numberInputMax); - const coarseStep = useAppSelector((s) => s.config.sd.height.coarseStep); - const fineStep = useAppSelector((s) => s.config.sd.height.fineStep); + const height = useAppSelector(selectHeight); + const config = useAppSelector(selectHeightConfig); const onChange = useCallback( (v: number) => { @@ -25,7 +25,10 @@ export const ParamHeight = memo(() => { [dispatch] ); - const marks = useMemo(() => [sliderMin, optimalDimension, sliderMax], [sliderMin, optimalDimension, sliderMax]); + const marks = useMemo( + () => [config.sliderMin, optimalDimension, config.sliderMax], + [config.sliderMin, config.sliderMax, optimalDimension] + ); return ( @@ -36,20 +39,20 @@ export const ParamHeight = memo(() => { value={height} defaultValue={optimalDimension} onChange={onChange} - min={sliderMin} - max={sliderMax} - step={coarseStep} - fineStep={fineStep} + min={config.sliderMin} + max={config.sliderMax} + step={config.coarseStep} + fineStep={config.fineStep} marks={marks} /> ); diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamWidth.tsx index 66dc071d639..71d6b4945ca 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamWidth.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamWidth.tsx @@ -1,22 +1,22 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { bboxWidthChanged } from 'features/controlLayers/store/canvasV2Slice'; -import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; +import { bboxWidthChanged } from 'features/controlLayers/store/canvasSlice'; +import { selectCanvasSlice, selectOptimalDimension } from 'features/controlLayers/store/selectors'; +import { selectConfigSlice } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +const selectWidth = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.rect.width); +const selectWidthConfig = createSelector(selectConfigSlice, (config) => config.sd.width); + export const ParamWidth = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const width = useAppSelector((s) => s.canvasV2.bbox.rect.width); + const width = useAppSelector(selectWidth); const optimalDimension = useAppSelector(selectOptimalDimension); - const sliderMin = useAppSelector((s) => s.config.sd.width.sliderMin); - const sliderMax = useAppSelector((s) => s.config.sd.width.sliderMax); - const numberInputMin = useAppSelector((s) => s.config.sd.width.numberInputMin); - const numberInputMax = useAppSelector((s) => s.config.sd.width.numberInputMax); - const coarseStep = useAppSelector((s) => s.config.sd.width.coarseStep); - const fineStep = useAppSelector((s) => s.config.sd.width.fineStep); + const config = useAppSelector(selectWidthConfig); const onChange = useCallback( (v: number) => { @@ -25,7 +25,10 @@ export const ParamWidth = memo(() => { [dispatch] ); - const marks = useMemo(() => [sliderMin, optimalDimension, sliderMax], [sliderMin, optimalDimension, sliderMax]); + const marks = useMemo( + () => [config.sliderMin, optimalDimension, config.sliderMax], + [config.sliderMax, config.sliderMin, optimalDimension] + ); return ( @@ -36,20 +39,20 @@ export const ParamWidth = memo(() => { value={width} onChange={onChange} defaultValue={optimalDimension} - min={sliderMin} - max={sliderMax} - step={coarseStep} - fineStep={fineStep} + min={config.sliderMin} + max={config.sliderMax} + step={config.coarseStep} + fineStep={config.fineStep} marks={marks} /> ); diff --git a/invokeai/frontend/web/src/features/parameters/components/DocumentSize/AspectRatioSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/AspectRatioSelect.tsx index f2c3fc66dea..f8af1e562ad 100644 --- a/invokeai/frontend/web/src/features/parameters/components/DocumentSize/AspectRatioSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/AspectRatioSelect.tsx @@ -1,18 +1,22 @@ import type { ComboboxOption, SystemStyleObject } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import type { SingleValue } from 'chakra-react-select'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { bboxAspectRatioIdChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { bboxAspectRatioIdChanged } from 'features/controlLayers/store/canvasSlice'; +import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; import { ASPECT_RATIO_OPTIONS } from 'features/parameters/components/DocumentSize/constants'; import { isAspectRatioID } from 'features/parameters/components/DocumentSize/types'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +const selectAspectRatioID = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.aspectRatio.id); + export const AspectRatioSelect = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const id = useAppSelector((s) => s.canvasV2.bbox.aspectRatio.id); + const id = useAppSelector(selectAspectRatioID); const onChange = useCallback( (v: SingleValue) => { diff --git a/invokeai/frontend/web/src/features/parameters/components/DocumentSize/LockAspectRatioButton.tsx b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/LockAspectRatioButton.tsx index 47846ad8a03..a3a765a20e3 100644 --- a/invokeai/frontend/web/src/features/parameters/components/DocumentSize/LockAspectRatioButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/LockAspectRatioButton.tsx @@ -1,14 +1,18 @@ import { IconButton } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { bboxAspectRatioLockToggled } from 'features/controlLayers/store/canvasV2Slice'; +import { bboxAspectRatioLockToggled } from 'features/controlLayers/store/canvasSlice'; +import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiLockSimpleFill, PiLockSimpleOpenBold } from 'react-icons/pi'; +const selectAspectRatioIsLocked = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.aspectRatio.isLocked); + export const LockAspectRatioButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const isLocked = useAppSelector((s) => s.canvasV2.bbox.aspectRatio.isLocked); + const isLocked = useAppSelector(selectAspectRatioIsLocked); const onClick = useCallback(() => { dispatch(bboxAspectRatioLockToggled()); }, [dispatch]); diff --git a/invokeai/frontend/web/src/features/parameters/components/DocumentSize/SetOptimalSizeButton.tsx b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/SetOptimalSizeButton.tsx index 377ae4d4b24..df43afff1dc 100644 --- a/invokeai/frontend/web/src/features/parameters/components/DocumentSize/SetOptimalSizeButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/SetOptimalSizeButton.tsx @@ -1,17 +1,21 @@ import { IconButton } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { bboxSizeOptimized } from 'features/controlLayers/store/canvasV2Slice'; -import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; +import { bboxSizeOptimized } from 'features/controlLayers/store/canvasSlice'; +import { selectCanvasSlice, selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { getIsSizeTooLarge, getIsSizeTooSmall } from 'features/parameters/util/optimalDimension'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { RiSparklingFill } from 'react-icons/ri'; +const selectWidth = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.rect.width); +const selectHeight = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.rect.height); + export const SetOptimalSizeButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const width = useAppSelector((s) => s.canvasV2.bbox.rect.width); - const height = useAppSelector((s) => s.canvasV2.bbox.rect.height); + const width = useAppSelector(selectWidth); + const height = useAppSelector(selectHeight); const optimalDimension = useAppSelector(selectOptimalDimension); const isSizeTooSmall = useMemo( () => getIsSizeTooSmall(width, height, optimalDimension), diff --git a/invokeai/frontend/web/src/features/parameters/components/DocumentSize/SwapDimensionsButton.tsx b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/SwapDimensionsButton.tsx index f4f586c0f82..745024a1b6c 100644 --- a/invokeai/frontend/web/src/features/parameters/components/DocumentSize/SwapDimensionsButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/DocumentSize/SwapDimensionsButton.tsx @@ -1,6 +1,6 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; -import { bboxDimensionsSwapped } from 'features/controlLayers/store/canvasV2Slice'; +import { bboxDimensionsSwapped } from 'features/controlLayers/store/canvasSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowsDownUpBold } from 'react-icons/pi'; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx index a0b9e018299..8adcc23426f 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx @@ -3,7 +3,7 @@ import { Expander, Flex, FormControlGroup, StandaloneAccordion } from '@invoke-a import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; -import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors'; +import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; import { HrfSettings } from 'features/hrf/components/HrfSettings'; import { selectHrfSlice } from 'features/hrf/store/hrfSlice'; import ParamScaleBeforeProcessing from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing'; @@ -20,15 +20,15 @@ import { memo } from 'react'; import { useTranslation } from 'react-i18next'; const selector = createMemoizedSelector( - [selectHrfSlice, selectCanvasV2Slice, selectParamsSlice], - (hrf, canvasV2, params) => { + [selectHrfSlice, selectCanvasSlice, selectParamsSlice], + (hrf, canvas, params) => { const { shouldRandomizeSeed, model } = params; const { hrfEnabled } = hrf; const badges: string[] = []; const isSDXL = model?.base === 'sdxl'; - const { aspectRatio } = canvasV2.bbox; - const { width, height } = canvasV2.bbox.rect; + const { aspectRatio } = canvas.bbox; + const { width, height } = canvas.bbox.rect; badges.push(`${width}×${height}`); badges.push(aspectRatio.id); From fe672ba5e0dba529a8bfba9b5f9230e39e02da59 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 27 Aug 2024 13:19:14 +1000 Subject: [PATCH 507/678] perf(ui): optimize all selectors 1 I learned that the inline selector syntax recreates the selector function on every render: ```ts const val = useAppSelector((s) => s.slice.val) ``` Not good! Better is to create a selector outside the function and use it. Doing that for all selectors now, most of the way through now. Feels snappier. --- .../components/AppErrorBoundaryFallback.tsx | 6 +- .../frontend/web/src/app/logging/useLogger.ts | 12 +++- .../InformationalPopover.tsx | 7 ++- .../src/common/hooks/useFullscreenDropzone.ts | 3 +- .../common/hooks/useGroupedModelCombobox.ts | 10 +++- .../src/common/hooks/useImageUploadButton.tsx | 3 +- .../components/ChangeBoardModal.tsx | 8 ++- .../CanvasEntityListMenuItems.tsx | 7 +-- .../components/CanvasModeSwitcher.tsx | 7 ++- .../components/CanvasPanelContent.tsx | 4 +- .../ControlLayerControlAdapterModel.tsx | 3 +- .../ControlLayer/ControlLayerEntityList.tsx | 7 ++- .../components/Filters/FilterTypeSelect.tsx | 9 +-- .../components/IPAdapter/IPAdapterModel.tsx | 3 +- .../CanvasSettingsAutoSaveCheckbox.tsx | 7 ++- .../CanvasSettingsClipToBboxCheckbox.tsx | 7 ++- .../CanvasSettingsDynamicGridSwitch.tsx | 10 +++- .../CanvasSettingsInvertScrollCheckbox.tsx | 7 ++- .../components/StageComponent.tsx | 6 +- .../StagingArea/StagingAreaIsStagingGate.tsx | 3 +- .../StagingArea/StagingAreaToolbar.tsx | 42 +++++++++----- .../components/Tool/ToolBboxButton.tsx | 3 +- .../components/Tool/ToolBrushWidth.tsx | 6 +- .../components/Tool/ToolColorPickerButton.tsx | 3 +- .../components/Tool/ToolEraserWidth.tsx | 6 +- .../components/Tool/ToolFillColorPicker.tsx | 7 ++- .../components/Tool/ToolViewButton.tsx | 3 +- .../components/common/CanvasEntityOpacity.tsx | 35 ++++++----- .../components/common/Weight.tsx | 32 +++++----- .../hooks/useCanvasDeleteLayerHotkey.ts | 11 +--- .../hooks/useLayerControlAdapter.ts | 5 +- .../controlLayers/store/paramsSlice.ts | 3 +- .../features/controlLayers/store/selectors.ts | 6 +- .../features/controlLayers/store/toolSlice.ts | 4 +- .../components/DeleteImageButton.tsx | 3 +- .../components/DeleteImageModal.tsx | 13 ++++- .../features/dnd/components/DragPreview.tsx | 3 +- .../ParamDynamicPromptsMaxPrompts.tsx | 30 +++++----- .../components/ParamDynamicPromptsPreview.tsx | 15 +++-- .../ParamDynamicPromptsSeedBehaviour.tsx | 11 +++- .../ShowDynamicPromptsPreviewButton.tsx | 11 +++- .../components/Boards/BoardAutoAddSelect.tsx | 5 +- .../components/Boards/BoardContextMenu.tsx | 7 ++- .../Boards/BoardsList/AddBoardButton.tsx | 3 +- .../Boards/BoardsList/BoardsList.tsx | 14 +++-- .../Boards/BoardsList/BoardsListWrapper.tsx | 3 +- .../Boards/BoardsList/BoardsSearch.tsx | 3 +- .../Boards/BoardsList/GalleryBoard.tsx | 11 +++- .../Boards/BoardsList/NoBoardBoard.tsx | 11 +++- .../Boards/NoBoardBoardContextMenu.tsx | 8 ++- .../features/gallery/components/Gallery.tsx | 13 +++-- .../components/GalleryPanelContent.tsx | 3 +- .../AlwaysShowImageSizeCheckbox.tsx | 10 +++- .../AutoAssignBoardCheckbox.tsx | 3 +- .../AutoSwitchCheckbox.tsx | 7 ++- .../ImageMinimumWidthSlider.tsx | 3 +- .../ShowArchivedBoardsCheckbox.tsx | 10 +++- .../ShowStarredFirstCheckbox.tsx | 7 ++- .../SortDirectionCombobox.tsx | 7 ++- .../ImageContextMenu/ImageContextMenu.tsx | 3 +- .../SingleSelectionMenuItems.tsx | 11 +++- .../components/ImageGrid/GalleryImage.tsx | 23 ++++++-- .../components/ImageGrid/GalleryImageGrid.tsx | 4 +- .../ImageGrid/useGallerySearchTerm.ts | 3 +- .../components/ImageViewer/CompareToolbar.tsx | 5 +- .../ImageViewer/CurrentImagePreview.tsx | 13 ++--- .../ImageViewer/ImageComparison.tsx | 3 +- .../ImageViewer/ImageComparisonHover.tsx | 3 +- .../ImageViewer/ImageComparisonSlider.tsx | 3 +- .../components/ImageViewer/ImageViewer.tsx | 11 ++-- .../components/ImageViewer/ProgressImage.tsx | 9 ++- .../ToggleMetadataViewerButton.tsx | 3 +- .../ImageViewer/ToggleProgressButton.tsx | 3 +- .../components/ImageViewer/ViewerToolbar.tsx | 16 ++--- .../components/ImageViewer/useImageViewer.ts | 58 +++++++++++-------- .../gallery/hooks/useGalleryNavigation.ts | 22 ++++--- .../gallery/hooks/useGalleryPagination.ts | 9 ++- .../features/gallery/hooks/useImageActions.ts | 4 +- .../features/gallery/hooks/useMultiselect.ts | 3 +- .../gallery/store/gallerySelectors.ts | 27 ++++++++- .../features/hrf/components/HrfSettings.tsx | 3 +- .../hrf/components/ParamHrfMethod.tsx | 4 +- .../hrf/components/ParamHrfStrength.tsx | 36 ++++++------ .../hrf/components/ParamHrfToggle.tsx | 4 +- .../web/src/features/hrf/store/hrfSlice.ts | 9 ++- .../features/lora/components/LoRASelect.tsx | 7 ++- .../nodes/CurrentImage/CurrentImageNode.tsx | 3 +- .../stylePresets/store/stylePresetSlice.ts | 10 +++- .../features/system/store/configSelectors.ts | 5 +- .../src/features/ui/components/AppContent.tsx | 14 ++++- .../src/features/ui/hooks/usePanelStorage.ts | 13 ++++- .../web/src/features/ui/store/uiSelectors.ts | 2 + 92 files changed, 561 insertions(+), 294 deletions(-) diff --git a/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx b/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx index ced3037a405..ef2cb32eaa9 100644 --- a/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx +++ b/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx @@ -1,5 +1,7 @@ import { Button, Flex, Heading, Image, Link, Text } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; +import { selectConfigSlice } from 'features/system/store/configSlice'; import { toast } from 'features/toast/toast'; import newGithubIssueUrl from 'new-github-issue-url'; import InvokeLogoYellow from 'public/assets/images/invoke-symbol-ylw-lrg.svg'; @@ -13,9 +15,11 @@ type Props = { resetErrorBoundary: () => void; }; +const selectIsLocal = createSelector(selectConfigSlice, (config) => config.isLocal); + const AppErrorBoundaryFallback = ({ error, resetErrorBoundary }: Props) => { const { t } = useTranslation(); - const isLocal = useAppSelector((s) => s.config.isLocal); + const isLocal = useAppSelector(selectIsLocal); const handleCopy = useCallback(() => { const text = JSON.stringify(serializeError(error), null, 2); diff --git a/invokeai/frontend/web/src/app/logging/useLogger.ts b/invokeai/frontend/web/src/app/logging/useLogger.ts index c86844f5ed4..c0a15297476 100644 --- a/invokeai/frontend/web/src/app/logging/useLogger.ts +++ b/invokeai/frontend/web/src/app/logging/useLogger.ts @@ -1,15 +1,21 @@ +import { createSelector } from '@reduxjs/toolkit'; import { createLogWriter } from '@roarr/browser-log-writer'; import { useAppSelector } from 'app/store/storeHooks'; +import { selectSystemSlice } from 'features/system/store/systemSlice'; import { useEffect, useMemo } from 'react'; import { ROARR, Roarr } from 'roarr'; import type { LogNamespace } from './logger'; import { $logger, BASE_CONTEXT, LOG_LEVEL_MAP, logger } from './logger'; +const selectLogLevel = createSelector(selectSystemSlice, (system) => system.logLevel); +const selectLogNamespaces = createSelector(selectSystemSlice, (system) => system.logNamespaces); +const selectLogIsEnabled = createSelector(selectSystemSlice, (system) => system.logIsEnabled); + export const useLogger = (namespace: LogNamespace) => { - const logLevel = useAppSelector((s) => s.system.logLevel); - const logNamespaces = useAppSelector((s) => s.system.logNamespaces); - const logIsEnabled = useAppSelector((s) => s.system.logIsEnabled); + const logLevel = useAppSelector(selectLogLevel); + const logNamespaces = useAppSelector(selectLogNamespaces); + const logIsEnabled = useAppSelector(selectLogIsEnabled); // The provided Roarr browser log writer uses localStorage to config logging to console useEffect(() => { diff --git a/invokeai/frontend/web/src/common/components/InformationalPopover/InformationalPopover.tsx b/invokeai/frontend/web/src/common/components/InformationalPopover/InformationalPopover.tsx index cb295bb0f4c..8d678546790 100644 --- a/invokeai/frontend/web/src/common/components/InformationalPopover/InformationalPopover.tsx +++ b/invokeai/frontend/web/src/common/components/InformationalPopover/InformationalPopover.tsx @@ -13,8 +13,9 @@ import { Spacer, Text, } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { setShouldEnableInformationalPopovers } from 'features/system/store/systemSlice'; +import { selectSystemSlice, setShouldEnableInformationalPopovers } from 'features/system/store/systemSlice'; import { toast } from 'features/toast/toast'; import { merge, omit } from 'lodash-es'; import type { ReactElement } from 'react'; @@ -31,8 +32,10 @@ type Props = { children: ReactElement; }; +const selectShouldEnableInformationalPopovers = createSelector(selectSystemSlice, system => system.shouldEnableInformationalPopovers); + export const InformationalPopover = memo(({ feature, children, inPortal = true, ...rest }: Props) => { - const shouldEnableInformationalPopovers = useAppSelector((s) => s.system.shouldEnableInformationalPopovers); + const shouldEnableInformationalPopovers = useAppSelector(selectShouldEnableInformationalPopovers); const data = useMemo(() => POPOVER_DATA[feature], [feature]); diff --git a/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts b/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts index 9c84e66bfcc..8b8b3fa844b 100644 --- a/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts +++ b/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts @@ -1,5 +1,6 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; +import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors'; import { toast } from 'features/toast/toast'; import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { useCallback, useEffect, useState } from 'react'; @@ -26,7 +27,7 @@ const selectPostUploadAction = createMemoizedSelector(selectActiveTab, (activeTa export const useFullscreenDropzone = () => { const { t } = useTranslation(); - const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId); + const autoAddBoardId = useAppSelector(selectAutoAddBoardId); const [isHandlingUpload, setIsHandlingUpload] = useState(false); const postUploadAction = useAppSelector(selectPostUploadAction); const [uploadImage] = useUploadImageMutation(); diff --git a/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts b/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts index a06979b0346..f077855ae99 100644 --- a/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts +++ b/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts @@ -1,6 +1,8 @@ import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import type { GroupBase } from 'chakra-react-select'; +import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; import type { ModelIdentifierField } from 'features/nodes/types/common'; import { groupBy, reduce } from 'lodash-es'; import { useCallback, useMemo } from 'react'; @@ -28,11 +30,13 @@ const groupByBaseFunc = (model: T) => model.base.toUpp const groupByBaseAndTypeFunc = (model: T) => `${model.base.toUpperCase()} / ${model.type.replaceAll('_', ' ').toUpperCase()}`; +const selectBaseWithSDXLFallback = createSelector(selectParamsSlice, (params) => params.model?.base ?? 'sdxl'); + export const useGroupedModelCombobox = ( arg: UseGroupedModelComboboxArg ): UseGroupedModelComboboxReturn => { const { t } = useTranslation(); - const base_model = useAppSelector((s) => s.params.model?.base ?? 'sdxl'); + const base = useAppSelector(selectBaseWithSDXLFallback); const { modelConfigs, selectedModel, getIsDisabled, onChange, isLoading, groupByType = false } = arg; const options = useMemo[]>(() => { if (!modelConfigs) { @@ -54,9 +58,9 @@ export const useGroupedModelCombobox = ( }, [] as GroupBase[] ); - _options.sort((a) => (a.label?.split('/')[0]?.toLowerCase().includes(base_model) ? -1 : 1)); + _options.sort((a) => (a.label?.split('/')[0]?.toLowerCase().includes(base) ? -1 : 1)); return _options; - }, [modelConfigs, groupByType, getIsDisabled, base_model]); + }, [modelConfigs, groupByType, getIsDisabled, base]); const value = useMemo( () => diff --git a/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx b/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx index 011f49ec269..d49ce460ceb 100644 --- a/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx +++ b/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx @@ -1,4 +1,5 @@ import { useAppSelector } from 'app/store/storeHooks'; +import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors'; import { useCallback } from 'react'; import { useDropzone } from 'react-dropzone'; import { useUploadImageMutation } from 'services/api/endpoints/images'; @@ -29,7 +30,7 @@ type UseImageUploadButtonArgs = { * // hidden, handles native upload functionality */ export const useImageUploadButton = ({ postUploadAction, isDisabled }: UseImageUploadButtonArgs) => { - const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId); + const autoAddBoardId = useAppSelector(selectAutoAddBoardId); const [uploadImage] = useUploadImageMutation(); const onDropAccepted = useCallback( (files: File[]) => { diff --git a/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx b/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx index 489085ebf52..175bf9fedeb 100644 --- a/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx +++ b/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx @@ -1,5 +1,6 @@ import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; import { Combobox, ConfirmationAlertDialog, Flex, FormControl, Text } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { @@ -18,12 +19,17 @@ const selectImagesToChange = createMemoizedSelector( (changeBoardModal) => changeBoardModal.imagesToChange ); +const selectIsModalOpen = createSelector( + selectChangeBoardModalSlice, + (changeBoardModal) => changeBoardModal.isModalOpen +); + const ChangeBoardModal = () => { const dispatch = useAppDispatch(); const [selectedBoard, setSelectedBoard] = useState(); const queryArgs = useAppSelector(selectListBoardsQueryArgs); const { data: boards, isFetching } = useListAllBoardsQuery(queryArgs); - const isModalOpen = useAppSelector((s) => s.changeBoardModal.isModalOpen); + const isModalOpen = useAppSelector(selectIsModalOpen); const imagesToChange = useAppSelector(selectImagesToChange); const [addImagesToBoard] = useAddImagesToBoardMutation(); const [removeImagesFromBoard] = useRemoveImagesFromBoardMutation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuItems.tsx index 82ceb609296..993e42c57f7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuItems.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuItems.tsx @@ -8,7 +8,7 @@ import { rasterLayerAdded, rgAdded, } from 'features/controlLayers/store/canvasSlice'; -import { selectEntityCount } from 'features/controlLayers/store/selectors'; +import { selectHasEntities } from 'features/controlLayers/store/selectors'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPlusBold, PiTrashSimpleBold } from 'react-icons/pi'; @@ -16,10 +16,7 @@ import { PiPlusBold, PiTrashSimpleBold } from 'react-icons/pi'; export const CanvasEntityListMenuItems = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const hasEntities = useAppSelector((s) => { - const count = selectEntityCount(s); - return count > 0; - }); + const hasEntities = useAppSelector(selectHasEntities); const addInpaintMask = useCallback(() => { dispatch(inpaintMaskAdded({ isSelected: true })); }, [dispatch]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasModeSwitcher.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasModeSwitcher.tsx index 7613782e6fc..e052c1a214d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasModeSwitcher.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasModeSwitcher.tsx @@ -1,13 +1,16 @@ import { Button, ButtonGroup } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { sessionModeChanged } from 'features/controlLayers/store/canvasSessionSlice'; +import { selectCanvasSessionSlice, sessionModeChanged } from 'features/controlLayers/store/canvasSessionSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; +const selectCanvasMode = createSelector(selectCanvasSessionSlice, (canvasSession) => canvasSession.mode); + export const CanvasModeSwitcher = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const mode = useAppSelector((s) => s.canvasSession.mode); + const mode = useAppSelector(selectCanvasMode); const onClickGenerate = useCallback(() => dispatch(sessionModeChanged({ mode: 'generate' })), [dispatch]); const onClickCompose = useCallback(() => dispatch(sessionModeChanged({ mode: 'compose' })), [dispatch]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx index 406cba7d7d2..16e348ef397 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx @@ -4,11 +4,11 @@ import { CanvasAddEntityButtons } from 'features/controlLayers/components/Canvas import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityList'; import { CanvasEntityListMenuItems } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuItems'; import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; -import { selectEntityCount } from 'features/controlLayers/store/selectors'; +import { selectHasEntities } from 'features/controlLayers/store/selectors'; import { memo, useCallback } from 'react'; export const CanvasPanelContent = memo(() => { - const hasEntities = useAppSelector((s) => selectEntityCount(s) > 0); + const hasEntities = useAppSelector(selectHasEntities); const renderMenu = useCallback( () => ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx index 139f0a3af59..84f268ac23f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx @@ -3,6 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { selectBase } from 'features/controlLayers/store/paramsSlice'; import { IMAGE_FILTERS, isFilterType } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -18,7 +19,7 @@ export const ControlLayerControlAdapterModel = memo(({ modelKey, onChange: onCha const { t } = useTranslation(); const entityIdentifier = useEntityIdentifierContext(); const canvasManager = useCanvasManager(); - const currentBaseModel = useAppSelector((s) => s.params.model?.base); + const currentBaseModel = useAppSelector(selectBase); const [modelConfigs, { isLoading }] = useControlNetAndT2IAdapterModels(); const selectedModel = useMemo(() => modelConfigs.find((m) => m.key === modelKey), [modelConfigs, modelKey]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx index 42c66ddd52b..9421804090e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx @@ -1,3 +1,4 @@ +import { createSelector } from '@reduxjs/toolkit'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList'; @@ -10,8 +11,12 @@ const selectEntityIds = createMemoizedSelector(selectCanvasSlice, (canvas) => { return canvas.controlLayers.entities.map(mapId).reverse(); }); +const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selectedEntityIdentifier) => { + return selectedEntityIdentifier?.type === 'control_layer'; +}); + export const ControlLayerEntityList = memo(() => { - const isSelected = useAppSelector((s) => selectSelectedEntityIdentifier(s)?.type === 'control_layer'); + const isSelected = useAppSelector(selectIsSelected); const layerIds = useAppSelector(selectEntityIds); if (layerIds.length === 0) { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterTypeSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterTypeSelect.tsx index fb2537e7ac6..bc512dc019c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterTypeSelect.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterTypeSelect.tsx @@ -1,20 +1,17 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import type { FilterConfig } from 'features/controlLayers/store/types'; import { IMAGE_FILTERS, isFilterType } from 'features/controlLayers/store/types'; -import { configSelector } from 'features/system/store/configSelectors'; +import { selectConfigSlice } from 'features/system/store/configSlice'; import { includes, map } from 'lodash-es'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { assert } from 'tsafe'; -const selectDisabledProcessors = createMemoizedSelector( - configSelector, - (config) => config.sd.disabledControlNetProcessors -); +const selectDisabledProcessors = createSelector(selectConfigSlice, (config) => config.sd.disabledControlNetProcessors); type Props = { filterType: FilterConfig['type']; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterModel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterModel.tsx index b14b761951a..bacd81f9e14 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterModel.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterModel.tsx @@ -2,6 +2,7 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; +import { selectBase } from 'features/controlLayers/store/paramsSlice'; import type { CLIPVisionModelV2 } from 'features/controlLayers/store/types'; import { isCLIPVisionModelV2 } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; @@ -24,7 +25,7 @@ type Props = { export const IPAdapterModel = memo(({ modelKey, onChangeModel, clipVisionModel, onChangeCLIPVisionModel }: Props) => { const { t } = useTranslation(); - const currentBaseModel = useAppSelector((s) => s.params.model?.base); + const currentBaseModel = useAppSelector(selectBase); const [modelConfigs, { isLoading }] = useIPAdapterModels(); const selectedModel = useMemo(() => modelConfigs.find((m) => m.key === modelKey), [modelConfigs, modelKey]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsAutoSaveCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsAutoSaveCheckbox.tsx index ddfcfe8f3a2..53dfaa718e1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsAutoSaveCheckbox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsAutoSaveCheckbox.tsx @@ -1,13 +1,16 @@ import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { settingsAutoSaveToggled } from 'features/controlLayers/store/canvasSettingsSlice'; +import { selectCanvasSettingsSlice, settingsAutoSaveToggled } from 'features/controlLayers/store/canvasSettingsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; +const selectAutoSave = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.autoSave); + export const CanvasSettingsAutoSaveCheckbox = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const autoSave = useAppSelector((s) => s.canvasSettings.autoSave); + const autoSave = useAppSelector(selectAutoSave); const onChange = useCallback(() => dispatch(settingsAutoSaveToggled()), [dispatch]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox.tsx index 60b8cf4ff90..7795d7d1b3c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox.tsx @@ -1,14 +1,17 @@ import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { clipToBboxChanged } from 'features/controlLayers/store/canvasSettingsSlice'; +import { clipToBboxChanged, selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; +const selectClipToBbox = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.clipToBbox); + export const CanvasSettingsClipToBboxCheckbox = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const clipToBbox = useAppSelector((s) => s.canvasSettings.clipToBbox); + const clipToBbox = useAppSelector(selectClipToBbox); const onChange = useCallback( (e: ChangeEvent) => dispatch(clipToBboxChanged(e.target.checked)), [dispatch] diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsDynamicGridSwitch.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsDynamicGridSwitch.tsx index f29b5ac16f2..a625c8e6222 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsDynamicGridSwitch.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsDynamicGridSwitch.tsx @@ -1,13 +1,19 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { settingsDynamicGridToggled } from 'features/controlLayers/store/canvasSettingsSlice'; +import { + selectCanvasSettingsSlice, + settingsDynamicGridToggled, +} from 'features/controlLayers/store/canvasSettingsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; +const selectDynamicGrid = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.dynamicGrid); + export const CanvasSettingsDynamicGridSwitch = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const dynamicGrid = useAppSelector((s) => s.canvasSettings.dynamicGrid); + const dynamicGrid = useAppSelector(selectDynamicGrid); const onChange = useCallback(() => { dispatch(settingsDynamicGridToggled()); }, [dispatch]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox.tsx index e4a8abd1b0a..a8f6d2e18a6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox.tsx @@ -1,14 +1,17 @@ import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { invertScrollChanged } from 'features/controlLayers/store/toolSlice'; +import { invertScrollChanged, selectToolSlice } from 'features/controlLayers/store/toolSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; +const selectInvertScroll = createSelector(selectToolSlice, (tool) => tool.invertScroll); + export const CanvasSettingsInvertScrollCheckbox = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const invertScroll = useAppSelector((s) => s.tool.invertScroll); + const invertScroll = useAppSelector(selectInvertScroll); const onChange = useCallback( (e: ChangeEvent) => dispatch(invertScrollChanged(e.target.checked)), [dispatch] diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index f7679346c02..2c365ef20df 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -1,5 +1,6 @@ import { Flex } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; +import { createSelector } from '@reduxjs/toolkit'; import { $socket } from 'app/hooks/useSocketIO'; import { logger } from 'app/logging/logger'; import { useAppStore } from 'app/store/nanostores/store'; @@ -7,6 +8,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay'; import { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants'; +import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice'; import Konva from 'konva'; import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react'; import { useDevicePixelRatio } from 'use-device-pixel-ratio'; @@ -51,8 +53,10 @@ type Props = { asPreview?: boolean; }; +const selectDynamicGrid = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.dynamicGrid); + export const StageComponent = memo(({ asPreview = false }: Props) => { - const dynamicGrid = useAppSelector((s) => s.canvasSettings.dynamicGrid); + const dynamicGrid = useAppSelector(selectDynamicGrid); const [stage] = useState( () => diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaIsStagingGate.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaIsStagingGate.tsx index c5cbe43755b..2f86d30f67d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaIsStagingGate.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaIsStagingGate.tsx @@ -1,9 +1,10 @@ import { useAppSelector } from 'app/store/storeHooks'; +import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice'; import type { PropsWithChildren } from 'react'; import { memo } from 'react'; export const StagingAreaIsStagingGate = memo((props: PropsWithChildren) => { - const isStaging = useAppSelector((s) => s.canvasSession.isStaging); + const isStaging = useAppSelector(selectIsStaging); if (!isStaging) { return null; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx index 12d56517f5d..8d875eb8999 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx @@ -1,9 +1,11 @@ import { Button, ButtonGroup, IconButton } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { INTERACTION_SCOPES, useScopeOnMount } from 'common/hooks/interactionScopes'; import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { + selectCanvasSessionSlice, sessionNextStagedImageSelected, sessionPrevStagedImageSelected, sessionStagedImageDiscarded, @@ -25,15 +27,25 @@ import { } from 'react-icons/pi'; import { useChangeImageIsIntermediateMutation } from 'services/api/endpoints/images'; +const selectStagedImageIndex = createSelector( + selectCanvasSessionSlice, + (canvasSession) => canvasSession.selectedStagedImageIndex +); + +const selectSelectedImage = createSelector( + [selectCanvasSessionSlice, selectStagedImageIndex], + (canvasSession, index) => canvasSession.stagedImages[index] ?? null +); + +const selectImageCount = createSelector(selectCanvasSessionSlice, (canvasSession) => canvasSession.stagedImages.length); + export const StagingAreaToolbar = memo(() => { const dispatch = useAppDispatch(); - const session = useAppSelector((s) => s.canvasSession); const canvasManager = useCanvasManager(); + const index = useAppSelector(selectStagedImageIndex); + const selectedImage = useAppSelector(selectSelectedImage); + const imageCount = useAppSelector(selectImageCount); const shouldShowStagedImage = useStore(canvasManager.stateApi.$shouldShowStagedImage); - const images = useMemo(() => session.stagedImages, [session]); - const selectedImage = useMemo(() => { - return images[session.selectedStagedImageIndex] ?? null; - }, [images, session.selectedStagedImageIndex]); const isCanvasActive = useStore(INTERACTION_SCOPES.canvas.$isActive); const [changeIsImageIntermediate] = useChangeImageIsIntermediateMutation(); useScopeOnMount('stagingArea'); @@ -52,19 +64,19 @@ export const StagingAreaToolbar = memo(() => { if (!selectedImage) { return; } - dispatch(sessionStagingAreaImageAccepted({ index: session.selectedStagedImageIndex })); - }, [dispatch, selectedImage, session.selectedStagedImageIndex]); + dispatch(sessionStagingAreaImageAccepted({ index })); + }, [dispatch, index, selectedImage]); const onDiscardOne = useCallback(() => { if (!selectedImage) { return; } - if (images.length === 1) { + if (imageCount === 1) { dispatch(sessionStagingAreaReset()); } else { - dispatch(sessionStagedImageDiscarded({ index: session.selectedStagedImageIndex })); + dispatch(sessionStagedImageDiscarded({ index })); } - }, [selectedImage, images.length, dispatch, session.selectedStagedImageIndex]); + }, [selectedImage, imageCount, dispatch, index]); const onDiscardAll = useCallback(() => { dispatch(sessionStagingAreaReset()); @@ -112,12 +124,12 @@ export const StagingAreaToolbar = memo(() => { ); const counterText = useMemo(() => { - if (images.length > 0) { - return `${(session.selectedStagedImageIndex ?? 0) + 1} of ${images.length}`; + if (imageCount > 0) { + return `${(index ?? 0) + 1} of ${imageCount}`; } else { return `0 of 0`; } - }, [images.length, session.selectedStagedImageIndex]); + }, [imageCount, index]); return ( <> @@ -128,7 +140,7 @@ export const StagingAreaToolbar = memo(() => { icon={} onClick={onPrev} colorScheme="invokeBlue" - isDisabled={images.length <= 1 || !shouldShowStagedImage} + isDisabled={imageCount <= 1 || !shouldShowStagedImage} /> + ); +}); + +CanvasSettingsClearHistoryButton.displayName = 'CanvasSettingsClearHistoryButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsPopover.tsx index 34f1a11e4ec..f0e39e9afca 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsPopover.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsPopover.tsx @@ -11,6 +11,7 @@ import { } from '@invoke-ai/ui-library'; import { CanvasSettingsAutoSaveCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsAutoSaveCheckbox'; import { CanvasSettingsClearCachesButton } from 'features/controlLayers/components/Settings/CanvasSettingsClearCachesButton'; +import { CanvasSettingsClearHistoryButton } from 'features/controlLayers/components/Settings/CanvasSettingsClearHistoryButton'; import { CanvasSettingsClipToBboxCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox'; import { CanvasSettingsDynamicGridSwitch } from 'features/controlLayers/components/Settings/CanvasSettingsDynamicGridSwitch'; import { CanvasSettingsInvertScrollCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox'; @@ -60,6 +61,7 @@ const DebugSettings = () => { + ); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/UndoRedoButtonGroup.tsx b/invokeai/frontend/web/src/features/controlLayers/components/UndoRedoButtonGroup.tsx index df90d86496a..ec8ced184b6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/UndoRedoButtonGroup.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/UndoRedoButtonGroup.tsx @@ -1,28 +1,27 @@ /* eslint-disable i18next/no-literal-string */ import { ButtonGroup, IconButton } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; +import { canvasRedo, canvasUndo } from 'features/controlLayers/store/canvasSlice'; +import { selectCanvasMayRedo, selectCanvasMayUndo } from 'features/controlLayers/store/selectors'; import { memo, useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiArrowClockwiseBold, PiArrowCounterClockwiseBold } from 'react-icons/pi'; import { useDispatch } from 'react-redux'; -import { ActionCreators } from 'redux-undo'; export const UndoRedoButtonGroup = memo(() => { const { t } = useTranslation(); const dispatch = useDispatch(); - const mayUndo = useAppSelector(() => true); + const mayUndo = useAppSelector(selectCanvasMayUndo); const handleUndo = useCallback(() => { - // TODO(psyche): Implement undo - dispatch(ActionCreators.undo()); + dispatch(canvasUndo()); }, [dispatch]); useHotkeys(['meta+z', 'ctrl+z'], handleUndo, { enabled: mayUndo, preventDefault: true }, [mayUndo, handleUndo]); - const mayRedo = useAppSelector(() => true); + const mayRedo = useAppSelector(selectCanvasMayRedo); const handleRedo = useCallback(() => { - // TODO(psyche): Implement redo - dispatch(ActionCreators.redo()); + dispatch(canvasRedo()); }, [dispatch]); useHotkeys(['meta+shift+z', 'ctrl+shift+z'], handleRedo, { enabled: mayRedo, preventDefault: true }, [ mayRedo, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index 9c8e3168516..b5daddf355f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -601,10 +601,12 @@ export class CanvasTransformer extends CanvasModuleBase { // If the bbox has no width or height, that means the layer is fully transparent. This can happen if it is only // eraser lines, fully clipped brush lines or if it has been fully erased. if (this.pixelRect.width === 0 || this.pixelRect.height === 0) { - // We shouldn't reset on the first render - the bbox will be calculated on the next render - // The layer is fully transparent but has objects - reset it - this.manager.stateApi.resetEntity({ entityIdentifier: this.parent.getEntityIdentifier() }); - this.syncInteractionState(); + // If the layer already has no objects, we don't need to reset the entity state. This would cause a push to the + // undo stack and clear the redo stack. + if (this.parent.renderer.hasObjects()) { + this.manager.stateApi.resetEntity({ entityIdentifier: this.parent.getEntityIdentifier() }); + this.syncInteractionState(); + } } else { this.syncInteractionState(); this.update(this.parent.state.position, this.pixelRect); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index 9a466c70ea7..3b0a729c35d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -1,4 +1,4 @@ -import type { PayloadAction } from '@reduxjs/toolkit'; +import type { PayloadAction, UnknownAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import type { PersistConfig } from 'app/store/store'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; @@ -27,6 +27,7 @@ import type { AspectRatioID } from 'features/parameters/components/DocumentSize/ import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension'; import type { IRect } from 'konva/lib/types'; import { isEqual, merge, omit } from 'lodash-es'; +import type { UndoableOptions } from 'redux-undo'; import type { ControlNetModelConfig, ImageDTO, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; @@ -1032,6 +1033,9 @@ export const canvasSlice = createSlice({ newState.bbox.scaledSize = scaledSize; return newState; }, + canvasUndo: () => {}, + canvasRedo: () => {}, + canvasClearHistory: () => {}, }, extraReducers(builder) { builder.addCase(modelChanged, (state, action) => { @@ -1062,6 +1066,9 @@ export const canvasSlice = createSlice({ export const { canvasReset, + canvasUndo, + canvasRedo, + canvasClearHistory, // All entities entitySelected, entityNameChanged, @@ -1154,3 +1161,47 @@ const syncScaledSize = (state: CanvasState) => { state.bbox.scaledSize = getScaledBoundingBoxDimensions({ width, height }, state.bbox.optimalDimension); } }; + +let filter = true; + +export const canvasUndoableConfig: UndoableOptions = { + limit: 64, + undoType: canvasUndo.type, + redoType: canvasRedo.type, + clearHistoryType: canvasClearHistory.type, + filter: (action, _state, _history) => { + // Ignore all actions from other slices + if (!action.type.startsWith(canvasSlice.name)) { + return false; + } + // Throttle rapid actions of the same type + filter = actionsThrottlingFilter(action); + return filter; + }, + // This is pretty spammy, leave commented out unless you need it + // debug: import.meta.env.MODE === 'development', +}; + +// Store rapid actions of the same type at most once every x time. +// See: https://github.com/omnidan/redux-undo/blob/master/examples/throttled-drag/util/undoFilter.js +const THROTTLE_MS = 1000; +let ignoreRapid = false; +let prevActionType: string | null = null; +function actionsThrottlingFilter(action: UnknownAction) { + // If the actions are of a different type, reset the throttle and allow the action + if (action.type !== prevActionType) { + ignoreRapid = false; + prevActionType = action.type; + return true; + } + // Else, if the actions are of the same type, throttle them. Ignore the action if the flag is set. + if (ignoreRapid) { + return false; + } + // We are allowing this action - set the flag and a timeout to clear it. + ignoreRapid = true; + window.setTimeout(() => { + ignoreRapid = false; + }, THROTTLE_MS); + return true; +} diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts index 91806809111..92acd8ac1e8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts @@ -190,3 +190,6 @@ export const selectIsSelectedEntityDrawable = createSelector( return isDrawableEntityType(selectedEntityIdentifier.type); } ); + +export const selectCanvasMayUndo = (state: RootState) => state.canvas.past.length > 0; +export const selectCanvasMayRedo = (state: RootState) => state.canvas.future.length > 0; From 11bc318d8dd347de0335e66b6eb57bdcfb65d9a2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 27 Aug 2024 17:27:38 +1000 Subject: [PATCH 513/678] tidy(ui): rename some classes to be consistent --- .../contexts/EntityAdapterContext.tsx | 12 ++-- .../controlLayers/hooks/useEntityAdapter.ts | 8 ++- .../konva/CanvasBackgroundModule.ts | 4 +- .../controlLayers/konva/CanvasBboxModule.ts | 4 +- .../controlLayers/konva/CanvasCacheModule.ts | 4 +- .../konva/CanvasCompositorModule.ts | 4 +- ...Adapter.ts => CanvasEntityLayerAdapter.ts} | 22 +++--- ...kAdapter.ts => CanvasEntityMaskAdapter.ts} | 24 +++---- ...ectRenderer.ts => CanvasEntityRenderer.ts} | 68 ++++++++++--------- ...nsformer.ts => CanvasEntityTransformer.ts} | 64 ++++++++--------- .../controlLayers/konva/CanvasFilterModule.ts | 8 +-- .../controlLayers/konva/CanvasManager.ts | 18 ++--- ...CanvasModuleBase.ts => CanvasModuleABC.ts} | 2 +- ...ne.ts => CanvasObjectBrushLineRenderer.ts} | 12 ++-- ...e.ts => CanvasObjectEraserLineRenderer.ts} | 12 ++-- ...sImage.ts => CanvasObjectImageRenderer.ts} | 12 ++-- ...vasRect.ts => CanvasObjectRectRenderer.ts} | 12 ++-- .../konva/CanvasPreviewModule.ts | 4 +- .../konva/CanvasProgressImageModule.ts | 4 +- .../konva/CanvasRenderingModule.ts | 16 ++--- .../controlLayers/konva/CanvasStageModule.ts | 4 +- .../konva/CanvasStagingAreaModule.ts | 10 +-- .../konva/CanvasStateApiModule.ts | 16 ++--- .../controlLayers/konva/CanvasToolModule.ts | 4 +- .../controlLayers/konva/CanvasWorkerModule.ts | 4 +- 25 files changed, 180 insertions(+), 172 deletions(-) rename invokeai/frontend/web/src/features/controlLayers/konva/{CanvasLayerAdapter.ts => CanvasEntityLayerAdapter.ts} (90%) rename invokeai/frontend/web/src/features/controlLayers/konva/{CanvasMaskAdapter.ts => CanvasEntityMaskAdapter.ts} (85%) rename invokeai/frontend/web/src/features/controlLayers/konva/{CanvasObjectRenderer.ts => CanvasEntityRenderer.ts} (88%) rename invokeai/frontend/web/src/features/controlLayers/konva/{CanvasTransformer.ts => CanvasEntityTransformer.ts} (92%) rename invokeai/frontend/web/src/features/controlLayers/konva/{CanvasModuleBase.ts => CanvasModuleABC.ts} (93%) rename invokeai/frontend/web/src/features/controlLayers/konva/{CanvasBrushLine.ts => CanvasObjectBrushLineRenderer.ts} (86%) rename invokeai/frontend/web/src/features/controlLayers/konva/{CanvasEraserLine.ts => CanvasObjectEraserLineRenderer.ts} (86%) rename invokeai/frontend/web/src/features/controlLayers/konva/{CanvasImage.ts => CanvasObjectImageRenderer.ts} (93%) rename invokeai/frontend/web/src/features/controlLayers/konva/{CanvasRect.ts => CanvasObjectRectRenderer.ts} (85%) diff --git a/invokeai/frontend/web/src/features/controlLayers/contexts/EntityAdapterContext.tsx b/invokeai/frontend/web/src/features/controlLayers/contexts/EntityAdapterContext.tsx index cd0c455b55c..42f99875a3b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/contexts/EntityAdapterContext.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/contexts/EntityAdapterContext.tsx @@ -1,18 +1,18 @@ import type { SyncableMap } from 'common/util/SyncableMap/SyncableMap'; import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter'; -import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; +import type { CanvasEntityLayerAdapter } from 'features/controlLayers/konva/CanvasEntityLayerAdapter'; +import type { CanvasEntityMaskAdapter } from 'features/controlLayers/konva/CanvasEntityMaskAdapter'; import type { PropsWithChildren } from 'react'; import { createContext, memo, useContext, useMemo, useSyncExternalStore } from 'react'; import { assert } from 'tsafe'; -const EntityAdapterContext = createContext(null); +const EntityAdapterContext = createContext(null); export const EntityLayerAdapterGate = memo(({ children }: PropsWithChildren) => { const canvasManager = useCanvasManager(); const entityIdentifier = useEntityIdentifierContext(); - const store = useMemo>(() => { + const store = useMemo>(() => { if (entityIdentifier.type === 'raster_layer') { return canvasManager.adapters.rasterLayers; } @@ -45,7 +45,7 @@ EntityLayerAdapterGate.displayName = 'EntityLayerAdapterGate'; export const EntityMaskAdapterGate = memo(({ children }: PropsWithChildren) => { const canvasManager = useCanvasManager(); const entityIdentifier = useEntityIdentifierContext(); - const store = useMemo>(() => { + const store = useMemo>(() => { if (entityIdentifier.type === 'inpaint_mask') { return canvasManager.adapters.inpaintMasks; } @@ -75,7 +75,7 @@ EntityMaskAdapterGate.displayName = 'EntityMaskAdapterGate'; // return adapter; // }; -export const useEntityAdapter = (): CanvasLayerAdapter | CanvasMaskAdapter => { +export const useEntityAdapter = (): CanvasEntityLayerAdapter | CanvasEntityMaskAdapter => { const adapter = useContext(EntityAdapterContext); assert(adapter, 'useEntityAdapter must be used within a CanvasRasterLayerAdapterGate'); return adapter; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityAdapter.ts index 9913faa3a4f..246156e8eed 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityAdapter.ts @@ -1,11 +1,13 @@ import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; -import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter'; -import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; +import type { CanvasEntityLayerAdapter } from 'features/controlLayers/konva/CanvasEntityLayerAdapter'; +import type { CanvasEntityMaskAdapter } from 'features/controlLayers/konva/CanvasEntityMaskAdapter'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; import { assert } from 'tsafe'; -export const useEntityAdapter = (entityIdentifier: CanvasEntityIdentifier): CanvasLayerAdapter | CanvasMaskAdapter => { +export const useEntityAdapter = ( + entityIdentifier: CanvasEntityIdentifier +): CanvasEntityLayerAdapter | CanvasEntityMaskAdapter => { const canvasManager = useCanvasManager(); const adapter = useMemo(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts index b8f0bd6b2dd..042921eabc9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts @@ -1,11 +1,11 @@ import { getArbitraryBaseColor } from '@invoke-ai/ui-library'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; +import { CanvasModuleABC } from 'features/controlLayers/konva/CanvasModuleABC'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import Konva from 'konva'; import type { Logger } from 'roarr'; -export class CanvasBackgroundModule extends CanvasModuleBase { +export class CanvasBackgroundModule extends CanvasModuleABC { readonly type = 'background'; static GRID_LINE_COLOR_COARSE = getArbitraryBaseColor(27); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts index a84ba5c713e..a9512f626d4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts @@ -1,7 +1,7 @@ import type { SerializableObject } from 'common/types'; import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMultiple'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; +import { CanvasModuleABC } from 'features/controlLayers/konva/CanvasModuleABC'; import type { CanvasPreviewModule } from 'features/controlLayers/konva/CanvasPreviewModule'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { Rect } from 'features/controlLayers/store/types'; @@ -23,7 +23,7 @@ const ALL_ANCHORS: string[] = [ const CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right']; const NO_ANCHORS: string[] = []; -export class CanvasBboxModule extends CanvasModuleBase { +export class CanvasBboxModule extends CanvasModuleABC { readonly type = 'bbox'; id: string; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCacheModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCacheModule.ts index 90feeed82b2..86f3011ca90 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCacheModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCacheModule.ts @@ -1,11 +1,11 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; +import { CanvasModuleABC } from 'features/controlLayers/konva/CanvasModuleABC'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { GenerationMode } from 'features/controlLayers/store/types'; import { LRUCache } from 'lru-cache'; import type { Logger } from 'roarr'; -export class CanvasCacheModule extends CanvasModuleBase { +export class CanvasCacheModule extends CanvasModuleABC { readonly type = 'cache'; id: string; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts index 2a93e79b145..3302e783f29 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts @@ -1,6 +1,6 @@ import type { SerializableObject } from 'common/types'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; +import { CanvasModuleABC } from 'features/controlLayers/konva/CanvasModuleABC'; import { canvasToBlob, canvasToImageData, @@ -15,7 +15,7 @@ import type { ImageDTO } from 'services/api/types'; import stableHash from 'stable-hash'; import { assert } from 'tsafe'; -export class CanvasCompositorModule extends CanvasModuleBase { +export class CanvasCompositorModule extends CanvasModuleABC { readonly type = 'compositor'; id: string; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityLayerAdapter.ts similarity index 90% rename from invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityLayerAdapter.ts index 50ef8faa47b..3f59044bbc6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityLayerAdapter.ts @@ -1,9 +1,9 @@ import type { SerializableObject } from 'common/types'; import { deepClone } from 'common/util/deepClone'; +import { CanvasEntityRenderer } from 'features/controlLayers/konva/CanvasEntityRenderer'; +import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntityTransformer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; -import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; -import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; +import { CanvasModuleABC } from 'features/controlLayers/konva/CanvasModuleABC'; import { getLastPointOfLine } from 'features/controlLayers/konva/util'; import type { CanvasBrushLineState, @@ -22,8 +22,8 @@ import type { Logger } from 'roarr'; import stableHash from 'stable-hash'; import { assert } from 'tsafe'; -export class CanvasLayerAdapter extends CanvasModuleBase { - readonly type = 'layer_adapter'; +export class CanvasEntityLayerAdapter extends CanvasModuleABC { + readonly type = 'entity_layer_adapter'; id: string; path: string[]; @@ -36,12 +36,12 @@ export class CanvasLayerAdapter extends CanvasModuleBase { konva: { layer: Konva.Layer; }; - transformer: CanvasTransformer; - renderer: CanvasObjectRenderer; + transformer: CanvasEntityTransformer; + renderer: CanvasEntityRenderer; isFirstRender: boolean = true; - constructor(state: CanvasLayerAdapter['state'], manager: CanvasLayerAdapter['manager']) { + constructor(state: CanvasEntityLayerAdapter['state'], manager: CanvasEntityLayerAdapter['manager']) { super(); this.id = state.id; this.manager = manager; @@ -63,8 +63,8 @@ export class CanvasLayerAdapter extends CanvasModuleBase { }), }; - this.renderer = new CanvasObjectRenderer(this); - this.transformer = new CanvasTransformer(this); + this.renderer = new CanvasEntityRenderer(this); + this.transformer = new CanvasEntityTransformer(this); } /** @@ -82,7 +82,7 @@ export class CanvasLayerAdapter extends CanvasModuleBase { this.konva.layer.destroy(); }; - update = async (arg?: { state: CanvasLayerAdapter['state'] }) => { + update = async (arg?: { state: CanvasEntityLayerAdapter['state'] }) => { const state = get(arg, 'state', this.state); if (!this.isFirstRender && state === this.state) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityMaskAdapter.ts similarity index 85% rename from invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityMaskAdapter.ts index cd0f16d73cf..45a5cd3bbae 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityMaskAdapter.ts @@ -1,9 +1,9 @@ import type { SerializableObject } from 'common/types'; import { deepClone } from 'common/util/deepClone'; +import { CanvasEntityRenderer } from 'features/controlLayers/konva/CanvasEntityRenderer'; +import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntityTransformer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; -import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; -import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; +import { CanvasModuleABC } from 'features/controlLayers/konva/CanvasModuleABC'; import { getLastPointOfLine } from 'features/controlLayers/konva/util'; import type { CanvasBrushLineState, @@ -21,8 +21,8 @@ import { get, omit } from 'lodash-es'; import type { Logger } from 'roarr'; import stableHash from 'stable-hash'; -export class CanvasMaskAdapter extends CanvasModuleBase { - readonly type = 'mask_adapter'; +export class CanvasEntityMaskAdapter extends CanvasModuleABC { + readonly type = 'entity_mask_adapter'; id: string; path: string[]; @@ -32,8 +32,8 @@ export class CanvasMaskAdapter extends CanvasModuleBase { state: CanvasInpaintMaskState | CanvasRegionalGuidanceState; - transformer: CanvasTransformer; - renderer: CanvasObjectRenderer; + transformer: CanvasEntityTransformer; + renderer: CanvasEntityRenderer; isFirstRender: boolean = true; @@ -41,7 +41,7 @@ export class CanvasMaskAdapter extends CanvasModuleBase { layer: Konva.Layer; }; - constructor(state: CanvasMaskAdapter['state'], manager: CanvasMaskAdapter['manager']) { + constructor(state: CanvasEntityMaskAdapter['state'], manager: CanvasEntityMaskAdapter['manager']) { super(); this.id = state.id; this.manager = manager; @@ -63,8 +63,8 @@ export class CanvasMaskAdapter extends CanvasModuleBase { }), }; - this.renderer = new CanvasObjectRenderer(this); - this.transformer = new CanvasTransformer(this); + this.renderer = new CanvasEntityRenderer(this); + this.transformer = new CanvasEntityTransformer(this); } /** @@ -82,7 +82,7 @@ export class CanvasMaskAdapter extends CanvasModuleBase { this.konva.layer.destroy(); }; - update = async (arg?: { state: CanvasMaskAdapter['state'] }) => { + update = async (arg?: { state: CanvasEntityMaskAdapter['state'] }) => { const state = get(arg, 'state', this.state); if (!this.isFirstRender && state === this.state && state.fill === this.state.fill) { @@ -163,7 +163,7 @@ export class CanvasMaskAdapter extends CanvasModuleBase { }; getHashableState = (): SerializableObject => { - const keysToOmit: (keyof CanvasMaskAdapter['state'])[] = ['fill', 'name', 'opacity']; + const keysToOmit: (keyof CanvasEntityMaskAdapter['state'])[] = ['fill', 'name', 'opacity']; return omit(this.state, keysToOmit); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts similarity index 88% rename from invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts index b586976f1a6..5a87d708ac0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts @@ -1,12 +1,12 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; -import { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine'; -import { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine'; -import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; -import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter'; +import type { CanvasEntityLayerAdapter } from 'features/controlLayers/konva/CanvasEntityLayerAdapter'; +import type { CanvasEntityMaskAdapter } from 'features/controlLayers/konva/CanvasEntityMaskAdapter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; -import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; +import { CanvasModuleABC } from 'features/controlLayers/konva/CanvasModuleABC'; +import { CanvasObjectBrushLineRenderer } from 'features/controlLayers/konva/CanvasObjectBrushLineRenderer'; +import { CanvasObjectEraserLineRenderer } from 'features/controlLayers/konva/CanvasObjectEraserLineRenderer'; +import { CanvasObjectImageRenderer } from 'features/controlLayers/konva/CanvasObjectImageRenderer'; +import { CanvasObjectRectRenderer } from 'features/controlLayers/konva/CanvasObjectRectRenderer'; import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; import { getPatternSVG } from 'features/controlLayers/konva/patterns/getPatternSVG'; import { @@ -47,7 +47,11 @@ function setFillPatternImage(shape: Konva.Shape, ...args: Parameters(null); - constructor(parent: CanvasLayerAdapter | CanvasMaskAdapter) { + constructor(parent: CanvasEntityLayerAdapter | CanvasEntityMaskAdapter) { super(); this.id = getPrefixedId(this.type); this.parent = parent; @@ -171,7 +175,7 @@ export class CanvasObjectRenderer extends CanvasModuleBase { // need to update the compositing rect to match the stage. this.subscriptions.add( this.manager.stateApi.$stageAttrs.listen(() => { - if (this.konva.compositing && this.parent.type === 'mask_adapter') { + if (this.konva.compositing && this.parent.type === 'entity_mask_adapter') { this.updateCompositingRectSize(); } }) @@ -282,40 +286,40 @@ export class CanvasObjectRenderer extends CanvasModuleBase { const isFirstRender = !renderer; if (objectState.type === 'brush_line') { - assert(renderer instanceof CanvasBrushLineRenderer || !renderer); + assert(renderer instanceof CanvasObjectBrushLineRenderer || !renderer); if (!renderer) { - renderer = new CanvasBrushLineRenderer(objectState, this); + renderer = new CanvasObjectBrushLineRenderer(objectState, this); this.renderers.set(renderer.id, renderer); this.konva.objectGroup.add(renderer.konva.group); } didRender = renderer.update(objectState, force || isFirstRender); } else if (objectState.type === 'eraser_line') { - assert(renderer instanceof CanvasEraserLineRenderer || !renderer); + assert(renderer instanceof CanvasObjectEraserLineRenderer || !renderer); if (!renderer) { - renderer = new CanvasEraserLineRenderer(objectState, this); + renderer = new CanvasObjectEraserLineRenderer(objectState, this); this.renderers.set(renderer.id, renderer); this.konva.objectGroup.add(renderer.konva.group); } didRender = renderer.update(objectState, force || isFirstRender); } else if (objectState.type === 'rect') { - assert(renderer instanceof CanvasRectRenderer || !renderer); + assert(renderer instanceof CanvasObjectRectRenderer || !renderer); if (!renderer) { - renderer = new CanvasRectRenderer(objectState, this); + renderer = new CanvasObjectRectRenderer(objectState, this); this.renderers.set(renderer.id, renderer); this.konva.objectGroup.add(renderer.konva.group); } didRender = renderer.update(objectState, force || isFirstRender); } else if (objectState.type === 'image') { - assert(renderer instanceof CanvasImageRenderer || !renderer); + assert(renderer instanceof CanvasObjectImageRenderer || !renderer); if (!renderer) { - renderer = new CanvasImageRenderer(objectState, this); + renderer = new CanvasObjectImageRenderer(objectState, this); this.renderers.set(renderer.id, renderer); this.konva.objectGroup.add(renderer.konva.group); } @@ -342,37 +346,37 @@ export class CanvasObjectRenderer extends CanvasModuleBase { } if (this.bufferState.type === 'brush_line') { - assert(this.bufferRenderer instanceof CanvasBrushLineRenderer || !this.bufferRenderer); + assert(this.bufferRenderer instanceof CanvasObjectBrushLineRenderer || !this.bufferRenderer); if (!this.bufferRenderer) { - this.bufferRenderer = new CanvasBrushLineRenderer(this.bufferState, this); + this.bufferRenderer = new CanvasObjectBrushLineRenderer(this.bufferState, this); this.konva.bufferGroup.add(this.bufferRenderer.konva.group); } didRender = this.bufferRenderer.update(this.bufferState, true); } else if (this.bufferState.type === 'eraser_line') { - assert(this.bufferRenderer instanceof CanvasEraserLineRenderer || !this.bufferRenderer); + assert(this.bufferRenderer instanceof CanvasObjectEraserLineRenderer || !this.bufferRenderer); if (!this.bufferRenderer) { - this.bufferRenderer = new CanvasEraserLineRenderer(this.bufferState, this); + this.bufferRenderer = new CanvasObjectEraserLineRenderer(this.bufferState, this); this.konva.bufferGroup.add(this.bufferRenderer.konva.group); } didRender = this.bufferRenderer.update(this.bufferState, true); } else if (this.bufferState.type === 'rect') { - assert(this.bufferRenderer instanceof CanvasRectRenderer || !this.bufferRenderer); + assert(this.bufferRenderer instanceof CanvasObjectRectRenderer || !this.bufferRenderer); if (!this.bufferRenderer) { - this.bufferRenderer = new CanvasRectRenderer(this.bufferState, this); + this.bufferRenderer = new CanvasObjectRectRenderer(this.bufferState, this); this.konva.bufferGroup.add(this.bufferRenderer.konva.group); } didRender = this.bufferRenderer.update(this.bufferState, true); } else if (this.bufferState.type === 'image') { - assert(this.bufferRenderer instanceof CanvasImageRenderer || !this.bufferRenderer); + assert(this.bufferRenderer instanceof CanvasObjectImageRenderer || !this.bufferRenderer); if (!this.bufferRenderer) { - this.bufferRenderer = new CanvasImageRenderer(this.bufferState, this); + this.bufferRenderer = new CanvasObjectImageRenderer(this.bufferState, this); this.konva.bufferGroup.add(this.bufferRenderer.konva.group); } didRender = await this.bufferRenderer.update(this.bufferState, true); @@ -478,9 +482,9 @@ export class CanvasObjectRenderer extends CanvasModuleBase { needsPixelBbox = (): boolean => { let needsPixelBbox = false; for (const renderer of this.renderers.values()) { - const isEraserLine = renderer instanceof CanvasEraserLineRenderer; - const isImage = renderer instanceof CanvasImageRenderer; - const hasClip = renderer instanceof CanvasBrushLineRenderer && renderer.state.clip; + const isEraserLine = renderer instanceof CanvasObjectEraserLineRenderer; + const isImage = renderer instanceof CanvasObjectImageRenderer; + const hasClip = renderer instanceof CanvasObjectBrushLineRenderer && renderer.state.clip; if (isEraserLine || hasClip || isImage) { needsPixelBbox = true; break; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts similarity index 92% rename from invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts index b5daddf355f..f49b4b2f0e0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts @@ -1,7 +1,7 @@ -import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter'; +import type { CanvasEntityLayerAdapter } from 'features/controlLayers/konva/CanvasEntityLayerAdapter'; +import type { CanvasEntityMaskAdapter } from 'features/controlLayers/konva/CanvasEntityMaskAdapter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; +import { CanvasModuleABC } from 'features/controlLayers/konva/CanvasModuleABC'; import { canvasToImageData, getEmptyRect, getPrefixedId } from 'features/controlLayers/konva/util'; import type { Coordinate, Rect } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -18,14 +18,14 @@ import type { Logger } from 'roarr'; * * It renders an outline when dragging and resizing the entity, with transform anchors for resizing and rotation. */ -export class CanvasTransformer extends CanvasModuleBase { +export class CanvasEntityTransformer extends CanvasModuleABC { readonly type = 'entity_transformer'; static RECT_CALC_DEBOUNCE_MS = 300; static OUTLINE_PADDING = 0; static OUTLINE_COLOR = 'hsl(200 76% 50% / 1)'; // invokeBlue.500 - static ANCHOR_FILL_COLOR = CanvasTransformer.OUTLINE_COLOR; + static ANCHOR_FILL_COLOR = CanvasEntityTransformer.OUTLINE_COLOR; static ANCHOR_STROKE_COLOR = 'hsl(200 76% 77% / 1)'; // invokeBlue.200 static ANCHOR_CORNER_RADIUS_RATIO = 0.5; static ANCHOR_STROKE_WIDTH = 2; @@ -39,7 +39,7 @@ export class CanvasTransformer extends CanvasModuleBase { id: string; path: string[]; - parent: CanvasLayerAdapter | CanvasMaskAdapter; + parent: CanvasEntityLayerAdapter | CanvasEntityMaskAdapter; manager: CanvasManager; log: Logger; @@ -98,7 +98,7 @@ export class CanvasTransformer extends CanvasModuleBase { outlineRect: Konva.Rect; }; - constructor(parent: CanvasLayerAdapter | CanvasMaskAdapter) { + constructor(parent: CanvasEntityLayerAdapter | CanvasEntityMaskAdapter) { super(); this.id = getPrefixedId(this.type); this.parent = parent; @@ -112,7 +112,7 @@ export class CanvasTransformer extends CanvasModuleBase { listening: false, draggable: false, name: `${this.type}:outline_rect`, - stroke: CanvasTransformer.OUTLINE_COLOR, + stroke: CanvasEntityTransformer.OUTLINE_COLOR, perfectDrawEnabled: false, strokeHitEnabled: false, }), @@ -128,36 +128,38 @@ export class CanvasTransformer extends CanvasModuleBase { // Transforming will retain aspect ratio only when shift is held keepRatio: false, // The padding is the distance between the transformer bbox and the nodes - padding: CanvasTransformer.OUTLINE_PADDING, + padding: CanvasEntityTransformer.OUTLINE_PADDING, // This is `invokeBlue.400` - stroke: CanvasTransformer.OUTLINE_COLOR, - anchorFill: CanvasTransformer.ANCHOR_FILL_COLOR, - anchorStroke: CanvasTransformer.ANCHOR_STROKE_COLOR, - anchorStrokeWidth: CanvasTransformer.ANCHOR_STROKE_WIDTH, - anchorSize: CanvasTransformer.RESIZE_ANCHOR_SIZE, - anchorCornerRadius: CanvasTransformer.RESIZE_ANCHOR_SIZE * CanvasTransformer.ANCHOR_CORNER_RADIUS_RATIO, + stroke: CanvasEntityTransformer.OUTLINE_COLOR, + anchorFill: CanvasEntityTransformer.ANCHOR_FILL_COLOR, + anchorStroke: CanvasEntityTransformer.ANCHOR_STROKE_COLOR, + anchorStrokeWidth: CanvasEntityTransformer.ANCHOR_STROKE_WIDTH, + anchorSize: CanvasEntityTransformer.RESIZE_ANCHOR_SIZE, + anchorCornerRadius: + CanvasEntityTransformer.RESIZE_ANCHOR_SIZE * CanvasEntityTransformer.ANCHOR_CORNER_RADIUS_RATIO, // This function is called for each anchor to style it (and do anything else you might want to do). anchorStyleFunc: (anchor) => { // Give the rotater special styling if (anchor.hasName('rotater')) { anchor.setAttrs({ - height: CanvasTransformer.ROTATE_ANCHOR_SIZE, - width: CanvasTransformer.ROTATE_ANCHOR_SIZE, - cornerRadius: CanvasTransformer.ROTATE_ANCHOR_SIZE * CanvasTransformer.ANCHOR_CORNER_RADIUS_RATIO, - fill: CanvasTransformer.ROTATE_ANCHOR_FILL_COLOR, - stroke: CanvasTransformer.ANCHOR_FILL_COLOR, - offsetX: CanvasTransformer.ROTATE_ANCHOR_SIZE / 2, - offsetY: CanvasTransformer.ROTATE_ANCHOR_SIZE / 2, + height: CanvasEntityTransformer.ROTATE_ANCHOR_SIZE, + width: CanvasEntityTransformer.ROTATE_ANCHOR_SIZE, + cornerRadius: + CanvasEntityTransformer.ROTATE_ANCHOR_SIZE * CanvasEntityTransformer.ANCHOR_CORNER_RADIUS_RATIO, + fill: CanvasEntityTransformer.ROTATE_ANCHOR_FILL_COLOR, + stroke: CanvasEntityTransformer.ANCHOR_FILL_COLOR, + offsetX: CanvasEntityTransformer.ROTATE_ANCHOR_SIZE / 2, + offsetY: CanvasEntityTransformer.ROTATE_ANCHOR_SIZE / 2, }); } // Add some padding to the hit area of the anchors anchor.hitFunc((context) => { context.beginPath(); context.rect( - -CanvasTransformer.ANCHOR_HIT_PADDING, - -CanvasTransformer.ANCHOR_HIT_PADDING, - anchor.width() + CanvasTransformer.ANCHOR_HIT_PADDING * 2, - anchor.height() + CanvasTransformer.ANCHOR_HIT_PADDING * 2 + -CanvasEntityTransformer.ANCHOR_HIT_PADDING, + -CanvasEntityTransformer.ANCHOR_HIT_PADDING, + anchor.width() + CanvasEntityTransformer.ANCHOR_HIT_PADDING * 2, + anchor.height() + CanvasEntityTransformer.ANCHOR_HIT_PADDING * 2 ); context.closePath(); context.fillStrokeShape(anchor); @@ -337,8 +339,8 @@ export class CanvasTransformer extends CanvasModuleBase { // The bbox should be updated to reflect the new position of the interaction rect, taking into account its padding // and border this.konva.outlineRect.setAttrs({ - x: this.konva.proxyRect.x() - this.manager.stage.getScaledPixels(CanvasTransformer.OUTLINE_PADDING), - y: this.konva.proxyRect.y() - this.manager.stage.getScaledPixels(CanvasTransformer.OUTLINE_PADDING), + x: this.konva.proxyRect.x() - this.manager.stage.getScaledPixels(CanvasEntityTransformer.OUTLINE_PADDING), + y: this.konva.proxyRect.y() - this.manager.stage.getScaledPixels(CanvasEntityTransformer.OUTLINE_PADDING), }); // The object group is translated by the difference between the interaction rect's new and old positions (which is @@ -400,7 +402,7 @@ export class CanvasTransformer extends CanvasModuleBase { */ update = (position: Coordinate, bbox: Rect) => { const onePixel = this.manager.stage.getScaledPixels(1); - const bboxPadding = this.manager.stage.getScaledPixels(CanvasTransformer.OUTLINE_PADDING); + const bboxPadding = this.manager.stage.getScaledPixels(CanvasEntityTransformer.OUTLINE_PADDING); this.konva.outlineRect.setAttrs({ x: position.x + bbox.x - bboxPadding, @@ -467,7 +469,7 @@ export class CanvasTransformer extends CanvasModuleBase { */ syncScale = () => { const onePixel = this.manager.stage.getScaledPixels(1); - const bboxPadding = this.manager.stage.getScaledPixels(CanvasTransformer.OUTLINE_PADDING); + const bboxPadding = this.manager.stage.getScaledPixels(CanvasEntityTransformer.OUTLINE_PADDING); this.konva.outlineRect.setAttrs({ x: this.konva.proxyRect.x() - bboxPadding, @@ -672,7 +674,7 @@ export class CanvasTransformer extends CanvasModuleBase { this.updateBbox(); } ); - }, CanvasTransformer.RECT_CALC_DEBOUNCE_MS); + }, CanvasEntityTransformer.RECT_CALC_DEBOUNCE_MS); requestRectCalculation = () => { this.isPendingRectCalculation = true; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts index b0ce4268327..c7b70cb9d66 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts @@ -1,7 +1,7 @@ import type { SerializableObject } from 'common/types'; -import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter'; +import type { CanvasEntityLayerAdapter } from 'features/controlLayers/konva/CanvasEntityLayerAdapter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; +import { CanvasModuleABC } from 'features/controlLayers/konva/CanvasModuleABC'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { CanvasEntityIdentifier, CanvasImageState, FilterConfig } from 'features/controlLayers/store/types'; import { IMAGE_FILTERS, imageDTOToImageObject } from 'features/controlLayers/store/types'; @@ -11,7 +11,7 @@ import { getImageDTO } from 'services/api/endpoints/images'; import type { BatchConfig, ImageDTO, S } from 'services/api/types'; import { assert } from 'tsafe'; -export class CanvasFilterModule extends CanvasModuleBase { +export class CanvasFilterModule extends CanvasModuleABC { readonly type = 'canvas_filter'; id: string; @@ -22,7 +22,7 @@ export class CanvasFilterModule extends CanvasModuleBase { imageState: CanvasImageState | null = null; - $adapter = atom(null); + $adapter = atom(null); $isFiltering = computed(this.$adapter, (adapter) => Boolean(adapter)); $isProcessing = atom(false); $config = atom(IMAGE_FILTERS.canny_image_processor.buildDefaults()); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 072a7a5d122..6545cbe834d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -6,7 +6,7 @@ import { SyncableMap } from 'common/util/SyncableMap/SyncableMap'; import { CanvasCacheModule } from 'features/controlLayers/konva/CanvasCacheModule'; import { CanvasCompositorModule } from 'features/controlLayers/konva/CanvasCompositorModule'; import { CanvasFilterModule } from 'features/controlLayers/konva/CanvasFilterModule'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; +import { CanvasModuleABC } from 'features/controlLayers/konva/CanvasModuleABC'; import { CanvasRenderingModule } from 'features/controlLayers/konva/CanvasRenderingModule'; import { CanvasStageModule } from 'features/controlLayers/konva/CanvasStageModule'; import { CanvasWorkerModule } from 'features/controlLayers/konva/CanvasWorkerModule.js'; @@ -16,14 +16,14 @@ import { atom } from 'nanostores'; import type { Logger } from 'roarr'; import { CanvasBackgroundModule } from './CanvasBackgroundModule'; -import type { CanvasLayerAdapter } from './CanvasLayerAdapter'; -import type { CanvasMaskAdapter } from './CanvasMaskAdapter'; +import type { CanvasEntityLayerAdapter } from './CanvasEntityLayerAdapter'; +import type { CanvasEntityMaskAdapter } from './CanvasEntityMaskAdapter'; import { CanvasPreviewModule } from './CanvasPreviewModule'; import { CanvasStateApiModule } from './CanvasStateApiModule'; export const $canvasManager = atom(null); -export class CanvasManager extends CanvasModuleBase { +export class CanvasManager extends CanvasModuleABC { readonly type = 'manager'; id: string; @@ -37,11 +37,11 @@ export class CanvasManager extends CanvasModuleBase { subscriptions = new Set<() => void>(); adapters = { - rasterLayers: new SyncableMap(), - controlLayers: new SyncableMap(), - regionMasks: new SyncableMap(), - inpaintMasks: new SyncableMap(), - getAll: (): (CanvasLayerAdapter | CanvasMaskAdapter)[] => { + rasterLayers: new SyncableMap(), + controlLayers: new SyncableMap(), + regionMasks: new SyncableMap(), + inpaintMasks: new SyncableMap(), + getAll: (): (CanvasEntityLayerAdapter | CanvasEntityMaskAdapter)[] => { return [ ...this.adapters.rasterLayers.values(), ...this.adapters.controlLayers.values(), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasModuleBase.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasModuleABC.ts similarity index 93% rename from invokeai/frontend/web/src/features/controlLayers/konva/CanvasModuleBase.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/CanvasModuleABC.ts index 894c9622f7a..495565fb1e7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasModuleBase.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasModuleABC.ts @@ -2,7 +2,7 @@ import type { SerializableObject } from 'common/types'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { Logger } from 'roarr'; -export abstract class CanvasModuleBase { +export abstract class CanvasModuleABC { abstract id: string; abstract type: string; abstract path: string[]; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectBrushLineRenderer.ts similarity index 86% rename from invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectBrushLineRenderer.ts index 3dac0b77cc7..425f1d64b78 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectBrushLineRenderer.ts @@ -1,18 +1,18 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; +import type { CanvasEntityRenderer } from 'features/controlLayers/konva/CanvasEntityRenderer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; -import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; +import { CanvasModuleABC } from 'features/controlLayers/konva/CanvasModuleABC'; import type { CanvasBrushLineState } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; -export class CanvasBrushLineRenderer extends CanvasModuleBase { - readonly type = 'brush_line_renderer'; +export class CanvasObjectBrushLineRenderer extends CanvasModuleABC { + readonly type = 'object_brush_line_renderer'; id: string; path: string[]; - parent: CanvasObjectRenderer; + parent: CanvasEntityRenderer; manager: CanvasManager; log: Logger; subscriptions = new Set<() => void>(); @@ -23,7 +23,7 @@ export class CanvasBrushLineRenderer extends CanvasModuleBase { line: Konva.Line; }; - constructor(state: CanvasBrushLineState, parent: CanvasObjectRenderer) { + constructor(state: CanvasBrushLineState, parent: CanvasEntityRenderer) { super(); const { id, clip } = state; this.id = id; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectEraserLineRenderer.ts similarity index 86% rename from invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectEraserLineRenderer.ts index 29b06ab9b6d..fca2e4a712d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectEraserLineRenderer.ts @@ -1,17 +1,17 @@ import { deepClone } from 'common/util/deepClone'; +import type { CanvasEntityRenderer } from 'features/controlLayers/konva/CanvasEntityRenderer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; -import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; +import { CanvasModuleABC } from 'features/controlLayers/konva/CanvasModuleABC'; import type { CanvasEraserLineState } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; -export class CanvasEraserLineRenderer extends CanvasModuleBase { - readonly type = 'eraser_line_renderer'; +export class CanvasObjectEraserLineRenderer extends CanvasModuleABC { + readonly type = 'object_eraser_line_renderer'; id: string; path: string[]; - parent: CanvasObjectRenderer; + parent: CanvasEntityRenderer; manager: CanvasManager; log: Logger; subscriptions = new Set<() => void>(); @@ -22,7 +22,7 @@ export class CanvasEraserLineRenderer extends CanvasModuleBase { line: Konva.Line; }; - constructor(state: CanvasEraserLineState, parent: CanvasObjectRenderer) { + constructor(state: CanvasEraserLineState, parent: CanvasEntityRenderer) { super(); const { id, clip } = state; this.id = id; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectImageRenderer.ts similarity index 93% rename from invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectImageRenderer.ts index 6db8db5a7c7..61056bf1238 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectImageRenderer.ts @@ -1,9 +1,9 @@ import { Mutex } from 'async-mutex'; import { deepClone } from 'common/util/deepClone'; +import type { CanvasEntityRenderer } from 'features/controlLayers/konva/CanvasEntityRenderer'; import type { CanvasFilterModule } from 'features/controlLayers/konva/CanvasFilterModule'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; -import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; +import { CanvasModuleABC } from 'features/controlLayers/konva/CanvasModuleABC'; import type { CanvasStagingAreaModule } from 'features/controlLayers/konva/CanvasStagingAreaModule'; import { loadImage } from 'features/controlLayers/konva/util'; import type { CanvasImageState } from 'features/controlLayers/store/types'; @@ -12,12 +12,12 @@ import Konva from 'konva'; import type { Logger } from 'roarr'; import { getImageDTO } from 'services/api/endpoints/images'; -export class CanvasImageRenderer extends CanvasModuleBase { - readonly type = 'image_renderer'; +export class CanvasObjectImageRenderer extends CanvasModuleABC { + readonly type = 'object_image_renderer'; id: string; path: string[]; - parent: CanvasObjectRenderer | CanvasStagingAreaModule | CanvasFilterModule; + parent: CanvasEntityRenderer | CanvasStagingAreaModule | CanvasFilterModule; manager: CanvasManager; log: Logger; subscriptions = new Set<() => void>(); @@ -33,7 +33,7 @@ export class CanvasImageRenderer extends CanvasModuleBase { isError: boolean = false; mutex = new Mutex(); - constructor(state: CanvasImageState, parent: CanvasObjectRenderer | CanvasStagingAreaModule | CanvasFilterModule) { + constructor(state: CanvasImageState, parent: CanvasEntityRenderer | CanvasStagingAreaModule | CanvasFilterModule) { super(); const { id, image } = state; const { width, height } = image; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRectRenderer.ts similarity index 85% rename from invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRectRenderer.ts index 3cb0525e3fb..93c8129934c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRectRenderer.ts @@ -1,18 +1,18 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; +import type { CanvasEntityRenderer } from 'features/controlLayers/konva/CanvasEntityRenderer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; -import type { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; +import { CanvasModuleABC } from 'features/controlLayers/konva/CanvasModuleABC'; import type { CanvasRectState } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; -export class CanvasRectRenderer extends CanvasModuleBase { - readonly type = 'rect_renderer'; +export class CanvasObjectRectRenderer extends CanvasModuleABC { + readonly type = 'object_rect_renderer'; id: string; path: string[]; - parent: CanvasObjectRenderer; + parent: CanvasEntityRenderer; manager: CanvasManager; log: Logger; subscriptions = new Set<() => void>(); @@ -24,7 +24,7 @@ export class CanvasRectRenderer extends CanvasModuleBase { }; isFirstRender: boolean = false; - constructor(state: CanvasRectState, parent: CanvasObjectRenderer) { + constructor(state: CanvasRectState, parent: CanvasEntityRenderer) { super(); const { id } = state; this.id = id; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreviewModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreviewModule.ts index 0a3f47ee61f..fa00ba96904 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreviewModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreviewModule.ts @@ -1,5 +1,5 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; +import { CanvasModuleABC } from 'features/controlLayers/konva/CanvasModuleABC'; import { CanvasProgressImageModule } from 'features/controlLayers/konva/CanvasProgressImageModule'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import Konva from 'konva'; @@ -9,7 +9,7 @@ import { CanvasBboxModule } from './CanvasBboxModule'; import { CanvasStagingAreaModule } from './CanvasStagingAreaModule'; import { CanvasToolModule } from './CanvasToolModule'; -export class CanvasPreviewModule extends CanvasModuleBase { +export class CanvasPreviewModule extends CanvasModuleABC { readonly type = 'preview'; id: string; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImageModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImageModule.ts index fb972aededb..03cdc69b15d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImageModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImageModule.ts @@ -1,13 +1,13 @@ import { Mutex } from 'async-mutex'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; +import { CanvasModuleABC } from 'features/controlLayers/konva/CanvasModuleABC'; import type { CanvasPreviewModule } from 'features/controlLayers/konva/CanvasPreviewModule'; import { getPrefixedId, loadImage } from 'features/controlLayers/konva/util'; import Konva from 'konva'; import type { Logger } from 'roarr'; import type { S } from 'services/api/types'; -export class CanvasProgressImageModule extends CanvasModuleBase { +export class CanvasProgressImageModule extends CanvasModuleABC { readonly type = 'progress_image'; id: string; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts index ce994085b74..b92bfef0e4b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts @@ -1,15 +1,15 @@ import type { SerializableObject } from 'common/types'; -import { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter'; +import { CanvasEntityLayerAdapter } from 'features/controlLayers/konva/CanvasEntityLayerAdapter'; +import { CanvasEntityMaskAdapter } from 'features/controlLayers/konva/CanvasEntityMaskAdapter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; +import { CanvasModuleABC } from 'features/controlLayers/konva/CanvasModuleABC'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { CanvasSessionState } from 'features/controlLayers/store/canvasSessionSlice'; import type { CanvasSettingsState } from 'features/controlLayers/store/canvasSettingsSlice'; import type { CanvasState } from 'features/controlLayers/store/types'; import type { Logger } from 'roarr'; -export class CanvasRenderingModule extends CanvasModuleBase { +export class CanvasRenderingModule extends CanvasModuleABC { readonly type = 'canvas_renderer'; id: string; @@ -139,7 +139,7 @@ export class CanvasRenderingModule extends CanvasModuleBase { for (const entityState of state.rasterLayers.entities) { let adapter = adapterMap.get(entityState.id); if (!adapter) { - adapter = new CanvasLayerAdapter(entityState, this.manager); + adapter = new CanvasEntityLayerAdapter(entityState, this.manager); adapterMap.set(adapter.id, adapter); this.manager.stage.addLayer(adapter.konva.layer); } @@ -168,7 +168,7 @@ export class CanvasRenderingModule extends CanvasModuleBase { for (const entityState of state.controlLayers.entities) { let adapter = adapterMap.get(entityState.id); if (!adapter) { - adapter = new CanvasLayerAdapter(entityState, this.manager); + adapter = new CanvasEntityLayerAdapter(entityState, this.manager); adapterMap.set(adapter.id, adapter); this.manager.stage.addLayer(adapter.konva.layer); } @@ -202,7 +202,7 @@ export class CanvasRenderingModule extends CanvasModuleBase { for (const entityState of state.regions.entities) { let adapter = adapterMap.get(entityState.id); if (!adapter) { - adapter = new CanvasMaskAdapter(entityState, this.manager); + adapter = new CanvasEntityMaskAdapter(entityState, this.manager); adapterMap.set(adapter.id, adapter); this.manager.stage.addLayer(adapter.konva.layer); } @@ -236,7 +236,7 @@ export class CanvasRenderingModule extends CanvasModuleBase { for (const entityState of state.inpaintMasks.entities) { let adapter = adapterMap.get(entityState.id); if (!adapter) { - adapter = new CanvasMaskAdapter(entityState, this.manager); + adapter = new CanvasEntityMaskAdapter(entityState, this.manager); adapterMap.set(adapter.id, adapter); this.manager.stage.addLayer(adapter.konva.layer); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts index a23bfad9c71..c8e65d88a04 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts @@ -1,5 +1,5 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; +import { CanvasModuleABC } from 'features/controlLayers/konva/CanvasModuleABC'; import { CANVAS_SCALE_BY } from 'features/controlLayers/konva/constants'; import { getPrefixedId, getRectUnion } from 'features/controlLayers/konva/util'; import type { Coordinate, Dimensions, Rect } from 'features/controlLayers/store/types'; @@ -8,7 +8,7 @@ import type { KonvaEventObject } from 'konva/lib/Node'; import { clamp } from 'lodash-es'; import type { Logger } from 'roarr'; -export class CanvasStageModule extends CanvasModuleBase { +export class CanvasStageModule extends CanvasModuleABC { readonly type = 'stage'; static MIN_CANVAS_SCALE = 0.1; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts index 202b367edbc..45abeb207e6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts @@ -1,14 +1,14 @@ import type { SerializableObject } from 'common/types'; -import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; +import { CanvasModuleABC } from 'features/controlLayers/konva/CanvasModuleABC'; +import { CanvasObjectImageRenderer } from 'features/controlLayers/konva/CanvasObjectImageRenderer'; import type { CanvasPreviewModule } from 'features/controlLayers/konva/CanvasPreviewModule'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { imageDTOToImageWithDims, type StagingAreaImage } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; -export class CanvasStagingAreaModule extends CanvasModuleBase { +export class CanvasStagingAreaModule extends CanvasModuleABC { readonly type = 'staging_area'; id: string; @@ -19,7 +19,7 @@ export class CanvasStagingAreaModule extends CanvasModuleBase { konva: { group: Konva.Group }; - image: CanvasImageRenderer | null; + image: CanvasObjectImageRenderer | null; selectedImage: StagingAreaImage | null; /** @@ -58,7 +58,7 @@ export class CanvasStagingAreaModule extends CanvasModuleBase { if (!this.image) { const { image_name } = imageDTO; - this.image = new CanvasImageRenderer( + this.image = new CanvasObjectImageRenderer( { id: 'staging-area-image', type: 'image', diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index aa3be585f2d..05fe1ccb255 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -1,9 +1,9 @@ import { $alt, $ctrl, $meta, $shift } from '@invoke-ai/ui-library'; import type { AppStore } from 'app/store/store'; -import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter'; +import type { CanvasEntityLayerAdapter } from 'features/controlLayers/konva/CanvasEntityLayerAdapter'; +import type { CanvasEntityMaskAdapter } from 'features/controlLayers/konva/CanvasEntityMaskAdapter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; +import { CanvasModuleABC } from 'features/controlLayers/konva/CanvasModuleABC'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { bboxChanged, @@ -54,28 +54,28 @@ type EntityStateAndAdapter = id: string; type: CanvasRasterLayerState['type']; state: CanvasRasterLayerState; - adapter: CanvasLayerAdapter; + adapter: CanvasEntityLayerAdapter; } | { id: string; type: CanvasControlLayerState['type']; state: CanvasControlLayerState; - adapter: CanvasLayerAdapter; + adapter: CanvasEntityLayerAdapter; } | { id: string; type: CanvasInpaintMaskState['type']; state: CanvasInpaintMaskState; - adapter: CanvasMaskAdapter; + adapter: CanvasEntityMaskAdapter; } | { id: string; type: CanvasRegionalGuidanceState['type']; state: CanvasRegionalGuidanceState; - adapter: CanvasMaskAdapter; + adapter: CanvasEntityMaskAdapter; }; -export class CanvasStateApiModule extends CanvasModuleBase { +export class CanvasStateApiModule extends CanvasModuleABC { readonly type = 'state_api'; id: string; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts index 38bc0dd3ae7..d9b06749f4e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts @@ -1,6 +1,6 @@ import { rgbaColorToString, rgbColorToString } from 'common/util/colorCodeTransformers'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; +import { CanvasModuleABC } from 'features/controlLayers/konva/CanvasModuleABC'; import type { CanvasPreviewModule } from 'features/controlLayers/konva/CanvasPreviewModule'; import { BRUSH_BORDER_INNER_COLOR, @@ -31,7 +31,7 @@ import Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; import type { Logger } from 'roarr'; -export class CanvasToolModule extends CanvasModuleBase { +export class CanvasToolModule extends CanvasModuleABC { readonly type = 'tool'; static readonly COLOR_PICKER_RADIUS = 25; static readonly COLOR_PICKER_THICKNESS = 15; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasWorkerModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasWorkerModule.ts index 422b1f5ef4c..ec15537bb45 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasWorkerModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasWorkerModule.ts @@ -1,10 +1,10 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; +import { CanvasModuleABC } from 'features/controlLayers/konva/CanvasModuleABC'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { Extents, ExtentsResult, GetBboxTask, WorkerLogMessage } from 'features/controlLayers/konva/worker'; import type { Logger } from 'roarr'; -export class CanvasWorkerModule extends CanvasModuleBase { +export class CanvasWorkerModule extends CanvasModuleABC { readonly type = 'worker'; id: string; From c74b1303032195b480a946377503300b4f214bf5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 27 Aug 2024 17:59:21 +1000 Subject: [PATCH 514/678] feat(ui): collapsible entity groups --- .../CanvasEntityList/CanvasEntityList.tsx | 2 +- .../common/CanvasEntityGroupList.tsx | 54 ++++++++++++++++--- .../common/CanvasEntityTypeIsHiddenToggle.tsx | 11 ++-- 3 files changed, 55 insertions(+), 12 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityList.tsx index dac827da5d1..e09d1484204 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityList.tsx @@ -11,7 +11,7 @@ import { memo } from 'react'; export const CanvasEntityList = memo(() => { return ( - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupList.tsx index 51d8ed00175..ee3c56d9727 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupList.tsx @@ -1,27 +1,65 @@ -import { Flex, Spacer, Text } from '@invoke-ai/ui-library'; +import type { SystemStyleObject } from '@invoke-ai/ui-library'; +import { Button, Collapse, Flex, Icon, Spacer, Text } from '@invoke-ai/ui-library'; +import { useBoolean } from 'common/hooks/useBoolean'; import { CanvasEntityTypeIsHiddenToggle } from 'features/controlLayers/components/common/CanvasEntityTypeIsHiddenToggle'; import { useEntityTypeTitle } from 'features/controlLayers/hooks/useEntityTypeTitle'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import type { PropsWithChildren } from 'react'; import { memo } from 'react'; +import { PiCaretDownBold } from 'react-icons/pi'; type Props = PropsWithChildren<{ isSelected: boolean; type: CanvasEntityIdentifier['type']; }>; +const _hover: SystemStyleObject = { + opacity: 1, +}; + export const CanvasEntityGroupList = memo(({ isSelected, type, children }: Props) => { const title = useEntityTypeTitle(type); + const collapse = useBoolean(true); return ( - - - - {title} - - + + + + + + {title} + + + {type !== 'ip_adapter' && } - {children} + + + {children} + + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTypeIsHiddenToggle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTypeIsHiddenToggle.tsx index 25ab3b9a426..3f33c2302fb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTypeIsHiddenToggle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTypeIsHiddenToggle.tsx @@ -4,6 +4,7 @@ import { useEntityTypeIsHidden } from 'features/controlLayers/hooks/useEntityTyp import { useEntityTypeString } from 'features/controlLayers/hooks/useEntityTypeString'; import { allEntitiesOfTypeIsHiddenToggled } from 'features/controlLayers/store/canvasSlice'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import type { MouseEventHandler } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiEyeBold, PiEyeClosedBold } from 'react-icons/pi'; @@ -17,9 +18,13 @@ export const CanvasEntityTypeIsHiddenToggle = memo(({ type }: Props) => { const dispatch = useAppDispatch(); const isHidden = useEntityTypeIsHidden(type); const typeString = useEntityTypeString(type); - const onClick = useCallback(() => { - dispatch(allEntitiesOfTypeIsHiddenToggled({ type })); - }, [dispatch, type]); + const onClick = useCallback( + (e) => { + e.stopPropagation(); + dispatch(allEntitiesOfTypeIsHiddenToggled({ type })); + }, + [dispatch, type] + ); return ( Date: Tue, 27 Aug 2024 19:08:57 +1000 Subject: [PATCH 515/678] feat(ui): iterate on layer actions - Add lock toggle - Tweak lock and enabled styles - Update entity list action bar w/ delete & delete all - Move add layer menu to action bar - Adjust opacity slider style --- invokeai/frontend/web/public/locales/en.json | 5 ++- .../CanvasEntityList/CanvasEntityList.tsx | 4 +- .../CanvasEntityList/EntityListActionBar.tsx | 18 +++++++++ ...EntityListActionBarAddLayerMenuButton.tsx} | 17 ++++---- ... EntityListActionBarAddLayerMenuItems.tsx} | 16 ++------ .../EntityListActionBarDeleteButton.tsx | 39 +++++++++++++++++++ ...ityListActionBarSelectedEntityOpacity.tsx} | 6 ++- .../components/CanvasPanelContent.tsx | 28 +++++-------- .../components/ControlLayer/ControlLayer.tsx | 2 + .../components/InpaintMask/InpaintMask.tsx | 2 + .../components/RasterLayer/RasterLayer.tsx | 2 + .../RegionalGuidance/RegionalGuidance.tsx | 2 + .../RegionalGuidanceEntityList.tsx | 2 +- .../common/CanvasEntityEnabledToggle.tsx | 15 ++++--- .../common/CanvasEntityGroupList.tsx | 4 +- .../common/CanvasEntityIsLockedToggle.tsx | 37 ++++++++++++++++++ .../controlLayers/hooks/useEntityIsLocked.ts | 22 +++++++++++ .../controlLayers/store/canvasSlice.ts | 14 +++++++ .../src/features/controlLayers/store/types.ts | 23 +++++------ .../ParametersPanelTextToImage.tsx | 5 +-- 20 files changed, 190 insertions(+), 73 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBar.tsx rename invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/{CanvasEntityListMenuButton.tsx => EntityListActionBarAddLayerMenuButton.tsx} (53%) rename invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/{CanvasEntityListMenuItems.tsx => EntityListActionBarAddLayerMenuItems.tsx} (72%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarDeleteButton.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{common/CanvasEntityOpacity.tsx => CanvasEntityList/EntityListActionBarSelectedEntityOpacity.tsx} (95%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityIsLockedToggle.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsLocked.ts diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index ba14fb28912..7005d321735 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1658,7 +1658,6 @@ "autoSave": "Auto-save to Gallery", "resetCanvas": "Reset Canvas", "resetAll": "Reset All", - "deleteAll": "Delete All", "clearCaches": "Clear Caches", "recalculateRects": "Recalculate Rects", "clipToBbox": "Clip Strokes to Bbox", @@ -1735,6 +1734,10 @@ "showingType": "Showing {{type}}", "dynamicGrid": "Dynamic Grid", "logDebugInfo": "Log Debug Info", + "locked": "Locked", + "unlocked": "Unlocked", + "deleteSelected": "Delete Selected", + "deleteAll": "Delete All", "fill": { "fillStyle": "Fill Style", "solid": "Solid", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityList.tsx index e09d1484204..3fe4475c747 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityList.tsx @@ -1,6 +1,5 @@ import { Flex } from '@invoke-ai/ui-library'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; -import { CanvasEntityOpacity } from 'features/controlLayers/components/common/CanvasEntityOpacity'; import { ControlLayerEntityList } from 'features/controlLayers/components/ControlLayer/ControlLayerEntityList'; import { InpaintMaskList } from 'features/controlLayers/components/InpaintMask/InpaintMaskList'; import { IPAdapterList } from 'features/controlLayers/components/IPAdapter/IPAdapterList'; @@ -11,8 +10,7 @@ import { memo } from 'react'; export const CanvasEntityList = memo(() => { return ( - - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBar.tsx new file mode 100644 index 00000000000..a0ddfc0fc91 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBar.tsx @@ -0,0 +1,18 @@ +import { Flex, Spacer } from '@invoke-ai/ui-library'; +import { EntityListActionBarAddLayerButton } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuButton'; +import { EntityListActionBarDeleteButton } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarDeleteButton'; +import { SelectedEntityOpacity } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityOpacity'; +import { memo } from 'react'; + +export const EntityListActionBar = memo(() => { + return ( + + + + + + + ); +}); + +EntityListActionBar.displayName = 'EntityListActionBar'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuButton.tsx similarity index 53% rename from invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuButton.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuButton.tsx index cb7231c2070..610edec6e7b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuButton.tsx @@ -1,21 +1,22 @@ import { IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library'; -import { CanvasEntityListMenuItems } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuItems'; +import { CanvasEntityListMenuItems } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuItems'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -import { PiDotsThreeOutlineFill } from 'react-icons/pi'; +import { PiPlusBold } from 'react-icons/pi'; -export const CanvasEntityListMenuButton = memo(() => { +export const EntityListActionBarAddLayerButton = memo(() => { const { t } = useTranslation(); return ( } - variant="link" + size="sm" + tooltip={t('controlLayers.addLayer')} + aria-label={t('controlLayers.addLayer')} + icon={} + variant="ghost" data-testid="control-layers-add-layer-menu-button" - alignSelf="stretch" /> @@ -24,4 +25,4 @@ export const CanvasEntityListMenuButton = memo(() => { ); }); -CanvasEntityListMenuButton.displayName = 'CanvasEntityListMenuButton'; +EntityListActionBarAddLayerButton.displayName = 'EntityListActionBarAddLayerButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuItems.tsx similarity index 72% rename from invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuItems.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuItems.tsx index 993e42c57f7..810422ad8ab 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuItems.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuItems.tsx @@ -1,22 +1,19 @@ -import { MenuDivider, MenuItem } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { MenuItem } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; import { - allEntitiesDeleted, controlLayerAdded, inpaintMaskAdded, ipaAdded, rasterLayerAdded, rgAdded, } from 'features/controlLayers/store/canvasSlice'; -import { selectHasEntities } from 'features/controlLayers/store/selectors'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { PiPlusBold, PiTrashSimpleBold } from 'react-icons/pi'; +import { PiPlusBold } from 'react-icons/pi'; export const CanvasEntityListMenuItems = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const hasEntities = useAppSelector(selectHasEntities); const addInpaintMask = useCallback(() => { dispatch(inpaintMaskAdded({ isSelected: true })); }, [dispatch]); @@ -32,9 +29,6 @@ export const CanvasEntityListMenuItems = memo(() => { const addIPAdapter = useCallback(() => { dispatch(ipaAdded({ isSelected: true })); }, [dispatch]); - const deleteAll = useCallback(() => { - dispatch(allEntitiesDeleted()); - }, [dispatch]); return ( <> @@ -53,10 +47,6 @@ export const CanvasEntityListMenuItems = memo(() => { } onClick={addIPAdapter}> {t('controlLayers.ipAdapter', { count: 1 })} - - } color="error.300" isDisabled={!hasEntities}> - {t('controlLayers.deleteAll', { count: 1 })} - ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarDeleteButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarDeleteButton.tsx new file mode 100644 index 00000000000..dfe40c0d859 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarDeleteButton.tsx @@ -0,0 +1,39 @@ +import { IconButton, useShiftModifier } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { allEntitiesDeleted, entityDeleted } from 'features/controlLayers/store/canvasSlice'; +import { selectEntityCount, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiTrashSimpleFill } from 'react-icons/pi'; + +export const EntityListActionBarDeleteButton = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); + const entityCount = useAppSelector(selectEntityCount); + const shift = useShiftModifier(); + const onClick = useCallback(() => { + if (shift) { + dispatch(allEntitiesDeleted()); + return; + } + if (!selectedEntityIdentifier) { + return; + } + dispatch(entityDeleted({ entityIdentifier: selectedEntityIdentifier })); + }, [dispatch, selectedEntityIdentifier, shift]); + + return ( + } + /> + ); +}); + +EntityListActionBarDeleteButton.displayName = 'EntityListActionBarDeleteButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityOpacity.tsx similarity index 95% rename from invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityOpacity.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityOpacity.tsx index 682add1411c..9100089e6ad 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityOpacity.tsx @@ -77,7 +77,7 @@ const selectOpacity = createSelector(selectCanvasSlice, (canvas) => { return selectedEntity.opacity; }); -export const CanvasEntityOpacity = memo(() => { +export const SelectedEntityOpacity = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); @@ -151,6 +151,8 @@ export const CanvasEntityOpacity = memo(() => { defaultValue={1} onKeyDown={onKeyDown} clampValueOnBlur={false} + variant="outline" + isDisabled={selectedEntityIdentifier === null || selectedEntityIdentifier.type === 'ip_adapter'} > @@ -186,4 +188,4 @@ export const CanvasEntityOpacity = memo(() => { ); }); -CanvasEntityOpacity.displayName = 'CanvasEntityOpacity'; +SelectedEntityOpacity.displayName = 'SelectedEntityOpacity'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx index 16e348ef397..d6691ac271b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx @@ -1,32 +1,22 @@ -import { Box, ContextMenu, MenuList } from '@invoke-ai/ui-library'; +import { Divider, Flex } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { CanvasAddEntityButtons } from 'features/controlLayers/components/CanvasAddEntityButtons'; import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityList'; -import { CanvasEntityListMenuItems } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuItems'; +import { EntityListActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBar'; import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { selectHasEntities } from 'features/controlLayers/store/selectors'; -import { memo, useCallback } from 'react'; +import { memo } from 'react'; export const CanvasPanelContent = memo(() => { const hasEntities = useAppSelector(selectHasEntities); - const renderMenu = useCallback( - () => ( - - - - ), - [] - ); return ( - renderMenu={renderMenu}> - {(ref) => ( - - {!hasEntities && } - {hasEntities && } - - )} - + + + + {!hasEntities && } + {hasEntities && } + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx index 594a2f871cb..5db974dd424 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx @@ -2,6 +2,7 @@ import { Spacer } from '@invoke-ai/ui-library'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; +import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle'; import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage'; import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper'; import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit'; @@ -28,6 +29,7 @@ export const ControlLayer = memo(({ id }: Props) => { + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx index 12021e02093..e50221e7ef2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx @@ -2,6 +2,7 @@ import { Spacer } from '@invoke-ai/ui-library'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; +import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle'; import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage'; import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit'; import { EntityMaskAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext'; @@ -26,6 +27,7 @@ export const InpaintMask = memo(({ id }: Props) => { + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx index fbb89f4da82..bf64f5dd5bf 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx @@ -2,6 +2,7 @@ import { Spacer } from '@invoke-ai/ui-library'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; +import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle'; import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage'; import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit'; import { EntityLayerAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext'; @@ -24,6 +25,7 @@ export const RasterLayer = memo(({ id }: Props) => { + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx index ea5e85ee1d7..b122504785b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx @@ -2,6 +2,7 @@ import { Spacer } from '@invoke-ai/ui-library'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; +import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle'; import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage'; import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit'; import { RegionalGuidanceBadges } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges'; @@ -30,6 +31,7 @@ export const RegionalGuidance = memo(({ id }: Props) => { + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList.tsx index a7271d10dbc..79fa4efa33d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList.tsx @@ -11,7 +11,7 @@ const selectEntityIds = createMemoizedSelector(selectCanvasSlice, (canvas) => { return canvas.regions.entities.map(mapId).reverse(); }); const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selectedEntityIdentifier) => { - return selectedEntityIdentifier?.type === 'raster_layer'; + return selectedEntityIdentifier?.type === 'regional_guidance'; }); export const RegionalGuidanceEntityList = memo(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx index 77ccc94812a..014a3878485 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx @@ -1,32 +1,35 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; -import { stopPropagation } from 'common/util/stopPropagation'; +import { useBoolean } from 'common/hooks/useBoolean'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { useEntityIsEnabled } from 'features/controlLayers/hooks/useEntityIsEnabled'; import { entityIsEnabledToggled } from 'features/controlLayers/store/canvasSlice'; -import { memo, useCallback } from 'react'; +import { memo, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { PiCheckBold } from 'react-icons/pi'; export const CanvasEntityEnabledToggle = memo(() => { const { t } = useTranslation(); const entityIdentifier = useEntityIdentifierContext(); - + const ref = useRef(null); const isEnabled = useEntityIsEnabled(entityIdentifier); const dispatch = useAppDispatch(); const onClick = useCallback(() => { dispatch(entityIsEnabledToggled({ entityIdentifier })); }, [dispatch, entityIdentifier]); + const isHovered = useBoolean(false); return ( : undefined} + variant="ghost" + icon={isEnabled || isHovered.isTrue ? : undefined} onClick={onClick} - onDoubleClick={stopPropagation} // double click expands the layer /> ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupList.tsx index ee3c56d9727..c6650409052 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupList.tsx @@ -38,13 +38,13 @@ export const CanvasEntityGroupList = memo(({ isSelected, type, children }: Props boxSize={4} as={PiCaretDownBold} transform={collapse.isTrue ? undefined : 'rotate(-90deg)'} - fill={isSelected ? 'invokeBlue.300' : 'base.300'} + fill={isSelected ? 'base.200' : 'base.500'} transitionProperty="common" transitionDuration="fast" /> { + const { t } = useTranslation(); + const entityIdentifier = useEntityIdentifierContext(); + const ref = useRef(null); + const isLocked = useEntityIsLocked(entityIdentifier); + const dispatch = useAppDispatch(); + const onClick = useCallback(() => { + dispatch(entityIsLockedToggled({ entityIdentifier })); + }, [dispatch, entityIdentifier]); + const isHovered = useBoolean(false); + + return ( + : undefined} + onClick={onClick} + /> + ); +}); + +CanvasEntityIsLockedToggle.displayName = 'CanvasEntityIsLockedToggle'; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsLocked.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsLocked.ts new file mode 100644 index 00000000000..cf8cf93ead6 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsLocked.ts @@ -0,0 +1,22 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { useAppSelector } from 'app/store/storeHooks'; +import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { useMemo } from 'react'; + +export const useEntityIsLocked = (entityIdentifier: CanvasEntityIdentifier) => { + const selectIsLocked = useMemo( + () => + createSelector(selectCanvasSlice, (canvas) => { + const entity = selectEntity(canvas, entityIdentifier); + if (!entity) { + return false; + } else { + return entity.isLocked; + } + }), + [entityIdentifier] + ); + const isLocked = useAppSelector(selectIsLocked); + return isLocked; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index 3b0a729c35d..33ea62395d6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -131,6 +131,7 @@ export const canvasSlice = createSlice({ name: null, type: 'raster_layer', isEnabled: true, + isLocked: false, objects: [], opacity: 1, position: { x: 0, y: 0 }, @@ -191,6 +192,7 @@ export const canvasSlice = createSlice({ name: null, type: 'control_layer', isEnabled: true, + isLocked: false, withTransparencyEffect: true, objects: [], opacity: 1, @@ -332,6 +334,7 @@ export const canvasSlice = createSlice({ id, type: 'ip_adapter', name: null, + isLocked: false, isEnabled: true, ipAdapter: deepClone(initialIPAdapter), }; @@ -420,6 +423,7 @@ export const canvasSlice = createSlice({ const entity: CanvasRegionalGuidanceState = { id, name: null, + isLocked: false, type: 'regional_guidance', isEnabled: true, objects: [], @@ -630,6 +634,7 @@ export const canvasSlice = createSlice({ name: null, type: 'inpaint_mask', isEnabled: true, + isLocked: false, objects: [], opacity: 1, position: { x: 0, y: 0 }, @@ -849,6 +854,14 @@ export const canvasSlice = createSlice({ } entity.isEnabled = !entity.isEnabled; }, + entityIsLockedToggled: (state, action: PayloadAction) => { + const { entityIdentifier } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } + entity.isLocked = !entity.isLocked; + }, entityMoved: (state, action: PayloadAction) => { const { entityIdentifier, position } = action.payload; const entity = selectEntity(state, entityIdentifier); @@ -1074,6 +1087,7 @@ export const { entityNameChanged, entityReset, entityIsEnabledToggled, + entityIsLockedToggled, entityMoved, entityDuplicated, entityRasterized, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 35437810075..1986ddaca13 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -529,11 +529,15 @@ const zIPAdapterConfig = z.object({ }); export type IPAdapterConfig = z.infer; -const zCanvasIPAdapterState = z.object({ +const zCanvasEntityBase = z.object({ id: zId, name: zName, - type: z.literal('ip_adapter'), isEnabled: z.boolean(), + isLocked: z.boolean(), +}); + +const zCanvasIPAdapterState = zCanvasEntityBase.extend({ + type: z.literal('ip_adapter'), ipAdapter: zIPAdapterConfig, }); export type CanvasIPAdapterState = z.infer; @@ -555,11 +559,8 @@ const zRegionalGuidanceIPAdapterConfig = z.object({ }); export type RegionalGuidanceIPAdapterConfig = z.infer; -const zCanvasRegionalGuidanceState = z.object({ - id: zId, - name: zName, +const zCanvasRegionalGuidanceState = zCanvasEntityBase.extend({ type: z.literal('regional_guidance'), - isEnabled: z.boolean(), position: zCoordinate, opacity: zOpacity, objects: z.array(zCanvasObjectState), @@ -571,11 +572,8 @@ const zCanvasRegionalGuidanceState = z.object({ }); export type CanvasRegionalGuidanceState = z.infer; -const zCanvasInpaintMaskState = z.object({ - id: zId, - name: zName, +const zCanvasInpaintMaskState = zCanvasEntityBase.extend({ type: z.literal('inpaint_mask'), - isEnabled: z.boolean(), position: zCoordinate, fill: zFill, opacity: zOpacity, @@ -600,11 +598,8 @@ const zT2IAdapterConfig = z.object({ }); export type T2IAdapterConfig = z.infer; -export const zCanvasRasterLayerState = z.object({ - id: zId, - name: zName, +export const zCanvasRasterLayerState = zCanvasEntityBase.extend({ type: z.literal('raster_layer'), - isEnabled: z.boolean(), position: zCoordinate, opacity: zOpacity, objects: z.array(zCanvasObjectState), diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx index 68844bf082b..52d8cdbde98 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx @@ -1,9 +1,8 @@ import type { ChakraProps } from '@invoke-ai/ui-library'; -import { Box, Flex, Spacer, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library'; +import { Box, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants'; -import { CanvasEntityListMenuButton } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuButton'; import { CanvasPanelContent } from 'features/controlLayers/components/CanvasPanelContent'; import { selectIsSDXL } from 'features/controlLayers/store/paramsSlice'; import { selectEntityCount } from 'features/controlLayers/store/selectors'; @@ -100,8 +99,6 @@ const ParametersPanelTextToImage = () => { > {controlLayersTitle} - - From 97414f188675952dd09a5c8e9c0778b678085cc1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 27 Aug 2024 19:27:38 +1000 Subject: [PATCH 516/678] feat(ui): implement interaction locking on layers --- .../konva/CanvasEntityLayerAdapter.ts | 19 ++++++++++------ .../konva/CanvasEntityMaskAdapter.ts | 22 +++++++++++-------- .../konva/CanvasEntityTransformer.ts | 2 +- .../controlLayers/konva/CanvasToolModule.ts | 22 +++++++++++-------- 4 files changed, 39 insertions(+), 26 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityLayerAdapter.ts index 3f59044bbc6..36740ba630e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityLayerAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityLayerAdapter.ts @@ -85,24 +85,30 @@ export class CanvasEntityLayerAdapter extends CanvasModuleABC { update = async (arg?: { state: CanvasEntityLayerAdapter['state'] }) => { const state = get(arg, 'state', this.state); - if (!this.isFirstRender && state === this.state) { + const prevState = this.state; + this.state = state; + + if (!this.isFirstRender && prevState === state) { this.log.trace('State unchanged, skipping update'); return; } this.log.debug('Updating'); - const { position, objects, opacity, isEnabled } = state; + const { position, objects, opacity, isEnabled, isLocked } = state; - if (this.isFirstRender || isEnabled !== this.state.isEnabled) { + if (this.isFirstRender || isEnabled !== prevState.isEnabled) { this.updateVisibility({ isEnabled }); } - if (this.isFirstRender || objects !== this.state.objects) { + if (this.isFirstRender || isLocked !== prevState.isLocked) { + this.transformer.syncInteractionState(); + } + if (this.isFirstRender || objects !== prevState.objects) { await this.updateObjects({ objects }); } - if (this.isFirstRender || position !== this.state.position) { + if (this.isFirstRender || position !== prevState.position) { this.transformer.updatePosition({ position }); } - if (this.isFirstRender || opacity !== this.state.opacity) { + if (this.isFirstRender || opacity !== prevState.opacity) { this.renderer.updateOpacity(opacity); } @@ -117,7 +123,6 @@ export class CanvasEntityLayerAdapter extends CanvasModuleABC { this.transformer.updateBbox(); } - this.state = state; this.isFirstRender = false; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityMaskAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityMaskAdapter.ts index 45a5cd3bbae..ee77abd2927 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityMaskAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityMaskAdapter.ts @@ -85,28 +85,33 @@ export class CanvasEntityMaskAdapter extends CanvasModuleABC { update = async (arg?: { state: CanvasEntityMaskAdapter['state'] }) => { const state = get(arg, 'state', this.state); - if (!this.isFirstRender && state === this.state && state.fill === this.state.fill) { + const prevState = this.state; + this.state = state; + + if (!this.isFirstRender && prevState === state && prevState.fill === state.fill) { this.log.trace('State unchanged, skipping update'); return; } this.log.debug('Updating'); - const { position, objects, isEnabled, opacity } = state; + const { position, objects, isEnabled, isLocked, opacity } = state; - if (this.isFirstRender || objects !== this.state.objects) { + if (this.isFirstRender || objects !== prevState.objects) { await this.updateObjects({ objects }); } - if (this.isFirstRender || position !== this.state.position) { + if (this.isFirstRender || position !== prevState.position) { this.transformer.updatePosition({ position }); } - if (this.isFirstRender || opacity !== this.state.opacity) { + if (this.isFirstRender || opacity !== prevState.opacity) { this.renderer.updateOpacity(opacity); } - if (this.isFirstRender || isEnabled !== this.state.isEnabled) { + if (this.isFirstRender || isEnabled !== prevState.isEnabled) { this.updateVisibility({ isEnabled }); } - - if (this.isFirstRender || state.fill !== this.state.fill) { + if (this.isFirstRender || isLocked !== prevState.isLocked) { + this.transformer.syncInteractionState(); + } + if (this.isFirstRender || state.fill !== prevState.fill) { this.renderer.updateCompositingRectFill(state.fill); } @@ -118,7 +123,6 @@ export class CanvasEntityMaskAdapter extends CanvasModuleABC { this.transformer.updateBbox(); } - this.state = state; this.isFirstRender = false; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts index f49b4b2f0e0..846b9c59156 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts @@ -436,7 +436,7 @@ export class CanvasEntityTransformer extends CanvasModuleABC { const tool = this.manager.stateApi.$tool.get(); const isSelected = this.manager.stateApi.getIsSelected(this.parent.id); - if (!this.parent.renderer.hasObjects()) { + if (!this.parent.renderer.hasObjects() || this.parent.state.isLocked || !this.parent.state.isEnabled) { // The layer is totally empty, we can just disable the layer this.parent.konva.layer.listening(false); this.setInteractionMode('off'); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts index d9b06749f4e..2c9eba8b07b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts @@ -244,9 +244,9 @@ export class CanvasToolModule extends CanvasModuleABC { this.subscriptions.add(cleanupListeners); } - setToolVisibility = (tool: Tool) => { - this.konva.brush.group.visible(tool === 'brush'); - this.konva.eraser.group.visible(tool === 'eraser'); + setToolVisibility = (tool: Tool, isDrawable: boolean) => { + this.konva.brush.group.visible(isDrawable && tool === 'brush'); + this.konva.eraser.group.visible(isDrawable && tool === 'eraser'); this.konva.colorPicker.group.visible(tool === 'colorPicker'); }; @@ -259,7 +259,11 @@ export class CanvasToolModule extends CanvasModuleABC { const isMouseDown = this.manager.stateApi.$isMouseDown.get(); const tool = this.manager.stateApi.$tool.get(); - const isDrawable = selectedEntity && selectedEntity.state.isEnabled && isDrawableEntity(selectedEntity.state); + const isDrawable = + !!selectedEntity && + selectedEntity.state.isEnabled && + !selectedEntity.state.isLocked && + isDrawableEntity(selectedEntity.state); // Update the stage's pointer style if (Boolean(this.manager.stateApi.$transformingEntity.get()) || renderedEntityCount === 0) { @@ -433,7 +437,7 @@ export class CanvasToolModule extends CanvasModuleABC { }); } - this.setToolVisibility(tool); + this.setToolVisibility(tool, isDrawable); } }; @@ -539,7 +543,7 @@ export class CanvasToolModule extends CanvasModuleABC { } this.render(); } else { - const isDrawable = selectedEntity?.state.isEnabled; + const isDrawable = selectedEntity?.state.isEnabled && !selectedEntity.state.isLocked; if (pos && isDrawable && !this.manager.stateApi.$spaceKey.get() && getIsPrimaryMouseDown(e)) { this.manager.stateApi.$lastMouseDownPos.set(pos); const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); @@ -638,7 +642,7 @@ export class CanvasToolModule extends CanvasModuleABC { this.manager.stateApi.$isMouseDown.set(false); const pos = this.manager.stateApi.$lastCursorPos.get(); const selectedEntity = this.manager.stateApi.getSelectedEntity(); - const isDrawable = selectedEntity?.state.isEnabled; + const isDrawable = selectedEntity?.state.isEnabled && !selectedEntity.state.isLocked; const tool = this.manager.stateApi.$tool.get(); if (pos && isDrawable && !this.manager.stateApi.$spaceKey.get()) { @@ -686,7 +690,7 @@ export class CanvasToolModule extends CanvasModuleABC { this.manager.stateApi.$colorUnderCursor.set(color); } } else { - const isDrawable = selectedEntity?.state.isEnabled; + const isDrawable = selectedEntity?.state.isEnabled && !selectedEntity.state.isLocked; if (pos && isDrawable && !this.manager.stateApi.$spaceKey.get() && getIsPrimaryMouseDown(e)) { if (tool === 'brush') { const drawingBuffer = selectedEntity.adapter.renderer.bufferState; @@ -786,7 +790,7 @@ export class CanvasToolModule extends CanvasModuleABC { this.manager.stateApi.$lastMouseDownPos.set(null); const selectedEntity = this.manager.stateApi.getSelectedEntity(); const toolState = this.manager.stateApi.getToolState(); - const isDrawable = selectedEntity?.state.isEnabled; + const isDrawable = selectedEntity?.state.isEnabled && !selectedEntity.state.isLocked; const tool = this.manager.stateApi.$tool.get(); if (pos && isDrawable && !this.manager.stateApi.$spaceKey.get() && getIsPrimaryMouseDown(e)) { From b69b001755d22f5bfc14def688f4bf8d340d2d61 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 27 Aug 2024 19:56:57 +1000 Subject: [PATCH 517/678] feat(ui): generalize mask fill, add to action bar --- invokeai/frontend/web/public/locales/en.json | 2 +- .../CanvasEntityList/EntityListActionBar.tsx | 4 +- .../EntityListActionBarSelectedEntityFill.tsx | 70 ++++++++++++++++++ .../components/InpaintMask/InpaintMask.tsx | 3 - .../InpaintMaskMaskFillColorPicker.tsx | 62 ---------------- .../RegionalGuidance/RegionalGuidance.tsx | 3 - .../RegionalGuidanceMaskFillColorPicker.tsx | 61 ---------------- .../controlLayers/store/canvasSlice.ts | 73 ++++++------------- .../features/controlLayers/store/selectors.ts | 17 +++++ .../src/features/controlLayers/store/types.ts | 6 ++ 10 files changed, 121 insertions(+), 180 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityFill.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 7005d321735..c33357efa7a 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1680,7 +1680,7 @@ "resetRegion": "Reset Region", "debugLayers": "Debug Layers", "rectangle": "Rectangle", - "maskPreviewColor": "Mask Preview Color", + "maskFill": "Mask Fill", "addPositivePrompt": "Add $t(common.positivePrompt)", "addNegativePrompt": "Add $t(common.negativePrompt)", "addIPAdapter": "Add $t(common.ipAdapter)", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBar.tsx index a0ddfc0fc91..758eb36b4e1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBar.tsx @@ -1,14 +1,16 @@ import { Flex, Spacer } from '@invoke-ai/ui-library'; import { EntityListActionBarAddLayerButton } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuButton'; import { EntityListActionBarDeleteButton } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarDeleteButton'; +import { EntityListActionBarSelectedEntityFill } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityFill'; import { SelectedEntityOpacity } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityOpacity'; import { memo } from 'react'; export const EntityListActionBar = memo(() => { return ( - + + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityFill.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityFill.tsx new file mode 100644 index 00000000000..83c53f1f867 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityFill.tsx @@ -0,0 +1,70 @@ +import { Box, Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger, Tooltip } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import RgbColorPicker from 'common/components/RgbColorPicker'; +import { rgbColorToString } from 'common/util/colorCodeTransformers'; +import { MaskFillStyle } from 'features/controlLayers/components/common/MaskFillStyle'; +import { entityFillColorChanged, entityFillStyleChanged } from 'features/controlLayers/store/canvasSlice'; +import { selectSelectedEntityFill, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; +import { type FillStyle, isMaskEntityIdentifier, type RgbColor } from 'features/controlLayers/store/types'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const EntityListActionBarSelectedEntityFill = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); + const fill = useAppSelector(selectSelectedEntityFill); + + const onChangeFillColor = useCallback( + (color: RgbColor) => { + if (!selectedEntityIdentifier) { + return; + } + if (!isMaskEntityIdentifier(selectedEntityIdentifier)) { + return; + } + dispatch(entityFillColorChanged({ entityIdentifier: selectedEntityIdentifier, color })); + }, + [dispatch, selectedEntityIdentifier] + ); + const onChangeFillStyle = useCallback( + (style: FillStyle) => { + if (!selectedEntityIdentifier) { + return; + } + if (!isMaskEntityIdentifier(selectedEntityIdentifier)) { + return; + } + dispatch(entityFillStyleChanged({ entityIdentifier: selectedEntityIdentifier, style })); + }, + [dispatch, selectedEntityIdentifier] + ); + + if (!selectedEntityIdentifier || !fill) { + return null; + } + + return ( + + + + + + + + + + + + + + + + + + + + ); +}); + +EntityListActionBarSelectedEntityFill.displayName = 'EntityListActionBarSelectedEntityFill'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx index e50221e7ef2..9dcd945505c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx @@ -10,8 +10,6 @@ import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityI import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; -import { InpaintMaskMaskFillColorPicker } from './InpaintMaskMaskFillColorPicker'; - type Props = { id: string; }; @@ -28,7 +26,6 @@ export const InpaintMask = memo(({ id }: Props) => { - diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx deleted file mode 100644 index 6d2879eac60..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library'; -import { createSelector } from '@reduxjs/toolkit'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import RgbColorPicker from 'common/components/RgbColorPicker'; -import { rgbColorToString } from 'common/util/colorCodeTransformers'; -import { MaskFillStyle } from 'features/controlLayers/components/common/MaskFillStyle'; -import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { inpaintMaskFillColorChanged, inpaintMaskFillStyleChanged } from 'features/controlLayers/store/canvasSlice'; -import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; -import type { FillStyle, RgbColor } from 'features/controlLayers/store/types'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -export const InpaintMaskMaskFillColorPicker = memo(() => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const entityIdentifier = useEntityIdentifierContext('inpaint_mask'); - const selectFill = useMemo( - () => createSelector(selectCanvasSlice, (canvas) => selectEntityOrThrow(canvas, entityIdentifier).fill), - [entityIdentifier] - ); - const fill = useAppSelector(selectFill); - - const onChangeFillColor = useCallback( - (color: RgbColor) => { - dispatch(inpaintMaskFillColorChanged({ entityIdentifier, color })); - }, - [dispatch, entityIdentifier] - ); - const onChangeFillStyle = useCallback( - (style: FillStyle) => { - dispatch(inpaintMaskFillStyleChanged({ entityIdentifier, style })); - }, - [dispatch, entityIdentifier] - ); - return ( - - - - - - - - - - - - - - ); -}); - -InpaintMaskMaskFillColorPicker.displayName = 'InpaintMaskMaskFillColorPicker'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx index b122504785b..e788cfba5fc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx @@ -12,8 +12,6 @@ import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityI import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; -import { RegionalGuidanceMaskFillColorPicker } from './RegionalGuidanceMaskFillColorPicker'; - type Props = { id: string; }; @@ -30,7 +28,6 @@ export const RegionalGuidance = memo(({ id }: Props) => { - diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx deleted file mode 100644 index b950cf99956..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library'; -import { createSelector } from '@reduxjs/toolkit'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import RgbColorPicker from 'common/components/RgbColorPicker'; -import { rgbColorToString } from 'common/util/colorCodeTransformers'; -import { MaskFillStyle } from 'features/controlLayers/components/common/MaskFillStyle'; -import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { rgFillColorChanged, rgFillStyleChanged } from 'features/controlLayers/store/canvasSlice'; -import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; -import type { FillStyle, RgbColor } from 'features/controlLayers/store/types'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -export const RegionalGuidanceMaskFillColorPicker = memo(() => { - const entityIdentifier = useEntityIdentifierContext('regional_guidance'); - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const selectFill = useMemo( - () => createSelector(selectCanvasSlice, (canvas) => selectEntityOrThrow(canvas, entityIdentifier).fill), - [entityIdentifier] - ); - const fill = useAppSelector(selectFill); - const onChangeFillColor = useCallback( - (color: RgbColor) => { - dispatch(rgFillColorChanged({ entityIdentifier, color })); - }, - [dispatch, entityIdentifier] - ); - const onChangeFillStyle = useCallback( - (style: FillStyle) => { - dispatch(rgFillStyleChanged({ entityIdentifier, style })); - }, - [dispatch, entityIdentifier] - ); - return ( - - - - - - - - - - - - - - ); -}); - -RegionalGuidanceMaskFillColorPicker.displayName = 'RegionalGuidanceMaskFillColorPicker'; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index 33ea62395d6..c3c4958660d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -475,29 +475,6 @@ export const canvasSlice = createSlice({ } entity.negativePrompt = prompt; }, - rgFillColorChanged: ( - state, - action: PayloadAction> - ) => { - const { entityIdentifier, color } = action.payload; - const entity = selectEntity(state, entityIdentifier); - if (!entity) { - return; - } - entity.fill.color = color; - }, - rgFillStyleChanged: ( - state, - action: PayloadAction> - ) => { - const { entityIdentifier, style } = action.payload; - const entity = selectEntity(state, entityIdentifier); - if (!entity) { - return; - } - entity.fill.style = style; - }, - rgAutoNegativeToggled: (state, action: PayloadAction>) => { const { entityIdentifier } = action.payload; const rg = selectEntity(state, entityIdentifier); @@ -658,28 +635,6 @@ export const canvasSlice = createSlice({ state.inpaintMasks.entities = [data]; state.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id }; }, - inpaintMaskFillColorChanged: ( - state, - action: PayloadAction> - ) => { - const { color, entityIdentifier } = action.payload; - const entity = selectEntity(state, entityIdentifier); - if (!entity) { - return; - } - entity.fill.color = color; - }, - inpaintMaskFillStyleChanged: ( - state, - action: PayloadAction> - ) => { - const { style, entityIdentifier } = action.payload; - const entity = selectEntity(state, entityIdentifier); - if (!entity) { - return; - } - entity.fill.style = style; - }, //#region BBox bboxScaledSizeChanged: (state, action: PayloadAction>) => { state.bbox.scaledSize = { ...state.bbox.scaledSize, ...action.payload }; @@ -862,6 +817,28 @@ export const canvasSlice = createSlice({ } entity.isLocked = !entity.isLocked; }, + entityFillColorChanged: ( + state, + action: PayloadAction> + ) => { + const { color, entityIdentifier } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } + entity.fill.color = color; + }, + entityFillStyleChanged: ( + state, + action: PayloadAction> + ) => { + const { style, entityIdentifier } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } + entity.fill.style = style; + }, entityMoved: (state, action: PayloadAction) => { const { entityIdentifier, position } = action.payload; const entity = selectEntity(state, entityIdentifier); @@ -1088,6 +1065,8 @@ export const { entityReset, entityIsEnabledToggled, entityIsLockedToggled, + entityFillColorChanged, + entityFillStyleChanged, entityMoved, entityDuplicated, entityRasterized, @@ -1139,8 +1118,6 @@ export const { // rgRecalled, rgPositivePromptChanged, rgNegativePromptChanged, - rgFillColorChanged, - rgFillStyleChanged, rgAutoNegativeToggled, rgIPAdapterAdded, rgIPAdapterDeleted, @@ -1153,8 +1130,6 @@ export const { // Inpaint mask inpaintMaskAdded, // inpaintMaskRecalled, - inpaintMaskFillColorChanged, - inpaintMaskFillStyleChanged, } = canvasSlice.actions; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts index 92acd8ac1e8..5ee05fdfca5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts @@ -193,3 +193,20 @@ export const selectIsSelectedEntityDrawable = createSelector( export const selectCanvasMayUndo = (state: RootState) => state.canvas.past.length > 0; export const selectCanvasMayRedo = (state: RootState) => state.canvas.future.length > 0; +export const selectSelectedEntityFill = createSelector( + selectCanvasSlice, + selectSelectedEntityIdentifier, + (canvas, selectedEntityIdentifier) => { + if (!selectedEntityIdentifier) { + return null; + } + const entity = selectEntity(canvas, selectedEntityIdentifier); + if (!entity) { + return null; + } + if (entity.type !== 'inpaint_mask' && entity.type !== 'regional_guidance') { + return null; + } + return entity.fill; + } +); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 1986ddaca13..30a3edaef65 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -788,3 +788,9 @@ export const getEntityIdentifier = ( ): CanvasEntityIdentifier => { return { id: entity.id, type: entity.type }; }; + +export const isMaskEntityIdentifier = ( + entityIdentifier: CanvasEntityIdentifier +): entityIdentifier is CanvasEntityIdentifier<'inpaint_mask' | 'regional_guidance'> => { + return entityIdentifier.type === 'inpaint_mask' || entityIdentifier.type === 'regional_guidance'; +}; From 69d1edc036a9dfb3134b389da7f33d1535b594c4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 27 Aug 2024 19:57:02 +1000 Subject: [PATCH 518/678] chore(ui): lint --- invokeai/frontend/web/src/common/util/stopPropagation.ts | 4 ---- invokeai/frontend/web/src/features/metadata/util/parsers.ts | 3 +++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/invokeai/frontend/web/src/common/util/stopPropagation.ts b/invokeai/frontend/web/src/common/util/stopPropagation.ts index 0c6a1fc5078..0ad2b4b98a3 100644 --- a/invokeai/frontend/web/src/common/util/stopPropagation.ts +++ b/invokeai/frontend/web/src/common/util/stopPropagation.ts @@ -1,7 +1,3 @@ -export const stopPropagation = (e: React.MouseEvent) => { - e.stopPropagation(); -}; - export const preventDefault = (e: React.MouseEvent) => { e.preventDefault(); }; diff --git a/invokeai/frontend/web/src/features/metadata/util/parsers.ts b/invokeai/frontend/web/src/features/metadata/util/parsers.ts index efcfffd0914..8c41730293a 100644 --- a/invokeai/frontend/web/src/features/metadata/util/parsers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/parsers.ts @@ -504,6 +504,7 @@ const parseControlNetToControlAdapterLayer: MetadataParseFunc = id: getPrefixedId('ip_adapter'), type: 'ip_adapter', isEnabled: true, + isLocked: false, name: null, ipAdapter: { model: zModelIdentifierField.parse(ipAdapterModel), From c1a039ef91ac8d3a39badf4da12ff4d55bb91453 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 27 Aug 2024 19:57:34 +1000 Subject: [PATCH 519/678] chore: release v4.2.9.dev5 --- invokeai/version/invokeai_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/version/invokeai_version.py b/invokeai/version/invokeai_version.py index 421b13861d8..5a768e14bb2 100644 --- a/invokeai/version/invokeai_version.py +++ b/invokeai/version/invokeai_version.py @@ -1 +1 @@ -__version__ = "4.2.9.dev4" +__version__ = "4.2.9.dev5" From ec9fdace22bc0183e5840cdc811dec0f548feed0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 28 Aug 2024 07:05:14 +1000 Subject: [PATCH 520/678] fix(ui): randomize seed toggle linked to prompt concat --- .../web/src/features/controlLayers/store/paramsSlice.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts index 7a343093889..9008c718d22 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts @@ -317,7 +317,7 @@ export const selectScheduler = createParamsSelector((params) => params.scheduler export const selectSeamlessXAxis = createParamsSelector((params) => params.seamlessXAxis); export const selectSeamlessYAxis = createParamsSelector((params) => params.seamlessYAxis); export const selectSeed = createParamsSelector((params) => params.seed); -export const selectShouldRandomizeSeed = createParamsSelector((params) => params.shouldConcatPrompts); +export const selectShouldRandomizeSeed = createParamsSelector((params) => params.shouldRandomizeSeed); export const selectVAEPrecision = createParamsSelector((params) => params.vaePrecision); export const selectIterations = createParamsSelector((params) => params.iterations); export const selectShouldUseCPUNoise = createParamsSelector((params) => params.shouldUseCpuNoise); From 1497afcfdaa0e18e1fa291dfaf3d06087cbb9a7b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 28 Aug 2024 07:11:05 +1000 Subject: [PATCH 521/678] tidy(ui): use helper to sync scaled bbox size on model change --- .../web/src/features/controlLayers/store/canvasSlice.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index c3c4958660d..805aed37609 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -1045,10 +1045,7 @@ export const canvasSlice = createSlice({ const bboxDims = calculateNewSize(state.bbox.aspectRatio.value, optimalDimension * optimalDimension); state.bbox.rect.width = bboxDims.width; state.bbox.rect.height = bboxDims.height; - - if (state.bbox.scaleMethod === 'auto') { - state.bbox.scaledSize = getScaledBoundingBoxDimensions(bboxDims, optimalDimension); - } + syncScaledSize(state); } }); }, From 1749abbd97214f066dab9b088c54e22053965ed0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 28 Aug 2024 07:57:15 +1000 Subject: [PATCH 522/678] feat(ui): add flip & reset to transform --- invokeai/frontend/web/public/locales/en.json | 2 + .../components/ControlLayersEditor.tsx | 4 +- .../controlLayers/components/Transform.tsx | 55 +++++++++++---- .../konva/CanvasEntityTransformer.ts | 68 +++++++++++++++---- 4 files changed, 103 insertions(+), 26 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index c33357efa7a..cd3be44e938 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1738,6 +1738,8 @@ "unlocked": "Unlocked", "deleteSelected": "Delete Selected", "deleteAll": "Delete All", + "flipHorizontal": "Flip Horizontal", + "flipVertical": "Flip Vertical", "fill": { "fillStyle": "Fill Style", "solid": "Solid", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx index fabc93604c3..494a5a4ba05 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx @@ -32,14 +32,14 @@ export const CanvasEditor = memo(() => { > - + - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Transform.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Transform.tsx index 84783d0fb85..d6b84cc2039 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Transform.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Transform.tsx @@ -6,9 +6,15 @@ import { useEntityIdentifierContext, } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { useEntityAdapter } from 'features/controlLayers/hooks/useEntityAdapter'; -import { memo, useCallback } from 'react'; +import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -import { PiCheckBold, PiXBold } from 'react-icons/pi'; +import { + PiArrowsCounterClockwiseBold, + PiCheckBold, + PiFlipHorizontalFill, + PiFlipVerticalFill, + PiXBold, +} from 'react-icons/pi'; const TransformBox = memo(() => { const { t } = useTranslation(); @@ -16,14 +22,6 @@ const TransformBox = memo(() => { const adapter = useEntityAdapter(entityIdentifier); const isProcessing = useStore(adapter.transformer.$isProcessing); - const applyTransform = useCallback(() => { - adapter.transformer.applyTransform(); - }, [adapter.transformer]); - - const cancelFilter = useCallback(() => { - adapter.transformer.stopTransform(); - }, [adapter.transformer]); - return ( { {t('controlLayers.tool.transform')} + + + + + - diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts index 846b9c59156..8d556a43e97 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts @@ -259,13 +259,7 @@ export class CanvasEntityTransformer extends CanvasModuleABC { // This is called when a transform anchor is dragged. By this time, the transform constraints in the above // callbacks have been enforced, and the transformer has updated its nodes' attributes. We need to pass the // updated attributes to the object group, propagating the transformation on down. - this.parent.renderer.konva.objectGroup.setAttrs({ - x: this.konva.proxyRect.x(), - y: this.konva.proxyRect.y(), - scaleX: this.konva.proxyRect.scaleX(), - scaleY: this.konva.proxyRect.scaleY(), - rotation: this.konva.proxyRect.rotation(), - }); + this.syncObjectGroupWithProxyRect(); }); this.konva.transformer.on('transformend', () => { @@ -395,6 +389,53 @@ export class CanvasEntityTransformer extends CanvasModuleABC { this.parent.konva.layer.add(this.konva.transformer); } + flipHorizontal = () => { + if (!this.isTransforming || this.$isProcessing.get()) { + return; + } + + // Flipping horizontally = flipping across the vertical axis: + // - Flip by negating the x scale + // - Restore position by translating the rect rightwards by the width of the rect + const x = this.konva.proxyRect.x(); + const width = this.konva.proxyRect.width(); + const scaleX = this.konva.proxyRect.scaleX(); + this.konva.proxyRect.setAttrs({ + scaleX: -scaleX, + x: x + width * scaleX, + }); + + this.syncObjectGroupWithProxyRect(); + }; + + flipVertical = () => { + if (!this.isTransforming || this.$isProcessing.get()) { + return; + } + + // Flipping vertically = flipping across the horizontal axis: + // - Flip by negating the y scale + // - Restore position by translating the rect downwards by the height of the rect + const y = this.konva.proxyRect.y(); + const height = this.konva.proxyRect.height(); + const scaleY = this.konva.proxyRect.scaleY(); + this.konva.proxyRect.setAttrs({ + scaleY: -scaleY, + y: y + height * scaleY, + }); + this.syncObjectGroupWithProxyRect(); + }; + + syncObjectGroupWithProxyRect = () => { + this.parent.renderer.konva.objectGroup.setAttrs({ + x: this.konva.proxyRect.x(), + y: this.konva.proxyRect.y(), + scaleX: this.konva.proxyRect.scaleX(), + scaleY: this.konva.proxyRect.scaleY(), + rotation: this.konva.proxyRect.rotation(), + }); + }; + /** * Updates the transformer's visual components to match the parent entity's position and bounding box. * @param position The position of the parent entity @@ -510,6 +551,12 @@ export class CanvasEntityTransformer extends CanvasModuleABC { this.stopTransform(); }; + resetTransform = () => { + this.resetScale(); + this.updatePosition(); + this.updateBbox(); + }; + /** * Stops the transformation of the entity. If the transformation is in progress, the entity will be reset to its * original state. @@ -520,12 +567,9 @@ export class CanvasEntityTransformer extends CanvasModuleABC { this.isTransforming = false; this.setInteractionMode('off'); - // Reset the scale of the the entity. We've either replaced the transformed objects with a rasterized image, or + // Reset the transform of the the entity. We've either replaced the transformed objects with a rasterized image, or // canceled a transformation. In either case, the scale should be reset. - this.resetScale(); - - this.updatePosition(); - this.updateBbox(); + this.resetTransform(); this.syncInteractionState(); this.manager.stateApi.$transformingEntity.set(null); this.$isProcessing.set(false); From f4a031a4125f1cec01f06c05d2d6302e66085bf4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 28 Aug 2024 08:07:36 +1000 Subject: [PATCH 523/678] feat(ui): tweak filter styling --- .../components/Filters/Filter.tsx | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx index c88ed9c3edd..d6d9d75794f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx @@ -15,18 +15,6 @@ export const Filter = memo(() => { const isFiltering = useStore(canvasManager.filter.$isFiltering); const isProcessing = useStore(canvasManager.filter.$isProcessing); - const previewFilter = useCallback(() => { - canvasManager.filter.previewFilter(); - }, [canvasManager.filter]); - - const applyFilter = useCallback(() => { - canvasManager.filter.applyFilter(); - }, [canvasManager.filter]); - - const cancelFilter = useCallback(() => { - canvasManager.filter.cancelFilter(); - }, [canvasManager.filter]); - const onChangeFilterConfig = useCallback( (filterConfig: FilterConfig) => { canvasManager.filter.$config.set(filterConfig); @@ -65,24 +53,27 @@ export const Filter = memo(() => { - + - - + - - - - - - - - - - - + + + + {t('common.viewing')} + {t('common.viewingDesc')} + + } + > + } + onClick={imageViewer.onOpen} + variant={imageViewer.isOpen ? 'solid' : 'outline'} + colorScheme={imageViewer.isOpen ? 'invokeBlue' : 'base'} + aria-label={t('common.viewing')} + w={12} + /> + + + {t('common.editing')} + {t('common.editingDesc')} + + } + > + } + onClick={imageViewer.onClose} + variant={!imageViewer.isOpen ? 'solid' : 'outline'} + colorScheme={!imageViewer.isOpen ? 'invokeBlue' : 'base'} + aria-label={t('common.editing')} + w={12} + /> + + + ); }; From a8079e58542e44ba5de70faf8f700e41393dee75 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 28 Aug 2024 16:04:37 +1000 Subject: [PATCH 531/678] feat(ui): tidy canvas toolbar buttons --- .../components/Settings/CanvasSettingsPopover.tsx | 2 +- .../features/controlLayers/components/UndoRedoButtonGroup.tsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsPopover.tsx index f0e39e9afca..dae5e143c89 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsPopover.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsPopover.tsx @@ -27,7 +27,7 @@ export const CanvasSettingsPopover = memo(() => { return ( - } /> + } variant="ghost" /> diff --git a/invokeai/frontend/web/src/features/controlLayers/components/UndoRedoButtonGroup.tsx b/invokeai/frontend/web/src/features/controlLayers/components/UndoRedoButtonGroup.tsx index ec8ced184b6..fb3dae9b905 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/UndoRedoButtonGroup.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/UndoRedoButtonGroup.tsx @@ -29,13 +29,14 @@ export const UndoRedoButtonGroup = memo(() => { ]); return ( - + } isDisabled={!mayUndo} + variant="ghost" /> { onClick={handleRedo} icon={} isDisabled={!mayRedo} + variant="ghost" /> ); From 93b97920963bc6411ac777d3239840fb69618d5c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 28 Aug 2024 16:08:12 +1000 Subject: [PATCH 532/678] fix(ui): transparency effect not updating --- .../features/controlLayers/konva/CanvasEntityLayerAdapter.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityLayerAdapter.ts index 36740ba630e..e815548fbaa 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityLayerAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityLayerAdapter.ts @@ -112,12 +112,11 @@ export class CanvasEntityLayerAdapter extends CanvasModuleABC { this.renderer.updateOpacity(opacity); } - if (state.type === 'control_layer' && this.state.type === 'control_layer') { - if (this.isFirstRender || state.withTransparencyEffect !== this.state.withTransparencyEffect) { + if (state.type === 'control_layer' && prevState.type === 'control_layer') { + if (this.isFirstRender || state.withTransparencyEffect !== prevState.withTransparencyEffect) { this.renderer.updateTransparencyEffect(state.withTransparencyEffect); } } - // this.transformer.syncInteractionState(); if (this.isFirstRender) { this.transformer.updateBbox(); From eeca521bafdcd5e549858eb5d0134e993cb7a6e7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 28 Aug 2024 16:37:50 +1000 Subject: [PATCH 533/678] feat(ui): fix queue item count badge positioning Previously this badge, floating over the queue menu button next to the invoke button, was rendered within the existing layout. When I initially positioned it, the app layout interfered - it would extend into an area reserved for a flex gap, which cut off the badge. As a (bad) workaround, I had shifted the whole app down a few pixels to make room for it. What I should have done is what I've done in this commit - render the badge in a portal to take it out of the layout so we don't need that extra vertical padding. Sleekified some styling a bit too. --- .../components/ControlLayersEditor.tsx | 2 -- .../components/StageComponent.tsx | 6 ++-- .../components/GalleryPanelContent.tsx | 2 +- .../components/ImageViewer/ImageViewer.tsx | 3 +- .../components/QueueActionsMenuButton.tsx | 31 +++++++++++++++---- .../queue/components/QueueControls.tsx | 2 +- .../features/ui/components/VerticalNavBar.tsx | 2 +- 7 files changed, 31 insertions(+), 17 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx index 494a5a4ba05..02aa1c973ef 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx @@ -19,8 +19,6 @@ export const CanvasEditor = memo(() => { { ); return ( - + {!dynamicGrid && ( { left={0} ref={containerRef} borderRadius="base" - border={1} - borderStyle="solid" - borderColor="base.700" overflow="hidden" data-testid="control-layers-canvas" /> diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryPanelContent.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryPanelContent.tsx index 962ae6cf9aa..2406be7a084 100644 --- a/invokeai/frontend/web/src/features/gallery/components/GalleryPanelContent.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/GalleryPanelContent.tsx @@ -59,7 +59,7 @@ const GalleryPanelContent = () => { }, [boardSearchText.length, boardSearchDisclosure, boardsListPanel, dispatch]); return ( - + diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx index 46c7bcfbda6..431af16e23a 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx @@ -21,7 +21,7 @@ export const ImageViewer = memo(() => { { right={0} bottom={0} left={0} - p={2} rowGap={4} alignItems="center" justifyContent="center" diff --git a/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx b/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx index 99456b042f4..ce963226065 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx @@ -7,16 +7,18 @@ import { MenuDivider, MenuItem, MenuList, + Portal, useDisclosure, } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; +import type { Coordinate } from 'features/controlLayers/store/types'; import { useClearQueueConfirmationAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog'; import { useClearQueue } from 'features/queue/hooks/useClearQueue'; import { usePauseProcessor } from 'features/queue/hooks/usePauseProcessor'; import { useResumeProcessor } from 'features/queue/hooks/useResumeProcessor'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { setActiveTab } from 'features/ui/store/uiSlice'; -import { memo, useCallback } from 'react'; +import { memo, useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPauseFill, PiPlayFill, PiTrashSimpleBold } from 'react-icons/pi'; import { RiListCheck, RiPlayList2Fill } from 'react-icons/ri'; @@ -26,6 +28,8 @@ export const QueueActionsMenuButton = memo(() => { const { isOpen, onOpen, onClose } = useDisclosure(); const dispatch = useAppDispatch(); const { t } = useTranslation(); + const [badgePos, setBadgePos] = useState(null); + const menuButtonRef = useRef(null); const dialogState = useClearQueueConfirmationAlertDialog(); const isPauseEnabled = useFeatureStatus('pauseQueue'); const isResumeEnabled = useFeatureStatus('resumeQueue'); @@ -49,10 +53,17 @@ export const QueueActionsMenuButton = memo(() => { dispatch(setActiveTab('queue')); }, [dispatch]); + useEffect(() => { + if (menuButtonRef.current) { + const { x, y } = menuButtonRef.current.getBoundingClientRect(); + setBadgePos({ x: x - 10, y: y - 10 }); + } + }, []); + return ( - } /> + } /> { - {queueSize > 0 && ( - - {queueSize} - + {queueSize > 0 && badgePos !== null && ( + + + {queueSize} + + )} ); diff --git a/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx b/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx index 570708642db..1acbd6b6970 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx @@ -11,7 +11,7 @@ import { QueueActionsMenuButton } from './QueueActionsMenuButton'; const QueueControls = () => { const isPrependEnabled = useFeatureStatus('prependQueue'); return ( - + {isPrependEnabled && } diff --git a/invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx b/invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx index 402ba6aa264..28168b563e6 100644 --- a/invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx +++ b/invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx @@ -18,7 +18,7 @@ export const VerticalNavBar = memo(() => { const customNavComponent = useStore($customNavComponent); return ( - + From 2df0056ef1c4a123823bfa47882b574d3f45de19 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 28 Aug 2024 16:49:19 +1000 Subject: [PATCH 534/678] perf(ui): disable `useInert` on modals This hook forcibly updates _all_ portals with `data-hidden=true` when the modal opens - then reverts it when the modal closes. It's intended to help screen readers. Unfortunately, this absolutely tanks performance because we have many portals. React needs to do alot of layout calculations (not re-renders). IMO this behaviour is a bug in chakra. The modals which generated the portals are hidden by default, so this data attr should really be set by default. Dunno why it isn't. --- .../features/changeBoardModal/components/ChangeBoardModal.tsx | 1 + .../features/deleteImageModal/components/DeleteImageModal.tsx | 1 + .../dynamicPrompts/components/DynamicPromptsPreviewModal.tsx | 2 +- .../subpanels/ModelManagerPanel/ModelListItem.tsx | 1 + .../modelManagerV2/subpanels/ModelPanel/ModelConvertButton.tsx | 1 + .../nodes/components/flow/panels/TopPanel/ClearFlowButton.tsx | 1 + .../flow/panels/TopRightPanel/WorkflowEditorSettings.tsx | 2 +- .../queue/components/ClearQueueConfirmationAlertDialog.tsx | 1 + .../components/StylePresetForm/StylePresetModal.tsx | 2 +- .../features/stylePresets/components/StylePresetListItem.tsx | 1 + .../src/features/system/components/AboutModal/AboutModal.tsx | 2 +- .../features/system/components/HotkeysModal/HotkeysModal.tsx | 2 +- .../LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal.tsx | 2 +- .../components/NewWorkflowConfirmationAlertDialog.tsx | 1 + .../workflowLibrary/components/WorkflowLibraryModal.tsx | 2 +- 15 files changed, 15 insertions(+), 7 deletions(-) diff --git a/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx b/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx index 175bf9fedeb..d6d9a467318 100644 --- a/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx +++ b/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx @@ -83,6 +83,7 @@ const ChangeBoardModal = () => { acceptCallback={handleChangeBoard} acceptButtonText={t('boards.move')} cancelButtonText={t('boards.cancel')} + useInert={false} > diff --git a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx index 0cd46302433..6eff406b36e 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx +++ b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx @@ -81,6 +81,7 @@ const DeleteImageModal = () => { cancelButtonText={t('boards.cancel')} acceptButtonText={t('controlnet.delete')} acceptCallback={handleDelete} + useInert={false} > diff --git a/invokeai/frontend/web/src/features/dynamicPrompts/components/DynamicPromptsPreviewModal.tsx b/invokeai/frontend/web/src/features/dynamicPrompts/components/DynamicPromptsPreviewModal.tsx index 620bda597f4..f2929fee791 100644 --- a/invokeai/frontend/web/src/features/dynamicPrompts/components/DynamicPromptsPreviewModal.tsx +++ b/invokeai/frontend/web/src/features/dynamicPrompts/components/DynamicPromptsPreviewModal.tsx @@ -20,7 +20,7 @@ export const DynamicPromptsModal = memo(() => { const { isOpen, onClose } = useDynamicPromptsModal(); return ( - + {t('dynamicPrompts.dynamicPrompts')} diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx index 8bfcbd73518..60e4964b6dd 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx @@ -123,6 +123,7 @@ const ModelListItem = ({ model }: ModelListItemProps) => { title={t('modelManager.deleteModel')} acceptCallback={handleModelDelete} acceptButtonText={t('modelManager.delete')} + useInert={false} > {t('modelManager.deleteMsg1')} diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelConvertButton.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelConvertButton.tsx index 70775842f80..14119374d46 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelConvertButton.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelConvertButton.tsx @@ -67,6 +67,7 @@ export const ModelConvertButton = memo(({ modelConfig }: ModelConvertProps) => { acceptButtonText={`${t('modelManager.convert')}`} isOpen={isOpen} onClose={onClose} + useInert={false} > {t('modelManager.convertToDiffusersHelpText1')} diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/ClearFlowButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/ClearFlowButton.tsx index 0804fe02fff..87f7ad71ce1 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/ClearFlowButton.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/ClearFlowButton.tsx @@ -47,6 +47,7 @@ const ClearFlowButton = () => { onClose={onClose} title={t('nodes.clearWorkflow')} acceptCallback={handleNewWorkflow} + useInert={false} > {t('nodes.clearWorkflowDesc')} diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings.tsx index 5a3f8103060..395492f68f3 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings.tsx @@ -102,7 +102,7 @@ const WorkflowEditorSettings = ({ children }: Props) => { <> {children({ onOpen })} - + {t('nodes.workflowSettings')} diff --git a/invokeai/frontend/web/src/features/queue/components/ClearQueueConfirmationAlertDialog.tsx b/invokeai/frontend/web/src/features/queue/components/ClearQueueConfirmationAlertDialog.tsx index b90d66073ee..3a5833eee9b 100644 --- a/invokeai/frontend/web/src/features/queue/components/ClearQueueConfirmationAlertDialog.tsx +++ b/invokeai/frontend/web/src/features/queue/components/ClearQueueConfirmationAlertDialog.tsx @@ -22,6 +22,7 @@ export const ClearQueueConfirmationsAlertDialog = memo(() => { title={t('queue.clearTooltip')} acceptCallback={clearQueue} acceptButtonText={t('queue.clear')} + useInert={false} > {t('queue.clearQueueAlertDialog')}
diff --git a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetForm/StylePresetModal.tsx b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetForm/StylePresetModal.tsx index 5ae1db7af92..30c99b43f70 100644 --- a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetForm/StylePresetModal.tsx +++ b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetForm/StylePresetModal.tsx @@ -67,7 +67,7 @@ export const StylePresetModal = () => { }, [stylePresetModalState.prefilledFormData]); return ( - + {modalTitle} diff --git a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetListItem.tsx b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetListItem.tsx index 1387b964b85..30f5e876272 100644 --- a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetListItem.tsx +++ b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetListItem.tsx @@ -179,6 +179,7 @@ export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordWithI acceptCallback={handleDeletePreset} acceptButtonText={t('common.delete')} cancelButtonText={t('common.cancel')} + useInert={false} >

{t('stylePresets.deleteTemplate2')}

diff --git a/invokeai/frontend/web/src/features/system/components/AboutModal/AboutModal.tsx b/invokeai/frontend/web/src/features/system/components/AboutModal/AboutModal.tsx index d333b64ebfe..4eb6182ecc5 100644 --- a/invokeai/frontend/web/src/features/system/components/AboutModal/AboutModal.tsx +++ b/invokeai/frontend/web/src/features/system/components/AboutModal/AboutModal.tsx @@ -53,7 +53,7 @@ const AboutModal = ({ children }: AboutModalProps) => { {cloneElement(children, { onClick: onOpen, })} - + {t('accessibility.about')} diff --git a/invokeai/frontend/web/src/features/system/components/HotkeysModal/HotkeysModal.tsx b/invokeai/frontend/web/src/features/system/components/HotkeysModal/HotkeysModal.tsx index 0a823b8dfad..5189520ad3a 100644 --- a/invokeai/frontend/web/src/features/system/components/HotkeysModal/HotkeysModal.tsx +++ b/invokeai/frontend/web/src/features/system/components/HotkeysModal/HotkeysModal.tsx @@ -70,7 +70,7 @@ const HotkeysModal = ({ children }: HotkeysModalProps) => { {cloneElement(children, { onClick: onOpen, })} - + {t('hotkeys.keyboardShortcuts')} diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal.tsx index 6ecb51a528f..9f06ce321a8 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal.tsx @@ -62,7 +62,7 @@ export const LoadWorkflowFromGraphModal = () => { onClose(); }, [dispatch, onClose, workflowRaw]); return ( - + {t('workflows.loadFromGraph')} diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/NewWorkflowConfirmationAlertDialog.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/NewWorkflowConfirmationAlertDialog.tsx index 704412052a0..c0b5fee85b8 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/components/NewWorkflowConfirmationAlertDialog.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/NewWorkflowConfirmationAlertDialog.tsx @@ -46,6 +46,7 @@ export const NewWorkflowConfirmationAlertDialog = memo((props: Props) => { onClose={onClose} title={t('nodes.newWorkflow')} acceptCallback={handleNewWorkflow} + useInert={false} > {t('nodes.newWorkflowDesc')} diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryModal.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryModal.tsx index 7aaac5ec142..ca3148d9cb1 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryModal.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryModal.tsx @@ -16,7 +16,7 @@ const WorkflowLibraryModal = () => { const { t } = useTranslation(); const { isOpen, onClose } = useWorkflowLibraryModalContext(); return ( - + {t('workflows.workflowLibrary')} From be58b031370d856dcf26764cbde5d6122c203bc8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 28 Aug 2024 16:51:38 +1000 Subject: [PATCH 535/678] feat(ui): split settings modal --- .../frontend/web/src/app/components/App.tsx | 4 + .../SettingsModal/RefreshAfterResetModal.tsx | 72 +++++ .../components/SettingsModal/SettingsMenu.tsx | 11 +- .../SettingsModal/SettingsModal.tsx | 278 ++++++++---------- 4 files changed, 202 insertions(+), 163 deletions(-) create mode 100644 invokeai/frontend/web/src/features/system/components/SettingsModal/RefreshAfterResetModal.tsx diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index 07f484d2009..68acdb4aec2 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -17,6 +17,8 @@ import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterM import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog'; import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal'; import { activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice'; +import RefreshAfterResetModal from 'features/system/components/SettingsModal/RefreshAfterResetModal'; +import SettingsModal from 'features/system/components/SettingsModal/SettingsModal'; import { configChanged } from 'features/system/store/configSlice'; import { selectLanguage } from 'features/system/store/systemSelectors'; import { AppContent } from 'features/ui/components/AppContent'; @@ -135,6 +137,8 @@ const App = ({ + + ); }; diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/RefreshAfterResetModal.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/RefreshAfterResetModal.tsx new file mode 100644 index 00000000000..2530b705d1d --- /dev/null +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/RefreshAfterResetModal.tsx @@ -0,0 +1,72 @@ +import { + Flex, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Text, +} from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { buildUseBoolean } from 'common/hooks/useBoolean'; +import { atom } from 'nanostores'; +import { memo, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +const $refreshAfterResetModalState = atom(false); +export const useRefreshAfterResetModal = buildUseBoolean($refreshAfterResetModalState); + +const RefreshAfterResetModal = () => { + const { t } = useTranslation(); + const [countdown, setCountdown] = useState(3); + + const refreshModal = useRefreshAfterResetModal(); + const isOpen = useStore(refreshModal.$boolean); + + useEffect(() => { + if (!isOpen) { + return; + } + const i = window.setInterval(() => setCountdown((prev) => prev - 1), 1000); + return () => { + window.clearInterval(i); + }; + }, [isOpen]); + + useEffect(() => { + if (countdown <= 0) { + window.location.reload(); + } + }, [countdown]); + + return ( + <> + + + + + + + + + {t('settings.resetComplete')} {t('settings.reloadingIn')} {countdown}... + + + + + + + + + ); +}; + +export default memo(RefreshAfterResetModal); diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsMenu.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsMenu.tsx index 33455e50fa7..82c8c264af8 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsMenu.tsx +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsMenu.tsx @@ -25,12 +25,13 @@ import { } from 'react-icons/pi'; import { RiDiscordFill, RiGithubFill, RiSettings4Line } from 'react-icons/ri'; -import SettingsModal from './SettingsModal'; +import { useSettingsModal } from './SettingsModal'; import { SettingsUpsellMenuItem } from './SettingsUpsellMenuItem'; const SettingsMenu = () => { const { t } = useTranslation(); const { isOpen, onOpen, onClose } = useDisclosure(); useGlobalMenuClose(onClose); + const settingsModal = useSettingsModal(); const isBugLinkEnabled = useFeatureStatus('bugLink'); const isDiscordLinkEnabled = useFeatureStatus('discordLink'); @@ -75,11 +76,9 @@ const SettingsMenu = () => { {t('common.hotkeysLabel')} - - }> - {t('common.settingsLabel')} - - + }> + {t('common.settingsLabel')} + diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx index b9569a1a5c0..4ee58506543 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx @@ -13,13 +13,15 @@ import { ModalOverlay, Switch, Text, - useDisclosure, } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; +import { buildUseBoolean } from 'common/hooks/useBoolean'; import { useClearStorage } from 'common/hooks/useClearStorage'; import { selectShouldUseCPUNoise, shouldUseCpuNoiseChanged } from 'features/controlLayers/store/paramsSlice'; +import { useRefreshAfterResetModal } from 'features/system/components/SettingsModal/RefreshAfterResetModal'; import { SettingsDeveloperLogIsEnabled } from 'features/system/components/SettingsModal/SettingsDeveloperLogIsEnabled'; import { SettingsDeveloperLogLevel } from 'features/system/components/SettingsModal/SettingsDeveloperLogLevel'; import { SettingsDeveloperLogNamespaces } from 'features/system/components/SettingsModal/SettingsDeveloperLogNamespaces'; @@ -40,8 +42,9 @@ import { } from 'features/system/store/systemSlice'; import { selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors'; import { setShouldShowProgressInViewer } from 'features/ui/store/uiSlice'; -import type { ChangeEvent, ReactElement } from 'react'; -import { cloneElement, memo, useCallback, useEffect, useState } from 'react'; +import { atom } from 'nanostores'; +import type { ChangeEvent } from 'react'; +import { memo, useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useGetAppConfigQuery } from 'services/api/endpoints/appInfo'; @@ -54,27 +57,29 @@ type ConfigOptions = { shouldShowLocalizationToggle?: boolean; }; +const defaultConfig: ConfigOptions = { + shouldShowDeveloperSettings: true, + shouldShowResetWebUiText: true, + shouldShowClearIntermediates: true, + shouldShowLocalizationToggle: true, +}; + type SettingsModalProps = { - /* The button to open the Settings Modal */ - children: ReactElement; config?: ConfigOptions; }; -const SettingsModal = ({ children, config }: SettingsModalProps) => { +const $settingsModal = atom(false); +export const useSettingsModal = buildUseBoolean($settingsModal); + +const SettingsModal = ({ config = defaultConfig }: SettingsModalProps) => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const [countdown, setCountdown] = useState(3); - - const shouldShowDeveloperSettings = config?.shouldShowDeveloperSettings ?? true; - const shouldShowResetWebUiText = config?.shouldShowResetWebUiText ?? true; - const shouldShowClearIntermediates = config?.shouldShowClearIntermediates ?? true; - const shouldShowLocalizationToggle = config?.shouldShowLocalizationToggle ?? true; useEffect(() => { - if (!shouldShowDeveloperSettings) { + if (!config?.shouldShowDeveloperSettings) { dispatch(logIsEnabledChanged(false)); } - }, [shouldShowDeveloperSettings, dispatch]); + }, [dispatch, config?.shouldShowDeveloperSettings]); const { isNSFWCheckerAvailable, isWatermarkerAvailable } = useGetAppConfigQuery(undefined, { selectFromResult: ({ data }) => ({ @@ -89,11 +94,10 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => { intermediatesCount, isLoading: isLoadingClearIntermediates, refetchIntermediatesCount, - } = useClearIntermediates(shouldShowClearIntermediates); - - const { isOpen: isSettingsModalOpen, onOpen: _onSettingsModalOpen, onClose: onSettingsModalClose } = useDisclosure(); - - const { isOpen: isRefreshModalOpen, onOpen: onRefreshModalOpen, onClose: onRefreshModalClose } = useDisclosure(); + } = useClearIntermediates(Boolean(config?.shouldShowClearIntermediates)); + const settingsModal = useSettingsModal(); + const settingsModalIsOpen = useStore(settingsModal.$boolean); + const refreshModal = useRefreshAfterResetModal(); const shouldUseCpuNoise = useAppSelector(selectShouldUseCPUNoise); const shouldConfirmOnDelete = useAppSelector(selectSystemShouldConfirmOnDelete); @@ -105,25 +109,17 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => { const clearStorage = useClearStorage(); - const handleOpenSettingsModel = useCallback(() => { - if (shouldShowClearIntermediates) { + useEffect(() => { + if (settingsModalIsOpen && Boolean(config?.shouldShowClearIntermediates)) { refetchIntermediatesCount(); } - _onSettingsModalOpen(); - }, [_onSettingsModalOpen, refetchIntermediatesCount, shouldShowClearIntermediates]); + }, [config?.shouldShowClearIntermediates, refetchIntermediatesCount, settingsModalIsOpen]); const handleClickResetWebUI = useCallback(() => { clearStorage(); - onSettingsModalClose(); - onRefreshModalOpen(); - setInterval(() => setCountdown((prev) => prev - 1), 1000); - }, [clearStorage, onSettingsModalClose, onRefreshModalOpen]); - - useEffect(() => { - if (countdown <= 0) { - window.location.reload(); - } - }, [countdown]); + settingsModal.setFalse(); + refreshModal.setTrue(); + }, [clearStorage, settingsModal, refreshModal]); const handleChangeShouldConfirmOnDelete = useCallback( (e: ChangeEvent) => { @@ -169,139 +165,107 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => { ); return ( - <> - {cloneElement(children, { - onClick: handleOpenSettingsModel, - })} + + + + {t('common.settingsLabel')} + + + + + + + + {t('settings.confirmOnDelete')} + + + - - - - {t('common.settingsLabel')} - - - - - - - - {t('settings.confirmOnDelete')} - - - + + + {t('settings.enableNSFWChecker')} + + + + {t('settings.enableInvisibleWatermark')} + + + - - - {t('settings.enableNSFWChecker')} - - - - {t('settings.enableInvisibleWatermark')} - - - + + + {t('settings.showProgressInViewer')} + + + + {t('settings.antialiasProgressImages')} + + + + + {t('parameters.useCpuNoise')} + + + + {Boolean(config?.shouldShowLocalizationToggle) && } + + {t('settings.enableInformationalPopovers')} + + + - - - {t('settings.showProgressInViewer')} - - - - {t('settings.antialiasProgressImages')} - - - - - {t('parameters.useCpuNoise')} - - - - {shouldShowLocalizationToggle && } - - {t('settings.enableInformationalPopovers')} - - + {Boolean(config?.shouldShowDeveloperSettings) && ( + + + + + )} - {shouldShowDeveloperSettings && ( - - - - - - )} - - {shouldShowClearIntermediates && ( - - - {t('settings.clearIntermediatesDesc1')} - {t('settings.clearIntermediatesDesc2')} - {t('settings.clearIntermediatesDesc3')} - - )} - - - - {shouldShowResetWebUiText && ( - <> - {t('settings.resetWebUIDesc1')} - {t('settings.resetWebUIDesc2')} - - )} + {t('settings.clearIntermediatesDesc1')} + {t('settings.clearIntermediatesDesc2')} + {t('settings.clearIntermediatesDesc3')} - - - - - - - - + )} - - - - - - - - - {t('settings.resetComplete')} {t('settings.reloadingIn')} {countdown}... - - + + + {Boolean(config?.shouldShowResetWebUiText) && ( + <> + {t('settings.resetWebUIDesc1')} + {t('settings.resetWebUIDesc2')} + + )} + + - - - - - + + + + + + ); }; From db0b97cca1e3b0afa7a313eb574f3e2ed1eda2ae Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 28 Aug 2024 16:51:44 +1000 Subject: [PATCH 536/678] fix(ui): unnecessary z-index on invoke button --- .../web/src/features/queue/components/InvokeQueueBackButton.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx b/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx index b37ce123042..08950671a62 100644 --- a/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx +++ b/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx @@ -25,7 +25,6 @@ export const InvokeQueueBackButton = memo(() => { isDisabled={isDisabled} rightIcon={} variant="solid" - zIndex={1} colorScheme="invokeYellow" size="lg" w="calc(100% - 60px)" From 4463e6da5d7ad35926d3b67d9cde4cfcc59a7579 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 28 Aug 2024 21:06:33 +1000 Subject: [PATCH 537/678] fix(app): node_pack not added to openapi schema correctly --- invokeai/app/invocations/baseinvocation.py | 54 +++++++++------------- 1 file changed, 23 insertions(+), 31 deletions(-) diff --git a/invokeai/app/invocations/baseinvocation.py b/invokeai/app/invocations/baseinvocation.py index b527de41bc6..ec4bb923557 100644 --- a/invokeai/app/invocations/baseinvocation.py +++ b/invokeai/app/invocations/baseinvocation.py @@ -20,7 +20,6 @@ Type, TypeVar, Union, - cast, ) import semver @@ -80,7 +79,7 @@ class UIConfigBase(BaseModel): version: str = Field( description='The node\'s version. Should be a valid semver string e.g. "1.0.0" or "3.8.13".', ) - node_pack: Optional[str] = Field(default=None, description="Whether or not this is a custom node") + node_pack: str = Field(description="The node pack that this node belongs to, will be 'invokeai' for built-in nodes") classification: Classification = Field(default=Classification.Stable, description="The node's classification") model_config = ConfigDict( @@ -230,18 +229,16 @@ def get_output_annotation(cls) -> BaseInvocationOutput: @staticmethod def json_schema_extra(schema: dict[str, Any], model_class: Type[BaseInvocation]) -> None: """Adds various UI-facing attributes to the invocation's OpenAPI schema.""" - uiconfig = cast(UIConfigBase | None, getattr(model_class, "UIConfig", None)) - if uiconfig is not None: - if uiconfig.title is not None: - schema["title"] = uiconfig.title - if uiconfig.tags is not None: - schema["tags"] = uiconfig.tags - if uiconfig.category is not None: - schema["category"] = uiconfig.category - if uiconfig.node_pack is not None: - schema["node_pack"] = uiconfig.node_pack - schema["classification"] = uiconfig.classification - schema["version"] = uiconfig.version + if title := model_class.UIConfig.title: + schema["title"] = title + if tags := model_class.UIConfig.tags: + schema["tags"] = tags + if category := model_class.UIConfig.category: + schema["category"] = category + if node_pack := model_class.UIConfig.node_pack: + schema["node_pack"] = node_pack + schema["classification"] = model_class.UIConfig.classification + schema["version"] = model_class.UIConfig.version if "required" not in schema or not isinstance(schema["required"], list): schema["required"] = [] schema["class"] = "invocation" @@ -312,7 +309,7 @@ def invoke_internal(self, context: InvocationContext, services: "InvocationServi json_schema_extra={"field_kind": FieldKind.NodeAttribute}, ) - UIConfig: ClassVar[Type[UIConfigBase]] + UIConfig: ClassVar[UIConfigBase] model_config = ConfigDict( protected_namespaces=(), @@ -441,30 +438,25 @@ def wrapper(cls: Type[TBaseInvocation]) -> Type[TBaseInvocation]: validate_fields(cls.model_fields, invocation_type) # Add OpenAPI schema extras - uiconfig_name = cls.__qualname__ + ".UIConfig" - if not hasattr(cls, "UIConfig") or cls.UIConfig.__qualname__ != uiconfig_name: - cls.UIConfig = type(uiconfig_name, (UIConfigBase,), {}) - cls.UIConfig.title = title - cls.UIConfig.tags = tags - cls.UIConfig.category = category - cls.UIConfig.classification = classification - - # Grab the node pack's name from the module name, if it's a custom node - is_custom_node = cls.__module__.rsplit(".", 1)[0] == "invokeai.app.invocations" - if is_custom_node: - cls.UIConfig.node_pack = cls.__module__.split(".")[0] - else: - cls.UIConfig.node_pack = None + uiconfig: dict[str, Any] = {} + uiconfig["title"] = title + uiconfig["tags"] = tags + uiconfig["category"] = category + uiconfig["classification"] = classification + # The node pack is the module name - will be "invokeai" for built-in nodes + uiconfig["node_pack"] = cls.__module__.split(".")[0] if version is not None: try: semver.Version.parse(version) except ValueError as e: raise InvalidVersionError(f'Invalid version string for node "{invocation_type}": "{version}"') from e - cls.UIConfig.version = version + uiconfig["version"] = version else: logger.warn(f'No version specified for node "{invocation_type}", using "1.0.0"') - cls.UIConfig.version = "1.0.0" + uiconfig["version"] = "1.0.0" + + cls.UIConfig = UIConfigBase(**uiconfig) if use_cache is not None: cls.model_fields["use_cache"].default = use_cache From 4129cddbeb7a065d9df2eaa128020c33160057b4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 28 Aug 2024 21:06:45 +1000 Subject: [PATCH 538/678] chore(ui): typegen --- invokeai/frontend/web/src/services/api/schema.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index b9ca8a3a60c..a61e5bcddc8 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -15451,10 +15451,9 @@ export type components = { version: string; /** * Node Pack - * @description Whether or not this is a custom node - * @default null + * @description The node pack that this node belongs to, will be 'invokeai' for built-in nodes */ - node_pack: string | null; + node_pack: string; /** * @description The node's classification * @default stable From 28feb013d7b1328c37c65936c28b68b649aa82d2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 28 Aug 2024 21:31:15 +1000 Subject: [PATCH 539/678] fix(ui): schema parsing now that node_pack is guaranteed to be present --- .../frontend/web/src/features/nodes/store/util/testUtils.ts | 4 ++++ invokeai/frontend/web/src/features/nodes/types/invocation.ts | 4 ++-- .../web/src/features/nodes/util/node/buildInvocationNode.ts | 1 + .../web/src/features/nodes/util/workflow/graphToWorkflow.ts | 1 + .../src/features/nodes/util/workflow/validateWorkflow.test.ts | 2 ++ 5 files changed, 10 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts b/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts index 83988d55ea3..b81920e1662 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/testUtils.ts @@ -163,6 +163,7 @@ export const collect: InvocationTemplate = { }, }, useCache: true, + nodePack: 'invokeai', classification: 'stable', }; @@ -480,6 +481,7 @@ const iterate: InvocationTemplate = { }, }, useCache: true, + nodePack: 'invokeai', classification: 'stable', }; @@ -1152,6 +1154,7 @@ export const schema = { type: 'object', required: ['type', 'id'], title: 'CollectInvocation', + node_pack: 'invokeai', description: 'Collects values into a collection', classification: 'stable', version: '1.0.0', @@ -1513,6 +1516,7 @@ export const schema = { title: 'IterateInvocation', description: 'Iterates over a list of items', classification: 'stable', + node_pack: 'invokeai', version: '1.1.0', output: { $ref: '#/components/schemas/IterateInvocationOutput', diff --git a/invokeai/frontend/web/src/features/nodes/types/invocation.ts b/invokeai/frontend/web/src/features/nodes/types/invocation.ts index 0a7149bd6bb..0cfa02ed836 100644 --- a/invokeai/frontend/web/src/features/nodes/types/invocation.ts +++ b/invokeai/frontend/web/src/features/nodes/types/invocation.ts @@ -16,7 +16,7 @@ const zInvocationTemplate = z.object({ outputType: z.string().min(1), version: zSemVer, useCache: z.boolean(), - nodePack: z.string().min(1).nullish(), + nodePack: z.string().min(1).default('invokeai'), classification: zClassification, }); export type InvocationTemplate = z.infer; @@ -26,7 +26,7 @@ export type InvocationTemplate = z.infer; export const zInvocationNodeData = z.object({ id: z.string().trim().min(1), version: zSemVer, - nodePack: z.string().min(1).nullish(), + nodePack: z.string().min(1).default('invokeai'), label: z.string(), notes: z.string(), type: z.string().trim().min(1), diff --git a/invokeai/frontend/web/src/features/nodes/util/node/buildInvocationNode.ts b/invokeai/frontend/web/src/features/nodes/util/node/buildInvocationNode.ts index af19aa86eaf..6daadcf103e 100644 --- a/invokeai/frontend/web/src/features/nodes/util/node/buildInvocationNode.ts +++ b/invokeai/frontend/web/src/features/nodes/util/node/buildInvocationNode.ts @@ -38,6 +38,7 @@ export const buildInvocationNode = (position: XYPosition, template: InvocationTe isOpen: true, isIntermediate: type === 'save_image' ? false : true, useCache: template.useCache, + nodePack: template.nodePack, inputs, }, }; diff --git a/invokeai/frontend/web/src/features/nodes/util/workflow/graphToWorkflow.ts b/invokeai/frontend/web/src/features/nodes/util/workflow/graphToWorkflow.ts index 6560977ece8..00e8a43a94b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/workflow/graphToWorkflow.ts +++ b/invokeai/frontend/web/src/features/nodes/util/workflow/graphToWorkflow.ts @@ -85,6 +85,7 @@ export const graphToWorkflow = (graph: NonNullableGraph, autoLayout = true): Wor isOpen: true, isIntermediate: node.is_intermediate ?? false, useCache: node.use_cache ?? true, + nodePack: template.nodePack, inputs, }, }); diff --git a/invokeai/frontend/web/src/features/nodes/util/workflow/validateWorkflow.test.ts b/invokeai/frontend/web/src/features/nodes/util/workflow/validateWorkflow.test.ts index 6438d96773b..93321c97e06 100644 --- a/invokeai/frontend/web/src/features/nodes/util/workflow/validateWorkflow.test.ts +++ b/invokeai/frontend/web/src/features/nodes/util/workflow/validateWorkflow.test.ts @@ -28,6 +28,7 @@ describe('validateWorkflow', () => { isOpen: true, isIntermediate: true, useCache: true, + nodePack: 'invokeai', inputs: { model: { name: 'model', @@ -56,6 +57,7 @@ describe('validateWorkflow', () => { isOpen: true, isIntermediate: true, useCache: true, + nodePack: 'invokeai', inputs: { board: { name: 'board', From ac349573f2cc3fa7e433c62ae6f451990a28f81a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 28 Aug 2024 21:32:10 +1000 Subject: [PATCH 540/678] feat(ui): migrate add node popover to cmdk Put this together as a way to figure out the library before moving on to the full app cmdk. Works great. --- invokeai/frontend/web/package.json | 2 +- invokeai/frontend/web/pnpm-lock.yaml | 329 +++++++++++++- .../features/nodes/components/NodeEditor.tsx | 4 +- .../flow/AddNodeCmdk/AddNodeCmdk.tsx | 420 ++++++++++++++++++ .../flow/AddNodePopover/AddNodePopover.tsx | 267 ----------- .../features/nodes/components/flow/Flow.tsx | 4 +- .../flow/panels/TopPanel/AddNodeButton.tsx | 5 +- .../src/features/nodes/hooks/useConnection.ts | 4 +- .../src/features/nodes/store/nodesSlice.ts | 11 +- 9 files changed, 755 insertions(+), 291 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx delete mode 100644 invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 4fdb81ea676..4e98e287705 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -64,6 +64,7 @@ "@roarr/browser-log-writer": "^1.3.0", "async-mutex": "^0.5.0", "chakra-react-select": "^4.9.1", + "cmdk": "^1.0.0", "compare-versions": "^6.1.1", "dateformat": "^5.0.3", "fracturedjsonjs": "^4.0.2", @@ -92,7 +93,6 @@ "react-icons": "^5.2.1", "react-redux": "9.1.2", "react-resizable-panels": "^2.0.23", - "react-select": "5.8.0", "react-use": "^17.5.1", "react-virtuoso": "^4.9.0", "reactflow": "^11.11.4", diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index a487b8d6f33..2dad5456f4e 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -41,6 +41,9 @@ dependencies: chakra-react-select: specifier: ^4.9.1 version: 4.9.1(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.13.3)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + cmdk: + specifier: ^1.0.0 + version: 1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) compare-versions: specifier: ^6.1.1 version: 6.1.1 @@ -125,9 +128,6 @@ dependencies: react-resizable-panels: specifier: ^2.0.23 version: 2.0.23(react-dom@18.3.1)(react@18.3.1) - react-select: - specifier: 5.8.0 - version: 5.8.0(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) react-use: specifier: ^17.5.1 version: 17.5.1(react-dom@18.3.1)(react@18.3.1) @@ -2053,7 +2053,7 @@ packages: dependencies: '@chakra-ui/dom-utils': 2.1.0 react: 18.3.1 - react-focus-lock: 2.12.1(@types/react@18.3.3)(react@18.3.1) + react-focus-lock: 2.13.0(@types/react@18.3.3)(react@18.3.1) transitivePeerDependencies: - '@types/react' dev: false @@ -3784,6 +3784,288 @@ packages: resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} dev: false + /@radix-ui/primitive@1.0.1: + resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==} + dependencies: + '@babel/runtime': 7.25.4 + dev: false + + /@radix-ui/react-compose-refs@1.0.1(@types/react@18.3.3)(react@18.3.1): + resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.25.4 + '@types/react': 18.3.3 + react: 18.3.1 + dev: false + + /@radix-ui/react-context@1.0.1(@types/react@18.3.3)(react@18.3.1): + resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.25.4 + '@types/react': 18.3.3 + react: 18.3.1 + dev: false + + /@radix-ui/react-dialog@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.25.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-id': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.5.5(@types/react@18.3.3)(react@18.3.1) + dev: false + + /@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.25.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.3.3)(react@18.3.1) + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-focus-guards@1.0.1(@types/react@18.3.3)(react@18.3.1): + resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.25.4 + '@types/react': 18.3.3 + react: 18.3.1 + dev: false + + /@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.25.4 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-id@1.0.1(@types/react@18.3.3)(react@18.3.1): + resolution: {integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.25.4 + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@types/react': 18.3.3 + react: 18.3.1 + dev: false + + /@radix-ui/react-portal@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.25.4 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-presence@1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.25.4 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-primitive@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.25.4 + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@18.3.1) + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-slot@1.0.2(@types/react@18.3.3)(react@18.3.1): + resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.25.4 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@types/react': 18.3.3 + react: 18.3.1 + dev: false + + /@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.3.3)(react@18.3.1): + resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.25.4 + '@types/react': 18.3.3 + react: 18.3.1 + dev: false + + /@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.3.3)(react@18.3.1): + resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.25.4 + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@types/react': 18.3.3 + react: 18.3.1 + dev: false + + /@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.3.3)(react@18.3.1): + resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.25.4 + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@types/react': 18.3.3 + react: 18.3.1 + dev: false + + /@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.3.3)(react@18.3.1): + resolution: {integrity: sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.25.4 + '@types/react': 18.3.3 + react: 18.3.1 + dev: false + /@reactflow/background@11.3.14(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==} peerDependencies: @@ -5210,7 +5492,6 @@ packages: resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} dependencies: '@types/react': 18.3.3 - dev: true /@types/react-transition-group@4.4.10: resolution: {integrity: sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==} @@ -6268,6 +6549,21 @@ packages: requiresBuild: true dev: true + /cmdk@1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + dev: false + /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: @@ -9712,8 +10008,8 @@ packages: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} dev: false - /react-focus-lock@2.12.1(@types/react@18.3.3)(react@18.3.1): - resolution: {integrity: sha512-lfp8Dve4yJagkHiFrC1bGtib3mF2ktqwPJw4/WGcgPW+pJ/AVQA5X2vI7xgp13FcxFEpYBBHpXai/N2DBNC0Jw==} + /react-focus-lock@2.13.0(@types/react@18.3.3)(react@18.3.1): + resolution: {integrity: sha512-w7aIcTwZwNzUp2fYQDMICy+khFwVmKmOrLF8kNsPS+dz4Oi/oxoVJ2wCMVvX6rWGriM/+mYaTyp1MRmkcs2amw==} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -9867,6 +10163,25 @@ packages: use-sidecar: 1.1.2(@types/react@18.3.3)(react@18.3.1) dev: false + /react-remove-scroll@2.5.5(@types/react@18.3.3)(react@18.3.1): + resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.3.3 + react: 18.3.1 + react-remove-scroll-bar: 2.3.6(@types/react@18.3.3)(react@18.3.1) + react-style-singleton: 2.2.1(@types/react@18.3.3)(react@18.3.1) + tslib: 2.7.0 + use-callback-ref: 1.3.2(@types/react@18.3.3)(react@18.3.1) + use-sidecar: 1.1.2(@types/react@18.3.3)(react@18.3.1) + dev: false + /react-resizable-panels@2.0.23(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-8ZKTwTU11t/FYwiwhMdtZYYyFxic5U5ysRu2YwfkAgDbUJXFvnWSJqhnzkSlW+mnDoNAzDCrJhdOSXBPA76wug==} peerDependencies: diff --git a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx index 27e2006b08a..18ac2abdc47 100644 --- a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx @@ -2,6 +2,7 @@ import 'reactflow/dist/style.css'; import { Flex } from '@invoke-ai/ui-library'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; +import { AddNodeCmdk } from 'features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk'; import TopPanel from 'features/nodes/components/flow/panels/TopPanel/TopPanel'; import { LoadWorkflowFromGraphModal } from 'features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal'; import { SaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog/SaveWorkflowAsDialog'; @@ -10,7 +11,6 @@ import { useTranslation } from 'react-i18next'; import { MdDeviceHub } from 'react-icons/md'; import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo'; -import AddNodePopover from './flow/AddNodePopover/AddNodePopover'; import { Flow } from './flow/Flow'; import BottomLeftPanel from './flow/panels/BottomLeftPanel/BottomLeftPanel'; import MinimapPanel from './flow/panels/MinimapPanel/MinimapPanel'; @@ -31,7 +31,7 @@ const NodeEditor = () => { {data && ( <> - + diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx new file mode 100644 index 00000000000..207b7cf0aa6 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx @@ -0,0 +1,420 @@ +import type { SystemStyleObject } from '@invoke-ai/ui-library'; +import { + Box, + Flex, + Icon, + Input, + Modal, + ModalBody, + ModalContent, + ModalOverlay, + Spacer, + Text, +} from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { useAppStore } from 'app/store/storeHooks'; +import { CommandEmpty, CommandItem, CommandList, CommandRoot } from 'cmdk'; +import { IAINoContentFallback } from 'common/components/IAIImageFallback'; +import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; +import { useBuildNode } from 'features/nodes/hooks/useBuildNode'; +import { + $addNodeCmdk, + $cursorPos, + $edgePendingUpdate, + $pendingConnection, + $templates, + edgesChanged, + nodesChanged, + useAddNodeCmdk, +} from 'features/nodes/store/nodesSlice'; +import { selectNodesSlice } from 'features/nodes/store/selectors'; +import { findUnoccupiedPosition } from 'features/nodes/store/util/findUnoccupiedPosition'; +import { getFirstValidConnection } from 'features/nodes/store/util/getFirstValidConnection'; +import { connectionToEdge } from 'features/nodes/store/util/reactFlowUtil'; +import { validateConnectionTypes } from 'features/nodes/store/util/validateConnectionTypes'; +import { isInvocationNode } from 'features/nodes/types/invocation'; +import { toast } from 'features/toast/toast'; +import { memoize } from 'lodash-es'; +import { computed } from 'nanostores'; +import type { ChangeEvent } from 'react'; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; +import { PiFlaskBold, PiHammerBold } from 'react-icons/pi'; +import type { EdgeChange, NodeChange } from 'reactflow'; +import type { S } from 'services/api/types'; + +const useThrottle = (value: T, limit: number) => { + const [throttledValue, setThrottledValue] = useState(value); + const lastRan = useRef(Date.now()); + + useEffect(() => { + const handler = setTimeout( + function () { + if (Date.now() - lastRan.current >= limit) { + setThrottledValue(value); + lastRan.current = Date.now(); + } + }, + limit - (Date.now() - lastRan.current) + ); + + return () => { + clearTimeout(handler); + }; + }, [value, limit]); + + return throttledValue; +}; + +const useAddNode = () => { + const { t } = useTranslation(); + const store = useAppStore(); + const buildInvocation = useBuildNode(); + const templates = useStore($templates); + const pendingConnection = useStore($pendingConnection); + + const addNode = useCallback( + (nodeType: string): void => { + const node = buildInvocation(nodeType); + if (!node) { + const errorMessage = t('nodes.unknownNode', { + nodeType: nodeType, + }); + toast({ + status: 'error', + title: errorMessage, + }); + return; + } + + // Find a cozy spot for the node + const cursorPos = $cursorPos.get(); + const { nodes, edges } = selectNodesSlice(store.getState()); + node.position = findUnoccupiedPosition(nodes, cursorPos?.x ?? node.position.x, cursorPos?.y ?? node.position.y); + node.selected = true; + + // Deselect all other nodes and edges + const nodeChanges: NodeChange[] = [{ type: 'add', item: node }]; + const edgeChanges: EdgeChange[] = []; + nodes.forEach(({ id, selected }) => { + if (selected) { + nodeChanges.push({ type: 'select', id, selected: false }); + } + }); + edges.forEach(({ id, selected }) => { + if (selected) { + edgeChanges.push({ type: 'select', id, selected: false }); + } + }); + + // Onwards! + if (nodeChanges.length > 0) { + store.dispatch(nodesChanged(nodeChanges)); + } + if (edgeChanges.length > 0) { + store.dispatch(edgesChanged(edgeChanges)); + } + + // Auto-connect an edge if we just added a node and have a pending connection + if (pendingConnection && isInvocationNode(node)) { + const edgePendingUpdate = $edgePendingUpdate.get(); + const { handleType } = pendingConnection; + + const source = handleType === 'source' ? pendingConnection.nodeId : node.id; + const sourceHandle = handleType === 'source' ? pendingConnection.handleId : null; + const target = handleType === 'target' ? pendingConnection.nodeId : node.id; + const targetHandle = handleType === 'target' ? pendingConnection.handleId : null; + + const { nodes, edges } = selectNodesSlice(store.getState()); + const connection = getFirstValidConnection( + source, + sourceHandle, + target, + targetHandle, + nodes, + edges, + templates, + edgePendingUpdate + ); + if (connection) { + const newEdge = connectionToEdge(connection); + store.dispatch(edgesChanged([{ type: 'add', item: newEdge }])); + } + } + }, + [buildInvocation, pendingConnection, store, t, templates] + ); + + return addNode; +}; + +const cmdkRootSx: SystemStyleObject = { + '[cmdk-root]': { + w: 'full', + h: 'full', + }, + '[cmdk-list]': { + w: 'full', + h: 'full', + }, +}; + +export const AddNodeCmdk = memo(() => { + const { t } = useTranslation(); + const addNodeCmdk = useAddNodeCmdk(); + const addNodeCmdkIsOpen = useStore(addNodeCmdk.$boolean); + const inputRef = useRef(null); + const [searchTerm, setSearchTerm] = useState(''); + const addNode = useAddNode(); + const throttledSearchTerm = useThrottle(searchTerm, 100); + + useHotkeys(['shift+a', 'space'], addNodeCmdk.setTrue, { preventDefault: true }); + + const onChange = useCallback((e: ChangeEvent) => { + setSearchTerm(e.target.value); + }, []); + + const onSelect = useCallback( + (value: string) => { + addNode(value); + $addNodeCmdk.set(false); + setSearchTerm(''); + }, + [addNode] + ); + + const onClose = useCallback(() => { + addNodeCmdk.setFalse(); + setSearchTerm(''); + $pendingConnection.set(null); + }, [addNodeCmdk]); + + return ( + + + + + + + + + + + + + + + + + + + + + + + ); +}); + +AddNodeCmdk.displayName = 'AddNodeCmdk'; + +const cmdkItemSx: SystemStyleObject = { + '&[data-selected="true"]': { + bg: 'base.700', + }, +}; + +type NodeCommandItemData = { + value: string; + label: string; + description: string; + classification: S['Classification']; + nodePack: string; +}; + +const $templatesArray = computed($templates, (templates) => Object.values(templates)); + +const createRegex = memoize( + (inputValue: string) => + new RegExp( + inputValue + .trim() + .replace(/[-[\]{}()*+!<=:?./\\^$|#,]/g, '') + .split(' ') + .join('.*'), + 'gi' + ) +); + +// Filterable items are a subset of Invocation template - we also want to filter for notes or current image node, +// so we are using a less specific type instead of `InvocationTemplate` +type FilterableItem = { + type: string; + title: string; + description: string; + tags: string[]; + classification: S['Classification']; + nodePack: string; +}; + +const filter = memoize( + (item: FilterableItem, searchTerm: string) => { + const regex = createRegex(searchTerm); + + if (!searchTerm) { + return true; + } + + if (item.title.includes(searchTerm) || regex.test(item.title)) { + return true; + } + + if (item.type.includes(searchTerm) || regex.test(item.type)) { + return true; + } + + if (item.description.includes(searchTerm) || regex.test(item.description)) { + return true; + } + + if (item.nodePack.includes(searchTerm) || regex.test(item.nodePack)) { + return true; + } + + if (item.classification.includes(searchTerm) || regex.test(item.classification)) { + return true; + } + + for (const tag of item.tags) { + if (tag.includes(searchTerm) || regex.test(tag)) { + return true; + } + } + + return false; + }, + (item: FilterableItem, searchTerm: string) => `${item.type}-${searchTerm}` +); + +const NodeCommandList = memo(({ searchTerm, onSelect }: { searchTerm: string; onSelect: (value: string) => void }) => { + const { t } = useTranslation(); + const templatesArray = useStore($templatesArray); + const pendingConnection = useStore($pendingConnection); + const currentImageFilterItem = useMemo( + () => ({ + type: 'current_image', + title: t('nodes.currentImage'), + description: t('nodes.currentImageDescription'), + tags: ['progress', 'image', 'current'], + classification: 'stable', + nodePack: 'invokeai', + }), + [t] + ); + const notesFilterItem = useMemo( + () => ({ + type: 'notes', + title: t('nodes.notes'), + description: t('nodes.notesDescription'), + tags: ['notes'], + classification: 'stable', + nodePack: 'invokeai', + }), + [t] + ); + + const items = useMemo(() => { + // If we have a connection in progress, we need to filter the node choices + const _items: NodeCommandItemData[] = []; + + if (!pendingConnection) { + for (const template of templatesArray) { + if (filter(template, searchTerm)) { + _items.push({ + label: template.title, + value: template.type, + description: template.description, + classification: template.classification, + nodePack: template.nodePack, + }); + } + } + + for (const item of [currentImageFilterItem, notesFilterItem]) { + if (filter(item, searchTerm)) { + _items.push({ + label: item.title, + value: item.type, + description: item.description, + classification: item.classification, + nodePack: item.nodePack, + }); + } + } + } else { + for (const template of templatesArray) { + if (filter(template, searchTerm)) { + const candidateFields = pendingConnection.handleType === 'source' ? template.inputs : template.outputs; + + for (const field of Object.values(candidateFields)) { + const sourceType = + pendingConnection.handleType === 'source' ? field.type : pendingConnection.fieldTemplate.type; + const targetType = + pendingConnection.handleType === 'target' ? field.type : pendingConnection.fieldTemplate.type; + + if (validateConnectionTypes(sourceType, targetType)) { + _items.push({ + label: template.title, + value: template.type, + description: template.description, + classification: template.classification, + nodePack: template.nodePack, + }); + break; + } + } + } + } + } + + return _items; + }, [pendingConnection, currentImageFilterItem, searchTerm, notesFilterItem, templatesArray]); + + return ( + <> + {items.map((item) => ( + + + + {item.classification === 'beta' && } + {item.classification === 'prototype' && } + {item.label} + + + {item.nodePack} + + + {item.description && {item.description}} + + + ))} + + ); +}); + +NodeCommandList.displayName = 'CommandListItems'; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx deleted file mode 100644 index cb6516efd97..00000000000 --- a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx +++ /dev/null @@ -1,267 +0,0 @@ -import 'reactflow/dist/style.css'; - -import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; -import { Combobox, Flex, Popover, PopoverAnchor, PopoverBody, PopoverContent } from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; -import { useAppDispatch, useAppStore } from 'app/store/storeHooks'; -import type { SelectInstance } from 'chakra-react-select'; -import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes'; -import { useBuildNode } from 'features/nodes/hooks/useBuildNode'; -import { - $cursorPos, - $edgePendingUpdate, - $isAddNodePopoverOpen, - $pendingConnection, - $templates, - closeAddNodePopover, - edgesChanged, - nodesChanged, - openAddNodePopover, -} from 'features/nodes/store/nodesSlice'; -import { selectNodesSlice } from 'features/nodes/store/selectors'; -import { findUnoccupiedPosition } from 'features/nodes/store/util/findUnoccupiedPosition'; -import { getFirstValidConnection } from 'features/nodes/store/util/getFirstValidConnection'; -import { connectionToEdge } from 'features/nodes/store/util/reactFlowUtil'; -import { validateConnectionTypes } from 'features/nodes/store/util/validateConnectionTypes'; -import type { AnyNode } from 'features/nodes/types/invocation'; -import { isInvocationNode } from 'features/nodes/types/invocation'; -import { toast } from 'features/toast/toast'; -import { filter, map, memoize, some } from 'lodash-es'; -import { memo, useCallback, useMemo, useRef } from 'react'; -import { flushSync } from 'react-dom'; -import { useHotkeys } from 'react-hotkeys-hook'; -import type { HotkeyCallback } from 'react-hotkeys-hook/dist/types'; -import { useTranslation } from 'react-i18next'; -import type { FilterOptionOption } from 'react-select/dist/declarations/src/filters'; -import type { EdgeChange, NodeChange } from 'reactflow'; - -const createRegex = memoize( - (inputValue: string) => - new RegExp( - inputValue - .trim() - .replace(/[-[\]{}()*+!<=:?./\\^$|#,]/g, '') - .split(' ') - .join('.*'), - 'gi' - ) -); - -const filterOption = memoize((option: FilterOptionOption, inputValue: string) => { - if (!inputValue) { - return true; - } - const regex = createRegex(inputValue); - return ( - regex.test(option.label) || - regex.test(option.data.description ?? '') || - (option.data.tags ?? []).some((tag) => regex.test(tag)) - ); -}); - -const AddNodePopover = () => { - const dispatch = useAppDispatch(); - const buildInvocation = useBuildNode(); - const { t } = useTranslation(); - const selectRef = useRef | null>(null); - const inputRef = useRef(null); - const templates = useStore($templates); - const pendingConnection = useStore($pendingConnection); - const isOpen = useStore($isAddNodePopoverOpen); - const store = useAppStore(); - const isWorkflowsActive = useStore(INTERACTION_SCOPES.workflows.$isActive); - - const filteredTemplates = useMemo(() => { - // If we have a connection in progress, we need to filter the node choices - const templatesArray = map(templates); - if (!pendingConnection) { - return templatesArray; - } - - return filter(templates, (template) => { - const candidateFields = pendingConnection.handleType === 'source' ? template.inputs : template.outputs; - return some(candidateFields, (field) => { - const sourceType = - pendingConnection.handleType === 'source' ? field.type : pendingConnection.fieldTemplate.type; - const targetType = - pendingConnection.handleType === 'target' ? field.type : pendingConnection.fieldTemplate.type; - return validateConnectionTypes(sourceType, targetType); - }); - }); - }, [templates, pendingConnection]); - - const options = useMemo(() => { - const _options: ComboboxOption[] = map(filteredTemplates, (template) => { - return { - label: template.title, - value: template.type, - description: template.description, - tags: template.tags, - }; - }); - - //We only want these nodes if we're not filtered - if (!pendingConnection) { - _options.push({ - label: t('nodes.currentImage'), - value: 'current_image', - description: t('nodes.currentImageDescription'), - tags: ['progress'], - }); - - _options.push({ - label: t('nodes.notes'), - value: 'notes', - description: t('nodes.notesDescription'), - tags: ['notes'], - }); - } - - _options.sort((a, b) => a.label.localeCompare(b.label)); - - return _options; - }, [filteredTemplates, pendingConnection, t]); - - const addNode = useCallback( - (nodeType: string): AnyNode | null => { - const node = buildInvocation(nodeType); - if (!node) { - const errorMessage = t('nodes.unknownNode', { - nodeType: nodeType, - }); - toast({ - status: 'error', - title: errorMessage, - }); - return null; - } - - // Find a cozy spot for the node - const cursorPos = $cursorPos.get(); - const { nodes, edges } = selectNodesSlice(store.getState()); - node.position = findUnoccupiedPosition(nodes, cursorPos?.x ?? node.position.x, cursorPos?.y ?? node.position.y); - node.selected = true; - - // Deselect all other nodes and edges - const nodeChanges: NodeChange[] = [{ type: 'add', item: node }]; - const edgeChanges: EdgeChange[] = []; - nodes.forEach(({ id, selected }) => { - if (selected) { - nodeChanges.push({ type: 'select', id, selected: false }); - } - }); - edges.forEach(({ id, selected }) => { - if (selected) { - edgeChanges.push({ type: 'select', id, selected: false }); - } - }); - - // Onwards! - if (nodeChanges.length > 0) { - dispatch(nodesChanged(nodeChanges)); - } - if (edgeChanges.length > 0) { - dispatch(edgesChanged(edgeChanges)); - } - return node; - }, - [buildInvocation, store, dispatch, t] - ); - - const onChange = useCallback( - (v) => { - if (!v) { - return; - } - const node = addNode(v.value); - - // Auto-connect an edge if we just added a node and have a pending connection - if (pendingConnection && isInvocationNode(node)) { - const edgePendingUpdate = $edgePendingUpdate.get(); - const { handleType } = pendingConnection; - - const source = handleType === 'source' ? pendingConnection.nodeId : node.id; - const sourceHandle = handleType === 'source' ? pendingConnection.handleId : null; - const target = handleType === 'target' ? pendingConnection.nodeId : node.id; - const targetHandle = handleType === 'target' ? pendingConnection.handleId : null; - - const { nodes, edges } = selectNodesSlice(store.getState()); - const connection = getFirstValidConnection( - source, - sourceHandle, - target, - targetHandle, - nodes, - edges, - templates, - edgePendingUpdate - ); - if (connection) { - const newEdge = connectionToEdge(connection); - dispatch(edgesChanged([{ type: 'add', item: newEdge }])); - } - } - - closeAddNodePopover(); - }, - [addNode, dispatch, pendingConnection, store, templates] - ); - - const handleHotkeyOpen: HotkeyCallback = useCallback((e) => { - if (!$isAddNodePopoverOpen.get()) { - e.preventDefault(); - openAddNodePopover(); - flushSync(() => { - selectRef.current?.inputRef?.focus(); - }); - } - }, []); - - useHotkeys(['shift+a', 'space'], handleHotkeyOpen, { enabled: isWorkflowsActive }, [isWorkflowsActive]); - - const noOptionsMessage = useCallback(() => t('nodes.noMatchingNodes'), [t]); - - return ( - - - - - - - - - - - ); -}; - -export default memo(AddNodePopover); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index f85131a1331..ad631d3125c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -8,10 +8,10 @@ import { useSyncExecutionState } from 'features/nodes/hooks/useExecutionState'; import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection'; import { useWorkflowWatcher } from 'features/nodes/hooks/useWorkflowWatcher'; import { + $addNodeCmdk, $cursorPos, $didUpdateEdge, $edgePendingUpdate, - $isAddNodePopoverOpen, $lastEdgeUpdateMouseEvent, $pendingConnection, $viewport, @@ -281,7 +281,7 @@ export const Flow = memo(() => { const onEscapeHotkey = useCallback(() => { if (!$edgePendingUpdate.get()) { $pendingConnection.set(null); - $isAddNodePopoverOpen.set(false); + $addNodeCmdk.set(false); cancelConnection(); } }, [cancelConnection]); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/AddNodeButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/AddNodeButton.tsx index c7eb9bdbb02..72beff084c4 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/AddNodeButton.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/AddNodeButton.tsx @@ -1,10 +1,11 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { openAddNodePopover } from 'features/nodes/store/nodesSlice'; +import { useAddNodeCmdk } from 'features/nodes/store/nodesSlice'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPlusBold } from 'react-icons/pi'; const AddNodeButton = () => { + const addNodeCmdk = useAddNodeCmdk(); const { t } = useTranslation(); return ( @@ -12,7 +13,7 @@ const AddNodeButton = () => { tooltip={t('nodes.addNodeToolTip')} aria-label={t('nodes.addNode')} icon={} - onClick={openAddNodePopover} + onClick={addNodeCmdk.setTrue} pointerEvents="auto" /> ); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts index f2cc8690bbf..c41ef9f6896 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts @@ -2,9 +2,9 @@ import { useStore } from '@nanostores/react'; import { useAppStore } from 'app/store/storeHooks'; import { $mouseOverNode } from 'features/nodes/hooks/useMouseOverNode'; import { + $addNodeCmdk, $didUpdateEdge, $edgePendingUpdate, - $isAddNodePopoverOpen, $pendingConnection, $templates, edgesChanged, @@ -107,7 +107,7 @@ export const useConnection = () => { $pendingConnection.set(null); } else { // The mouse is not over a node - we should open the add node popover - $isAddNodePopoverOpen.set(true); + $addNodeCmdk.set(true); } }, [store, templates, updateNodeInternals]); diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index d0b144dfa26..bdcc7ae8158 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -1,6 +1,7 @@ import type { PayloadAction, UnknownAction } from '@reduxjs/toolkit'; import { createSlice, isAnyOf } from '@reduxjs/toolkit'; import type { PersistConfig } from 'app/store/store'; +import { buildUseBoolean } from 'common/hooks/useBoolean'; import { workflowLoaded } from 'features/nodes/store/actions'; import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants'; import type { @@ -443,14 +444,8 @@ export const $didUpdateEdge = atom(false); export const $lastEdgeUpdateMouseEvent = atom(null); export const $viewport = atom({ x: 0, y: 0, zoom: 1 }); -export const $isAddNodePopoverOpen = atom(false); -export const closeAddNodePopover = () => { - $isAddNodePopoverOpen.set(false); - $pendingConnection.set(null); -}; -export const openAddNodePopover = () => { - $isAddNodePopoverOpen.set(true); -}; +export const $addNodeCmdk = atom(false); +export const useAddNodeCmdk = buildUseBoolean($addNodeCmdk); /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const migrateNodesState = (state: any): any => { From 42c7ddebaaa9d868c5971c2f7520b0284c5406c0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 28 Aug 2024 21:33:12 +1000 Subject: [PATCH 541/678] chore: release v4.2.9.dev6 --- invokeai/version/invokeai_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/version/invokeai_version.py b/invokeai/version/invokeai_version.py index 5a768e14bb2..c7fe84f0e2c 100644 --- a/invokeai/version/invokeai_version.py +++ b/invokeai/version/invokeai_version.py @@ -1 +1 @@ -__version__ = "4.2.9.dev5" +__version__ = "4.2.9.dev6" From 79168b637e61656f98cebe05974736a7543cd45e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 28 Aug 2024 21:38:57 +1000 Subject: [PATCH 542/678] chore(ui): lint --- .../components/RegionalGuidance/RegionalGuidanceSettings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx index 2d9a16b9175..fa3c775e67d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx @@ -35,7 +35,7 @@ export const RegionalGuidanceSettings = memo(() => { {flags.hasPositivePrompt && ( <> - {(!flags.hasNegativePrompt && flags.hasIPAdapters) && } + {!flags.hasNegativePrompt && flags.hasIPAdapters && } )} {flags.hasNegativePrompt && ( From a95fbfe6c6190fdae86a17e74e6870b518bc87cf Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 28 Aug 2024 22:05:03 +1000 Subject: [PATCH 543/678] fix(ui): pending node connection stuck --- .../flow/AddNodeCmdk/AddNodeCmdk.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx index 207b7cf0aa6..107850a0485 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx @@ -18,7 +18,6 @@ import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import { useBuildNode } from 'features/nodes/hooks/useBuildNode'; import { - $addNodeCmdk, $cursorPos, $edgePendingUpdate, $pendingConnection, @@ -175,21 +174,20 @@ export const AddNodeCmdk = memo(() => { setSearchTerm(e.target.value); }, []); - const onSelect = useCallback( - (value: string) => { - addNode(value); - $addNodeCmdk.set(false); - setSearchTerm(''); - }, - [addNode] - ); - const onClose = useCallback(() => { addNodeCmdk.setFalse(); setSearchTerm(''); $pendingConnection.set(null); }, [addNodeCmdk]); + const onSelect = useCallback( + (value: string) => { + addNode(value); + onClose(); + }, + [addNode, onClose] + ); + return ( Date: Wed, 28 Aug 2024 22:05:15 +1000 Subject: [PATCH 544/678] chore: release v4.2.9.dev7 --- invokeai/version/invokeai_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/version/invokeai_version.py b/invokeai/version/invokeai_version.py index c7fe84f0e2c..c4a9a539160 100644 --- a/invokeai/version/invokeai_version.py +++ b/invokeai/version/invokeai_version.py @@ -1 +1 @@ -__version__ = "4.2.9.dev6" +__version__ = "4.2.9.dev7" From cd2a80cf778367e6704a72394fd954749d367f0b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 29 Aug 2024 07:47:30 +1000 Subject: [PATCH 545/678] fix(ui): add node cmdk only enabled on workflows tab --- .../nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx index 107850a0485..b34ca3992b4 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx @@ -12,7 +12,7 @@ import { Text, } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; -import { useAppStore } from 'app/store/storeHooks'; +import { useAppSelector, useAppStore } from 'app/store/storeHooks'; import { CommandEmpty, CommandItem, CommandList, CommandRoot } from 'cmdk'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; @@ -33,6 +33,7 @@ import { connectionToEdge } from 'features/nodes/store/util/reactFlowUtil'; import { validateConnectionTypes } from 'features/nodes/store/util/validateConnectionTypes'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { toast } from 'features/toast/toast'; +import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { memoize } from 'lodash-es'; import { computed } from 'nanostores'; import type { ChangeEvent } from 'react'; @@ -166,9 +167,10 @@ export const AddNodeCmdk = memo(() => { const inputRef = useRef(null); const [searchTerm, setSearchTerm] = useState(''); const addNode = useAddNode(); + const tab = useAppSelector(selectActiveTab); const throttledSearchTerm = useThrottle(searchTerm, 100); - useHotkeys(['shift+a', 'space'], addNodeCmdk.setTrue, { preventDefault: true }); + useHotkeys(['shift+a', 'space'], addNodeCmdk.setTrue, { enabled: tab === 'workflows', preventDefault: true }, [tab]); const onChange = useCallback((e: ChangeEvent) => { setSearchTerm(e.target.value); From e57ee8db35e497ef552f19979bdd09409e280233 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 29 Aug 2024 08:13:16 +1000 Subject: [PATCH 546/678] fix(ui): queue count badge positioning --- .../components/QueueActionsMenuButton.tsx | 36 ++++++++++++++----- .../queue/components/QueueControls.tsx | 7 ++-- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx b/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx index ce963226065..8332aabd4bd 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx @@ -1,6 +1,5 @@ import { Badge, - Box, IconButton, Menu, MenuButton, @@ -18,13 +17,18 @@ import { usePauseProcessor } from 'features/queue/hooks/usePauseProcessor'; import { useResumeProcessor } from 'features/queue/hooks/useResumeProcessor'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { setActiveTab } from 'features/ui/store/uiSlice'; +import type { RefObject } from 'react'; import { memo, useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPauseFill, PiPlayFill, PiTrashSimpleBold } from 'react-icons/pi'; import { RiListCheck, RiPlayList2Fill } from 'react-icons/ri'; import { useGetQueueStatusQuery } from 'services/api/endpoints/queue'; -export const QueueActionsMenuButton = memo(() => { +type Props = { + containerRef: RefObject; +}; + +export const QueueActionsMenuButton = memo(({ containerRef }: Props) => { const { isOpen, onOpen, onClose } = useDisclosure(); const dispatch = useAppDispatch(); const { t } = useTranslation(); @@ -54,14 +58,30 @@ export const QueueActionsMenuButton = memo(() => { }, [dispatch]); useEffect(() => { - if (menuButtonRef.current) { - const { x, y } = menuButtonRef.current.getBoundingClientRect(); - setBadgePos({ x: x - 10, y: y - 10 }); + if (!containerRef.current || !menuButtonRef.current) { + return; } - }, []); + + const container = containerRef.current; + const menuButton = menuButtonRef.current; + + const cb = () => { + const { x, y } = menuButton.getBoundingClientRect(); + setBadgePos({ x: x - 10, y: y - 10 }); + }; + + // // update badge position on resize + const resizeObserver = new ResizeObserver(cb); + resizeObserver.observe(container); + cb(); + + return () => { + resizeObserver.disconnect(); + }; + }, [containerRef]); return ( - + <> } /> @@ -113,7 +133,7 @@ export const QueueActionsMenuButton = memo(() => { )} - + ); }); diff --git a/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx b/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx index 1acbd6b6970..34b481fc6b1 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueControls.tsx @@ -3,20 +3,21 @@ import { ClearQueueIconButton } from 'features/queue/components/ClearQueueIconBu import QueueFrontButton from 'features/queue/components/QueueFrontButton'; import ProgressBar from 'features/system/components/ProgressBar'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; -import { memo } from 'react'; +import { memo, useRef } from 'react'; import { InvokeQueueBackButton } from './InvokeQueueBackButton'; import { QueueActionsMenuButton } from './QueueActionsMenuButton'; const QueueControls = () => { const isPrependEnabled = useFeatureStatus('prependQueue'); + const containerRef = useRef(null); return ( - + {isPrependEnabled && } - + From e3c0c638c1237f026f21632bed63a8196b6d80e2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 29 Aug 2024 08:33:03 +1000 Subject: [PATCH 547/678] feat(ui): move canvas undo/redo to hook --- .../components/ControlLayersToolbar.tsx | 5 +-- .../useCanvasUndoRedo.tsx} | 35 +++---------------- 2 files changed, 8 insertions(+), 32 deletions(-) rename invokeai/frontend/web/src/features/controlLayers/{components/UndoRedoButtonGroup.tsx => hooks/useCanvasUndoRedo.tsx} (50%) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index 885d47d7ff2..f5bea9877ff 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -7,13 +7,15 @@ import { CanvasSettingsPopover } from 'features/controlLayers/components/Setting import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser'; import { ToolFillColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker'; import { ToolSettings } from 'features/controlLayers/components/Tool/ToolSettings'; -import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup'; import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; +import { useCanvasUndoRedo } from 'features/controlLayers/hooks/useCanvasUndoRedo'; import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton'; import { ViewerToggleMenu } from 'features/gallery/components/ImageViewer/ViewerToggleMenu'; import { memo } from 'react'; export const ControlLayersToolbar = memo(() => { + useCanvasUndoRedo(); + return ( @@ -27,7 +29,6 @@ export const ControlLayersToolbar = memo(() => { - diff --git a/invokeai/frontend/web/src/features/controlLayers/components/UndoRedoButtonGroup.tsx b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasUndoRedo.tsx similarity index 50% rename from invokeai/frontend/web/src/features/controlLayers/components/UndoRedoButtonGroup.tsx rename to invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasUndoRedo.tsx index fb3dae9b905..8013fd36fe7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/UndoRedoButtonGroup.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasUndoRedo.tsx @@ -1,16 +1,14 @@ /* eslint-disable i18next/no-literal-string */ -import { ButtonGroup, IconButton } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; +import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; import { canvasRedo, canvasUndo } from 'features/controlLayers/store/canvasSlice'; import { selectCanvasMayRedo, selectCanvasMayUndo } from 'features/controlLayers/store/selectors'; -import { memo, useCallback } from 'react'; +import { useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { PiArrowClockwiseBold, PiArrowCounterClockwiseBold } from 'react-icons/pi'; import { useDispatch } from 'react-redux'; -export const UndoRedoButtonGroup = memo(() => { - const { t } = useTranslation(); +export const useCanvasUndoRedo = () => { + useAssertSingleton('useCanvasUndoRedo'); const dispatch = useDispatch(); const mayUndo = useAppSelector(selectCanvasMayUndo); @@ -27,27 +25,4 @@ export const UndoRedoButtonGroup = memo(() => { mayRedo, handleRedo, ]); - - return ( - - } - isDisabled={!mayUndo} - variant="ghost" - /> - } - isDisabled={!mayRedo} - variant="ghost" - /> - - ); -}); - -UndoRedoButtonGroup.displayName = 'UndoRedoButtonGroup'; +}; From 16eede4288dcb4ab01225ff573ab58e45bacac9b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 29 Aug 2024 08:42:22 +1000 Subject: [PATCH 548/678] fix(ui): floating params panel invoke button loading state --- .../FloatingParametersPanelButtons.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx index 58e1461ec7b..34bcb13b03a 100644 --- a/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx +++ b/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx @@ -25,15 +25,13 @@ const FloatingSidePanelButtons = (props: Props) => { const { queueBack, isLoading, isDisabled } = useQueueBack(); const { data: queueStatus } = useGetQueueStatusQuery(); - const queueButtonIcon = useMemo( - () => - !isDisabled && queueStatus?.processor.is_processing ? ( - - ) : ( - - ), - [isDisabled, queueStatus?.processor.is_processing] - ); + const queueButtonIcon = useMemo(() => { + const isProcessing = (queueStatus?.queue.in_progress ?? 0) > 0; + if (!isDisabled && isProcessing) { + return ; + } + return ; + }, [isDisabled, queueStatus]); if (!props.panelApi.isCollapsed) { return null; From 057b5ec6467c3d5399f727a65f117adfa2c8385d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 29 Aug 2024 08:54:07 +1000 Subject: [PATCH 549/678] fix(ui): more fiddly queue count layout stuff --- .../features/queue/components/QueueActionsMenuButton.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx b/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx index 8332aabd4bd..663b7f9a22b 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx @@ -9,6 +9,7 @@ import { Portal, useDisclosure, } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { useAppDispatch } from 'app/store/storeHooks'; import type { Coordinate } from 'features/controlLayers/store/types'; import { useClearQueueConfirmationAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog'; @@ -16,7 +17,7 @@ import { useClearQueue } from 'features/queue/hooks/useClearQueue'; import { usePauseProcessor } from 'features/queue/hooks/usePauseProcessor'; import { useResumeProcessor } from 'features/queue/hooks/useResumeProcessor'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; -import { setActiveTab } from 'features/ui/store/uiSlice'; +import { $isParametersPanelOpen, setActiveTab } from 'features/ui/store/uiSlice'; import type { RefObject } from 'react'; import { memo, useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -34,6 +35,7 @@ export const QueueActionsMenuButton = memo(({ containerRef }: Props) => { const { t } = useTranslation(); const [badgePos, setBadgePos] = useState(null); const menuButtonRef = useRef(null); + const isParametersPanelOpen = useStore($isParametersPanelOpen); const dialogState = useClearQueueConfirmationAlertDialog(); const isPauseEnabled = useFeatureStatus('pauseQueue'); const isResumeEnabled = useFeatureStatus('resumeQueue'); @@ -66,6 +68,9 @@ export const QueueActionsMenuButton = memo(({ containerRef }: Props) => { const menuButton = menuButtonRef.current; const cb = () => { + if (!$isParametersPanelOpen.get()) { + return; + } const { x, y } = menuButton.getBoundingClientRect(); setBadgePos({ x: x - 10, y: y - 10 }); }; @@ -120,7 +125,7 @@ export const QueueActionsMenuButton = memo(({ containerRef }: Props) => { - {queueSize > 0 && badgePos !== null && ( + {queueSize > 0 && badgePos !== null && isParametersPanelOpen && ( Date: Thu, 29 Aug 2024 09:00:53 +1000 Subject: [PATCH 550/678] feat(ui): do not select layer on staging accept --- .../listeners/addCommitStagingAreaImageListener.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts index 2b5949e4145..2a8b2f46fbe 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts @@ -68,7 +68,7 @@ export const addStagingListeners = (startAppListening: AppStartListening) => { objects: [imageObject], }; - api.dispatch(rasterLayerAdded({ overrides, isSelected: true })); + api.dispatch(rasterLayerAdded({ overrides, isSelected: false })); api.dispatch(sessionStagingAreaReset()); }, }); From ce4948ec2349b08f9e934d132accd66038afe702 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 29 Aug 2024 09:07:20 +1000 Subject: [PATCH 551/678] feat(ui): tweak brush fill UI --- invokeai/frontend/web/public/locales/en.json | 1 + .../components/Tool/ToolFillColorPicker.tsx | 20 ++++++++----------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index cd3be44e938..7ae5fb648d7 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1741,6 +1741,7 @@ "flipHorizontal": "Flip Horizontal", "flipVertical": "Flip Vertical", "fill": { + "fillColor": "Fill Color", "fillStyle": "Fill Style", "solid": "Solid", "grid": "Grid", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolFillColorPicker.tsx index c84fa9ec047..d86830a96ed 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolFillColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolFillColorPicker.tsx @@ -1,4 +1,4 @@ -import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library'; +import { Box, Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger, Tooltip } from '@invoke-ai/ui-library'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIColorPicker from 'common/components/IAIColorPicker'; @@ -23,17 +23,13 @@ export const ToolFillColorPicker = memo(() => { return ( - + + + + + + + From cfa22b345a83d7d6a15d0297524a20fa52c40e55 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 29 Aug 2024 10:15:35 +1000 Subject: [PATCH 552/678] feat(ui): add + buttons to entity categories --- invokeai/frontend/web/public/locales/en.json | 31 ++++---- .../components/CanvasAddEntityButtons.tsx | 10 +-- .../EntityListActionBarAddLayerMenuItems.tsx | 10 +-- .../common/CanvasEntityAddOfTypeButton.tsx | 70 +++++++++++++++++++ .../common/CanvasEntityGroupList.tsx | 2 + .../controlLayers/hooks/useEntityTitle.ts | 10 +-- .../hooks/useEntityTypeString.ts | 10 +-- 7 files changed, 111 insertions(+), 32 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityAddOfTypeButton.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 7ae5fb648d7..ff512cb48e3 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1684,27 +1684,34 @@ "addPositivePrompt": "Add $t(common.positivePrompt)", "addNegativePrompt": "Add $t(common.negativePrompt)", "addIPAdapter": "Add $t(common.ipAdapter)", + "addRasterLayer": "Add $t(controlLayers.rasterLayer)", + "addControlLayer": "Add $t(controlLayers.controlLayer)", + "addInpaintMask": "Add $t(controlLayers.inpaintMask)", + "addRegionalGuidance": "Add $t(controlLayers.regionalGuidance)", "regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)", "raster": "Raster", - "rasterLayer_one": "Raster Layer", - "controlLayer_one": "Control Layer", - "inpaintMask_one": "Inpaint Mask", - "regionalGuidance_one": "Regional Guidance", - "ipAdapter_one": "IP Adapter", - "rasterLayer_other": "Raster Layers", - "controlLayer_other": "Control Layers", - "inpaintMask_other": "Inpaint Masks", - "regionalGuidance_other": "Regional Guidance", - "ipAdapter_other": "IP Adapters", + "rasterLayer": "Raster Layer", + "controlLayer": "Control Layer", + "inpaintMask": "Inpaint Mask", + "regionalGuidance": "Regional Guidance", + "ipAdapter": "IP Adapter", + "rasterLayer_withCount_one": "$t(controlLayers.rasterLayer)", + "controlLayer_withCount_one": "$t(controlLayers.controlLayer)", + "inpaintMask_withCount_one": "$t(controlLayers.inpaintMask)", + "regionalGuidance_withCount_one": "$t(controlLayers.regionalGuidance)", + "ipAdapter_withCount_one": "$t(controlLayers.ipAdapter)", + "rasterLayer_withCount_other": "Raster Layers", + "controlLayer_withCount_other": "Control Layers", + "inpaintMask_withCount_other": "Inpaint Masks", + "regionalGuidance_withCount_other": "Regional Guidance", + "ipAdapter_withCount_other": "IP Adapters", "opacity": "Opacity", "regionalGuidance_withCount_hidden": "Regional Guidance ({{count}} hidden)", - "controlAdapters_withCount_hidden": "Control Adapters ({{count}} hidden)", "controlLayers_withCount_hidden": "Control Layers ({{count}} hidden)", "rasterLayers_withCount_hidden": "Raster Layers ({{count}} hidden)", "ipAdapters_withCount_hidden": "IP Adapters ({{count}} hidden)", "inpaintMasks_withCount_hidden": "Inpaint Masks ({{count}} hidden)", "regionalGuidance_withCount_visible": "Regional Guidance ({{count}})", - "controlAdapters_withCount_visible": "Control Adapters ({{count}})", "controlLayers_withCount_visible": "Control Layers ({{count}})", "rasterLayers_withCount_visible": "Raster Layers ({{count}})", "ipAdapters_withCount_visible": "IP Adapters ({{count}})", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx index 7b507211859..0eea95edc6f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx @@ -34,19 +34,19 @@ export const CanvasAddEntityButtons = memo(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuItems.tsx index 810422ad8ab..492b28cbdd9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuItems.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuItems.tsx @@ -33,19 +33,19 @@ export const CanvasEntityListMenuItems = memo(() => { return ( <> } onClick={addInpaintMask}> - {t('controlLayers.inpaintMask', { count: 1 })} + {t('controlLayers.inpaintMask')} } onClick={addRegionalGuidance}> - {t('controlLayers.regionalGuidance', { count: 1 })} + {t('controlLayers.regionalGuidance')} } onClick={addRasterLayer}> - {t('controlLayers.rasterLayer', { count: 1 })} + {t('controlLayers.rasterLayer')} } onClick={addControlLayer}> - {t('controlLayers.controlLayer', { count: 1 })} + {t('controlLayers.controlLayer')} } onClick={addIPAdapter}> - {t('controlLayers.ipAdapter', { count: 1 })} + {t('controlLayers.ipAdapter')} ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityAddOfTypeButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityAddOfTypeButton.tsx new file mode 100644 index 00000000000..f3dd8a2c1ab --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityAddOfTypeButton.tsx @@ -0,0 +1,70 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { + controlLayerAdded, + inpaintMaskAdded, + ipaAdded, + rasterLayerAdded, + rgAdded, +} from 'features/controlLayers/store/canvasSlice'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiPlusBold } from 'react-icons/pi'; + +type Props = { + type: CanvasEntityIdentifier['type']; +}; + +export const CanvasEntityAddOfTypeButton = memo(({ type }: Props) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const onClick = useCallback(() => { + switch (type) { + case 'inpaint_mask': + dispatch(inpaintMaskAdded({ isSelected: true })); + break; + case 'regional_guidance': + dispatch(rgAdded({ isSelected: true })); + break; + case 'raster_layer': + dispatch(rasterLayerAdded({ isSelected: true })); + break; + case 'control_layer': + dispatch(controlLayerAdded({ isSelected: true })); + break; + case 'ip_adapter': + dispatch(ipaAdded({ isSelected: true })); + break; + } + }, [dispatch, type]); + + const label = useMemo(() => { + switch (type) { + case 'inpaint_mask': + return t('controlLayers.addInpaintMask'); + case 'regional_guidance': + return t('controlLayers.addRegionalGuidance'); + case 'raster_layer': + return t('controlLayers.addRasterLayer'); + case 'control_layer': + return t('controlLayers.addControlLayer'); + case 'ip_adapter': + return t('controlLayers.addIPAdapter'); + } + }, [type, t]); + + return ( + } + onClick={onClick} + alignSelf="stretch" + /> + ); +}); + +CanvasEntityAddOfTypeButton.displayName = 'CanvasEntityAddOfTypeButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupList.tsx index c6650409052..61b42bd4c4e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityGroupList.tsx @@ -1,6 +1,7 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library'; import { Button, Collapse, Flex, Icon, Spacer, Text } from '@invoke-ai/ui-library'; import { useBoolean } from 'common/hooks/useBoolean'; +import { CanvasEntityAddOfTypeButton } from 'features/controlLayers/components/common/CanvasEntityAddOfTypeButton'; import { CanvasEntityTypeIsHiddenToggle } from 'features/controlLayers/components/common/CanvasEntityTypeIsHiddenToggle'; import { useEntityTypeTitle } from 'features/controlLayers/hooks/useEntityTypeTitle'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; @@ -53,6 +54,7 @@ export const CanvasEntityGroupList = memo(({ isSelected, type, children }: Props
+ {type !== 'ip_adapter' && }
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts index d054083e162..6fd2f50799c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts @@ -29,15 +29,15 @@ export const useEntityTitle = (entityIdentifier: CanvasEntityIdentifier) => { const parts: string[] = []; if (entityIdentifier.type === 'inpaint_mask') { - parts.push(t('controlLayers.inpaintMask', { count: 1 })); + parts.push(t('controlLayers.inpaintMask')); } else if (entityIdentifier.type === 'control_layer') { - parts.push(t('controlLayers.controlLayer', { count: 1 })); + parts.push(t('controlLayers.controlLayer')); } else if (entityIdentifier.type === 'raster_layer') { - parts.push(t('controlLayers.rasterLayer', { count: 1 })); + parts.push(t('controlLayers.rasterLayer')); } else if (entityIdentifier.type === 'ip_adapter') { - parts.push(t('common.ipAdapter', { count: 1 })); + parts.push(t('common.ipAdapter')); } else if (entityIdentifier.type === 'regional_guidance') { - parts.push(t('controlLayers.regionalGuidance', { count: 1 })); + parts.push(t('controlLayers.regionalGuidance')); } else { assert(false, 'Unexpected entity type'); } diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeString.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeString.ts index 5c1682cfe5b..042f3ba21f6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeString.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeString.ts @@ -8,15 +8,15 @@ export const useEntityTypeString = (type: CanvasEntityIdentifier['type']): strin const typeString = useMemo(() => { switch (type) { case 'control_layer': - return t('controlLayers.controlLayer', { count: 0 }); + return t('controlLayers.controlLayer'); case 'raster_layer': - return t('controlLayers.rasterLayer', { count: 0 }); + return t('controlLayers.rasterLayer'); case 'inpaint_mask': - return t('controlLayers.inpaintMask', { count: 0 }); + return t('controlLayers.inpaintMask'); case 'regional_guidance': - return t('controlLayers.regionalGuidance', { count: 0 }); + return t('controlLayers.regionalGuidance'); case 'ip_adapter': - return t('controlLayers.ipAdapter', { count: 0 }); + return t('controlLayers.ipAdapter'); default: return ''; } From 913912d96b023f8607546f170e8caa43c61a0d54 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 29 Aug 2024 10:27:55 +1000 Subject: [PATCH 553/678] feat(ui): add delete button to each layer --- .../components/ControlLayer/ControlLayer.tsx | 6 ++-- .../components/IPAdapter/IPAdapter.tsx | 6 ++-- .../components/InpaintMask/InpaintMask.tsx | 6 ++-- .../components/RasterLayer/RasterLayer.tsx | 6 ++-- .../RegionalGuidance/RegionalGuidance.tsx | 6 ++-- .../common/CanvasEntityDeleteButton.tsx | 31 +++++++++++++++++++ .../common/CanvasEntityEnabledToggle.tsx | 3 +- .../CanvasEntityHeaderCommonActions.tsx | 20 ++++++++++++ .../common/CanvasEntityIsLockedToggle.tsx | 3 +- 9 files changed, 66 insertions(+), 21 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityDeleteButton.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderCommonActions.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx index 5db974dd424..fe65a54e6be 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx @@ -1,8 +1,7 @@ import { Spacer } from '@invoke-ai/ui-library'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; -import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; -import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle'; +import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions'; import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage'; import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper'; import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit'; @@ -29,8 +28,7 @@ export const ControlLayer = memo(({ id }: Props) => { - - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapter.tsx index 01b08f43562..9fac78cd22c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapter.tsx @@ -1,7 +1,7 @@ import { Spacer } from '@invoke-ai/ui-library'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; -import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; +import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions'; import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit'; import { IPAdapterSettings } from 'features/controlLayers/components/IPAdapter/IPAdapterSettings'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; @@ -18,10 +18,10 @@ export const IPAdapter = memo(({ id }: Props) => { return ( - + - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx index 9dcd945505c..6f489a14fc5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx @@ -1,8 +1,7 @@ import { Spacer } from '@invoke-ai/ui-library'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; -import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; -import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle'; +import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions'; import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage'; import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit'; import { EntityMaskAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext'; @@ -25,8 +24,7 @@ export const InpaintMask = memo(({ id }: Props) => { - - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx index bf64f5dd5bf..493b3cf373c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx @@ -1,8 +1,7 @@ import { Spacer } from '@invoke-ai/ui-library'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; -import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; -import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle'; +import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions'; import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage'; import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit'; import { EntityLayerAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext'; @@ -25,8 +24,7 @@ export const RasterLayer = memo(({ id }: Props) => { - - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx index e788cfba5fc..7f86fcda05c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx @@ -1,8 +1,7 @@ import { Spacer } from '@invoke-ai/ui-library'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; -import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; -import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle'; +import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions'; import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage'; import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit'; import { RegionalGuidanceBadges } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges'; @@ -28,8 +27,7 @@ export const RegionalGuidance = memo(({ id }: Props) => { - - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityDeleteButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityDeleteButton.tsx new file mode 100644 index 00000000000..82cbf65a9a0 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityDeleteButton.tsx @@ -0,0 +1,31 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { entityDeleted } from 'features/controlLayers/store/canvasSlice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiTrashSimpleFill } from 'react-icons/pi'; + +export const CanvasEntityDeleteButton = memo(() => { + const { t } = useTranslation(); + const entityIdentifier = useEntityIdentifierContext(); + const dispatch = useAppDispatch(); + const onClick = useCallback(() => { + dispatch(entityDeleted({ entityIdentifier })); + }, [dispatch, entityIdentifier]); + + return ( + } + onClick={onClick} + colorScheme='error' + /> + ); +}); + +CanvasEntityDeleteButton.displayName = 'CanvasEntityDeleteButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx index 1ff1a8cdbaf..ba2db501829 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx @@ -21,7 +21,8 @@ export const CanvasEntityEnabledToggle = memo(() => { size="sm" aria-label={t(isEnabled ? 'common.enabled' : 'common.disabled')} tooltip={t(isEnabled ? 'common.enabled' : 'common.disabled')} - variant="ghost" + variant="link" + alignSelf="stretch" icon={isEnabled ? : } onClick={onClick} /> diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderCommonActions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderCommonActions.tsx new file mode 100644 index 00000000000..7ae614cb090 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderCommonActions.tsx @@ -0,0 +1,20 @@ +import { Flex } from '@invoke-ai/ui-library'; +import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; +import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; +import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { memo } from 'react'; + +export const CanvasEntityHeaderCommonActions = memo(() => { + const entityIdentifier = useEntityIdentifierContext(); + + return ( + + {entityIdentifier.type !== 'ip_adapter' && } + + + + ); +}); + +CanvasEntityHeaderCommonActions.displayName = 'CanvasEntityHeaderCommonActions'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityIsLockedToggle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityIsLockedToggle.tsx index 0c7689a0cf4..e41a1d89e7b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityIsLockedToggle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityIsLockedToggle.tsx @@ -21,7 +21,8 @@ export const CanvasEntityIsLockedToggle = memo(() => { size="sm" aria-label={t(isLocked ? 'controlLayers.locked' : 'controlLayers.unlocked')} tooltip={t(isLocked ? 'controlLayers.locked' : 'controlLayers.unlocked')} - variant="ghost" + variant="link" + alignSelf="stretch" icon={isLocked ? : } onClick={onClick} /> From 79d7f023e8226ca8bba59cf882906a22b5a507de Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 29 Aug 2024 10:35:49 +1000 Subject: [PATCH 554/678] feat(ui): restore context menu for entity list --- .../components/CanvasPanelContent.tsx | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx index d6691ac271b..f14ae8deed6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx @@ -1,21 +1,37 @@ -import { Divider, Flex } from '@invoke-ai/ui-library'; +import { Box, ContextMenu, Divider, Flex, MenuList } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { CanvasAddEntityButtons } from 'features/controlLayers/components/CanvasAddEntityButtons'; import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityList'; import { EntityListActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBar'; +import { CanvasEntityListMenuItems } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuItems'; import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { selectHasEntities } from 'features/controlLayers/store/selectors'; -import { memo } from 'react'; +import { memo, useCallback } from 'react'; export const CanvasPanelContent = memo(() => { const hasEntities = useAppSelector(selectHasEntities); + const renderMenu = useCallback( + () => ( + + + + ), + [] + ); + return ( - {!hasEntities && } - {hasEntities && } + renderMenu={renderMenu} stopImmediatePropagation stopPropagation> + {(ref) => ( + + {!hasEntities && } + {hasEntities && } + + )} + ); From 1a5dc202cbef875ae23f3061f37d242ada4889a9 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 29 Aug 2024 10:43:55 +1000 Subject: [PATCH 555/678] feat(ui): tweak add entity button layout --- .../controlLayers/components/CanvasAddEntityButtons.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx index 0eea95edc6f..80975ae7bda 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx @@ -31,8 +31,8 @@ export const CanvasAddEntityButtons = memo(() => { }, [dispatch]); return ( - - + + From f078e325f872783c4773133156ecef784ebd46f8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 29 Aug 2024 10:52:17 +1000 Subject: [PATCH 556/678] feat(ui): alt quick switches to color picker --- .../src/features/controlLayers/konva/CanvasToolModule.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts index 2c9eba8b07b..f98acc9cef9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts @@ -864,6 +864,10 @@ export class CanvasToolModule extends CanvasModuleABC { this.manager.stateApi.$spaceKey.set(true); this.manager.stateApi.$lastCursorPos.set(null); this.manager.stateApi.$lastMouseDownPos.set(null); + } else if (e.key === 'Alt') { + // Select the color picker on alt key down + this.manager.stateApi.$toolBuffer.set(this.manager.stateApi.$tool.get()); + this.manager.stateApi.$tool.set('colorPicker'); } }; @@ -880,6 +884,11 @@ export class CanvasToolModule extends CanvasModuleABC { this.manager.stateApi.$tool.set(toolBuffer ?? 'move'); this.manager.stateApi.$toolBuffer.set(null); this.manager.stateApi.$spaceKey.set(false); + } else if (e.key === 'Alt') { + // Revert the tool to the previous tool on alt key up + const toolBuffer = this.manager.stateApi.$toolBuffer.get(); + this.manager.stateApi.$tool.set(toolBuffer ?? 'move'); + this.manager.stateApi.$toolBuffer.set(null); } }; From b0414a81eceda3f50b68023a0924447c5b806f61 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 29 Aug 2024 10:58:14 +1000 Subject: [PATCH 557/678] tidy(ui): ViewerToggleMenu -> ViewerToggle --- .../controlLayers/components/ControlLayersToolbar.tsx | 4 ++-- .../gallery/components/ImageViewer/ViewerToggleMenu.tsx | 7 +++++-- .../gallery/components/ImageViewer/ViewerToolbar.tsx | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index f5bea9877ff..56133a57497 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -10,7 +10,7 @@ import { ToolSettings } from 'features/controlLayers/components/Tool/ToolSetting import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { useCanvasUndoRedo } from 'features/controlLayers/hooks/useCanvasUndoRedo'; import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton'; -import { ViewerToggleMenu } from 'features/gallery/components/ImageViewer/ViewerToggleMenu'; +import { ViewerToggle } from 'features/gallery/components/ImageViewer/ViewerToggleMenu'; import { memo } from 'react'; export const ControlLayersToolbar = memo(() => { @@ -30,7 +30,7 @@ export const ControlLayersToolbar = memo(() => { - + ); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx index 78a57bf1923..c37a460b122 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx @@ -1,10 +1,11 @@ import { ButtonGroup, Flex, IconButton, Text, Tooltip } from '@invoke-ai/ui-library'; import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer'; +import { memo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiEyeBold, PiPencilBold } from 'react-icons/pi'; -export const ViewerToggleMenu = () => { +export const ViewerToggle = memo(() => { const { t } = useTranslation(); const imageViewer = useImageViewer(); useHotkeys('z', imageViewer.onToggle, [imageViewer]); @@ -52,4 +53,6 @@ export const ViewerToggleMenu = () => { ); -}; +}); + +ViewerToggle.displayName = 'ViewerToggle'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar.tsx index c0d772efab3..0d00941077f 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar.tsx @@ -7,7 +7,7 @@ import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { memo } from 'react'; import CurrentImageButtons from './CurrentImageButtons'; -import { ViewerToggleMenu } from './ViewerToggleMenu'; +import { ViewerToggle } from './ViewerToggleMenu'; const selectShowToggle = createSelector(selectActiveTab, (tab) => { if (tab === 'upscaling' || tab === 'workflows') { @@ -31,7 +31,7 @@ export const ViewerToolbar = memo(() => {
- {showToggle && } + {showToggle && }
From 9ced9efc649b5027ecce147fdee64e65288408ef Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 29 Aug 2024 17:49:06 +1000 Subject: [PATCH 558/678] feat(app): add `destination` column to `session_queue` The frontend needs to know where queue items came from (i.e. which tab), and where results are going to (i.e. send images to gallery or canvas). The `origin` column is not quite enough to represent this cleanly. A `destination` column provides the frontend what it needs to handle incoming generations. --- invokeai/app/services/events/events_common.py | 8 ++++++- .../session_queue/session_queue_common.py | 21 +++++++++++++++++-- .../session_queue/session_queue_sqlite.py | 11 ++++++---- .../migrations/migration_15.py | 3 +++ 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/invokeai/app/services/events/events_common.py b/invokeai/app/services/events/events_common.py index c348611bab4..adcb2267995 100644 --- a/invokeai/app/services/events/events_common.py +++ b/invokeai/app/services/events/events_common.py @@ -88,7 +88,8 @@ class QueueItemEventBase(QueueEventBase): item_id: int = Field(description="The ID of the queue item") batch_id: str = Field(description="The ID of the queue batch") - origin: str | None = Field(default=None, description="The origin of the batch") + origin: str | None = Field(default=None, description="The origin of the queue item") + destination: str | None = Field(default=None, description="The destination of the queue item") class InvocationEventBase(QueueItemEventBase): @@ -114,6 +115,7 @@ def build(cls, queue_item: SessionQueueItem, invocation: AnyInvocation) -> "Invo item_id=queue_item.item_id, batch_id=queue_item.batch_id, origin=queue_item.origin, + destination=queue_item.destination, session_id=queue_item.session_id, invocation=invocation, invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id], @@ -148,6 +150,7 @@ def build( item_id=queue_item.item_id, batch_id=queue_item.batch_id, origin=queue_item.origin, + destination=queue_item.destination, session_id=queue_item.session_id, invocation=invocation, invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id], @@ -186,6 +189,7 @@ def build( item_id=queue_item.item_id, batch_id=queue_item.batch_id, origin=queue_item.origin, + destination=queue_item.destination, session_id=queue_item.session_id, invocation=invocation, invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id], @@ -219,6 +223,7 @@ def build( item_id=queue_item.item_id, batch_id=queue_item.batch_id, origin=queue_item.origin, + destination=queue_item.destination, session_id=queue_item.session_id, invocation=invocation, invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id], @@ -257,6 +262,7 @@ def build( item_id=queue_item.item_id, batch_id=queue_item.batch_id, origin=queue_item.origin, + destination=queue_item.destination, session_id=queue_item.session_id, status=queue_item.status, error_type=queue_item.error_type, diff --git a/invokeai/app/services/session_queue/session_queue_common.py b/invokeai/app/services/session_queue/session_queue_common.py index 1a546dab9c8..8e37e205d29 100644 --- a/invokeai/app/services/session_queue/session_queue_common.py +++ b/invokeai/app/services/session_queue/session_queue_common.py @@ -77,7 +77,14 @@ class BatchDatum(BaseModel): class Batch(BaseModel): batch_id: str = Field(default_factory=uuid_string, description="The ID of the batch") - origin: str | None = Field(default=None, description="The origin of this batch.") + origin: str | None = Field( + default=None, + description="The origin of this queue item. This data is used by the frontend to determine how to handle results.", + ) + destination: str | None = Field( + default=None, + description="The origin of this queue item. This data is used by the frontend to determine how to handle results", + ) data: Optional[BatchDataCollection] = Field(default=None, description="The batch data collection.") graph: Graph = Field(description="The graph to initialize the session with") workflow: Optional[WorkflowWithoutID] = Field( @@ -196,7 +203,14 @@ class SessionQueueItemWithoutGraph(BaseModel): status: QUEUE_ITEM_STATUS = Field(default="pending", description="The status of this queue item") priority: int = Field(default=0, description="The priority of this queue item") batch_id: str = Field(description="The ID of the batch associated with this queue item") - origin: str | None = Field(default=None, description="The origin of this queue item. ") + origin: str | None = Field( + default=None, + description="The origin of this queue item. This data is used by the frontend to determine how to handle results.", + ) + destination: str | None = Field( + default=None, + description="The origin of this queue item. This data is used by the frontend to determine how to handle results", + ) session_id: str = Field( description="The ID of the session associated with this queue item. The session doesn't exist in graph_executions until the queue item is executed." ) @@ -297,6 +311,7 @@ class BatchStatus(BaseModel): queue_id: str = Field(..., description="The ID of the queue") batch_id: str = Field(..., description="The ID of the batch") origin: str | None = Field(..., description="The origin of the batch") + destination: str | None = Field(..., description="The destination of the batch") pending: int = Field(..., description="Number of queue items with status 'pending'") in_progress: int = Field(..., description="Number of queue items with status 'in_progress'") completed: int = Field(..., description="Number of queue items with status 'complete'") @@ -443,6 +458,7 @@ class SessionQueueValueToInsert(NamedTuple): priority: int # priority workflow: Optional[str] # workflow json origin: str | None + destination: str | None ValuesToInsert: TypeAlias = list[SessionQueueValueToInsert] @@ -464,6 +480,7 @@ def prepare_values_to_insert(queue_id: str, batch: Batch, priority: int, max_new priority, # priority json.dumps(workflow, default=to_jsonable_python) if workflow else None, # workflow (json) batch.origin, # origin + batch.destination, # destination ) ) return values_to_insert diff --git a/invokeai/app/services/session_queue/session_queue_sqlite.py b/invokeai/app/services/session_queue/session_queue_sqlite.py index 265c6065a59..d536aeba755 100644 --- a/invokeai/app/services/session_queue/session_queue_sqlite.py +++ b/invokeai/app/services/session_queue/session_queue_sqlite.py @@ -128,8 +128,8 @@ def enqueue_batch(self, queue_id: str, batch: Batch, prepend: bool) -> EnqueueBa self.__cursor.executemany( """--sql - INSERT INTO session_queue (queue_id, session, session_id, batch_id, field_values, priority, workflow, origin) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO session_queue (queue_id, session, session_id, batch_id, field_values, priority, workflow, origin, destination) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, values_to_insert, ) @@ -579,7 +579,8 @@ def list_queue_items( session_id, batch_id, queue_id, - origin + origin, + destination FROM session_queue WHERE queue_id = ? """ @@ -659,7 +660,7 @@ def get_batch_status(self, queue_id: str, batch_id: str) -> BatchStatus: self.__lock.acquire() self.__cursor.execute( """--sql - SELECT status, count(*), origin + SELECT status, count(*), origin, destination FROM session_queue WHERE queue_id = ? @@ -672,6 +673,7 @@ def get_batch_status(self, queue_id: str, batch_id: str) -> BatchStatus: total = sum(row[1] for row in result) counts: dict[str, int] = {row[0]: row[1] for row in result} origin = result[0]["origin"] if result else None + destination = result[0]["destination"] if result else None except Exception: self.__conn.rollback() raise @@ -681,6 +683,7 @@ def get_batch_status(self, queue_id: str, batch_id: str) -> BatchStatus: return BatchStatus( batch_id=batch_id, origin=origin, + destination=destination, queue_id=queue_id, pending=counts.get("pending", 0), in_progress=counts.get("in_progress", 0), diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_15.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_15.py index 026df180f31..455ff71ab5b 100644 --- a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_15.py +++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_15.py @@ -10,9 +10,11 @@ def __call__(self, cursor: sqlite3.Cursor) -> None: def _add_origin_col(self, cursor: sqlite3.Cursor) -> None: """ - Adds `origin` column to the session queue table. + - Adds `destination` column to the session queue table. """ cursor.execute("ALTER TABLE session_queue ADD COLUMN origin TEXT;") + cursor.execute("ALTER TABLE session_queue ADD COLUMN destination TEXT;") def build_migration_15() -> Migration: @@ -21,6 +23,7 @@ def build_migration_15() -> Migration: This migration does the following: - Adds `origin` column to the session queue table. + - Adds `destination` column to the session queue table. """ migration_15 = Migration( from_version=14, From 14dea8bad3ea252bd44a42bbeacccc12a2cd47b6 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 29 Aug 2024 17:49:15 +1000 Subject: [PATCH 559/678] chore(ui): typegen --- .../frontend/web/src/services/api/schema.ts | 66 ++++++++++++++++--- 1 file changed, 58 insertions(+), 8 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index a61e5bcddc8..d6116352cf3 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -1681,9 +1681,14 @@ export type components = { batch_id?: string; /** * Origin - * @description The origin of this batch. + * @description The origin of this queue item. This data is used by the frontend to determine how to handle results. */ origin?: string | null; + /** + * Destination + * @description The origin of this queue item. This data is used by the frontend to determine how to handle results + */ + destination?: string | null; /** * Data * @description The batch data collection. @@ -1777,6 +1782,11 @@ export type components = { * @description The origin of the batch */ origin: string | null; + /** + * Destination + * @description The destination of the batch + */ + destination: string | null; /** * Pending * @description Number of queue items with status 'pending' @@ -8857,10 +8867,16 @@ export type components = { batch_id: string; /** * Origin - * @description The origin of the batch + * @description The origin of the queue item * @default null */ origin: string | null; + /** + * Destination + * @description The destination of the queue item + * @default null + */ + destination: string | null; /** * Session Id * @description The ID of the session (aka graph execution state) @@ -8909,10 +8925,16 @@ export type components = { batch_id: string; /** * Origin - * @description The origin of the batch + * @description The origin of the queue item * @default null */ origin: string | null; + /** + * Destination + * @description The destination of the queue item + * @default null + */ + destination: string | null; /** * Session Id * @description The ID of the session (aka graph execution state) @@ -8978,10 +9000,16 @@ export type components = { batch_id: string; /** * Origin - * @description The origin of the batch + * @description The origin of the queue item * @default null */ origin: string | null; + /** + * Destination + * @description The destination of the queue item + * @default null + */ + destination: string | null; /** * Session Id * @description The ID of the session (aka graph execution state) @@ -9205,10 +9233,16 @@ export type components = { batch_id: string; /** * Origin - * @description The origin of the batch + * @description The origin of the queue item * @default null */ origin: string | null; + /** + * Destination + * @description The destination of the queue item + * @default null + */ + destination: string | null; /** * Session Id * @description The ID of the session (aka graph execution state) @@ -12279,10 +12313,16 @@ export type components = { batch_id: string; /** * Origin - * @description The origin of the batch + * @description The origin of the queue item * @default null */ origin: string | null; + /** + * Destination + * @description The destination of the queue item + * @default null + */ + destination: string | null; /** * Status * @description The new status of the queue item @@ -13641,9 +13681,14 @@ export type components = { batch_id: string; /** * Origin - * @description The origin of this queue item. + * @description The origin of this queue item. This data is used by the frontend to determine how to handle results. */ origin?: string | null; + /** + * Destination + * @description The origin of this queue item. This data is used by the frontend to determine how to handle results + */ + destination?: string | null; /** * Session Id * @description The ID of the session associated with this queue item. The session doesn't exist in graph_executions until the queue item is executed. @@ -13726,9 +13771,14 @@ export type components = { batch_id: string; /** * Origin - * @description The origin of this queue item. + * @description The origin of this queue item. This data is used by the frontend to determine how to handle results. */ origin?: string | null; + /** + * Destination + * @description The origin of this queue item. This data is used by the frontend to determine how to handle results + */ + destination?: string | null; /** * Session Id * @description The ID of the session associated with this queue item. The session doesn't exist in graph_executions until the queue item is executed. From 87bc3f5f0ab6d78d4535a665d8a04ecca76ae258 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 29 Aug 2024 17:58:02 +1000 Subject: [PATCH 560/678] feat(ui): revise generation mode logic - Canvas generation mode is replace with a boolean `sendToCanvas` flag. When off, images generated on the canvas go to the gallery. When on, they get added to the staging area. - When an image result is received, if its destination is the canvas, staging is automatically started. - Updated queue list to show the destination column. - Added `IconSwitch` component to represent binary choices, used for the new `sendToCanvas` flag and image viewer toggle. - Remove the queue actions menu in `QueueControls`. Move the queue count badge to the cancel button. - Redo layout of `QueueControls` to prevent duplicate queue count badges. - Fix issue where gallery and options panels could show thru transparent regions of queue tab. - Disable panel hotkeys when on mm/queue tabs. --- invokeai/frontend/web/public/locales/en.json | 22 ++-- .../listeners/enqueueRequestedLinear.ts | 8 +- .../listeners/enqueueRequestedNodes.ts | 1 + .../listeners/enqueueRequestedUpscale.ts | 2 +- .../web/src/common/components/IconSwitch.tsx | 104 ++++++++++++++++++ .../components/CanvasModeSwitcher.tsx | 29 ----- .../components/CanvasSendToToggle.tsx | 59 ++++++++++ .../components/ControlLayersToolbar.tsx | 2 - .../controlLayers/store/canvasSessionSlice.ts | 18 +-- .../ImageViewer/ViewerToggleMenu.tsx | 89 +++++++-------- .../sidePanel/NodeEditorPanelGroup.tsx | 2 - .../util/graph/buildLinearBatchConfig.ts | 7 +- .../nodes/util/graph/generation/addInpaint.ts | 6 +- .../util/graph/generation/addOutpaint.ts | 6 +- .../util/graph/generation/buildSD1Graph.ts | 2 +- .../util/graph/generation/buildSDXLGraph.ts | 2 +- .../queue/components/ClearQueueIconButton.tsx | 76 ++++--------- .../components/InvokeQueueBackButton.tsx | 2 +- .../queue/components/QueueControls.tsx | 18 +-- .../queue/components/QueueCountBadge.tsx | 77 +++++++++++++ .../QueueList/QueueItemComponent.tsx | 7 ++ .../components/QueueList/QueueItemDetail.tsx | 5 +- .../components/QueueList/QueueListHeader.tsx | 3 + .../queue/components/QueueList/constants.ts | 3 +- .../QueueList/useDestinationText.ts | 16 +++ .../components/QueueList/useOriginText.ts | 8 +- .../src/features/queue/hooks/useClearQueue.ts | 4 +- .../src/features/ui/components/AppContent.tsx | 48 +++++--- .../FloatingParametersPanelButtons.tsx | 17 ++- .../ParametersPanelTextToImage.tsx | 2 - .../ParametersPanelUpscale.tsx | 2 - .../src/features/ui/components/TabButton.tsx | 49 +++++---- .../ui/components/tabs/ModelManagerTab.tsx | 2 +- .../features/ui/components/tabs/QueueTab.tsx | 2 +- .../services/events/onInvocationComplete.ts | 16 +-- .../src/services/events/setEventListeners.tsx | 4 +- 36 files changed, 486 insertions(+), 234 deletions(-) create mode 100644 invokeai/frontend/web/src/common/components/IconSwitch.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasModeSwitcher.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasSendToToggle.tsx create mode 100644 invokeai/frontend/web/src/features/queue/components/QueueCountBadge.tsx create mode 100644 invokeai/frontend/web/src/features/queue/components/QueueList/useDestinationText.ts diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index ff512cb48e3..694a006778a 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -164,10 +164,10 @@ "alpha": "Alpha", "selected": "Selected", "tab": "Tab", - "viewing": "Viewing", - "viewingDesc": "Review images in a large gallery view", - "editing": "Editing", - "editingDesc": "Edit on the Control Layers canvas", + "view": "View", + "viewDesc": "Review images in a large gallery view", + "edit": "Edit", + "editDesc": "Edit on the Canvas", "comparing": "Comparing", "comparingDesc": "Comparing two images", "enabled": "Enabled", @@ -328,9 +328,13 @@ "completedIn": "Completed in", "batch": "Batch", "origin": "Origin", - "originCanvas": "Canvas", - "originWorkflows": "Workflows", - "originOther": "Other", + "destination": "Destination", + "upscaling": "Upscaling", + "canvas": "Canvas", + "generation": "Generation", + "workflows": "Workflows", + "other": "Other", + "gallery": "Gallery", "batchFieldValues": "Batch Field Values", "item": "Item", "session": "Session", @@ -1695,6 +1699,10 @@ "inpaintMask": "Inpaint Mask", "regionalGuidance": "Regional Guidance", "ipAdapter": "IP Adapter", + "sendToGallery": "Send To Gallery", + "sendToGalleryDesc": "Generations will be sent to the gallery.", + "sendToCanvas": "Send To Canvas", + "sendToCanvasDesc": "Generations will be staged onto the canvas.", "rasterLayer_withCount_one": "$t(controlLayers.rasterLayer)", "controlLayer_withCount_one": "$t(controlLayers.controlLayer)", "inpaintMask_withCount_one": "$t(controlLayers.inpaintMask)", diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index 89777f5081e..afd9489bc5a 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -31,7 +31,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) let didStartStaging = false; - if (!state.canvasSession.isStaging && state.canvasSession.mode === 'compose') { + if (!state.canvasSession.isStaging && state.canvasSession.sendToCanvas) { dispatch(sessionStartedStaging()); didStartStaging = true; } @@ -70,7 +70,11 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) const { g, noise, posCond } = buildGraphResult.value; - const prepareBatchResult = withResult(() => prepareLinearUIBatch(state, g, prepend, noise, posCond)); + const destination = state.canvasSession.sendToCanvas ? 'canvas' : 'gallery'; + + const prepareBatchResult = withResult(() => + prepareLinearUIBatch(state, g, prepend, noise, posCond, 'generation', destination) + ); if (isErr(prepareBatchResult)) { log.error({ error: serializeError(prepareBatchResult.error) }, 'Failed to prepare batch'); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts index 847c64f3c3d..42cd591e0cb 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts @@ -32,6 +32,7 @@ export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) = workflow: builtWorkflow, runs: state.params.iterations, origin: 'workflows', + destination: 'gallery', }, prepend: action.payload.prepend, }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedUpscale.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedUpscale.ts index 959a1bf5ae7..cbfaac6227e 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedUpscale.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedUpscale.ts @@ -16,7 +16,7 @@ export const addEnqueueRequestedUpscale = (startAppListening: AppStartListening) const { g, noise, posCond } = await buildMultidiffusionUpscaleGraph(state); - const batchConfig = prepareLinearUIBatch(state, g, prepend, noise, posCond); + const batchConfig = prepareLinearUIBatch(state, g, prepend, noise, posCond, 'upscaling', 'gallery'); const req = dispatch( queueApi.endpoints.enqueueBatch.initiate(batchConfig, { diff --git a/invokeai/frontend/web/src/common/components/IconSwitch.tsx b/invokeai/frontend/web/src/common/components/IconSwitch.tsx new file mode 100644 index 00000000000..43cf6d2f364 --- /dev/null +++ b/invokeai/frontend/web/src/common/components/IconSwitch.tsx @@ -0,0 +1,104 @@ +import type { SystemStyleObject } from '@invoke-ai/ui-library'; +import { Box, Flex, IconButton, Tooltip, useToken } from '@invoke-ai/ui-library'; +import type { ReactElement, ReactNode } from 'react'; +import { memo, useCallback, useMemo } from 'react'; + +type IconSwitchProps = { + isChecked: boolean; + onChange: (checked: boolean) => void; + iconChecked: ReactElement; + tooltipChecked?: ReactNode; + iconUnchecked: ReactElement; + tooltipUnchecked?: ReactNode; + ariaLabel: string; +}; + +const getSx = (padding: string | number): SystemStyleObject => ({ + transition: 'left 0.1s ease-in-out, transform 0.1s ease-in-out', + '&[data-checked="true"]': { + left: `calc(100% - ${padding})`, + transform: 'translateX(-100%)', + }, + '&[data-checked="false"]': { + left: padding, + transform: 'translateX(0)', + }, +}); + +export const IconSwitch = memo( + ({ + isChecked, + onChange, + iconChecked, + tooltipChecked, + iconUnchecked, + tooltipUnchecked, + ariaLabel, + }: IconSwitchProps) => { + const onUncheck = useCallback(() => { + onChange(false); + }, [onChange]); + const onCheck = useCallback(() => { + onChange(true); + }, [onChange]); + + const gap = useToken('space', 1.5); + const sx = useMemo(() => getSx(gap), [gap]); + + return ( + + + + + + + + + + ); + } +); + +IconSwitch.displayName = 'IconSwitch'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasModeSwitcher.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasModeSwitcher.tsx deleted file mode 100644 index e052c1a214d..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasModeSwitcher.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Button, ButtonGroup } from '@invoke-ai/ui-library'; -import { createSelector } from '@reduxjs/toolkit'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSessionSlice, sessionModeChanged } from 'features/controlLayers/store/canvasSessionSlice'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -const selectCanvasMode = createSelector(selectCanvasSessionSlice, (canvasSession) => canvasSession.mode); - -export const CanvasModeSwitcher = memo(() => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const mode = useAppSelector(selectCanvasMode); - const onClickGenerate = useCallback(() => dispatch(sessionModeChanged({ mode: 'generate' })), [dispatch]); - const onClickCompose = useCallback(() => dispatch(sessionModeChanged({ mode: 'compose' })), [dispatch]); - - return ( - - - - - ); -}); - -CanvasModeSwitcher.displayName = 'CanvasModeSwitcher'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasSendToToggle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasSendToToggle.tsx new file mode 100644 index 00000000000..4d572beccb0 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasSendToToggle.tsx @@ -0,0 +1,59 @@ +import { Flex, Text } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { IconSwitch } from 'common/components/IconSwitch'; +import { selectIsComposing, sessionSendToCanvasChanged } from 'features/controlLayers/store/canvasSessionSlice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiImageBold, PiPaintBrushBold } from 'react-icons/pi'; + +const TooltipSendToGallery = memo(() => { + const { t } = useTranslation(); + + return ( + + {t('controlLayers.sendToGallery')} + {t('controlLayers.sendToGalleryDesc')} + + ); +}); + +TooltipSendToGallery.displayName = 'TooltipSendToGallery'; + +const TooltipSendToCanvas = memo(() => { + const { t } = useTranslation(); + + return ( + + {t('controlLayers.sendToCanvas')} + {t('controlLayers.sendToCanvasDesc')} + + ); +}); + +TooltipSendToCanvas.displayName = 'TooltipSendToCanvas'; + +export const CanvasSendToToggle = memo(() => { + const dispatch = useAppDispatch(); + const isComposing = useAppSelector(selectIsComposing); + + const onChange = useCallback( + (isChecked: boolean) => { + dispatch(sessionSendToCanvasChanged(isChecked)); + }, + [dispatch] + ); + + return ( + } + tooltipUnchecked={} + iconChecked={} + tooltipChecked={} + ariaLabel="Toggle canvas mode" + /> + ); +}); + +CanvasSendToToggle.displayName = 'CanvasSendToToggle'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index 56133a57497..19ffcc25d7b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -1,6 +1,5 @@ /* eslint-disable i18next/no-literal-string */ import { Flex, Spacer } from '@invoke-ai/ui-library'; -import { CanvasModeSwitcher } from 'features/controlLayers/components/CanvasModeSwitcher'; import { CanvasResetViewButton } from 'features/controlLayers/components/CanvasResetViewButton'; import { CanvasScale } from 'features/controlLayers/components/CanvasScale'; import { CanvasSettingsPopover } from 'features/controlLayers/components/Settings/CanvasSettingsPopover'; @@ -28,7 +27,6 @@ export const ControlLayersToolbar = memo(() => { -
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSessionSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSessionSlice.ts index 6947ffd2df5..e115b6800bc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSessionSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSessionSlice.ts @@ -1,17 +1,17 @@ import { createAction, createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { canvasSlice } from 'features/controlLayers/store/canvasSlice'; -import type { SessionMode, StagingAreaImage } from 'features/controlLayers/store/types'; +import type { StagingAreaImage } from 'features/controlLayers/store/types'; export type CanvasSessionState = { - mode: SessionMode; + sendToCanvas: boolean; isStaging: boolean; stagedImages: StagingAreaImage[]; selectedStagedImageIndex: number; }; const initialState: CanvasSessionState = { - mode: 'generate', + sendToCanvas: false, isStaging: false, stagedImages: [], selectedStagedImageIndex: 0, @@ -27,6 +27,7 @@ export const canvasSessionSlice = createSlice({ }, sessionImageStaged: (state, action: PayloadAction<{ stagingAreaImage: StagingAreaImage }>) => { const { stagingAreaImage } = action.payload; + state.isStaging = true; state.stagedImages.push(stagingAreaImage); state.selectedStagedImageIndex = state.stagedImages.length - 1; }, @@ -50,9 +51,8 @@ export const canvasSessionSlice = createSlice({ state.stagedImages = []; state.selectedStagedImageIndex = 0; }, - sessionModeChanged: (state, action: PayloadAction<{ mode: SessionMode }>) => { - const { mode } = action.payload; - state.mode = mode; + sessionSendToCanvasChanged: (state, action: PayloadAction) => { + state.sendToCanvas = action.payload; }, }, }); @@ -64,7 +64,7 @@ export const { sessionStagingAreaReset, sessionNextStagedImageSelected, sessionPrevStagedImageSelected, - sessionModeChanged, + sessionSendToCanvasChanged, } = canvasSessionSlice.actions; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ @@ -85,3 +85,7 @@ export const sessionStagingAreaImageAccepted = createAction<{ index: number }>( export const selectCanvasSessionSlice = (s: RootState) => s.canvasSession; export const selectIsStaging = createSelector(selectCanvasSessionSlice, (canvasSession) => canvasSession.isStaging); +export const selectIsComposing = createSelector( + selectCanvasSessionSlice, + (canvasSession) => canvasSession.sendToCanvas +); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx index c37a460b122..71b94db4452 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx @@ -1,57 +1,60 @@ -import { ButtonGroup, Flex, IconButton, Text, Tooltip } from '@invoke-ai/ui-library'; +import { Flex, Text } from '@invoke-ai/ui-library'; +import { IconSwitch } from 'common/components/IconSwitch'; import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer'; -import { memo } from 'react'; +import { memo, useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiEyeBold, PiPencilBold } from 'react-icons/pi'; -export const ViewerToggle = memo(() => { +const TooltipEdit = memo(() => { const { t } = useTranslation(); + + return ( + + {t('common.edit')} + {t('common.editDesc')} + + ); +}); +TooltipEdit.displayName = 'TooltipEdit'; + +const TooltipView = memo(() => { + const { t } = useTranslation(); + + return ( + + {t('common.view')} + {t('common.viewDesc')} + + ); +}); +TooltipView.displayName = 'TooltipView'; + +export const ViewerToggle = memo(() => { const imageViewer = useImageViewer(); useHotkeys('z', imageViewer.onToggle, [imageViewer]); useHotkeys('esc', imageViewer.onClose, [imageViewer]); + const onChange = useCallback( + (isChecked: boolean) => { + if (isChecked) { + imageViewer.onClose(); + } else { + imageViewer.onOpen(); + } + }, + [imageViewer] + ); return ( - - - - {t('common.viewing')} - {t('common.viewingDesc')} - - } - > - } - onClick={imageViewer.onOpen} - variant={imageViewer.isOpen ? 'solid' : 'outline'} - colorScheme={imageViewer.isOpen ? 'invokeBlue' : 'base'} - aria-label={t('common.viewing')} - w={12} - /> - - - {t('common.editing')} - {t('common.editingDesc')} -
- } - > - } - onClick={imageViewer.onClose} - variant={!imageViewer.isOpen ? 'solid' : 'outline'} - colorScheme={!imageViewer.isOpen ? 'invokeBlue' : 'base'} - aria-label={t('common.editing')} - w={12} - /> - -
- + } + tooltipUnchecked={} + iconChecked={} + tooltipChecked={} + ariaLabel="Toggle viewer" + /> ); }); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/NodeEditorPanelGroup.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/NodeEditorPanelGroup.tsx index 0fd1bb6ea7c..6b7f019f897 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/NodeEditorPanelGroup.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/NodeEditorPanelGroup.tsx @@ -3,7 +3,6 @@ import 'reactflow/dist/style.css'; import { Flex } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { selectWorkflowMode } from 'features/nodes/store/workflowSlice'; -import QueueControls from 'features/queue/components/QueueControls'; import ResizeHandle from 'features/ui/components/tabs/ResizeHandle'; import { usePanelStorage } from 'features/ui/hooks/usePanelStorage'; import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton'; @@ -34,7 +33,6 @@ const NodeEditorPanelGroup = () => { return ( - diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts index 42622989515..6253dc30dcd 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts @@ -10,7 +10,9 @@ export const prepareLinearUIBatch = ( g: Graph, prepend: boolean, noise: Invocation<'noise'>, - posCond: Invocation<'compel' | 'sdxl_compel_prompt'> + posCond: Invocation<'compel' | 'sdxl_compel_prompt'>, + origin: 'generation' | 'workflows' | 'upscaling', + destination: 'canvas' | 'gallery' ): BatchConfig => { const { iterations, model, shouldRandomizeSeed, seed, shouldConcatPrompts } = state.params; const { prompts, seedBehaviour } = state.dynamicPrompts; @@ -103,7 +105,8 @@ export const prepareLinearUIBatch = ( graph: g.getGraph(), runs: 1, data, - origin: 'canvas', + origin, + destination, }, }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts index f9fa00c5d6c..c69820bd498 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts @@ -29,7 +29,7 @@ export const addInpaint = async ( const canvas = selectCanvasSlice(state); const { bbox } = canvas; - const { mode } = canvasSession; + const { sendToCanvas: isComposing } = canvasSession; const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect); const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect); @@ -99,7 +99,7 @@ export const addInpaint = async ( g.addEdge(resizeImageToOriginalSize, 'image', canvasPasteBack, 'generated_image'); g.addEdge(resizeMaskToOriginalSize, 'image', canvasPasteBack, 'mask'); - if (mode === 'generate') { + if (!isComposing) { canvasPasteBack.source_image = { image_name: initialImage.image_name }; } @@ -143,7 +143,7 @@ export const addInpaint = async ( g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image'); - if (mode === 'generate') { + if (!isComposing) { canvasPasteBack.source_image = { image_name: initialImage.image_name }; } diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index ecbc09f9162..80cdf5d53d9 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts @@ -30,7 +30,7 @@ export const addOutpaint = async ( const canvas = selectCanvasSlice(state); const { bbox } = canvas; - const { mode } = canvasSession; + const { sendToCanvas: isComposing } = canvasSession; const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect); const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect); @@ -123,7 +123,7 @@ export const addOutpaint = async ( g.addEdge(resizeOutputImageToOriginalSize, 'image', canvasPasteBack, 'generated_image'); g.addEdge(resizeOutputMaskToOriginalSize, 'image', canvasPasteBack, 'mask'); - if (mode === 'generate') { + if (!isComposing) { canvasPasteBack.source_image = { image_name: initialImage.image_name }; } @@ -173,7 +173,7 @@ export const addOutpaint = async ( g.addEdge(createGradientMask, 'expanded_mask_area', canvasPasteBack, 'mask'); g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image'); - if (mode === 'generate') { + if (!isComposing) { canvasPasteBack.source_image = { image_name: initialImage.image_name }; } diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index 112fb20a60c..693b194e521 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -282,7 +282,7 @@ export const buildSD1Graph = async ( canvasOutput = addWatermarker(g, canvasOutput); } - const shouldSaveToGallery = canvasSession.mode === 'generate' || canvasSettings.autoSave; + const shouldSaveToGallery = !canvasSession.sendToCanvas || canvasSettings.autoSave; g.updateNode(canvasOutput, { id: getPrefixedId('canvas_output'), diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index ddf8da0dcd8..3fbf2f2f567 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -285,7 +285,7 @@ export const buildSDXLGraph = async ( canvasOutput = addWatermarker(g, canvasOutput); } - const shouldSaveToGallery = canvasSession.mode === 'generate' || canvasSettings.autoSave; + const shouldSaveToGallery = !canvasSession.sendToCanvas || canvasSettings.autoSave; g.updateNode(canvasOutput, { id: getPrefixedId('canvas_output'), diff --git a/invokeai/frontend/web/src/features/queue/components/ClearQueueIconButton.tsx b/invokeai/frontend/web/src/features/queue/components/ClearQueueIconButton.tsx index 39a84c0216c..66d445e7c3b 100644 --- a/invokeai/frontend/web/src/features/queue/components/ClearQueueIconButton.tsx +++ b/invokeai/frontend/web/src/features/queue/components/ClearQueueIconButton.tsx @@ -1,67 +1,39 @@ -import type { IconButtonProps } from '@invoke-ai/ui-library'; import { IconButton, useShiftModifier } from '@invoke-ai/ui-library'; -import { useClearQueueConfirmationAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog'; +import { QueueCountBadge } from 'features/queue/components/QueueCountBadge'; import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem'; import { useClearQueue } from 'features/queue/hooks/useClearQueue'; -import { memo } from 'react'; +import { memo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { PiTrashSimpleBold, PiXBold } from 'react-icons/pi'; -type ClearQueueButtonProps = Omit; - -export const ClearAllQueueIconButton = memo((props: ClearQueueButtonProps) => { - const { t } = useTranslation(); - const dialogState = useClearQueueConfirmationAlertDialog(); - const { isLoading, isDisabled } = useClearQueue(); - - return ( - } - colorScheme="error" - onClick={dialogState.setTrue} - data-testid={t('queue.clear')} - {...props} - /> - ); -}); - -ClearAllQueueIconButton.displayName = 'ClearAllQueueIconButton'; - -const ClearSingleQueueItemIconButton = memo((props: ClearQueueButtonProps) => { +export const ClearQueueIconButton = memo((_) => { + const ref = useRef(null); const { t } = useTranslation(); - const { cancelQueueItem, isLoading, isDisabled } = useCancelCurrentQueueItem(); + const clearQueue = useClearQueue(); + const cancelCurrentQueueItem = useCancelCurrentQueueItem(); - return ( - } - colorScheme="error" - onClick={cancelQueueItem} - data-testid={t('queue.cancel')} - {...props} - /> - ); -}); - -ClearSingleQueueItemIconButton.displayName = 'ClearSingleQueueItemIconButton'; - -export const ClearQueueIconButton = memo((props: ClearQueueButtonProps) => { // Show the single item clear button when shift is pressed // Otherwise show the clear queue button const shift = useShiftModifier(); - if (shift) { - return ; - } - - return ; + return ( + <> + : } + colorScheme="error" + onClick={shift ? clearQueue.openDialog : cancelCurrentQueueItem.cancelQueueItem} + data-testid={shift ? t('queue.clear') : t('queue.cancel')} + /> + {/* The badge is dynamically positioned, needs a ref to the target element */} + + + ); }); ClearQueueIconButton.displayName = 'ClearQueueIconButton'; diff --git a/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx b/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx index 08950671a62..68356c28376 100644 --- a/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx +++ b/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx @@ -15,7 +15,7 @@ export const InvokeQueueBackButton = memo(() => { const isLoadingDynamicPrompts = useAppSelector(selectDynamicPromptsIsLoading); return ( - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuItems.tsx index e48b2b30b21..ebbd54b836c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuItems.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuItems.tsx @@ -48,7 +48,7 @@ export const CanvasEntityListMenuItems = memo(() => { {t('controlLayers.controlLayer')} } onClick={addIPAdapter}> - {t('controlLayers.ipAdapter')} + {t('controlLayers.globalIPAdapter')} ); diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts index 830e85eae7c..c3bdd55f6c3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts @@ -33,7 +33,7 @@ export const useEntityTitle = (entityIdentifier: CanvasEntityIdentifier) => { case 'raster_layer': return t('controlLayers.rasterLayer'); case 'ip_adapter': - return t('common.ipAdapter'); + return t('controlLayers.globalIPAdapter'); case 'regional_guidance': return t('controlLayers.regionalGuidance'); default: diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeString.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeString.ts index 042f3ba21f6..2280f845e05 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeString.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeString.ts @@ -16,7 +16,7 @@ export const useEntityTypeString = (type: CanvasEntityIdentifier['type']): strin case 'regional_guidance': return t('controlLayers.regionalGuidance'); case 'ip_adapter': - return t('controlLayers.ipAdapter'); + return t('controlLayers.globalIPAdapter'); default: return ''; } diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeTitle.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeTitle.ts index c693db533c9..3782612508d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeTitle.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeTitle.ts @@ -22,7 +22,7 @@ export const useEntityTypeTitle = (type: CanvasEntityIdentifier['type']): string case 'regional_guidance': return t('controlLayers.regionalGuidance_withCount', { count, context }); case 'ip_adapter': - return t('controlLayers.ipAdapters_withCount', { count, context }); + return t('controlLayers.globalIPAdapters_withCount', { count, context }); default: return ''; } From 0c3e2c45f159bb008f5f856e72e2bdc792247b1a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 1 Sep 2024 20:59:23 +1000 Subject: [PATCH 599/678] feat(ui): "Control Layers" -> "Layers" --- invokeai/frontend/web/public/locales/en.json | 4 ++-- .../web/src/common/hooks/useIsReadyToEnqueue.ts | 8 ++++---- .../ParametersPanels/ParametersPanelTextToImage.tsx | 12 ++---------- 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 7a5a8e773a4..fa3f0f3c175 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1743,8 +1743,8 @@ "clearProcessor": "Clear Processor", "resetProcessor": "Reset Processor to Defaults", "noLayersAdded": "No Layers Added", - "layers_one": "Layer", - "layers_other": "Layers", + "layer_one": "Layer", + "layer_other": "Layers", "objects_zero": "empty", "objects_one": "{{count}} object", "objects_other": "{{count}} objects", diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index 84dbfd3dc21..42f02f54936 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -128,7 +128,7 @@ const createSelector = (templates: Templates, isConnected: boolean) => canvas.controlLayers.entities .filter((controlLayer) => controlLayer.isEnabled) .forEach((controlLayer, i) => { - const layerLiteral = i18n.t('controlLayers.layers_one'); + const layerLiteral = i18n.t('controlLayers.layer_one'); const layerNumber = i + 1; const layerType = i18n.t(LAYER_TYPE_TO_TKEY['control_layer']); const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; @@ -158,7 +158,7 @@ const createSelector = (templates: Templates, isConnected: boolean) => canvas.ipAdapters.entities .filter((entity) => entity.isEnabled) .forEach((entity, i) => { - const layerLiteral = i18n.t('controlLayers.layers_one'); + const layerLiteral = i18n.t('controlLayers.layer_one'); const layerNumber = i + 1; const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]); const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; @@ -186,7 +186,7 @@ const createSelector = (templates: Templates, isConnected: boolean) => canvas.regions.entities .filter((entity) => entity.isEnabled) .forEach((entity, i) => { - const layerLiteral = i18n.t('controlLayers.layers_one'); + const layerLiteral = i18n.t('controlLayers.layer_one'); const layerNumber = i + 1; const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]); const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; @@ -223,7 +223,7 @@ const createSelector = (templates: Templates, isConnected: boolean) => canvas.rasterLayers.entities .filter((entity) => entity.isEnabled) .forEach((entity, i) => { - const layerLiteral = i18n.t('controlLayers.layers_one'); + const layerLiteral = i18n.t('controlLayers.layer_one'); const layerNumber = i + 1; const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]); const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx index b27183f7c02..986d9f0b33a 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx @@ -5,7 +5,6 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants'; import { CanvasPanelContent } from 'features/controlLayers/components/CanvasPanelContent'; import { selectIsSDXL } from 'features/controlLayers/store/paramsSlice'; -import { selectEntityCount } from 'features/controlLayers/store/selectors'; import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; import { Prompts } from 'features/parameters/components/Prompts/Prompts'; import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion'; @@ -18,7 +17,7 @@ import { StylePresetMenuTrigger } from 'features/stylePresets/components/StylePr import { $isMenuOpen } from 'features/stylePresets/store/isMenuOpen'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; import type { CSSProperties } from 'react'; -import { memo, useCallback, useMemo, useRef } from 'react'; +import { memo, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; const overlayScrollbarsStyles: CSSProperties = { @@ -41,13 +40,6 @@ const selectedStyles: ChakraProps['sx'] = { const ParametersPanelTextToImage = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const controlLayersCount = useAppSelector(selectEntityCount); - const controlLayersTitle = useMemo(() => { - if (controlLayersCount === 0) { - return t('controlLayers.controlLayers'); - } - return `${t('controlLayers.controlLayers')} (${controlLayersCount})`; - }, [controlLayersCount, t]); const isSDXL = useAppSelector(selectIsSDXL); const onChangeTabs = useCallback( (i: number) => { @@ -95,7 +87,7 @@ const ParametersPanelTextToImage = () => { _selected={selectedStyles} data-testid="generation-tab-control-layers-tab-button" > - {controlLayersTitle} + {t('controlLayers.layer_other')} From d3c2432bfbee642a2013dc8a68b6bea6c2427f15 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 1 Sep 2024 21:08:43 +1000 Subject: [PATCH 600/678] fix(ui): force dims on scaled bbox when manual scaling + locked aspect ratio Closes #5590 --- .../controlLayers/store/canvasSlice.ts | 20 ++++++++++++++----- .../InfillAndScaling/ParamScaledHeight.tsx | 4 ++-- .../InfillAndScaling/ParamScaledWidth.tsx | 4 ++-- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index ccc3d70fdb2..3d8f8557a63 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -42,7 +42,6 @@ import type { CLIPVisionModelV2, ControlModeV2, ControlNetConfig, - Dimensions, EntityBrushLineAddedPayload, EntityEraserLineAddedPayload, EntityIdentifierPayload, @@ -669,8 +668,17 @@ export const canvasSlice = createSlice({ state.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id }; }, //#region BBox - bboxScaledSizeChanged: (state, action: PayloadAction>) => { - state.bbox.scaledSize = { ...state.bbox.scaledSize, ...action.payload }; + bboxScaledWidthChanged: (state, action: PayloadAction) => { + state.bbox.scaledSize.width = action.payload; + if (state.bbox.aspectRatio.isLocked) { + state.bbox.scaledSize.height = roundToMultiple(state.bbox.scaledSize.width / state.bbox.aspectRatio.value, 8); + } + }, + bboxScaledHeightChanged: (state, action: PayloadAction) => { + state.bbox.scaledSize.height = action.payload; + if (state.bbox.aspectRatio.isLocked) { + state.bbox.scaledSize.width = roundToMultiple(state.bbox.scaledSize.height * state.bbox.aspectRatio.value, 8); + } }, bboxScaleMethodChanged: (state, action: PayloadAction) => { state.bbox.scaleMethod = action.payload; @@ -721,6 +729,7 @@ export const canvasSlice = createSlice({ }, bboxAspectRatioLockToggled: (state) => { state.bbox.aspectRatio.isLocked = !state.bbox.aspectRatio.isLocked; + syncScaledSize(state); }, bboxAspectRatioIdChanged: (state, action: PayloadAction<{ id: AspectRatioID }>) => { const { id } = action.payload; @@ -1118,7 +1127,8 @@ export const { allEntitiesOfTypeIsHiddenToggled, // bbox bboxChanged, - bboxScaledSizeChanged, + bboxScaledWidthChanged, + bboxScaledHeightChanged, bboxScaleMethodChanged, bboxWidthChanged, bboxHeightChanged, @@ -1180,7 +1190,7 @@ export const canvasPersistConfig: PersistConfig = { }; const syncScaledSize = (state: CanvasState) => { - if (state.bbox.scaleMethod === 'auto') { + if (state.bbox.scaleMethod === 'auto' || (state.bbox.scaleMethod === 'manual' && state.bbox.aspectRatio.isLocked)) { const { width, height } = state.bbox.rect; state.bbox.scaledSize = getScaledBoundingBoxDimensions({ width, height }, state.bbox.optimalDimension); } diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx index 01569a83b05..57f0e1b126f 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { bboxScaledSizeChanged } from 'features/controlLayers/store/canvasSlice'; +import { bboxScaledHeightChanged } from 'features/controlLayers/store/canvasSlice'; import { selectCanvasSlice, selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { selectConfigSlice } from 'features/system/store/configSlice'; import { memo, useCallback } from 'react'; @@ -24,7 +24,7 @@ const ParamScaledHeight = () => { const onChange = useCallback( (height: number) => { - dispatch(bboxScaledSizeChanged({ height })); + dispatch(bboxScaledHeightChanged(height)); }, [dispatch] ); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx index 5eb2430b1da..85d5253998f 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { bboxScaledSizeChanged } from 'features/controlLayers/store/canvasSlice'; +import { bboxScaledWidthChanged } from 'features/controlLayers/store/canvasSlice'; import { selectCanvasSlice, selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { selectConfigSlice } from 'features/system/store/configSlice'; import { memo, useCallback } from 'react'; @@ -23,7 +23,7 @@ const ParamScaledWidth = () => { const config = useAppSelector(selectScaledBoundingBoxWidthConfig); const onChange = useCallback( (width: number) => { - dispatch(bboxScaledSizeChanged({ width })); + dispatch(bboxScaledWidthChanged(width)); }, [dispatch] ); From a4fb8eb1718e679053b4dc4f31cc72fc7bc454ee Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 1 Sep 2024 22:34:05 +1000 Subject: [PATCH 601/678] feat(ui): add bookmark for quick switch --- invokeai/frontend/web/public/locales/en.json | 3 +- .../CanvasEntityHeaderCommonActions.tsx | 2 + ...EntityIsBookmarkedForQuickSwitchToggle.tsx | 38 +++++++++ .../hooks/useCanvasEntityQuickSwitchHotkey.ts | 83 +++++++++++-------- .../useEntityIsBookmarkedForQuickSwitch.ts | 18 ++++ .../controlLayers/store/canvasSlice.ts | 18 ++++ .../features/controlLayers/store/selectors.ts | 5 ++ .../src/features/controlLayers/store/types.ts | 1 + 8 files changed, 132 insertions(+), 36 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityIsBookmarkedForQuickSwitchToggle.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsBookmarkedForQuickSwitch.ts diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index fa3f0f3c175..c6e269979bb 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1654,7 +1654,8 @@ "storeNotInitialized": "Store is not initialized" }, "controlLayers": { - "saveCanvasToGallery": "Save Canvas To Gallery", + "bookmarkedForQuickSwitch": "Bookmarked for Quick Switch", + "notBookmarkedForQuickSwitch": "Not Bookmarked for Quick Switch", "saveBboxToGallery": "Save Bbox To Gallery", "savedToGalleryOk": "Saved to Gallery", "savedToGalleryError": "Error saving to gallery", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderCommonActions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderCommonActions.tsx index 7ae614cb090..2615771b0df 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderCommonActions.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderCommonActions.tsx @@ -1,6 +1,7 @@ import { Flex } from '@invoke-ai/ui-library'; import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; +import { CanvasEntityIsBookmarkedForQuickSwitchToggle } from 'features/controlLayers/components/common/CanvasEntityIsBookmarkedForQuickSwitchToggle'; import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { memo } from 'react'; @@ -10,6 +11,7 @@ export const CanvasEntityHeaderCommonActions = memo(() => { return ( + {entityIdentifier.type !== 'ip_adapter' && } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityIsBookmarkedForQuickSwitchToggle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityIsBookmarkedForQuickSwitchToggle.tsx new file mode 100644 index 00000000000..6bafb04ecca --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityIsBookmarkedForQuickSwitchToggle.tsx @@ -0,0 +1,38 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useEntityIsBookmarkedForQuickSwitch } from 'features/controlLayers/hooks/useEntityIsBookmarkedForQuickSwitch'; +import { entityIsBookmarkedForQuickSwitchChanged } from 'features/controlLayers/store/canvasSlice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiBookmarkSimpleBold, PiBookmarkSimpleFill } from 'react-icons/pi'; + +export const CanvasEntityIsBookmarkedForQuickSwitchToggle = memo(() => { + const { t } = useTranslation(); + const entityIdentifier = useEntityIdentifierContext(); + const isBookmarked = useEntityIsBookmarkedForQuickSwitch(entityIdentifier); + const dispatch = useAppDispatch(); + const onClick = useCallback(() => { + if (isBookmarked) { + dispatch(entityIsBookmarkedForQuickSwitchChanged({ entityIdentifier: null })); + } else { + dispatch(entityIsBookmarkedForQuickSwitchChanged({ entityIdentifier })); + } + }, [dispatch, entityIdentifier, isBookmarked]); + + return ( + : } + onClick={onClick} + /> + ); +}); + +CanvasEntityIsBookmarkedForQuickSwitchToggle.displayName = 'CanvasEntityIsBookmarkedForQuickSwitchToggle'; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasEntityQuickSwitchHotkey.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasEntityQuickSwitchHotkey.ts index 20fc4644659..9b48fcc51e6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasEntityQuickSwitchHotkey.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasEntityQuickSwitchHotkey.ts @@ -6,6 +6,7 @@ import { entitySelected } from 'features/controlLayers/store/canvasSlice'; import { selectCanvasSlice, selectEntity, + selectQuickSwitchEntityIdentifier, selectSelectedEntityIdentifier, } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; @@ -20,51 +21,63 @@ export const useCanvasEntityQuickSwitchHotkey = () => { const dispatch = useAppDispatch(); const selectedEntityBuffer = useStore($selectedEntityBuffer); - const selectedEntity = useAppSelector(selectSelectedEntityIdentifier); - const selectDoesBufferExist = useMemo( + const quickSwitchEntityIdentifier = useAppSelector(selectQuickSwitchEntityIdentifier); + const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); + const selectBufferEntityIdentifier = useMemo( () => - createSelector(selectCanvasSlice, (canvas) => { - if (!selectedEntityBuffer) { - return true; - } - const bufferEntity = selectEntity(canvas, selectedEntityBuffer); - if (bufferEntity) { - return true; - } - return false; - }), + createSelector(selectCanvasSlice, (canvas) => + selectedEntityBuffer ? (selectEntity(canvas, selectedEntityBuffer) ?? null) : null + ), [selectedEntityBuffer] ); - const doesBufferExist = useAppSelector(selectDoesBufferExist); + const bufferEntityIdentifier = useAppSelector(selectBufferEntityIdentifier); const quickSwitch = useCallback(() => { - // If there is no selected entity or buffer, we should not do anything - if (selectedEntity === null && selectedEntityBuffer === null) { - return; + if (quickSwitchEntityIdentifier !== null) { + // If there is a quick switch entity, we should switch between it and the buffer + if (quickSwitchEntityIdentifier.id !== selectedEntityIdentifier?.id) { + // The quick switch entity is not selected - select it + dispatch(entitySelected({ entityIdentifier: quickSwitchEntityIdentifier })); + $selectedEntityBuffer.set(selectedEntityIdentifier); + } else if (bufferEntityIdentifier !== null) { + // The quick switch entity is already selected - select the buffer + dispatch(entitySelected({ entityIdentifier: bufferEntityIdentifier })); + $selectedEntityBuffer.set(quickSwitchEntityIdentifier); + } + } else { + // No quick switch entity, so we should switch between buffer and selected entity + // If there is no selected entity or buffer, we should not do anything + if (selectedEntityIdentifier === null && bufferEntityIdentifier === null) { + return; + } + // If there is no selected entity but we do have a buffer, we should select the buffer + if (selectedEntityIdentifier === null && bufferEntityIdentifier !== null) { + dispatch(entitySelected({ entityIdentifier: bufferEntityIdentifier })); + return; + } + // If there is a selected entity but no buffer, we should buffer the selected entity + if (selectedEntityIdentifier !== null && bufferEntityIdentifier === null) { + $selectedEntityBuffer.set(selectedEntityIdentifier); + return; + } + // If there is a selected entity and a buffer, and they are different, we should swap the selected entity and the buffer + if ( + selectedEntityIdentifier !== null && + bufferEntityIdentifier !== null && + selectedEntityIdentifier.id !== bufferEntityIdentifier.id + ) { + $selectedEntityBuffer.set(selectedEntityIdentifier); + dispatch(entitySelected({ entityIdentifier: bufferEntityIdentifier })); + return; + } } - // If there is no selected entity but we do have a buffer, we should select the buffer - if (selectedEntity === null && selectedEntityBuffer !== null) { - dispatch(entitySelected({ entityIdentifier: selectedEntityBuffer })); - return; - } - // If there is a selected entity but no buffer, we should buffer the selected entity - if (selectedEntity !== null && selectedEntityBuffer === null) { - $selectedEntityBuffer.set(selectedEntity); - return; - } - // If there is a selected entity and a buffer, and they are different, we should swap the selected entity and the buffer - if (selectedEntity !== null && selectedEntityBuffer !== null && selectedEntity.id !== selectedEntityBuffer.id) { - $selectedEntityBuffer.set(selectedEntity); - dispatch(entitySelected({ entityIdentifier: selectedEntityBuffer })); - return; - } - }, [dispatch, selectedEntity, selectedEntityBuffer]); + }, [bufferEntityIdentifier, dispatch, quickSwitchEntityIdentifier, selectedEntityIdentifier]); useEffect(() => { - if (!doesBufferExist) { + if (!bufferEntityIdentifier) { $selectedEntityBuffer.set(null); } - }, [doesBufferExist]); + }, [bufferEntityIdentifier]); useHotkeys('q', quickSwitch, { enabled: true, preventDefault: true }, [quickSwitch]); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsBookmarkedForQuickSwitch.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsBookmarkedForQuickSwitch.ts new file mode 100644 index 00000000000..43040cafb31 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsBookmarkedForQuickSwitch.ts @@ -0,0 +1,18 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { useAppSelector } from 'app/store/storeHooks'; +import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { useMemo } from 'react'; + +export const useEntityIsBookmarkedForQuickSwitch = (entityIdentifier: CanvasEntityIdentifier) => { + const selectIsBookmarkedForQuickSwitch = useMemo( + () => + createSelector(selectCanvasSlice, (canvas) => { + return canvas.quickSwitchEntityIdentifier?.id === entityIdentifier.id; + }), + [entityIdentifier] + ); + const isBookmarkedForQuickSwitch = useAppSelector(selectIsBookmarkedForQuickSwitch); + + return isBookmarkedForQuickSwitch; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index 3d8f8557a63..a5916d32f9a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -84,6 +84,7 @@ const getRGMaskFill = (state: CanvasState): RgbColor => { const initialState: CanvasState = { _version: 3, selectedEntityIdentifier: null, + quickSwitchEntityIdentifier: null, rasterLayers: { isHidden: false, entities: [], @@ -791,6 +792,22 @@ export const canvasSlice = createSlice({ } state.selectedEntityIdentifier = entityIdentifier; }, + entityIsBookmarkedForQuickSwitchChanged: ( + state, + action: PayloadAction<{ entityIdentifier: CanvasEntityIdentifier | null }> + ) => { + const { entityIdentifier } = action.payload; + if (!entityIdentifier) { + state.quickSwitchEntityIdentifier = null; + return; + } + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + // Cannot select a non-existent entity + return; + } + state.quickSwitchEntityIdentifier = entityIdentifier; + }, entityNameChanged: (state, action: PayloadAction>) => { const { entityIdentifier, name } = action.payload; const entity = selectEntity(state, entityIdentifier); @@ -1105,6 +1122,7 @@ export const { canvasClearHistory, // All entities entitySelected, + entityIsBookmarkedForQuickSwitchChanged, entityNameChanged, entityReset, entityIsEnabledToggled, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts index 6246eef1538..9691979a944 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts @@ -181,6 +181,11 @@ export const selectSelectedEntityIdentifier = createSelector( (canvas) => canvas.selectedEntityIdentifier ); +export const selectQuickSwitchEntityIdentifier = createSelector( + selectCanvasSlice, + (canvas) => canvas.quickSwitchEntityIdentifier +); + export const selectIsSelectedEntityDrawable = createSelector( selectSelectedEntityIdentifier, (selectedEntityIdentifier) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 3422d39fffe..130abf791ac 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -688,6 +688,7 @@ export type StagingAreaImage = { export type CanvasState = { _version: 3; selectedEntityIdentifier: CanvasEntityIdentifier | null; + quickSwitchEntityIdentifier: CanvasEntityIdentifier | null; inpaintMasks: { isHidden: boolean; entities: CanvasInpaintMaskState[]; From 02061faaf0f03bcc42ca4bc08971d2f408b69716 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 1 Sep 2024 22:34:35 +1000 Subject: [PATCH 602/678] chore(ui): lint --- .../hooks/useEntityObjectCount.ts | 24 ------------------- 1 file changed, 24 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/useEntityObjectCount.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityObjectCount.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityObjectCount.ts deleted file mode 100644 index fe9fbedbd16..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityObjectCount.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors'; -import { type CanvasEntityIdentifier, isDrawableEntity } from 'features/controlLayers/store/types'; -import { useMemo } from 'react'; - -export const useEntityObjectCount = (entityIdentifier: CanvasEntityIdentifier) => { - const selectObjectCount = useMemo( - () => - createSelector(selectCanvasSlice, (canvas) => { - const entity = selectEntity(canvas, entityIdentifier); - if (!entity) { - return 0; - } else if (isDrawableEntity(entity)) { - return entity.objects.length; - } else { - return 0; - } - }), - [entityIdentifier] - ); - const objectCount = useAppSelector(selectObjectCount); - return objectCount; -}; From 277708d69e439f0d47e5071e34453e34a10098b9 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:48:53 +1000 Subject: [PATCH 603/678] fix(ui): edge cases in quick switch, simpler logic --- ...EntityIsBookmarkedForQuickSwitchToggle.tsx | 6 +- .../hooks/useCanvasEntityQuickSwitchHotkey.ts | 98 ++++++------------- .../useEntityIsBookmarkedForQuickSwitch.ts | 2 +- .../controlLayers/store/canvasSlice.ts | 13 +-- .../features/controlLayers/store/selectors.ts | 4 +- .../src/features/controlLayers/store/types.ts | 2 +- 6 files changed, 43 insertions(+), 82 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityIsBookmarkedForQuickSwitchToggle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityIsBookmarkedForQuickSwitchToggle.tsx index 6bafb04ecca..39a0fccccc7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityIsBookmarkedForQuickSwitchToggle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityIsBookmarkedForQuickSwitchToggle.tsx @@ -2,7 +2,7 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { useEntityIsBookmarkedForQuickSwitch } from 'features/controlLayers/hooks/useEntityIsBookmarkedForQuickSwitch'; -import { entityIsBookmarkedForQuickSwitchChanged } from 'features/controlLayers/store/canvasSlice'; +import { bookmarkedEntityChanged } from 'features/controlLayers/store/canvasSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiBookmarkSimpleBold, PiBookmarkSimpleFill } from 'react-icons/pi'; @@ -14,9 +14,9 @@ export const CanvasEntityIsBookmarkedForQuickSwitchToggle = memo(() => { const dispatch = useAppDispatch(); const onClick = useCallback(() => { if (isBookmarked) { - dispatch(entityIsBookmarkedForQuickSwitchChanged({ entityIdentifier: null })); + dispatch(bookmarkedEntityChanged({ entityIdentifier: null })); } else { - dispatch(entityIsBookmarkedForQuickSwitchChanged({ entityIdentifier })); + dispatch(bookmarkedEntityChanged({ entityIdentifier })); } }, [dispatch, entityIdentifier, isBookmarked]); diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasEntityQuickSwitchHotkey.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasEntityQuickSwitchHotkey.ts index 9b48fcc51e6..17d2feebc71 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasEntityQuickSwitchHotkey.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasEntityQuickSwitchHotkey.ts @@ -1,83 +1,47 @@ -import { useStore } from '@nanostores/react'; -import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; import { entitySelected } from 'features/controlLayers/store/canvasSlice'; import { - selectCanvasSlice, - selectEntity, - selectQuickSwitchEntityIdentifier, + selectBookmarkedEntityIdentifier, selectSelectedEntityIdentifier, } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; -import { atom } from 'nanostores'; -import { useCallback, useEffect, useMemo } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; -const $selectedEntityBuffer = atom(null); - export const useCanvasEntityQuickSwitchHotkey = () => { - useAssertSingleton('useCanvasEntityQuickSwitch'); - const dispatch = useAppDispatch(); - const selectedEntityBuffer = useStore($selectedEntityBuffer); - const quickSwitchEntityIdentifier = useAppSelector(selectQuickSwitchEntityIdentifier); - const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); - const selectBufferEntityIdentifier = useMemo( - () => - createSelector(selectCanvasSlice, (canvas) => - selectedEntityBuffer ? (selectEntity(canvas, selectedEntityBuffer) ?? null) : null - ), - [selectedEntityBuffer] - ); - const bufferEntityIdentifier = useAppSelector(selectBufferEntityIdentifier); + const [prev, setPrev] = useState(null); + const [current, setCurrent] = useState(null); + const selected = useAppSelector(selectSelectedEntityIdentifier); + const bookmarked = useAppSelector(selectBookmarkedEntityIdentifier); - const quickSwitch = useCallback(() => { - if (quickSwitchEntityIdentifier !== null) { - // If there is a quick switch entity, we should switch between it and the buffer - if (quickSwitchEntityIdentifier.id !== selectedEntityIdentifier?.id) { - // The quick switch entity is not selected - select it - dispatch(entitySelected({ entityIdentifier: quickSwitchEntityIdentifier })); - $selectedEntityBuffer.set(selectedEntityIdentifier); - } else if (bufferEntityIdentifier !== null) { - // The quick switch entity is already selected - select the buffer - dispatch(entitySelected({ entityIdentifier: bufferEntityIdentifier })); - $selectedEntityBuffer.set(quickSwitchEntityIdentifier); - } - } else { - // No quick switch entity, so we should switch between buffer and selected entity - // If there is no selected entity or buffer, we should not do anything - if (selectedEntityIdentifier === null && bufferEntityIdentifier === null) { - return; - } - // If there is no selected entity but we do have a buffer, we should select the buffer - if (selectedEntityIdentifier === null && bufferEntityIdentifier !== null) { - dispatch(entitySelected({ entityIdentifier: bufferEntityIdentifier })); - return; - } - // If there is a selected entity but no buffer, we should buffer the selected entity - if (selectedEntityIdentifier !== null && bufferEntityIdentifier === null) { - $selectedEntityBuffer.set(selectedEntityIdentifier); - return; - } - // If there is a selected entity and a buffer, and they are different, we should swap the selected entity and the buffer - if ( - selectedEntityIdentifier !== null && - bufferEntityIdentifier !== null && - selectedEntityIdentifier.id !== bufferEntityIdentifier.id - ) { - $selectedEntityBuffer.set(selectedEntityIdentifier); - dispatch(entitySelected({ entityIdentifier: bufferEntityIdentifier })); - return; - } + // Update prev and current when selected entity changes + useEffect(() => { + if (current?.id !== selected?.id) { + setPrev(current); + setCurrent(selected); } - }, [bufferEntityIdentifier, dispatch, quickSwitchEntityIdentifier, selectedEntityIdentifier]); + }, [current, selected]); - useEffect(() => { - if (!bufferEntityIdentifier) { - $selectedEntityBuffer.set(null); + const onQuickSwitch = useCallback(() => { + if (bookmarked) { + if (current?.id !== bookmarked.id) { + // Switch between current (non-bookmarked) and bookmarked + setPrev(current); + setCurrent(bookmarked); + dispatch(entitySelected({ entityIdentifier: bookmarked })); + } else if (prev) { + // Switch back to the last non-bookmarked entity + setCurrent(prev); + dispatch(entitySelected({ entityIdentifier: prev })); + } + } else if (prev !== null && current !== null) { + // Switch between prev and current if no bookmarked entity + setPrev(current); + setCurrent(prev); + dispatch(entitySelected({ entityIdentifier: prev })); } - }, [bufferEntityIdentifier]); + }, [bookmarked, current, dispatch, prev]); - useHotkeys('q', quickSwitch, { enabled: true, preventDefault: true }, [quickSwitch]); + useHotkeys('q', onQuickSwitch); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsBookmarkedForQuickSwitch.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsBookmarkedForQuickSwitch.ts index 43040cafb31..5c497bf6c35 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsBookmarkedForQuickSwitch.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsBookmarkedForQuickSwitch.ts @@ -8,7 +8,7 @@ export const useEntityIsBookmarkedForQuickSwitch = (entityIdentifier: CanvasEnti const selectIsBookmarkedForQuickSwitch = useMemo( () => createSelector(selectCanvasSlice, (canvas) => { - return canvas.quickSwitchEntityIdentifier?.id === entityIdentifier.id; + return canvas.bookmarkedEntityIdentifier?.id === entityIdentifier.id; }), [entityIdentifier] ); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index a5916d32f9a..ba62226c3ca 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -84,7 +84,7 @@ const getRGMaskFill = (state: CanvasState): RgbColor => { const initialState: CanvasState = { _version: 3, selectedEntityIdentifier: null, - quickSwitchEntityIdentifier: null, + bookmarkedEntityIdentifier: null, rasterLayers: { isHidden: false, entities: [], @@ -792,13 +792,10 @@ export const canvasSlice = createSlice({ } state.selectedEntityIdentifier = entityIdentifier; }, - entityIsBookmarkedForQuickSwitchChanged: ( - state, - action: PayloadAction<{ entityIdentifier: CanvasEntityIdentifier | null }> - ) => { + bookmarkedEntityChanged: (state, action: PayloadAction<{ entityIdentifier: CanvasEntityIdentifier | null }>) => { const { entityIdentifier } = action.payload; if (!entityIdentifier) { - state.quickSwitchEntityIdentifier = null; + state.bookmarkedEntityIdentifier = null; return; } const entity = selectEntity(state, entityIdentifier); @@ -806,7 +803,7 @@ export const canvasSlice = createSlice({ // Cannot select a non-existent entity return; } - state.quickSwitchEntityIdentifier = entityIdentifier; + state.bookmarkedEntityIdentifier = entityIdentifier; }, entityNameChanged: (state, action: PayloadAction>) => { const { entityIdentifier, name } = action.payload; @@ -1122,7 +1119,7 @@ export const { canvasClearHistory, // All entities entitySelected, - entityIsBookmarkedForQuickSwitchChanged, + bookmarkedEntityChanged, entityNameChanged, entityReset, entityIsEnabledToggled, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts index 9691979a944..13e46db058e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts @@ -181,9 +181,9 @@ export const selectSelectedEntityIdentifier = createSelector( (canvas) => canvas.selectedEntityIdentifier ); -export const selectQuickSwitchEntityIdentifier = createSelector( +export const selectBookmarkedEntityIdentifier = createSelector( selectCanvasSlice, - (canvas) => canvas.quickSwitchEntityIdentifier + (canvas) => canvas.bookmarkedEntityIdentifier ); export const selectIsSelectedEntityDrawable = createSelector( diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 130abf791ac..891b6eb546d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -688,7 +688,7 @@ export type StagingAreaImage = { export type CanvasState = { _version: 3; selectedEntityIdentifier: CanvasEntityIdentifier | null; - quickSwitchEntityIdentifier: CanvasEntityIdentifier | null; + bookmarkedEntityIdentifier: CanvasEntityIdentifier | null; inpaintMasks: { isHidden: boolean; entities: CanvasInpaintMaskState[]; From c38b05c00218498c0e74e6f8381ba127bdf009f5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:49:19 +1000 Subject: [PATCH 604/678] chore(ui): lint --- .../src/features/controlLayers/hooks/useLayerControlAdapter.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts index 79c9415e3e1..f10bcba0294 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts @@ -48,7 +48,6 @@ export const useDefaultControlAdapter = (): ControlNetConfig | T2IAdapterConfig return defaultControlAdapter; }; -/** @knipignore */ export const useDefaultIPAdapter = (): IPAdapterConfig => { const [modelConfigs] = useIPAdapterModels(); From 921ade4b0a1e5de6837a2fde8887b3cb88b86c65 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:55:12 +1000 Subject: [PATCH 605/678] feat(ui): brush preview opacity at 0.5 when drawing on mask --- .../src/features/controlLayers/konva/CanvasStateApiModule.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index 6cdc1ccca5c..ff8ed185d35 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -239,7 +239,7 @@ export class CanvasStateApiModule extends CanvasModuleBase { const selectedEntity = this.getSelectedEntity(); if (selectedEntity?.state.type === 'regional_guidance' || selectedEntity?.state.type === 'inpaint_mask') { // The brush should use the mask opacity for these enktity types - return { ...selectedEntity.state.fill.color, a: 1 }; + return { ...selectedEntity.state.fill.color, a: 0.5 }; } else { return this.getToolState().fill; } From 92c754df790ca950e6add7d15fd2d00e1bbd2c4e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:56:55 +1000 Subject: [PATCH 606/678] fix(ui): disable merge visible when 1 or fewer layers of type --- .../components/common/CanvasEntityMergeVisibleButton.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMergeVisibleButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMergeVisibleButton.tsx index 56156340122..f0b7bb83da4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMergeVisibleButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMergeVisibleButton.tsx @@ -3,6 +3,7 @@ import { logger } from 'app/logging/logger'; import { useAppDispatch } from 'app/store/storeHooks'; import { isOk, withResultAsync } from 'common/util/result'; import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; +import { useEntityTypeCount } from 'features/controlLayers/hooks/useEntityTypeCount'; import { inpaintMaskAdded, rasterLayerAdded } from 'features/controlLayers/store/canvasSlice'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { imageDTOToImageObject } from 'features/controlLayers/store/types'; @@ -22,6 +23,7 @@ export const CanvasEntityMergeVisibleButton = memo(({ type }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const canvasManager = useCanvasManager(); + const entityCount = useEntityTypeCount(type); const onClick = useCallback(async () => { if (type === 'raster_layer') { const rect = canvasManager.stage.getVisibleRect('raster_layer'); @@ -81,6 +83,7 @@ export const CanvasEntityMergeVisibleButton = memo(({ type }: Props) => { icon={} onClick={onClick} alignSelf="stretch" + isDisabled={entityCount <= 1} /> ); }); From 1628eb5900dfdd165f232f591fe75ef04535eba9 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 2 Sep 2024 12:36:08 +1000 Subject: [PATCH 607/678] tidy(ui): transformer organisation --- .../konva/CanvasEntityTransformer.ts | 382 ++++++++---------- .../src/features/controlLayers/store/types.ts | 5 + 2 files changed, 172 insertions(+), 215 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts index 7b2f032592b..90fc0769898 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts @@ -3,7 +3,7 @@ import type { CanvasEntityMaskAdapter } from 'features/controlLayers/konva/Canva import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { canvasToImageData, getEmptyRect, getPrefixedId } from 'features/controlLayers/konva/util'; -import type { Coordinate, Rect } from 'features/controlLayers/store/types'; +import type { Coordinate, Rect, RectWithRotation } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { GroupConfig } from 'konva/lib/Group'; import { debounce, get } from 'lodash-es'; @@ -182,100 +182,9 @@ export class CanvasEntityTransformer extends CanvasModuleBase { anchorSize: this.config.SCALE_ANCHOR_SIZE, anchorCornerRadius: this.config.SCALE_ANCHOR_SIZE * this.config.SCALE_ANCHOR_CORNER_RADIUS_RATIO, // This function is called for each anchor to style it (and do anything else you might want to do). - anchorStyleFunc: (anchor) => { - // Give the rotater special styling - if (anchor.hasName('rotater')) { - anchor.setAttrs({ - height: this.config.ROTATE_ANCHOR_SIZE, - width: this.config.ROTATE_ANCHOR_SIZE, - cornerRadius: this.config.ROTATE_ANCHOR_SIZE * this.config.SCALE_ANCHOR_CORNER_RADIUS_RATIO, - fill: this.config.ROTATE_ANCHOR_FILL_COLOR, - stroke: this.config.SCALE_ANCHOR_FILL_COLOR, - offsetX: this.config.ROTATE_ANCHOR_SIZE / 2, - offsetY: this.config.ROTATE_ANCHOR_SIZE / 2, - }); - } - // Add some padding to the hit area of the anchors - anchor.hitFunc((context) => { - context.beginPath(); - context.rect( - -this.config.ANCHOR_HIT_PADDING, - -this.config.ANCHOR_HIT_PADDING, - anchor.width() + this.config.ANCHOR_HIT_PADDING * 2, - anchor.height() + this.config.ANCHOR_HIT_PADDING * 2 - ); - context.closePath(); - context.fillStrokeShape(anchor); - }); - }, - anchorDragBoundFunc: (oldPos: Coordinate, newPos: Coordinate) => { - // The anchorDragBoundFunc callback puts constraints on the movement of the transformer anchors, which in - // turn constrain the transformation. It is called on every anchor move. We'll use this to snap the anchors - // to the nearest pixel. - - // If we are rotating, no need to do anything - just let the rotation happen. - if (this.konva.transformer.getActiveAnchor() === 'rotater') { - return newPos; - } - - // We need to snap the anchor to the nearest pixel, but the positions provided to this callback are absolute, - // scaled coordinates. They need to be converted to stage coordinates, snapped, then converted back to absolute - // before returning them. - const stageScale = this.manager.stage.getScale(); - const stagePos = this.manager.stage.getPosition(); - - // Unscale and round the target position to the nearest pixel. - const targetX = Math.round(newPos.x / stageScale); - const targetY = Math.round(newPos.y / stageScale); - - // The stage may be offset a fraction of a pixel. To ensure the anchor snaps to the nearest pixel, we need to - // calculate that offset and add it back to the target position. - - // Calculate the offset. It's the remainder of the stage position divided by the scale * desired grid size. In - // this case, the grid size is 1px. For example, if we wanted to snap to the nearest 8px, the calculation would - // be `stagePos.x % (stageScale * 8)`. - const scaledOffsetX = stagePos.x % stageScale; - const scaledOffsetY = stagePos.y % stageScale; - - // Unscale the target position and add the offset to get the absolute position for this anchor. - const scaledTargetX = targetX * stageScale + scaledOffsetX; - const scaledTargetY = targetY * stageScale + scaledOffsetY; - - this.log.trace( - { - oldPos, - newPos, - stageScale, - stagePos, - targetX, - targetY, - scaledOffsetX, - scaledOffsetY, - scaledTargetX, - scaledTargetY, - }, - 'Anchor drag bound' - ); - - return { x: scaledTargetX, y: scaledTargetY }; - }, - boundBoxFunc: (oldBoundBox, newBoundBox) => { - // Bail if we are not rotating, we don't need to do anything. - if (this.konva.transformer.getActiveAnchor() !== 'rotater') { - return newBoundBox; - } - - // This transform constraint operates on the bounding box of the transformer. This box has x, y, width, and - // height in stage coordinates, and rotation in radians. This can be used to snap the transformer rotation to - // the nearest 45 degrees when shift is held. - if (this.manager.stateApi.$shiftKey.get()) { - if (Math.abs(newBoundBox.rotation % (Math.PI / 4)) > 0) { - return oldBoundBox; - } - } - - return newBoundBox; - }, + anchorStyleFunc: this.anchorStyleFunc, + anchorDragBoundFunc: this.anchorDragBoundFunc, + boundBoxFunc: this.boxBoundFunc, }), proxyRect: new Konva.Rect({ name: `${this.type}:proxy_rect`, @@ -284,128 +193,14 @@ export class CanvasEntityTransformer extends CanvasModuleBase { }), }; - this.konva.transformer.on('transformstart', () => { - // Just logging in this callback. Called on mouse down of a transform anchor. - this.log.trace( - { - x: this.konva.proxyRect.x(), - y: this.konva.proxyRect.y(), - scaleX: this.konva.proxyRect.scaleX(), - scaleY: this.konva.proxyRect.scaleY(), - rotation: this.konva.proxyRect.rotation(), - }, - 'Transform started' - ); - }); - - this.konva.transformer.on('transform', () => { - // This is called when a transform anchor is dragged. By this time, the transform constraints in the above - // callbacks have been enforced, and the transformer has updated its nodes' attributes. We need to pass the - // updated attributes to the object group, propagating the transformation on down. - this.syncObjectGroupWithProxyRect(); - }); - - this.konva.transformer.on('transformend', () => { - // Called on mouse up on an anchor. We'll do some final snapping to ensure the transformer is pixel-perfect. - - // Snap the position to the nearest pixel. - const x = this.konva.proxyRect.x(); - const y = this.konva.proxyRect.y(); - const snappedX = Math.round(x); - const snappedY = Math.round(y); - - // The transformer doesn't modify the width and height. It only modifies scale. We'll need to apply the scale to - // the width and height, round them to the nearest pixel, and finally calculate a new scale that will result in - // the snapped width and height. - const width = this.konva.proxyRect.width(); - const height = this.konva.proxyRect.height(); - const scaleX = this.konva.proxyRect.scaleX(); - const scaleY = this.konva.proxyRect.scaleY(); - - // Determine the target width and height, rounded to the nearest pixel. Must be >= 1. Because the scales can be - // negative, we need to take the absolute value of the width and height. - const targetWidth = Math.max(Math.abs(Math.round(width * scaleX)), 1); - const targetHeight = Math.max(Math.abs(Math.round(height * scaleY)), 1); - - // Calculate the scale we need to use to get the target width and height. Restore the sign of the scales. - const snappedScaleX = (targetWidth / width) * Math.sign(scaleX); - const snappedScaleY = (targetHeight / height) * Math.sign(scaleY); - - // Update interaction rect and object group attributes. - this.konva.proxyRect.setAttrs({ - x: snappedX, - y: snappedY, - scaleX: snappedScaleX, - scaleY: snappedScaleY, - }); - this.parent.renderer.konva.objectGroup.setAttrs({ - x: snappedX, - y: snappedY, - scaleX: snappedScaleX, - scaleY: snappedScaleY, - }); - - // Rotation is only retrieved for logging purposes. - const rotation = this.konva.proxyRect.rotation(); - - this.log.trace( - { - x, - y, - width, - height, - scaleX, - scaleY, - rotation, - snappedX, - snappedY, - targetWidth, - targetHeight, - snappedScaleX, - snappedScaleY, - }, - 'Transform ended' - ); - }); - - this.konva.proxyRect.on('dragmove', () => { - // Snap the interaction rect to the nearest pixel - this.konva.proxyRect.x(Math.round(this.konva.proxyRect.x())); - this.konva.proxyRect.y(Math.round(this.konva.proxyRect.y())); - - // The bbox should be updated to reflect the new position of the interaction rect, taking into account its padding - // and border - this.konva.outlineRect.setAttrs({ - x: this.konva.proxyRect.x() - this.manager.stage.getScaledPixels(this.config.OUTLINE_PADDING), - y: this.konva.proxyRect.y() - this.manager.stage.getScaledPixels(this.config.OUTLINE_PADDING), - }); - - // The object group is translated by the difference between the interaction rect's new and old positions (which is - // stored as this.pixelRect) - this.parent.renderer.konva.objectGroup.setAttrs({ - x: this.konva.proxyRect.x(), - y: this.konva.proxyRect.y(), - }); - }); - this.konva.proxyRect.on('dragend', () => { - if (this.isTransforming) { - // If we are transforming the entity, we should not push the new position to the state. This will trigger a - // re-render of the entity and bork the transformation. - return; - } - - const position = { - x: this.konva.proxyRect.x() - this.pixelRect.x, - y: this.konva.proxyRect.y() - this.pixelRect.y, - }; - - this.log.trace({ position }, 'Position changed'); - this.manager.stateApi.setEntityPosition({ entityIdentifier: this.parent.getEntityIdentifier(), position }); - }); + this.konva.transformer.on('transform', this.syncObjectGroupWithProxyRect); + this.konva.transformer.on('transformend', this.snapProxyRectToPixelGrid); + this.konva.proxyRect.on('dragmove', this.onDragMove); + this.konva.proxyRect.on('dragend', this.onDragEnd); + // When the stage scale changes, we may need to re-scale some of the transformer's components. For example, + // the bbox outline should always be 1 screen pixel wide, so we need to update its stroke width. this.subscriptions.add( - // When the stage scale changes, we may need to re-scale some of the transformer's components. For example, - // the bbox outline should always be 1 screen pixel wide, so we need to update its stroke width. this.manager.stateApi.$stageAttrs.listen((newVal, oldVal) => { if (newVal.scale !== oldVal.scale) { this.syncScale(); @@ -432,6 +227,163 @@ export class CanvasEntityTransformer extends CanvasModuleBase { this.parent.konva.layer.add(this.konva.transformer); } + anchorStyleFunc = (anchor: Konva.Rect): void => { + // Give the rotater special styling + if (anchor.hasName('rotater')) { + anchor.setAttrs({ + height: this.config.ROTATE_ANCHOR_SIZE, + width: this.config.ROTATE_ANCHOR_SIZE, + cornerRadius: this.config.ROTATE_ANCHOR_SIZE * this.config.SCALE_ANCHOR_CORNER_RADIUS_RATIO, + fill: this.config.ROTATE_ANCHOR_FILL_COLOR, + stroke: this.config.SCALE_ANCHOR_FILL_COLOR, + offsetX: this.config.ROTATE_ANCHOR_SIZE / 2, + offsetY: this.config.ROTATE_ANCHOR_SIZE / 2, + }); + } + // Add some padding to the hit area of the anchors + anchor.hitFunc((context) => { + context.beginPath(); + context.rect( + -this.config.ANCHOR_HIT_PADDING, + -this.config.ANCHOR_HIT_PADDING, + anchor.width() + this.config.ANCHOR_HIT_PADDING * 2, + anchor.height() + this.config.ANCHOR_HIT_PADDING * 2 + ); + context.closePath(); + context.fillStrokeShape(anchor); + }); + }; + + anchorDragBoundFunc = (oldPos: Coordinate, newPos: Coordinate) => { + // The anchorDragBoundFunc callback puts constraints on the movement of the transformer anchors, which in + // turn constrain the transformation. It is called on every anchor move. We'll use this to snap the anchors + // to the nearest pixel. + + // If we are rotating, no need to do anything - just let the rotation happen. + if (this.konva.transformer.getActiveAnchor() === 'rotater') { + return newPos; + } + + // We need to snap the anchor to the nearest pixel, but the positions provided to this callback are absolute, + // scaled coordinates. They need to be converted to stage coordinates, snapped, then converted back to absolute + // before returning them. + const stageScale = this.manager.stage.getScale(); + const stagePos = this.manager.stage.getPosition(); + + // Unscale and round the target position to the nearest pixel. + const targetX = Math.round(newPos.x / stageScale); + const targetY = Math.round(newPos.y / stageScale); + + // The stage may be offset a fraction of a pixel. To ensure the anchor snaps to the nearest pixel, we need to + // calculate that offset and add it back to the target position. + + // Calculate the offset. It's the remainder of the stage position divided by the scale * desired grid size. In + // this case, the grid size is 1px. For example, if we wanted to snap to the nearest 8px, the calculation would + // be `stagePos.x % (stageScale * 8)`. + const scaledOffsetX = stagePos.x % stageScale; + const scaledOffsetY = stagePos.y % stageScale; + + // Unscale the target position and add the offset to get the absolute position for this anchor. + const scaledTargetX = targetX * stageScale + scaledOffsetX; + const scaledTargetY = targetY * stageScale + scaledOffsetY; + + return { x: scaledTargetX, y: scaledTargetY }; + }; + + boxBoundFunc = (oldBoundBox: RectWithRotation, newBoundBox: RectWithRotation) => { + // Bail if we are not rotating, we don't need to do anything. + if (this.konva.transformer.getActiveAnchor() !== 'rotater') { + return newBoundBox; + } + + // This transform constraint operates on the bounding box of the transformer. This box has x, y, width, and + // height in stage coordinates, and rotation in radians. This can be used to snap the transformer rotation to + // the nearest 45 degrees when shift is held. + if (this.manager.stateApi.$shiftKey.get()) { + if (Math.abs(newBoundBox.rotation % (Math.PI / 4)) > 0) { + return oldBoundBox; + } + } + + return newBoundBox; + }; + + /** + * Snaps the proxy rect to the nearest pixel, syncing the object group with the proxy rect. + */ + snapProxyRectToPixelGrid = () => { + // Called on mouse up on an anchor. We'll do some final snapping to ensure the transformer is pixel-perfect. + + // Snap the position to the nearest pixel. + const x = this.konva.proxyRect.x(); + const y = this.konva.proxyRect.y(); + const snappedX = Math.round(x); + const snappedY = Math.round(y); + + // The transformer doesn't modify the width and height. It only modifies scale. We'll need to apply the scale to + // the width and height, round them to the nearest pixel, and finally calculate a new scale that will result in + // the snapped width and height. + const width = this.konva.proxyRect.width(); + const height = this.konva.proxyRect.height(); + const scaleX = this.konva.proxyRect.scaleX(); + const scaleY = this.konva.proxyRect.scaleY(); + + // Determine the target width and height, rounded to the nearest pixel. Must be >= 1. Because the scales can be + // negative, we need to take the absolute value of the width and height. + const targetWidth = Math.max(Math.abs(Math.round(width * scaleX)), 1); + const targetHeight = Math.max(Math.abs(Math.round(height * scaleY)), 1); + + // Calculate the scale we need to use to get the target width and height. Restore the sign of the scales. + const snappedScaleX = (targetWidth / width) * Math.sign(scaleX); + const snappedScaleY = (targetHeight / height) * Math.sign(scaleY); + + // Update interaction rect and object group attributes. + this.konva.proxyRect.setAttrs({ + x: snappedX, + y: snappedY, + scaleX: snappedScaleX, + scaleY: snappedScaleY, + }); + + this.syncObjectGroupWithProxyRect(); + }; + + onDragMove = () => { + // Snap the interaction rect to the nearest pixel + this.konva.proxyRect.x(Math.round(this.konva.proxyRect.x())); + this.konva.proxyRect.y(Math.round(this.konva.proxyRect.y())); + + // The bbox should be updated to reflect the new position of the interaction rect, taking into account its padding + // and border + this.konva.outlineRect.setAttrs({ + x: this.konva.proxyRect.x() - this.manager.stage.getScaledPixels(this.config.OUTLINE_PADDING), + y: this.konva.proxyRect.y() - this.manager.stage.getScaledPixels(this.config.OUTLINE_PADDING), + }); + + // The object group is translated by the difference between the interaction rect's new and old positions (which is + // stored as this.pixelRect) + this.parent.renderer.konva.objectGroup.setAttrs({ + x: this.konva.proxyRect.x(), + y: this.konva.proxyRect.y(), + }); + }; + + onDragEnd = () => { + if (this.isTransforming) { + // If we are transforming the entity, we should not push the new position to the state. This will trigger a + // re-render of the entity and bork the transformation. + return; + } + + const position = { + x: this.konva.proxyRect.x() - this.pixelRect.x, + y: this.konva.proxyRect.y() - this.pixelRect.y, + }; + + this.log.trace({ position }, 'Position changed'); + this.manager.stateApi.setEntityPosition({ entityIdentifier: this.parent.getEntityIdentifier(), position }); + }; + // TODO(psyche): These don't work when the entity is rotated, need to do some math to offset the flip after rotation // flipHorizontal = () => { // if (!this.isTransforming || this.$isProcessing.get()) { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 891b6eb546d..0661259fc25 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -478,6 +478,11 @@ const zRect = z.object({ }); export type Rect = z.infer; +const zRectWithRotation = zRect.extend({ + rotation: z.number(), +}); +export type RectWithRotation = z.infer; + const zCanvasBrushLineState = z.object({ id: zId, type: z.literal('brush_line'), From 4f4eee76b9242f141ab3e9525f6844d75ffd22b1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 2 Sep 2024 12:36:55 +1000 Subject: [PATCH 608/678] feat(ui): add fit to bbox as transform helper --- invokeai/frontend/web/public/locales/en.json | 8 ++++++- .../components/Transform/Transform.tsx | 21 +++++++++++++------ .../common/CanvasEntityMenuItemsTransform.tsx | 2 +- .../konva/CanvasEntityTransformer.ts | 17 +++++++++++++++ 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index c6e269979bb..8855abc3f6d 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1781,7 +1781,6 @@ "bbox": "Bbox", "move": "Move", "view": "View", - "transform": "Transform", "colorPicker": "Color Picker" }, "filter": { @@ -1791,6 +1790,13 @@ "preview": "Preview", "apply": "Apply", "cancel": "Cancel" + }, + "transform": { + "transform": "Transform", + "fitToBbox": "Fit to Bbox", + "reset": "Reset", + "apply": "Apply", + "cancel": "Cancel" } }, "upscaling": { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Transform/Transform.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Transform/Transform.tsx index 755d60e4e10..715ba40ee23 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Transform/Transform.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Transform/Transform.tsx @@ -8,7 +8,7 @@ import { import { useEntityAdapter } from 'features/controlLayers/hooks/useEntityAdapter'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -import { PiArrowsCounterClockwiseBold, PiCheckBold, PiXBold } from 'react-icons/pi'; +import { PiArrowsCounterClockwiseBold, PiArrowsOutBold, PiCheckBold, PiXBold } from 'react-icons/pi'; const TransformBox = memo(() => { const { t } = useTranslation(); @@ -30,9 +30,19 @@ const TransformBox = memo(() => { transitionDuration="normal" > - {t('controlLayers.tool.transform')} + {t('controlLayers.transform.transform')} + + - diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsTransform.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsTransform.tsx index a4c5dab05d6..3d5fe022c3e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsTransform.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsTransform.tsx @@ -20,7 +20,7 @@ export const CanvasEntityMenuItemsTransform = memo(() => { return ( } isDisabled={Boolean(transformingEntity)}> - {t('controlLayers.tool.transform')} + {t('controlLayers.transform.transform')} ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts index 90fc0769898..9aa63b567f2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts @@ -348,6 +348,23 @@ export class CanvasEntityTransformer extends CanvasModuleBase { this.syncObjectGroupWithProxyRect(); }; + /** + * Fits the proxy rect to the bounding box of the parent entity, then syncs the object group with the proxy rect. + */ + fitProxyRectToBbox = () => { + const { rect } = this.manager.stateApi.getBbox(); + const scaleX = rect.width / this.konva.proxyRect.width(); + const scaleY = rect.height / this.konva.proxyRect.height(); + this.konva.proxyRect.setAttrs({ + x: rect.x, + y: rect.y, + scaleX, + scaleY, + rotation: 0, + }); + this.syncObjectGroupWithProxyRect(); + }; + onDragMove = () => { // Snap the interaction rect to the nearest pixel this.konva.proxyRect.x(Math.round(this.konva.proxyRect.x())); From 618473db02f9909395a4e624c86f5bb0d25cc24c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 2 Sep 2024 12:42:36 +1000 Subject: [PATCH 609/678] fix(ui): transform should ignore konva filters (e.g. transparency effect) --- .../src/features/controlLayers/konva/CanvasEntityTransformer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts index 9aa63b567f2..de21081c5a3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts @@ -559,7 +559,7 @@ export class CanvasEntityTransformer extends CanvasModuleBase { this.log.debug('Applying transform'); this.$isProcessing.set(true); const rect = this.getRelativeRect(); - await this.parent.renderer.rasterize({ rect, replaceObjects: true }); + await this.parent.renderer.rasterize({ rect, replaceObjects: true, attrs: { filters: [] } }); this.requestRectCalculation(); this.stopTransform(); }; From 2e4a48b71c3dca5dba9058cbd4631efbee978bca Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 2 Sep 2024 16:07:22 +1000 Subject: [PATCH 610/678] feat(ui): move transformer state to nanostores This provides some free reactivity for this canvas-manager-managed state. --- .../common/CanvasEntityPreviewImage.tsx | 2 +- .../konva/CanvasEntityRenderer.ts | 8 +- .../konva/CanvasEntityTransformer.ts | 140 +++++++++++------- 3 files changed, 93 insertions(+), 57 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityPreviewImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityPreviewImage.tsx index 74bcb9010e7..da0f45c1930 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityPreviewImage.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityPreviewImage.tsx @@ -69,7 +69,7 @@ export const CanvasEntityPreviewImage = memo(() => { ctx.globalCompositeOperation = 'source-in'; ctx.fillRect(0, 0, rect.width, rect.height); } - }, [adapter.transformer, adapter.transformer.nodeRect, adapter.transformer.pixelRect, cache, maskColor]); + }, [cache, maskColor]); return ( { - if (this.parent.transformer.isPendingRectCalculation) { + if (this.parent.transformer.$isPendingRectCalculation.get()) { return; } - if (this.parent.transformer.pixelRect.width === 0 || this.parent.transformer.pixelRect.height === 0) { + const pixelRect = this.parent.transformer.$pixelRect.get(); + if (pixelRect.width === 0 || pixelRect.height === 0) { return; } try { const canvas = this.konva.objectGroup._getCachedSceneCanvas()._canvas as HTMLCanvasElement | undefined | null; if (canvas) { - const nodeRect = this.parent.transformer.nodeRect; - const pixelRect = this.parent.transformer.pixelRect; + const nodeRect = this.parent.transformer.$nodeRect.get(); const rect = { x: pixelRect.x - nodeRect.x, y: pixelRect.y - nodeRect.y, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts index de21081c5a3..edefe15deb3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts @@ -90,21 +90,27 @@ export class CanvasEntityTransformer extends CanvasModuleBase { /** * The rect of the parent, _including_ transparent regions. * It is calculated via Konva's getClientRect method, which is fast but includes transparent regions. + * + * Stored as a nanostores atom for easy reactivity. */ - nodeRect = getEmptyRect(); + $nodeRect = atom(getEmptyRect()); /** * The rect of the parent, _excluding_ transparent regions. * If the parent's nodes have no possibility of transparent regions, this will be calculated the same way as nodeRect. * If the parent's nodes may have transparent regions, this will be calculated manually by rasterizing the parent and * checking the pixel data. + * + * Stored as a nanostores atom for easy reactivity. */ - pixelRect = getEmptyRect(); + $pixelRect = atom(getEmptyRect()); /** * Whether the transformer is currently calculating the rect of the parent. + * + * Stored as a nanostores atom for easy reactivity. */ - isPendingRectCalculation: boolean = true; + $isPendingRectCalculation = atom(true); /** * A set of subscriptions that should be cleaned up when the transformer is destroyed. @@ -113,27 +119,40 @@ export class CanvasEntityTransformer extends CanvasModuleBase { /** * Whether the transformer is currently transforming the entity. + * + * Stored as a nanostores atom for easy reactivity. */ - isTransforming: boolean = false; + $isTransforming = atom(false); /** * The current interaction mode of the transformer: * - 'all': The entity can be moved, resized, and rotated. * - 'drag': The entity can be moved. * - 'off': The transformer is not interactable. + * + * Stored as a nanostores atom for easy reactivity. */ - interactionMode: 'all' | 'drag' | 'off' = 'off'; + $interactionMode = atom<'all' | 'drag' | 'off'>('off'); /** * Whether dragging is enabled. Dragging is enabled in both 'all' and 'drag' interaction modes. + * + * Stored as a nanostores atom for easy reactivity. */ - isDragEnabled: boolean = false; + $isDragEnabled = atom(false); /** * Whether transforming is enabled. Transforming is enabled only in 'all' interaction mode. + * + * Stored as a nanostores atom for easy reactivity. */ - isTransformEnabled: boolean = false; + $isTransformEnabled = atom(false); + /** + * Whether the transformer is currently processing (rasterizing and uploading) the transformed entity. + * + * Stored as a nanostores atom for easy reactivity. + */ $isProcessing = atom(false); konva: { @@ -386,15 +405,17 @@ export class CanvasEntityTransformer extends CanvasModuleBase { }; onDragEnd = () => { - if (this.isTransforming) { + if (this.$isTransforming.get()) { // If we are transforming the entity, we should not push the new position to the state. This will trigger a // re-render of the entity and bork the transformation. return; } + const pixelRect = this.$pixelRect.get(); + const position = { - x: this.konva.proxyRect.x() - this.pixelRect.x, - y: this.konva.proxyRect.y() - this.pixelRect.y, + x: this.konva.proxyRect.x() - pixelRect.x, + y: this.konva.proxyRect.y() - pixelRect.y, }; this.log.trace({ position }, 'Position changed'); @@ -480,7 +501,10 @@ export class CanvasEntityTransformer extends CanvasModuleBase { syncInteractionState = () => { this.log.trace('Syncing interaction state'); - if (this.isPendingRectCalculation || this.pixelRect.width === 0 || this.pixelRect.height === 0) { + const pixelRect = this.$pixelRect.get(); + const isPendingRectCalculation = this.$isPendingRectCalculation.get(); + + if (isPendingRectCalculation || pixelRect.width === 0 || pixelRect.height === 0) { // If the rect is being calculated, or if the rect has no width or height, we can't interact with the transformer this.parent.konva.layer.listening(false); this.setInteractionMode('off'); @@ -497,11 +521,11 @@ export class CanvasEntityTransformer extends CanvasModuleBase { return; } - if (isSelected && !this.isTransforming && tool === 'move') { + if (isSelected && !this.$isTransforming.get() && tool === 'move') { // We are moving this layer, it must be listening this.parent.konva.layer.listening(true); this.setInteractionMode('drag'); - } else if (isSelected && this.isTransforming) { + } else if (isSelected && this.$isTransforming.get()) { // When transforming, we want the stage to still be movable if the view tool is selected. If the transformer is // active, it will interrupt the stage drag events. So we should disable listening when the view tool is selected. if (tool !== 'view') { @@ -540,7 +564,7 @@ export class CanvasEntityTransformer extends CanvasModuleBase { */ startTransform = () => { this.log.debug('Starting transform'); - this.isTransforming = true; + this.$isTransforming.set(true); this.manager.stateApi.$tool.set('move'); // When transforming, we want the stage to still be movable if the view tool is selected. If the transformer or // interaction rect are listening, it will interrupt the stage's drag events. So we should disable listening @@ -577,7 +601,7 @@ export class CanvasEntityTransformer extends CanvasModuleBase { stopTransform = () => { this.log.debug('Stopping transform'); - this.isTransforming = false; + this.$isTransforming.set(false); this.setInteractionMode('off'); // Reset the transform of the the entity. We've either replaced the transformed objects with a rasterized image, or @@ -613,16 +637,17 @@ export class CanvasEntityTransformer extends CanvasModuleBase { this.log.trace('Updating position'); const position = get(arg, 'position', this.parent.state.position); + const pixelRect = this.$pixelRect.get(); const groupAttrs: Partial = { - x: position.x + this.pixelRect.x, - y: position.y + this.pixelRect.y, - offsetX: this.pixelRect.x, - offsetY: this.pixelRect.y, + x: position.x + pixelRect.x, + y: position.y + pixelRect.y, + offsetX: pixelRect.x, + offsetY: pixelRect.y, }; this.parent.renderer.konva.objectGroup.setAttrs(groupAttrs); this.parent.renderer.konva.bufferGroup.setAttrs(groupAttrs); - this.update(position, this.pixelRect); + this.update(position, pixelRect); }; /** @@ -633,7 +658,7 @@ export class CanvasEntityTransformer extends CanvasModuleBase { * - 'off': The transformer is not interactable. */ setInteractionMode = (interactionMode: 'all' | 'drag' | 'off') => { - this.interactionMode = interactionMode; + this.$interactionMode.set(interactionMode); if (interactionMode === 'drag') { this._enableDrag(); this._disableTransform(); @@ -650,16 +675,19 @@ export class CanvasEntityTransformer extends CanvasModuleBase { }; updateBbox = () => { - this.log.trace({ nodeRect: this.nodeRect, pixelRect: this.pixelRect }, 'Updating bbox'); + const nodeRect = this.$nodeRect.get(); + const pixelRect = this.$pixelRect.get(); + + this.log.trace({ nodeRect, pixelRect }, 'Updating bbox'); - if (this.isPendingRectCalculation) { + if (this.$isPendingRectCalculation.get()) { this.syncInteractionState(); return; } // If the bbox has no width or height, that means the layer is fully transparent. This can happen if it is only // eraser lines, fully clipped brush lines or if it has been fully erased. - if (this.pixelRect.width === 0 || this.pixelRect.height === 0) { + if (pixelRect.width === 0 || pixelRect.height === 0) { // If the layer already has no objects, we don't need to reset the entity state. This would cause a push to the // undo stack and clear the redo stack. if (this.parent.renderer.hasObjects()) { @@ -668,12 +696,12 @@ export class CanvasEntityTransformer extends CanvasModuleBase { } } else { this.syncInteractionState(); - this.update(this.parent.state.position, this.pixelRect); + this.update(this.parent.state.position, pixelRect); const groupAttrs: Partial = { - x: this.parent.state.position.x + this.pixelRect.x, - y: this.parent.state.position.y + this.pixelRect.y, - offsetX: this.pixelRect.x, - offsetY: this.pixelRect.y, + x: this.parent.state.position.x + pixelRect.x, + y: this.parent.state.position.y + pixelRect.y, + offsetX: pixelRect.x, + offsetY: pixelRect.y, }; this.parent.renderer.konva.objectGroup.setAttrs(groupAttrs); this.parent.renderer.konva.bufferGroup.setAttrs(groupAttrs); @@ -685,13 +713,13 @@ export class CanvasEntityTransformer extends CanvasModuleBase { calculateRect = debounce(() => { this.log.debug('Calculating bbox'); - this.isPendingRectCalculation = true; + this.$isPendingRectCalculation.set(true); if (!this.parent.renderer.hasObjects()) { this.log.trace('No objects, resetting bbox'); - this.nodeRect = getEmptyRect(); - this.pixelRect = getEmptyRect(); - this.isPendingRectCalculation = false; + this.$nodeRect.set(getEmptyRect()); + this.$pixelRect.set(getEmptyRect()); + this.$isPendingRectCalculation.set(false); this.updateBbox(); return; } @@ -699,10 +727,10 @@ export class CanvasEntityTransformer extends CanvasModuleBase { const rect = this.parent.renderer.konva.objectGroup.getClientRect({ skipTransform: true }); if (!this.parent.renderer.needsPixelBbox()) { - this.nodeRect = { ...rect }; - this.pixelRect = { ...rect }; - this.log.trace({ nodeRect: this.nodeRect, pixelRect: this.pixelRect }, 'Got bbox from client rect'); - this.isPendingRectCalculation = false; + this.$nodeRect.set({ ...rect }); + this.$pixelRect.set({ ...rect }); + this.log.trace({ nodeRect: this.$nodeRect.get(), pixelRect: this.$pixelRect.get() }, 'Got bbox from client rect'); + this.$isPendingRectCalculation.set(false); this.updateBbox(); return; } @@ -715,26 +743,29 @@ export class CanvasEntityTransformer extends CanvasModuleBase { (extents) => { if (extents) { const { minX, minY, maxX, maxY } = extents; - this.nodeRect = { ...rect }; - this.pixelRect = { + this.$nodeRect.set({ ...rect }); + this.$pixelRect.set({ x: Math.round(rect.x) + minX, y: Math.round(rect.y) + minY, width: maxX - minX, height: maxY - minY, - }; + }); } else { - this.nodeRect = getEmptyRect(); - this.pixelRect = getEmptyRect(); + this.$nodeRect.set(getEmptyRect()); + this.$pixelRect.set(getEmptyRect()); } - this.log.trace({ nodeRect: this.nodeRect, pixelRect: this.pixelRect, extents }, `Got bbox from worker`); - this.isPendingRectCalculation = false; + this.log.trace( + { nodeRect: this.$nodeRect.get(), pixelRect: this.$pixelRect.get(), extents }, + `Got bbox from worker` + ); + this.$isPendingRectCalculation.set(false); this.updateBbox(); } ); }, this.config.RECT_CALC_DEBOUNCE_MS); requestRectCalculation = () => { - this.isPendingRectCalculation = true; + this.$isPendingRectCalculation.set(true); this.syncInteractionState(); this.calculateRect(); }; @@ -744,27 +775,27 @@ export class CanvasEntityTransformer extends CanvasModuleBase { }; _enableTransform = () => { - this.isTransformEnabled = true; + this.$isTransformEnabled.set(true); this.konva.transformer.visible(true); this.konva.transformer.listening(true); this.konva.transformer.nodes([this.konva.proxyRect]); }; _disableTransform = () => { - this.isTransformEnabled = false; + this.$isTransformEnabled.set(false); this.konva.transformer.visible(false); this.konva.transformer.listening(false); this.konva.transformer.nodes([]); }; _enableDrag = () => { - this.isDragEnabled = true; + this.$isDragEnabled.set(true); this.konva.proxyRect.visible(true); this.konva.proxyRect.listening(true); }; _disableDrag = () => { - this.isDragEnabled = false; + this.$isDragEnabled.set(false); this.konva.proxyRect.visible(false); this.konva.proxyRect.listening(false); }; @@ -782,9 +813,14 @@ export class CanvasEntityTransformer extends CanvasModuleBase { id: this.id, type: this.type, path: this.path, - mode: this.interactionMode, - isTransformEnabled: this.isTransformEnabled, - isDragEnabled: this.isDragEnabled, + nodeRect: this.$nodeRect.get(), + pixelRect: this.$pixelRect.get(), + isPendingRectCalculation: this.$isPendingRectCalculation.get(), + isTransforming: this.$isTransforming.get(), + interactionMode: this.$interactionMode.get(), + isDragEnabled: this.$isDragEnabled.get(), + isTransformEnabled: this.$isTransformEnabled.get(), + isProcessing: this.$isProcessing.get(), }; }; From 437129cf7029c7a6021f0cf47629d6f413c6ad02 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 2 Sep 2024 16:18:54 +1000 Subject: [PATCH 611/678] feat(ui): tweak bookmark verbiage --- invokeai/frontend/web/public/locales/en.json | 4 ++-- .../common/CanvasEntityIsBookmarkedForQuickSwitchToggle.tsx | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 8855abc3f6d..1c472673a11 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1654,8 +1654,8 @@ "storeNotInitialized": "Store is not initialized" }, "controlLayers": { - "bookmarkedForQuickSwitch": "Bookmarked for Quick Switch", - "notBookmarkedForQuickSwitch": "Not Bookmarked for Quick Switch", + "bookmark": "Bookmark for Quick Switch", + "removeBookmark": "Remove Bookmark", "saveBboxToGallery": "Save Bbox To Gallery", "savedToGalleryOk": "Saved to Gallery", "savedToGalleryError": "Error saving to gallery", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityIsBookmarkedForQuickSwitchToggle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityIsBookmarkedForQuickSwitchToggle.tsx index 39a0fccccc7..90cb9e953be 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityIsBookmarkedForQuickSwitchToggle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityIsBookmarkedForQuickSwitchToggle.tsx @@ -23,10 +23,8 @@ export const CanvasEntityIsBookmarkedForQuickSwitchToggle = memo(() => { return ( : } From 44be058ac70d6dde3f0dcddcb65562f16bda2fa9 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 2 Sep 2024 16:43:03 +1000 Subject: [PATCH 612/678] docs(ui): docstrings for $canvasCache --- .../controlLayers/konva/CanvasEntityRenderer.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts index 58ccb3a32f8..f21cdfa6996 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts @@ -125,6 +125,15 @@ export class CanvasEntityRenderer extends CanvasModuleBase { } | null; }; + /** + * The entity's object group as a canvas element along with the pixel rect of the entity at the time the canvas was + * drawn. + * + * Technically, this is an internal Konva object, created when a Konva node's `.cache()` method is called. We cache + * the object group after every update, so we get this as a "free" side effect. + * + * This is used to render the entity's preview in the control layer. + */ $canvasCache = atom<{ canvas: HTMLCanvasElement; rect: Rect } | null>(null); constructor(parent: CanvasEntityLayerAdapter | CanvasEntityMaskAdapter) { @@ -557,6 +566,7 @@ export class CanvasEntityRenderer extends CanvasModuleBase { return; } try { + // TODO(psyche): This is an internal Konva method, so it may break in the future. Can we make this API public? const canvas = this.konva.objectGroup._getCachedSceneCanvas()._canvas as HTMLCanvasElement | undefined | null; if (canvas) { const nodeRect = this.parent.transformer.$nodeRect.get(); From dbff929be56009e9ba38ecb8eac5bec19dbaa1b6 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 2 Sep 2024 17:03:54 +1000 Subject: [PATCH 613/678] tidy(ui): remove unused $isProcessingTransform atom --- .../web/src/features/controlLayers/konva/CanvasStateApiModule.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index ff8ed185d35..31b1ba09b95 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -246,7 +246,6 @@ export class CanvasStateApiModule extends CanvasModuleBase { }; $transformingEntity = atom(null); - $isProcessingTransform = atom(false); $toolState: WritableAtom = atom(); $currentFill: WritableAtom = atom(); From 4b753941e40684ef68608d1e1c43ac108a8175fe Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 2 Sep 2024 17:10:24 +1000 Subject: [PATCH 614/678] feat(ui): streamline manager -> react transform interface --- .../components/Transform/Transform.tsx | 21 ++++++------------- .../common/CanvasEntityMenuItemsTransform.tsx | 4 ++-- .../controlLayers/hooks/useIsTransforming.ts | 2 +- .../konva/CanvasEntityTransformer.ts | 4 ++-- .../controlLayers/konva/CanvasManager.ts | 2 +- .../konva/CanvasStateApiModule.ts | 5 +++-- .../controlLayers/konva/CanvasToolModule.ts | 2 +- 7 files changed, 16 insertions(+), 24 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Transform/Transform.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Transform/Transform.tsx index 715ba40ee23..ffcfacebef4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Transform/Transform.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Transform/Transform.tsx @@ -1,19 +1,14 @@ import { Button, ButtonGroup, Flex, Heading, Spacer } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; -import { - EntityIdentifierContext, - useEntityIdentifierContext, -} from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { useEntityAdapter } from 'features/controlLayers/hooks/useEntityAdapter'; +import type { CanvasEntityLayerAdapter } from 'features/controlLayers/konva/CanvasEntityLayerAdapter'; +import type { CanvasEntityMaskAdapter } from 'features/controlLayers/konva/CanvasEntityMaskAdapter'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowsCounterClockwiseBold, PiArrowsOutBold, PiCheckBold, PiXBold } from 'react-icons/pi'; -const TransformBox = memo(() => { +const TransformBox = memo(({ adapter }: { adapter: CanvasEntityLayerAdapter | CanvasEntityMaskAdapter }) => { const { t } = useTranslation(); - const entityIdentifier = useEntityIdentifierContext(); - const adapter = useEntityAdapter(entityIdentifier); const isProcessing = useStore(adapter.transformer.$isProcessing); return ( @@ -79,15 +74,11 @@ TransformBox.displayName = 'Transform'; export const Transform = () => { const canvasManager = useCanvasManager(); - const transformingEntity = useStore(canvasManager.stateApi.$transformingEntity); + const adapter = useStore(canvasManager.stateApi.$transformingAdapter); - if (!transformingEntity) { + if (!adapter) { return null; } - return ( - - - - ); + return ; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsTransform.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsTransform.tsx index 3d5fe022c3e..e3fdac1f721 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsTransform.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsTransform.tsx @@ -12,14 +12,14 @@ export const CanvasEntityMenuItemsTransform = memo(() => { const entityIdentifier = useEntityIdentifierContext(); const canvasManager = useCanvasManager(); const adapter = useEntityAdapter(entityIdentifier); - const transformingEntity = useStore(canvasManager.stateApi.$transformingEntity); + const isTransforming = useStore(canvasManager.stateApi.$isTranforming); const onClick = useCallback(() => { adapter.transformer.startTransform(); }, [adapter.transformer]); return ( - } isDisabled={Boolean(transformingEntity)}> + } isDisabled={isTransforming}> {t('controlLayers.transform.transform')} ); diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useIsTransforming.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useIsTransforming.ts index dac961350e9..bf2407af358 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useIsTransforming.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useIsTransforming.ts @@ -4,7 +4,7 @@ import { useMemo } from 'react'; export const useIsTransforming = () => { const canvasManager = useCanvasManager(); - const transformingEntity = useStore(canvasManager.stateApi.$transformingEntity); + const transformingEntity = useStore(canvasManager.stateApi.$transformingAdapter); const isTransforming = useMemo(() => { return Boolean(transformingEntity); }, [transformingEntity]); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts index edefe15deb3..ecc4128130f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts @@ -573,7 +573,7 @@ export class CanvasEntityTransformer extends CanvasModuleBase { const shouldListen = this.manager.stateApi.$tool.get() !== 'view'; this.parent.konva.layer.listening(shouldListen); this.setInteractionMode('all'); - this.manager.stateApi.$transformingEntity.set(this.parent.getEntityIdentifier()); + this.manager.stateApi.$transformingAdapter.set(this.parent); }; /** @@ -608,7 +608,7 @@ export class CanvasEntityTransformer extends CanvasModuleBase { // canceled a transformation. In either case, the scale should be reset. this.resetTransform(); this.syncInteractionState(); - this.manager.stateApi.$transformingEntity.set(null); + this.manager.stateApi.$transformingAdapter.set(null); this.$isProcessing.set(false); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 6fb136daf74..343fa7fd65a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -137,7 +137,7 @@ export class CanvasManager extends CanvasModuleBase { this.log.debug('Initializing canvas manager module'); // These atoms require the canvas manager to be set up before we can provide their initial values - this.stateApi.$transformingEntity.set(null); + this.stateApi.$transformingAdapter.set(null); this.stateApi.$toolState.set(this.stateApi.getToolState()); this.stateApi.$selectedEntityIdentifier.set(this.stateApi.getCanvasState().selectedEntityIdentifier); this.stateApi.$currentFill.set(this.stateApi.getCurrentFill()); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index 31b1ba09b95..9a7519c23e4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -43,7 +43,7 @@ import type { } from 'features/controlLayers/store/types'; import { RGBA_BLACK } from 'features/controlLayers/store/types'; import type { WritableAtom } from 'nanostores'; -import { atom } from 'nanostores'; +import { atom, computed } from 'nanostores'; import type { Logger } from 'roarr'; import { queueApi } from 'services/api/endpoints/queue'; import type { BatchConfig } from 'services/api/types'; @@ -245,7 +245,8 @@ export class CanvasStateApiModule extends CanvasModuleBase { } }; - $transformingEntity = atom(null); + $transformingAdapter = atom(null); + $isTranforming = computed(this.$transformingAdapter, (transformingAdapter) => Boolean(transformingAdapter)); $toolState: WritableAtom = atom(); $currentFill: WritableAtom = atom(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts index 87c2503c8b2..6f03b1580b6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts @@ -119,7 +119,7 @@ export class CanvasToolModule extends CanvasModuleBase { isDrawableEntity(selectedEntity.state); // Update the stage's pointer style - if (Boolean(this.manager.stateApi.$transformingEntity.get()) || renderedEntityCount === 0) { + if (this.manager.stateApi.$isTranforming.get() || renderedEntityCount === 0) { // We are transforming and/or have no layers, so we should not render any tool stage.container.style.cursor = 'default'; } else if (tool === 'view') { From 3ed4cf63aeb74337196246fc872b93202ddae517 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 2 Sep 2024 18:10:49 +1000 Subject: [PATCH 615/678] feat(ui): tidy stateApi atoms & add docstrings --- .../StagingArea/StagingAreaToolbar.tsx | 6 +- .../components/Tool/ToolSettings.tsx | 2 +- .../controlLayers/components/Tool/hooks.ts | 6 +- .../components/Toolbar/CanvasToolbarScale.tsx | 2 +- .../konva/CanvasBackgroundModule.ts | 2 +- .../controlLayers/konva/CanvasBboxModule.ts | 4 +- .../konva/CanvasBrushToolPreview.ts | 2 +- .../konva/CanvasColorPickerToolPreview.ts | 4 +- .../konva/CanvasEntityRenderer.ts | 6 +- .../konva/CanvasEntityTransformer.ts | 10 +- .../konva/CanvasEraserToolPreview.ts | 2 +- .../controlLayers/konva/CanvasFilterModule.ts | 2 +- .../controlLayers/konva/CanvasManager.ts | 11 +- .../controlLayers/konva/CanvasStageModule.ts | 29 ++- .../konva/CanvasStagingAreaModule.ts | 7 +- .../konva/CanvasStateApiModule.ts | 204 +++++++++++++++--- .../controlLayers/konva/CanvasToolModule.ts | 113 ++++++---- 17 files changed, 301 insertions(+), 111 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx index 2d5079ed657..1afdea86146 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx @@ -45,7 +45,7 @@ export const StagingAreaToolbar = memo(() => { const index = useAppSelector(selectStagedImageIndex); const selectedImage = useAppSelector(selectSelectedImage); const imageCount = useAppSelector(selectImageCount); - const shouldShowStagedImage = useStore(canvasManager.stateApi.$shouldShowStagedImage); + const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage); const isCanvasActive = useStore(INTERACTION_SCOPES.canvas.$isActive); const [changeIsImageIntermediate] = useChangeImageIsIntermediateMutation(); useScopeOnMount('stagingArea'); @@ -83,8 +83,8 @@ export const StagingAreaToolbar = memo(() => { }, [dispatch]); const onToggleShouldShowStagedImage = useCallback(() => { - canvasManager.stateApi.$shouldShowStagedImage.set(!shouldShowStagedImage); - }, [canvasManager.stateApi.$shouldShowStagedImage, shouldShowStagedImage]); + canvasManager.stagingArea.$shouldShowStagedImage.set(!shouldShowStagedImage); + }, [canvasManager.stagingArea.$shouldShowStagedImage, shouldShowStagedImage]); const onSaveStagingImage = useCallback(() => { if (!selectedImage) { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolSettings.tsx index aef9e5d2e3a..30e8e85067a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolSettings.tsx @@ -6,7 +6,7 @@ import { memo } from 'react'; export const ToolSettings = memo(() => { const canvasManager = useCanvasManager(); - const tool = useStore(canvasManager.stateApi.$tool); + const tool = useStore(canvasManager.tool.$tool); if (tool === 'brush') { return ; } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/hooks.ts b/invokeai/frontend/web/src/features/controlLayers/components/Tool/hooks.ts index 1bd546e7439..d90656e9db4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/hooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/hooks.ts @@ -6,14 +6,14 @@ import { useCallback } from 'react'; export const useToolIsSelected = (tool: Tool) => { const canvasManager = useCanvasManager(); - const isSelected = useStore(computed(canvasManager.stateApi.$tool, (t) => t === tool)); + const isSelected = useStore(computed(canvasManager.tool.$tool, (t) => t === tool)); return isSelected; }; export const useSelectTool = (tool: Tool) => { const canvasManager = useCanvasManager(); const setTool = useCallback(() => { - canvasManager.stateApi.$tool.set(tool); - }, [canvasManager.stateApi.$tool, tool]); + canvasManager.tool.$tool.set(tool); + }, [canvasManager.tool.$tool, tool]); return setTool; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbarScale.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbarScale.tsx index cab46341ce0..c51cea1443a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbarScale.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbarScale.tsx @@ -73,7 +73,7 @@ const snapCandidates = marks.slice(1, marks.length - 1); export const CanvasToolbarScale = memo(() => { const { t } = useTranslation(); const canvasManager = useCanvasManager(); - const scale = useStore(computed(canvasManager.stateApi.$stageAttrs, (attrs) => attrs.scale)); + const scale = useStore(computed(canvasManager.stage.$stageAttrs, (attrs) => attrs.scale)); const [localScale, setLocalScale] = useState(scale * 100); const onChangeSlider = useCallback( diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts index 980cccc35c5..2f1848272d7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts @@ -60,7 +60,7 @@ export class CanvasBackgroundModule extends CanvasModuleBase { * - position * - size */ - this.subscriptions.add(this.manager.stateApi.$stageAttrs.listen(this.render)); + this.subscriptions.add(this.manager.stage.$stageAttrs.listen(this.render)); } /** diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts index 2c76d0cde9b..d2271c8e76b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts @@ -112,7 +112,7 @@ export class CanvasBboxModule extends CanvasModuleBase { this.konva.group.add(this.konva.transformer); // We will listen to the tool state to determine if the bbox should be visible or not. - this.subscriptions.add(this.manager.stateApi.$tool.listen(this.render)); + this.subscriptions.add(this.manager.tool.$tool.listen(this.render)); } /** @@ -122,7 +122,7 @@ export class CanvasBboxModule extends CanvasModuleBase { this.log.trace('Rendering'); const { x, y, width, height } = this.manager.stateApi.getBbox().rect; - const tool = this.manager.stateApi.$tool.get(); + const tool = this.manager.tool.$tool.get(); this.konva.group.visible(true); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushToolPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushToolPreview.ts index e363e748493..f12b6c9156a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushToolPreview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushToolPreview.ts @@ -88,7 +88,7 @@ export class CanvasBrushToolPreview extends CanvasModuleBase { } render = () => { - const cursorPos = this.manager.stateApi.$lastCursorPos.get(); + const cursorPos = this.manager.tool.$lastCursorPos.get(); // If the cursor position is not available, do not update the brush preview. The tool module will handle visiblity. if (!cursorPos) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasColorPickerToolPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasColorPickerToolPreview.ts index 9d6f71af14e..ef7956b0202 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasColorPickerToolPreview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasColorPickerToolPreview.ts @@ -192,7 +192,7 @@ export class CanvasColorPickerToolPreview extends CanvasModuleBase { * Renders the color picker tool preview on the canvas. */ render = () => { - const cursorPos = this.manager.stateApi.$lastCursorPos.get(); + const cursorPos = this.manager.tool.$lastCursorPos.get(); // If the cursor position is not available, do not render the preview. The tool module will handle visibility. if (!cursorPos) { @@ -200,7 +200,7 @@ export class CanvasColorPickerToolPreview extends CanvasModuleBase { } const toolState = this.manager.stateApi.getToolState(); - const colorUnderCursor = this.manager.stateApi.$colorUnderCursor.get(); + const colorUnderCursor = this.parent.$colorUnderCursor.get(); const colorPickerInnerRadius = this.manager.stage.getScaledPixels(this.config.RING_INNER_RADIUS); const colorPickerOuterRadius = this.manager.stage.getScaledPixels(this.config.RING_OUTER_RADIUS); const onePixel = this.manager.stage.getScaledPixels(1); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts index f21cdfa6996..0d9ebe66e05 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts @@ -175,7 +175,7 @@ export class CanvasEntityRenderer extends CanvasModuleBase { // user switches tool mid-drawing, for example by pressing space to pan the stage. It's easy to press space // to pan _before_ releasing the mouse button, which would cause the buffer to be lost if we didn't commit it. this.subscriptions.add( - this.manager.stateApi.$tool.listen(() => { + this.manager.tool.$tool.listen(() => { this.commitBuffer(); }) ); @@ -183,7 +183,7 @@ export class CanvasEntityRenderer extends CanvasModuleBase { // The compositing rect must cover the whole stage at all times. When the stage is scaled, moved or resized, we // need to update the compositing rect to match the stage. this.subscriptions.add( - this.manager.stateApi.$stageAttrs.listen(() => { + this.manager.stage.$stageAttrs.listen(() => { if (this.konva.compositing && this.parent.type === 'entity_mask_adapter') { this.updateCompositingRectSize(); } @@ -256,7 +256,7 @@ export class CanvasEntityRenderer extends CanvasModuleBase { this.log.trace('Updating compositing rect size'); assert(this.konva.compositing, 'Missing compositing rect'); - const { x, y, width, height, scale } = this.manager.stateApi.$stageAttrs.get(); + const { x, y, width, height, scale } = this.manager.stage.$stageAttrs.get(); this.konva.compositing.rect.setAttrs({ x: -x / scale, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts index ecc4128130f..351502f6d4e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts @@ -220,7 +220,7 @@ export class CanvasEntityTransformer extends CanvasModuleBase { // When the stage scale changes, we may need to re-scale some of the transformer's components. For example, // the bbox outline should always be 1 screen pixel wide, so we need to update its stroke width. this.subscriptions.add( - this.manager.stateApi.$stageAttrs.listen((newVal, oldVal) => { + this.manager.stage.$stageAttrs.listen((newVal, oldVal) => { if (newVal.scale !== oldVal.scale) { this.syncScale(); } @@ -236,7 +236,7 @@ export class CanvasEntityTransformer extends CanvasModuleBase { ); // When the selected tool changes, we need to update the transformer's interaction state. - this.subscriptions.add(this.manager.stateApi.$tool.listen(this.syncInteractionState)); + this.subscriptions.add(this.manager.tool.$tool.listen(this.syncInteractionState)); // When the selected entity changes, we need to update the transformer's interaction state. this.subscriptions.add(this.manager.stateApi.$selectedEntityIdentifier.listen(this.syncInteractionState)); @@ -511,7 +511,7 @@ export class CanvasEntityTransformer extends CanvasModuleBase { return; } - const tool = this.manager.stateApi.$tool.get(); + const tool = this.manager.tool.$tool.get(); const isSelected = this.manager.stateApi.getIsSelected(this.parent.id); if (!this.parent.renderer.hasObjects() || this.parent.state.isLocked || !this.parent.state.isEnabled) { @@ -565,12 +565,12 @@ export class CanvasEntityTransformer extends CanvasModuleBase { startTransform = () => { this.log.debug('Starting transform'); this.$isTransforming.set(true); - this.manager.stateApi.$tool.set('move'); + this.manager.tool.$tool.set('move'); // When transforming, we want the stage to still be movable if the view tool is selected. If the transformer or // interaction rect are listening, it will interrupt the stage's drag events. So we should disable listening // when the view tool is selected // TODO(psyche): We just set the tool to 'move', why would it be 'view'? Investigate and figure out if this is needed - const shouldListen = this.manager.stateApi.$tool.get() !== 'view'; + const shouldListen = this.manager.tool.$tool.get() !== 'view'; this.parent.konva.layer.listening(shouldListen); this.setInteractionMode('all'); this.manager.stateApi.$transformingAdapter.set(this.parent); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserToolPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserToolPreview.ts index 85eb916ea52..be9bbca8e7b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserToolPreview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserToolPreview.ts @@ -79,7 +79,7 @@ export class CanvasEraserToolPreview extends CanvasModuleBase { } render = () => { - const cursorPos = this.manager.stateApi.$lastCursorPos.get(); + const cursorPos = this.manager.tool.$lastCursorPos.get(); if (!cursorPos) { return; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts index 6a303f81ad0..50861dd2800 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts @@ -50,7 +50,7 @@ export class CanvasFilterModule extends CanvasModuleBase { return; } this.$adapter.set(entity.adapter); - this.manager.stateApi.$tool.set('view'); + this.manager.tool.$tool.set('view'); }; previewFilter = async () => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 343fa7fd65a..876e56a6cfe 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -111,16 +111,15 @@ export class CanvasManager extends CanvasModuleBase { }; this.stage.addLayer(this.konva.previewLayer); + this.tool = new CanvasToolModule(this); this.stagingArea = new CanvasStagingAreaModule(this); - this.konva.previewLayer.add(this.stagingArea.konva.group); - this.progressImage = new CanvasProgressImageModule(this); - this.konva.previewLayer.add(this.progressImage.konva.group); - this.bbox = new CanvasBboxModule(this); - this.konva.previewLayer.add(this.bbox.konva.group); - this.tool = new CanvasToolModule(this); + // Must add in this order for correct z-index + this.konva.previewLayer.add(this.stagingArea.konva.group); + this.konva.previewLayer.add(this.progressImage.konva.group); + this.konva.previewLayer.add(this.bbox.konva.group); this.konva.previewLayer.add(this.tool.konva.group); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts index 70916957dcd..a212655d8a4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts @@ -1,10 +1,17 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { getPrefixedId, getRectUnion } from 'features/controlLayers/konva/util'; -import type { CanvasEntityIdentifier, Coordinate, Dimensions, Rect } from 'features/controlLayers/store/types'; +import type { + CanvasEntityIdentifier, + Coordinate, + Dimensions, + Rect, + StageAttrs, +} from 'features/controlLayers/store/types'; import type Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; import { clamp } from 'lodash-es'; +import { atom } from 'nanostores'; import type { Logger } from 'roarr'; type CanvasStageModuleConfig = { @@ -42,6 +49,14 @@ export class CanvasStageModule extends CanvasModuleBase { config: CanvasStageModuleConfig = DEFAULT_CONFIG; + $stageAttrs = atom({ + x: 0, + y: 0, + width: 0, + height: 0, + scale: 0, + }); + subscriptions = new Set<() => void>(); constructor(stage: Konva.Stage, container: HTMLDivElement, manager: CanvasManager) { @@ -89,7 +104,7 @@ export class CanvasStageModule extends CanvasModuleBase { this.log.trace('Fitting stage to container'); this.konva.stage.width(this.konva.stage.container().offsetWidth); this.konva.stage.height(this.konva.stage.container().offsetHeight); - this.manager.stateApi.$stageAttrs.set({ + this.$stageAttrs.set({ x: this.konva.stage.x(), y: this.konva.stage.y(), width: this.konva.stage.width(), @@ -149,8 +164,8 @@ export class CanvasStageModule extends CanvasModuleBase { scaleY: scale, }); - this.manager.stateApi.$stageAttrs.set({ - ...this.manager.stateApi.$stageAttrs.get(), + this.$stageAttrs.set({ + ...this.$stageAttrs.get(), x, y, scale, @@ -203,7 +218,7 @@ export class CanvasStageModule extends CanvasModuleBase { scaleY: newScale, }); - this.manager.stateApi.$stageAttrs.set({ + this.$stageAttrs.set({ x: Math.floor(this.konva.stage.x()), y: Math.floor(this.konva.stage.y()), width: this.konva.stage.width(), @@ -235,7 +250,7 @@ export class CanvasStageModule extends CanvasModuleBase { return; } - this.manager.stateApi.$stageAttrs.set({ + this.$stageAttrs.set({ // Stage position should always be an integer, else we get fractional pixels which are blurry x: Math.floor(this.konva.stage.x()), y: Math.floor(this.konva.stage.y()), @@ -250,7 +265,7 @@ export class CanvasStageModule extends CanvasModuleBase { return; } - this.manager.stateApi.$stageAttrs.set({ + this.$stageAttrs.set({ // Stage position should always be an integer, else we get fractional pixels which are blurry x: Math.floor(this.konva.stage.x()), y: Math.floor(this.konva.stage.y()), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts index d6548d78764..41254580a55 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts @@ -4,6 +4,7 @@ import { CanvasObjectImageRenderer } from 'features/controlLayers/konva/CanvasOb import { getPrefixedId } from 'features/controlLayers/konva/util'; import { imageDTOToImageWithDims, type StagingAreaImage } from 'features/controlLayers/store/types'; import Konva from 'konva'; +import { atom } from 'nanostores'; import type { Logger } from 'roarr'; export class CanvasStagingAreaModule extends CanvasModuleBase { @@ -20,6 +21,8 @@ export class CanvasStagingAreaModule extends CanvasModuleBase { image: CanvasObjectImageRenderer | null; selectedImage: StagingAreaImage | null; + $shouldShowStagedImage = atom(true); + constructor(manager: CanvasManager) { super(); this.id = getPrefixedId(this.type); @@ -34,14 +37,14 @@ export class CanvasStagingAreaModule extends CanvasModuleBase { this.image = null; this.selectedImage = null; - this.subscriptions.add(this.manager.stateApi.$shouldShowStagedImage.listen(this.render)); + this.subscriptions.add(this.$shouldShowStagedImage.listen(this.render)); } render = async () => { this.log.trace('Rendering staging area'); const session = this.manager.stateApi.getSession(); const { x, y, width, height } = this.manager.stateApi.getBbox().rect; - const shouldShowStagedImage = this.manager.stateApi.$shouldShowStagedImage.get(); + const shouldShowStagedImage = this.$shouldShowStagedImage.get(); this.selectedImage = session.stagedImages[session.selectedStagedImageIndex] ?? null; this.konva.group.position({ x, y }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index 9a7519c23e4..c95f12c1fd3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -13,7 +13,6 @@ import { entityRasterized, entityRectAdded, entityReset, - entitySelected, } from 'features/controlLayers/store/canvasSlice'; import { selectAllRenderableEntities, selectCanvasSlice } from 'features/controlLayers/store/selectors'; import { @@ -28,7 +27,6 @@ import type { CanvasInpaintMaskState, CanvasRasterLayerState, CanvasRegionalGuidanceState, - Coordinate, EntityBrushLineAddedPayload, EntityEraserLineAddedPayload, EntityIdentifierPayload, @@ -37,9 +35,6 @@ import type { EntityRectAddedPayload, Rect, RgbaColor, - RgbColor, - StageAttrs, - Tool, } from 'features/controlLayers/store/types'; import { RGBA_BLACK } from 'features/controlLayers/store/types'; import type { WritableAtom } from 'nanostores'; @@ -84,6 +79,9 @@ export class CanvasStateApiModule extends CanvasModuleBase { manager: CanvasManager; log: Logger; + /** + * The redux store. + */ store: AppStore; constructor(store: AppStore, manager: CanvasManager) { @@ -99,43 +97,88 @@ export class CanvasStateApiModule extends CanvasModuleBase { this.store = store; } - // Reminder - use arrow functions to avoid binding issues + /** + * Gets the canvas slice. + * + * The state is stored in redux. + */ getCanvasState = () => { return selectCanvasSlice(this.store.getState()); }; + + /** + * Resets an entity, pushing state to redux. + */ resetEntity = (arg: EntityIdentifierPayload) => { this.store.dispatch(entityReset(arg)); }; + + /** + * Updates an entity's position, pushing state to redux. + */ setEntityPosition = (arg: EntityMovedPayload) => { this.store.dispatch(entityMoved(arg)); }; + + /** + * Adds a brush line to an entity, pushing state to redux. + */ addBrushLine = (arg: EntityBrushLineAddedPayload) => { this.store.dispatch(entityBrushLineAdded(arg)); }; + + /** + * Adds an eraser line to an entity, pushing state to redux. + */ addEraserLine = (arg: EntityEraserLineAddedPayload) => { this.store.dispatch(entityEraserLineAdded(arg)); }; + + /** + * Adds a rectangle to an entity, pushing state to redux. + */ addRect = (arg: EntityRectAddedPayload) => { this.store.dispatch(entityRectAdded(arg)); }; + + /** + * Rasterizes an entity, pushing state to redux. + */ rasterizeEntity = (arg: EntityRasterizedPayload) => { this.store.dispatch(entityRasterized(arg)); }; - setSelectedEntity = (arg: EntityIdentifierPayload) => { - this.store.dispatch(entitySelected(arg)); - }; - setGenerationBbox = (bbox: Rect) => { - this.store.dispatch(bboxChanged(bbox)); + + /** + * Sets the generation bbox rect, pushing state to redux. + */ + setGenerationBbox = (rect: Rect) => { + this.store.dispatch(bboxChanged(rect)); }; + + /** + * Sets the brush width, pushing state to redux. + */ setBrushWidth = (width: number) => { this.store.dispatch(brushWidthChanged(width)); }; + + /** + * Sets the eraser width, pushing state to redux. + */ setEraserWidth = (width: number) => { this.store.dispatch(eraserWidthChanged(width)); }; + + /** + * Sets the fill color, pushing state to redux. + */ setFill = (fill: RgbaColor) => { return this.store.dispatch(fillChanged(fill)); }; + + /** + * Enqueues a batch, pushing state to redux. + */ enqueueBatch = (batch: BatchConfig) => { this.store.dispatch( queueApi.endpoints.enqueueBatch.initiate(batch, { @@ -143,35 +186,76 @@ export class CanvasStateApiModule extends CanvasModuleBase { }) ); }; + + /** + * Gets the generation bbox state from redux. + */ getBbox = () => { return this.getCanvasState().bbox; }; + /** + * Gets the tool state from redux. + */ getToolState = () => { return this.store.getState().tool; }; + + /** + * Gets the canvas settings from redux. + */ getSettings = () => { return this.store.getState().canvasSettings; }; + + /** + * Gets the regions state from redux. + */ getRegionsState = () => { return this.getCanvasState().regions; }; + + /** + * Gets the raster layers state from redux. + */ getRasterLayersState = () => { return this.getCanvasState().rasterLayers; }; + + /** + * Gets the control layers state from redux. + */ getControlLayersState = () => { return this.getCanvasState().controlLayers; }; + + /** + * Gets the inpaint masks state from redux. + */ getInpaintMasksState = () => { return this.getCanvasState().inpaintMasks; }; + + /** + * Gets the canvas session state from redux. + */ getSession = () => { return this.store.getState().canvasSession; }; - getIsSelected = (id: string) => { + + /** + * Checks if an entity is selected. + */ + getIsSelected = (id: string): boolean => { return this.getCanvasState().selectedEntityIdentifier?.id === id; }; + /** + * Gets an entity by its identifier. The entity's state is retrieved from the redux store, and its adapter is + * retrieved from the canvas manager. + * + * Both state and adapter must exist for the entity to be returned. + */ getEntity(identifier: CanvasEntityIdentifier): EntityStateAndAdapter | null { const state = this.getCanvasState(); @@ -204,6 +288,9 @@ export class CanvasStateApiModule extends CanvasModuleBase { return null; } + /** + * Gets the number of entities that are currently rendered on the canvas. + */ getRenderedEntityCount = () => { const renderableEntities = selectAllRenderableEntities(this.getCanvasState()); let count = 0; @@ -215,6 +302,10 @@ export class CanvasStateApiModule extends CanvasModuleBase { return count; }; + /** + * Gets the currently selected entity, if any. The entity's state is retrieved from the redux store, and its adapter + * is retrieved from the canvas manager. + */ getSelectedEntity = () => { const state = this.getCanvasState(); if (state.selectedEntityIdentifier) { @@ -223,6 +314,16 @@ export class CanvasStateApiModule extends CanvasModuleBase { return null; }; + /** + * Gets the current fill color. The fill color is determined by the tool state and the selected entity. + * + * The fill color is determined by the tool state, except when the selected entity is a regional guidance or inpaint + * mask. In that case, the fill color is always black. + * + * Regional guidance and inpaint mask entities use a compositing rect to draw with their selected color and texture, + * so the fill color for lines and rects doesn't matter - it is never seen. The only requirement is that it is opaque. + * For consistency with conventional black and white mask images, we use black as the fill color for these entities. + */ getCurrentFill = () => { let currentFill: RgbaColor = this.getToolState().fill; const selectedEntity = this.getSelectedEntity(); @@ -235,45 +336,86 @@ export class CanvasStateApiModule extends CanvasModuleBase { return currentFill; }; + /** + * Gets the brush preview fill color. The brush preview fill color is determined by the tool state and the selected + * entity. + * + * The color is the tool state's fill color, except when the selected entity is a regional guidance or inpaint mask. + * + * These entities have their own fill color and texture, so the brush preview should use those instead of the tool + * state's fill color. + */ getBrushPreviewFill = (): RgbaColor => { const selectedEntity = this.getSelectedEntity(); if (selectedEntity?.state.type === 'regional_guidance' || selectedEntity?.state.type === 'inpaint_mask') { - // The brush should use the mask opacity for these enktity types + // TODO(psyche): If we move the brush preview's Konva nodes to the selected entity renderer, we can draw them + // under the entity's compositing rect, so they would use selected entity's selected color and texture. As a + // temporary workaround to improve the UX when using a brush on a regional guidance or inpaint mask, we use the + // selected entity's fill color with 50% opacity. return { ...selectedEntity.state.fill.color, a: 0.5 }; } else { return this.getToolState().fill; } }; + /** + * The entity adapter being transformed, if any. + */ $transformingAdapter = atom(null); + + /** + * Whether an entity is currently being transformed. Derived from `$transformingAdapter`. + */ $isTranforming = computed(this.$transformingAdapter, (transformingAdapter) => Boolean(transformingAdapter)); + /** + * A nanostores atom, kept in sync with the redux store's tool state. + */ $toolState: WritableAtom = atom(); + + /** + * The current fill color, derived from the tool state and the selected entity. + */ $currentFill: WritableAtom = atom(); + + /** + * The currently selected entity, if any. Includes the entity latest state and its adapter. + */ $selectedEntity: WritableAtom = atom(); + + /** + * The currently selected entity's identifier, if an entity is selected. + */ $selectedEntityIdentifier: WritableAtom = atom(); - $colorUnderCursor: WritableAtom = atom(RGBA_BLACK); - - // Read-write state, ephemeral interaction state - $tool = atom('brush'); - $toolBuffer = atom(null); - $isDrawing = atom(false); - $isMouseDown = atom(false); - $lastAddedPoint = atom(null); - $lastMouseDownPos = atom(null); - $lastCursorPos = atom(null); + + /** + * The last canvas progress event. This is set in a global event listener. The staging area may set it to null when it + * consumes the event. + */ $lastCanvasProgressEvent = $lastCanvasProgressEvent; + + /** + * Whether the space key is currently pressed. + */ $spaceKey = atom(false); + + /** + * Whether the alt key is currently pressed. + */ $altKey = $alt; + + /** + * Whether the ctrl key is currently pressed. + */ $ctrlKey = $ctrl; + + /** + * Whether the meta key is currently pressed. + */ $metaKey = $meta; + + /** + * Whether the shift key is currently pressed. + */ $shiftKey = $shift; - $shouldShowStagedImage = atom(true); - $stageAttrs = atom({ - x: 0, - y: 0, - width: 0, - height: 0, - scale: 0, - }); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts index 6f03b1580b6..6d8c7603080 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts @@ -22,9 +22,10 @@ import type { RgbColor, Tool, } from 'features/controlLayers/store/types'; -import { isDrawableEntity } from 'features/controlLayers/store/types'; +import { isDrawableEntity, RGBA_BLACK } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; +import { atom } from 'nanostores'; import type { Logger } from 'roarr'; type CanvasToolModuleConfig = { @@ -51,6 +52,36 @@ export class CanvasToolModule extends CanvasModuleBase { eraserToolPreview: CanvasEraserToolPreview; colorPickerToolPreview: CanvasColorPickerToolPreview; + /** + * The currently selected tool. + */ + $tool = atom('brush'); + /** + * A buffer for the currently selected tool. This is used to temporarily store the tool while the user is using any + * hold-to-activate tools, like the view or color picker tools. + */ + $toolBuffer = atom(null); + /** + * The last point added to the current entity. + */ + $lastAddedPoint = atom(null); + /** + * Whether the mouse is currently down. + */ + $isMouseDown = atom(false); + /** + * The last position where the mouse was down. + */ + $lastMouseDownPos = atom(null); + /** + * The last cursor position. + */ + $lastCursorPos = atom(null); + /** + * The color currently under the cursor. Only has a value when the color picker tool is active. + */ + $colorUnderCursor = atom(RGBA_BLACK); + konva: { stage: Konva.Stage; group: Konva.Group; @@ -79,7 +110,7 @@ export class CanvasToolModule extends CanvasModuleBase { this.konva.group.add(this.eraserToolPreview.konva.group); this.konva.group.add(this.colorPickerToolPreview.konva.group); - this.subscriptions.add(this.manager.stateApi.$stageAttrs.listen(this.render)); + this.subscriptions.add(this.manager.stage.$stageAttrs.listen(this.render)); this.subscriptions.add( this.manager.stateApi.$toolState.listen((value, oldValue) => { if ( @@ -92,7 +123,7 @@ export class CanvasToolModule extends CanvasModuleBase { } }) ); - this.subscriptions.add(this.manager.stateApi.$tool.listen(this.render)); + this.subscriptions.add(this.$tool.listen(this.render)); const cleanupListeners = this.setEventListeners(); @@ -109,8 +140,8 @@ export class CanvasToolModule extends CanvasModuleBase { const stage = this.manager.stage; const renderedEntityCount = this.manager.stateApi.getRenderedEntityCount(); const selectedEntity = this.manager.stateApi.getSelectedEntity(); - const isMouseDown = this.manager.stateApi.$isMouseDown.get(); - const tool = this.manager.stateApi.$tool.get(); + const isMouseDown = this.$isMouseDown.get(); + const tool = this.$tool.get(); const isDrawable = !!selectedEntity && @@ -153,8 +184,8 @@ export class CanvasToolModule extends CanvasModuleBase { const stage = this.manager.stage; const renderedEntityCount = this.manager.stateApi.getRenderedEntityCount(); const selectedEntity = this.manager.stateApi.getSelectedEntity(); - const cursorPos = this.manager.stateApi.$lastCursorPos.get(); - const tool = this.manager.stateApi.$tool.get(); + const cursorPos = this.$lastCursorPos.get(); + const tool = this.$tool.get(); const isDrawable = !!selectedEntity && @@ -187,7 +218,7 @@ export class CanvasToolModule extends CanvasModuleBase { syncLastCursorPos = (): Coordinate | null => { const pos = getScaledCursorPosition(this.konva.stage); - this.manager.stateApi.$lastCursorPos.set(pos); + this.$lastCursorPos.set(pos); return pos; }; @@ -268,16 +299,16 @@ export class CanvasToolModule extends CanvasModuleBase { }; onStageMouseDown = async (e: KonvaEventObject) => { - this.manager.stateApi.$isMouseDown.set(true); + this.$isMouseDown.set(true); const toolState = this.manager.stateApi.getToolState(); - const tool = this.manager.stateApi.$tool.get(); + const tool = this.$tool.get(); const pos = this.syncLastCursorPos(); const selectedEntity = this.manager.stateApi.getSelectedEntity(); if (tool === 'colorPicker') { const color = this.getColorUnderCursor(); if (color) { - this.manager.stateApi.$colorUnderCursor.set(color); + this.$colorUnderCursor.set(color); } if (color) { this.manager.stateApi.setFill({ ...toolState.fill, ...color }); @@ -286,7 +317,7 @@ export class CanvasToolModule extends CanvasModuleBase { } else { const isDrawable = selectedEntity?.state.isEnabled && !selectedEntity.state.isLocked; if (pos && isDrawable && !this.manager.stateApi.$spaceKey.get() && getIsPrimaryMouseDown(e)) { - this.manager.stateApi.$lastMouseDownPos.set(pos); + this.$lastMouseDownPos.set(pos); const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); if (tool === 'brush') { @@ -325,7 +356,7 @@ export class CanvasToolModule extends CanvasModuleBase { clip: this.getClip(selectedEntity.state), }); } - this.manager.stateApi.$lastAddedPoint.set(alignedPoint); + this.$lastAddedPoint.set(alignedPoint); } if (tool === 'eraser') { @@ -361,7 +392,7 @@ export class CanvasToolModule extends CanvasModuleBase { clip: this.getClip(selectedEntity.state), }); } - this.manager.stateApi.$lastAddedPoint.set(alignedPoint); + this.$lastAddedPoint.set(alignedPoint); } if (tool === 'rect') { @@ -380,11 +411,11 @@ export class CanvasToolModule extends CanvasModuleBase { }; onStageMouseUp = (_: KonvaEventObject) => { - this.manager.stateApi.$isMouseDown.set(false); - const pos = this.manager.stateApi.$lastCursorPos.get(); + this.$isMouseDown.set(false); + const pos = this.$lastCursorPos.get(); const selectedEntity = this.manager.stateApi.getSelectedEntity(); const isDrawable = selectedEntity?.state.isEnabled && !selectedEntity.state.isLocked; - const tool = this.manager.stateApi.$tool.get(); + const tool = this.$tool.get(); if (pos && isDrawable && !this.manager.stateApi.$spaceKey.get()) { if (tool === 'brush') { @@ -414,7 +445,7 @@ export class CanvasToolModule extends CanvasModuleBase { } } - this.manager.stateApi.$lastMouseDownPos.set(null); + this.$lastMouseDownPos.set(null); } this.render(); }; @@ -423,12 +454,12 @@ export class CanvasToolModule extends CanvasModuleBase { const toolState = this.manager.stateApi.getToolState(); const pos = this.syncLastCursorPos(); const selectedEntity = this.manager.stateApi.getSelectedEntity(); - const tool = this.manager.stateApi.$tool.get(); + const tool = this.$tool.get(); if (tool === 'colorPicker') { const color = this.getColorUnderCursor(); if (color) { - this.manager.stateApi.$colorUnderCursor.set(color); + this.$colorUnderCursor.set(color); } } else { const isDrawable = selectedEntity?.state.isEnabled && !selectedEntity.state.isLocked; @@ -446,7 +477,7 @@ export class CanvasToolModule extends CanvasModuleBase { if (lastPoint.x !== alignedPoint.x || lastPoint.y !== alignedPoint.y) { drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); - this.manager.stateApi.$lastAddedPoint.set(alignedPoint); + this.$lastAddedPoint.set(alignedPoint); } } } else { @@ -466,7 +497,7 @@ export class CanvasToolModule extends CanvasModuleBase { color: this.manager.stateApi.getCurrentFill(), clip: this.getClip(selectedEntity.state), }); - this.manager.stateApi.$lastAddedPoint.set(alignedPoint); + this.$lastAddedPoint.set(alignedPoint); } } @@ -483,7 +514,7 @@ export class CanvasToolModule extends CanvasModuleBase { if (lastPoint.x !== alignedPoint.x || lastPoint.y !== alignedPoint.y) { drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); - this.manager.stateApi.$lastAddedPoint.set(alignedPoint); + this.$lastAddedPoint.set(alignedPoint); } } } else { @@ -502,7 +533,7 @@ export class CanvasToolModule extends CanvasModuleBase { strokeWidth: toolState.eraser.width, clip: this.getClip(selectedEntity.state), }); - this.manager.stateApi.$lastAddedPoint.set(alignedPoint); + this.$lastAddedPoint.set(alignedPoint); } } @@ -527,12 +558,12 @@ export class CanvasToolModule extends CanvasModuleBase { onStageMouseLeave = async (e: KonvaEventObject) => { const pos = this.syncLastCursorPos(); - this.manager.stateApi.$lastCursorPos.set(null); - this.manager.stateApi.$lastMouseDownPos.set(null); + this.$lastCursorPos.set(null); + this.$lastMouseDownPos.set(null); const selectedEntity = this.manager.stateApi.getSelectedEntity(); const toolState = this.manager.stateApi.getToolState(); const isDrawable = selectedEntity?.state.isEnabled && !selectedEntity.state.isLocked; - const tool = this.manager.stateApi.$tool.get(); + const tool = this.$tool.get(); if (pos && isDrawable && !this.manager.stateApi.$spaceKey.get() && getIsPrimaryMouseDown(e)) { const drawingBuffer = selectedEntity.adapter.renderer.bufferState; @@ -566,7 +597,7 @@ export class CanvasToolModule extends CanvasModuleBase { } const toolState = this.manager.stateApi.getToolState(); - const tool = this.manager.stateApi.$tool.get(); + const tool = this.$tool.get(); let delta = e.evt.deltaY; @@ -596,19 +627,19 @@ export class CanvasToolModule extends CanvasModuleBase { const selectedEntity = this.manager.stateApi.getSelectedEntity(); if (selectedEntity) { selectedEntity.adapter.renderer.clearBuffer(); - this.manager.stateApi.$lastMouseDownPos.set(null); + this.$lastMouseDownPos.set(null); } } else if (e.key === ' ') { // Select the view tool on space key down - this.manager.stateApi.$toolBuffer.set(this.manager.stateApi.$tool.get()); - this.manager.stateApi.$tool.set('view'); + this.$toolBuffer.set(this.$tool.get()); + this.$tool.set('view'); this.manager.stateApi.$spaceKey.set(true); - this.manager.stateApi.$lastCursorPos.set(null); - this.manager.stateApi.$lastMouseDownPos.set(null); + this.$lastCursorPos.set(null); + this.$lastMouseDownPos.set(null); } else if (e.key === 'Alt') { // Select the color picker on alt key down - this.manager.stateApi.$toolBuffer.set(this.manager.stateApi.$tool.get()); - this.manager.stateApi.$tool.set('colorPicker'); + this.$toolBuffer.set(this.$tool.get()); + this.$tool.set('colorPicker'); } }; @@ -621,15 +652,15 @@ export class CanvasToolModule extends CanvasModuleBase { } if (e.key === ' ') { // Revert the tool to the previous tool on space key up - const toolBuffer = this.manager.stateApi.$toolBuffer.get(); - this.manager.stateApi.$tool.set(toolBuffer ?? 'move'); - this.manager.stateApi.$toolBuffer.set(null); + const toolBuffer = this.$toolBuffer.get(); + this.$tool.set(toolBuffer ?? 'move'); + this.$toolBuffer.set(null); this.manager.stateApi.$spaceKey.set(false); } else if (e.key === 'Alt') { // Revert the tool to the previous tool on alt key up - const toolBuffer = this.manager.stateApi.$toolBuffer.get(); - this.manager.stateApi.$tool.set(toolBuffer ?? 'move'); - this.manager.stateApi.$toolBuffer.set(null); + const toolBuffer = this.$toolBuffer.get(); + this.$tool.set(toolBuffer ?? 'move'); + this.$toolBuffer.set(null); } }; From fcacc0f70aa202a9536a2c664c5b7a52bdcd838a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 2 Sep 2024 22:07:28 +1000 Subject: [PATCH 616/678] chore: release v4.2.9.dev11 --- invokeai/version/invokeai_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/version/invokeai_version.py b/invokeai/version/invokeai_version.py index cb53c3abd9e..0ca3f061a3d 100644 --- a/invokeai/version/invokeai_version.py +++ b/invokeai/version/invokeai_version.py @@ -1 +1 @@ -__version__ = "4.2.9.dev10" +__version__ = "4.2.9.dev11" From 397038e6e81067707fe3bc5c21282e57864d2676 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 3 Sep 2024 10:15:24 +1000 Subject: [PATCH 617/678] tidy(ui): mark canvas module attrs readonly --- .../konva/CanvasBackgroundModule.ts | 11 ++++---- .../controlLayers/konva/CanvasBboxModule.ts | 11 ++++---- .../konva/CanvasBrushToolPreview.ts | 11 ++++---- .../controlLayers/konva/CanvasCacheModule.ts | 11 ++++---- .../konva/CanvasColorPickerToolPreview.ts | 11 ++++---- .../konva/CanvasCompositorModule.ts | 11 ++++---- .../konva/CanvasEntityLayerAdapter.ts | 11 ++++---- .../konva/CanvasEntityMaskAdapter.ts | 11 ++++---- .../konva/CanvasEntityRenderer.ts | 11 ++++---- .../konva/CanvasEntityTransformer.ts | 11 ++++---- .../konva/CanvasEraserToolPreview.ts | 11 ++++---- .../controlLayers/konva/CanvasFilterModule.ts | 11 ++++---- .../controlLayers/konva/CanvasManager.ts | 11 ++++---- .../controlLayers/konva/CanvasModuleBase.ts | 25 +++++++++++-------- .../konva/CanvasObjectBrushLineRenderer.ts | 11 ++++---- .../konva/CanvasObjectEraserLineRenderer.ts | 11 ++++---- .../konva/CanvasObjectImageRenderer.ts | 11 ++++---- .../konva/CanvasObjectRectRenderer.ts | 11 ++++---- .../konva/CanvasProgressImageModule.ts | 11 ++++---- .../konva/CanvasRenderingModule.ts | 11 ++++---- .../controlLayers/konva/CanvasStageModule.ts | 11 ++++---- .../konva/CanvasStagingAreaModule.ts | 11 ++++---- .../konva/CanvasStateApiModule.ts | 13 +++++----- .../controlLayers/konva/CanvasToolModule.ts | 11 ++++---- .../controlLayers/konva/CanvasWorkerModule.ts | 11 ++++---- 25 files changed, 135 insertions(+), 156 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts index 2f1848272d7..dbcf6cb8c1b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts @@ -22,12 +22,11 @@ const DEFAULT_CONFIG: CanvasBackgroundModuleConfig = { */ export class CanvasBackgroundModule extends CanvasModuleBase { readonly type = 'background'; - - id: string; - path: string[]; - parent: CanvasManager; - manager: CanvasManager; - log: Logger; + readonly id: string; + readonly path: string[]; + readonly parent: CanvasManager; + readonly manager: CanvasManager; + readonly log: Logger; subscriptions = new Set<() => void>(); config: CanvasBackgroundModuleConfig = DEFAULT_CONFIG; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts index d2271c8e76b..cd6fedf5d7e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts @@ -26,12 +26,11 @@ const NO_ANCHORS: string[] = []; */ export class CanvasBboxModule extends CanvasModuleBase { readonly type = 'bbox'; - - id: string; - path: string[]; - parent: CanvasManager; - manager: CanvasManager; - log: Logger; + readonly id: string; + readonly path: string[]; + readonly parent: CanvasManager; + readonly manager: CanvasManager; + readonly log: Logger; subscriptions: Set<() => void> = new Set(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushToolPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushToolPreview.ts index f12b6c9156a..476a62f6cc5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushToolPreview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushToolPreview.ts @@ -27,12 +27,11 @@ const DEFAULT_CONFIG: BrushToolPreviewConfig = { */ export class CanvasBrushToolPreview extends CanvasModuleBase { readonly type = 'brush_tool_preview'; - - id: string; - path: string[]; - parent: CanvasToolModule; - manager: CanvasManager; - log: Logger; + readonly id: string; + readonly path: string[]; + readonly parent: CanvasToolModule; + readonly manager: CanvasManager; + readonly log: Logger; config: BrushToolPreviewConfig = DEFAULT_CONFIG; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCacheModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCacheModule.ts index d69cf9a3029..858417326a6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCacheModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCacheModule.ts @@ -32,12 +32,11 @@ const DEFAULT_CONFIG: CanvasCacheModuleConfig = { */ export class CanvasCacheModule extends CanvasModuleBase { readonly type = 'cache'; - - id: string; - path: string[]; - log: Logger; - parent: CanvasManager; - manager: CanvasManager; + readonly id: string; + readonly path: string[]; + readonly log: Logger; + readonly parent: CanvasManager; + readonly manager: CanvasManager; config: CanvasCacheModuleConfig = DEFAULT_CONFIG; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasColorPickerToolPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasColorPickerToolPreview.ts index ef7956b0202..d9c043e4f84 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasColorPickerToolPreview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasColorPickerToolPreview.ts @@ -68,12 +68,11 @@ const DEFAULT_CONFIG: ColorPickerToolConfig = { */ export class CanvasColorPickerToolPreview extends CanvasModuleBase { readonly type = 'color_picker_tool_preview'; - - id: string; - path: string[]; - parent: CanvasToolModule; - manager: CanvasManager; - log: Logger; + readonly id: string; + readonly path: string[]; + readonly parent: CanvasToolModule; + readonly manager: CanvasManager; + readonly log: Logger; config: ColorPickerToolConfig = DEFAULT_CONFIG; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts index fb0207177ee..eba298d519b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts @@ -23,12 +23,11 @@ import { assert } from 'tsafe'; */ export class CanvasCompositorModule extends CanvasModuleBase { readonly type = 'compositor'; - - id: string; - path: string[]; - log: Logger; - parent: CanvasManager; - manager: CanvasManager; + readonly id: string; + readonly path: string[]; + readonly log: Logger; + readonly parent: CanvasManager; + readonly manager: CanvasManager; constructor(manager: CanvasManager) { super(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityLayerAdapter.ts index 87dc7f207b0..9d5301315b2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityLayerAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityLayerAdapter.ts @@ -33,12 +33,11 @@ import { assert } from 'tsafe'; */ export class CanvasEntityLayerAdapter extends CanvasModuleBase { readonly type = 'entity_layer_adapter'; - - id: string; - path: string[]; - manager: CanvasManager; - parent: CanvasManager; - log: Logger; + readonly id: string; + readonly path: string[]; + readonly manager: CanvasManager; + readonly parent: CanvasManager; + readonly log: Logger; /** * The last known state of the entity. diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityMaskAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityMaskAdapter.ts index d5e9f3bd169..b9b7f467233 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityMaskAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityMaskAdapter.ts @@ -23,12 +23,11 @@ import stableHash from 'stable-hash'; export class CanvasEntityMaskAdapter extends CanvasModuleBase { readonly type = 'entity_mask_adapter'; - - id: string; - path: string[]; - parent: CanvasManager; - manager: CanvasManager; - log: Logger; + readonly id: string; + readonly path: string[]; + readonly parent: CanvasManager; + readonly manager: CanvasManager; + readonly log: Logger; state: CanvasInpaintMaskState | CanvasRegionalGuidanceState; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts index 0d9ebe66e05..f03ab1f527a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts @@ -62,12 +62,11 @@ type AnyObjectState = CanvasBrushLineState | CanvasEraserLineState | CanvasImage */ export class CanvasEntityRenderer extends CanvasModuleBase { readonly type = 'entity_renderer'; - - id: string; - path: string[]; - parent: CanvasEntityLayerAdapter | CanvasEntityMaskAdapter; - manager: CanvasManager; - log: Logger; + readonly id: string; + readonly path: string[]; + readonly parent: CanvasEntityLayerAdapter | CanvasEntityMaskAdapter; + readonly manager: CanvasManager; + readonly log: Logger; /** * A set of subscriptions that should be cleaned up when the transformer is destroyed. diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts index 351502f6d4e..3e8b1959563 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts @@ -78,12 +78,11 @@ const DEFAULT_CONFIG: CanvasEntityTransformerConfig = { export class CanvasEntityTransformer extends CanvasModuleBase { readonly type = 'entity_transformer'; - - id: string; - path: string[]; - parent: CanvasEntityLayerAdapter | CanvasEntityMaskAdapter; - manager: CanvasManager; - log: Logger; + readonly id: string; + readonly path: string[]; + readonly parent: CanvasEntityLayerAdapter | CanvasEntityMaskAdapter; + readonly manager: CanvasManager; + readonly log: Logger; config: CanvasEntityTransformerConfig = DEFAULT_CONFIG; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserToolPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserToolPreview.ts index be9bbca8e7b..6fa3376cf49 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserToolPreview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserToolPreview.ts @@ -23,12 +23,11 @@ const DEFAULT_CONFIG: EraserToolPreviewConfig = { export class CanvasEraserToolPreview extends CanvasModuleBase { readonly type = 'eraser_tool_preview'; - - id: string; - path: string[]; - parent: CanvasToolModule; - manager: CanvasManager; - log: Logger; + readonly id: string; + readonly path: string[]; + readonly parent: CanvasToolModule; + readonly manager: CanvasManager; + readonly log: Logger; config: EraserToolPreviewConfig = DEFAULT_CONFIG; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts index 50861dd2800..19155dfaba9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts @@ -13,12 +13,11 @@ import { assert } from 'tsafe'; export class CanvasFilterModule extends CanvasModuleBase { readonly type = 'canvas_filter'; - - id: string; - path: string[]; - parent: CanvasManager; - manager: CanvasManager; - log: Logger; + readonly id: string; + readonly path: string[]; + readonly parent: CanvasManager; + readonly manager: CanvasManager; + readonly log: Logger; imageState: CanvasImageState | null = null; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 876e56a6cfe..ab82d29949a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -28,12 +28,11 @@ export const $canvasManager = atom(null); export class CanvasManager extends CanvasModuleBase { readonly type = 'manager'; - - id: string; - path: string[]; - manager: CanvasManager; - parent: CanvasManager; - log: Logger; + readonly id: string; + readonly path: string[]; + readonly manager: CanvasManager; + readonly parent: CanvasManager; + readonly log: Logger; store: AppStore; socket: AppSocket; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasModuleBase.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasModuleBase.ts index aecd72f7725..f86a983ae2d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasModuleBase.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasModuleBase.ts @@ -2,7 +2,14 @@ import type { SerializableObject } from 'common/types'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { Logger } from 'roarr'; +/** + * Base class for all canvas modules. + */ export abstract class CanvasModuleBase { + /** + * The type of the module. + */ + abstract readonly type: string; /** * The unique identifier of the module. * @@ -15,11 +22,7 @@ export abstract class CanvasModuleBase { * // this.id -> "raster_layer:aS2NREsrlz" * ``` */ - abstract id: string; - /** - * The type of the module. - */ - abstract type: string; + abstract readonly id: string; /** * The path of the module in the canvas module tree. * @@ -31,15 +34,15 @@ export abstract class CanvasModuleBase { * // this.path -> ["manager:3PWJWmHbou", "raster_layer:aS2NREsrlz", "entity_renderer:sfLO4j1B0n", "brush_line:Zrsu8gpZMd"] * ``` */ - abstract path: string[]; + abstract readonly path: string[]; /** - * The canvas manager. + * The parent module. This may be the canvas manager or another module. */ - abstract manager: CanvasManager; + abstract readonly parent: CanvasModuleBase; /** - * The parent module. This may be the canvas manager or another module. + * The canvas manager. */ - abstract parent: CanvasModuleBase; + abstract readonly manager: CanvasManager; /** * The logger for the module. The logger must be a `ROARR` logger. * @@ -50,7 +53,7 @@ export abstract class CanvasModuleBase { * this.log = this.manager.buildLogger(this); * ``` */ - abstract log: Logger; + abstract readonly log: Logger; /** * Returns a logging context object that includes relevant information about the module. diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectBrushLineRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectBrushLineRenderer.ts index 87898128890..0d831b9112e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectBrushLineRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectBrushLineRenderer.ts @@ -9,12 +9,11 @@ import type { Logger } from 'roarr'; export class CanvasObjectBrushLineRenderer extends CanvasModuleBase { readonly type = 'object_brush_line_renderer'; - - id: string; - path: string[]; - parent: CanvasEntityRenderer; - manager: CanvasManager; - log: Logger; + readonly id: string; + readonly path: string[]; + readonly parent: CanvasEntityRenderer; + readonly manager: CanvasManager; + readonly log: Logger; state: CanvasBrushLineState; konva: { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectEraserLineRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectEraserLineRenderer.ts index 2cd16597792..5eb6ee4d9d8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectEraserLineRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectEraserLineRenderer.ts @@ -8,12 +8,11 @@ import type { Logger } from 'roarr'; export class CanvasObjectEraserLineRenderer extends CanvasModuleBase { readonly type = 'object_eraser_line_renderer'; - - id: string; - path: string[]; - parent: CanvasEntityRenderer; - manager: CanvasManager; - log: Logger; + readonly id: string; + readonly path: string[]; + readonly parent: CanvasEntityRenderer; + readonly manager: CanvasManager; + readonly log: Logger; state: CanvasEraserLineState; konva: { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectImageRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectImageRenderer.ts index 0410128fe61..3c4a6c6e5d3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectImageRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectImageRenderer.ts @@ -14,12 +14,11 @@ import { getImageDTO } from 'services/api/endpoints/images'; export class CanvasObjectImageRenderer extends CanvasModuleBase { readonly type = 'object_image_renderer'; - - id: string; - path: string[]; - parent: CanvasEntityRenderer | CanvasStagingAreaModule | CanvasFilterModule; - manager: CanvasManager; - log: Logger; + readonly id: string; + readonly path: string[]; + readonly parent: CanvasEntityRenderer | CanvasStagingAreaModule | CanvasFilterModule; + readonly manager: CanvasManager; + readonly log: Logger; state: CanvasImageState; konva: { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRectRenderer.ts index c044d40d750..68ee51000f8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRectRenderer.ts @@ -9,12 +9,11 @@ import type { Logger } from 'roarr'; export class CanvasObjectRectRenderer extends CanvasModuleBase { readonly type = 'object_rect_renderer'; - - id: string; - path: string[]; - parent: CanvasEntityRenderer; - manager: CanvasManager; - log: Logger; + readonly id: string; + readonly path: string[]; + readonly parent: CanvasEntityRenderer; + readonly manager: CanvasManager; + readonly log: Logger; state: CanvasRectState; konva: { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImageModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImageModule.ts index 53e37be119c..72e57721ca5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImageModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImageModule.ts @@ -8,12 +8,11 @@ import type { S } from 'services/api/types'; export class CanvasProgressImageModule extends CanvasModuleBase { readonly type = 'progress_image'; - - id: string; - path: string[]; - parent: CanvasManager; - manager: CanvasManager; - log: Logger; + readonly id: string; + readonly path: string[]; + readonly parent: CanvasManager; + readonly manager: CanvasManager; + readonly log: Logger; progressImageId: string | null = null; konva: { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts index 0e3084f2300..28de08889b6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts @@ -10,12 +10,11 @@ import type { Logger } from 'roarr'; export class CanvasRenderingModule extends CanvasModuleBase { readonly type = 'canvas_renderer'; - - id: string; - path: string[]; - log: Logger; - parent: CanvasManager; - manager: CanvasManager; + readonly id: string; + readonly path: string[]; + readonly log: Logger; + readonly parent: CanvasManager; + readonly manager: CanvasManager; state: CanvasState | null = null; settings: CanvasSettingsState | null = null; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts index a212655d8a4..9daae1b5ff1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts @@ -37,12 +37,11 @@ const DEFAULT_CONFIG: CanvasStageModuleConfig = { export class CanvasStageModule extends CanvasModuleBase { readonly type = 'stage'; - - id: string; - path: string[]; - parent: CanvasManager; - manager: CanvasManager; - log: Logger; + readonly id: string; + readonly path: string[]; + readonly parent: CanvasManager; + readonly manager: CanvasManager; + readonly log: Logger; container: HTMLDivElement; konva: { stage: Konva.Stage }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts index 41254580a55..2446cc3f13c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts @@ -9,12 +9,11 @@ import type { Logger } from 'roarr'; export class CanvasStagingAreaModule extends CanvasModuleBase { readonly type = 'staging_area'; - - id: string; - path: string[]; - parent: CanvasManager; - manager: CanvasManager; - log: Logger; + readonly id: string; + readonly path: string[]; + readonly parent: CanvasManager; + readonly manager: CanvasManager; + readonly log: Logger; subscriptions: Set<() => void> = new Set(); konva: { group: Konva.Group }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index c95f12c1fd3..829da941690 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -46,7 +46,7 @@ import { $lastCanvasProgressEvent } from 'services/events/setEventListeners'; type EntityStateAndAdapter = | { - id: string; + id: string; type: CanvasRasterLayerState['type']; state: CanvasRasterLayerState; adapter: CanvasEntityLayerAdapter; @@ -72,12 +72,11 @@ type EntityStateAndAdapter = export class CanvasStateApiModule extends CanvasModuleBase { readonly type = 'state_api'; - - id: string; - path: string[]; - parent: CanvasManager; - manager: CanvasManager; - log: Logger; + readonly id: string; + readonly path: string[]; + readonly parent: CanvasManager; + readonly manager: CanvasManager; + readonly log: Logger; /** * The redux store. diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts index 6d8c7603080..a3a6ed834ef 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts @@ -38,12 +38,11 @@ const DEFAULT_CONFIG: CanvasToolModuleConfig = { export class CanvasToolModule extends CanvasModuleBase { readonly type = 'tool'; - - id: string; - path: string[]; - parent: CanvasManager; - manager: CanvasManager; - log: Logger; + readonly id: string; + readonly path: string[]; + readonly parent: CanvasManager; + readonly manager: CanvasManager; + readonly log: Logger; subscriptions: Set<() => void> = new Set(); config: CanvasToolModuleConfig = DEFAULT_CONFIG; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasWorkerModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasWorkerModule.ts index b2f89fb0f49..1d454a0aaca 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasWorkerModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasWorkerModule.ts @@ -6,12 +6,11 @@ import type { Logger } from 'roarr'; export class CanvasWorkerModule extends CanvasModuleBase { readonly type = 'worker'; - - id: string; - path: string[]; - log: Logger; - parent: CanvasManager; - manager: CanvasManager; + readonly id: string; + readonly path: string[]; + readonly log: Logger; + readonly parent: CanvasManager; + readonly manager: CanvasManager; worker: Worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module', name: 'worker' }); tasks: Map void }> = new Map(); From ff12f163430971e5f9a57ddf5c47fe6a6f53b15d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 3 Sep 2024 10:16:00 +1000 Subject: [PATCH 618/678] feat(ui): update default logging context path to be string --- .../src/features/controlLayers/konva/CanvasModuleBase.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasModuleBase.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasModuleBase.ts index f86a983ae2d..84296d7c085 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasModuleBase.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasModuleBase.ts @@ -60,14 +60,14 @@ export abstract class CanvasModuleBase { * Canvas modules may override this method to include additional information in the logging context, but should * always include the parent's logging context. * - * The default implementation includes the parent context and the module's path. + * The default implementation includes the parent context and the module's path as a string. * * @example * ```ts * getLoggingContext = () => { * return { * ...this.parent.getLoggingContext(), - * path: this.path, + * path: this.path.join(' > '), * someImportantValue: this.someImportantValue, * }; * }; @@ -76,7 +76,7 @@ export abstract class CanvasModuleBase { getLoggingContext: () => SerializableObject = () => { return { ...this.parent.getLoggingContext(), - path: this.path, + path: this.path.join(' > '), }; }; From bb6b5c49412786688979ded511d39d7f445f7639 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 3 Sep 2024 10:24:09 +1000 Subject: [PATCH 619/678] feat(ui): better types on CanvasStateApiModule.getEntity --- .../konva/CanvasStateApiModule.ts | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index 829da941690..fecd9f4296d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -46,7 +46,7 @@ import { $lastCanvasProgressEvent } from 'services/events/setEventListeners'; type EntityStateAndAdapter = | { - id: string; + id: string; type: CanvasRasterLayerState['type']; state: CanvasRasterLayerState; adapter: CanvasEntityLayerAdapter; @@ -255,24 +255,32 @@ export class CanvasStateApiModule extends CanvasModuleBase { * * Both state and adapter must exist for the entity to be returned. */ - getEntity(identifier: CanvasEntityIdentifier): EntityStateAndAdapter | null { + getEntity({ + id, + type, + }: T): Extract | null { const state = this.getCanvasState(); - let entityState: EntityStateAndAdapter['state'] | null = null; - let entityAdapter: EntityStateAndAdapter['adapter'] | null = null; - - if (identifier.type === 'raster_layer') { - entityState = state.rasterLayers.entities.find((i) => i.id === identifier.id) ?? null; - entityAdapter = this.manager.adapters.rasterLayers.get(identifier.id) ?? null; - } else if (identifier.type === 'control_layer') { - entityState = state.controlLayers.entities.find((i) => i.id === identifier.id) ?? null; - entityAdapter = this.manager.adapters.controlLayers.get(identifier.id) ?? null; - } else if (identifier.type === 'regional_guidance') { - entityState = state.regions.entities.find((i) => i.id === identifier.id) ?? null; - entityAdapter = this.manager.adapters.regionMasks.get(identifier.id) ?? null; - } else if (identifier.type === 'inpaint_mask') { - entityState = state.inpaintMasks.entities.find((i) => i.id === identifier.id) ?? null; - entityAdapter = this.manager.adapters.inpaintMasks.get(identifier.id) ?? null; + let entityState: EntityStateAndAdapter['state'] | undefined = undefined; + let entityAdapter: EntityStateAndAdapter['adapter'] | undefined = undefined; + + switch (type) { + case 'raster_layer': + entityState = state.rasterLayers.entities.find((i) => i.id === id); + entityAdapter = this.manager.adapters.rasterLayers.get(id); + break; + case 'control_layer': + entityState = state.controlLayers.entities.find((i) => i.id === id); + entityAdapter = this.manager.adapters.controlLayers.get(id); + break; + case 'regional_guidance': + entityState = state.regions.entities.find((i) => i.id === id); + entityAdapter = this.manager.adapters.regionMasks.get(id); + break; + case 'inpaint_mask': + entityState = state.inpaintMasks.entities.find((i) => i.id === id); + entityAdapter = this.manager.adapters.inpaintMasks.get(id); + break; } if (entityState && entityAdapter) { @@ -281,7 +289,7 @@ export class CanvasStateApiModule extends CanvasModuleBase { type: entityState.type, state: entityState, adapter: entityAdapter, - } as EntityStateAndAdapter; // TODO(psyche): make TS happy w/o this cast + } as Extract; // TODO(psyche): make TS happy w/o this cast } return null; From 5074c97683441a7272e9dffc0d975050a4f2792e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 3 Sep 2024 11:44:16 +1000 Subject: [PATCH 620/678] feat(ui): tool buttons are only disabled when currently selected --- .../components/Tool/ToolBboxButton.tsx | 18 +++---------- .../components/Tool/ToolBrushButton.tsx | 23 ++++------------- .../components/Tool/ToolColorPickerButton.tsx | 25 ++++--------------- .../components/Tool/ToolEraserButton.tsx | 22 ++++------------ .../components/Tool/ToolMoveButton.tsx | 22 ++++------------ .../components/Tool/ToolRectButton.tsx | 23 ++++------------- .../components/Tool/ToolViewButton.tsx | 20 ++++----------- 7 files changed, 34 insertions(+), 119 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBboxButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBboxButton.tsx index 10817f71c84..b47f9de4296 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBboxButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBboxButton.tsx @@ -1,10 +1,6 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks'; -import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering'; -import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming'; -import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice'; -import { memo, useMemo } from 'react'; +import { memo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiBoundingBoxBold } from 'react-icons/pi'; @@ -13,14 +9,8 @@ export const ToolBboxButton = memo(() => { const { t } = useTranslation(); const selectBbox = useSelectTool('bbox'); const isSelected = useToolIsSelected('bbox'); - const isFiltering = useIsFiltering(); - const isTransforming = useIsTransforming(); - const isStaging = useAppSelector(selectIsStaging); - const isDisabled = useMemo(() => { - return isTransforming || isFiltering || isStaging; - }, [isFiltering, isStaging, isTransforming]); - useHotkeys('c', selectBbox, { enabled: !isDisabled || isSelected }, [selectBbox, isSelected, isDisabled]); + useHotkeys('c', selectBbox, { enabled: !isSelected }, [selectBbox, isSelected]); return ( { tooltip={`${t('controlLayers.tool.bbox')} (C)`} icon={} colorScheme={isSelected ? 'invokeBlue' : 'base'} - variant="outline" + variant="solid" onClick={selectBbox} - isDisabled={isDisabled} + isDisabled={isSelected} /> ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushButton.tsx index 1a998fd3b88..d6d62932827 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushButton.tsx @@ -1,29 +1,16 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks'; -import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering'; -import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming'; -import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice'; -import { selectIsSelectedEntityDrawable } from 'features/controlLayers/store/selectors'; -import { memo, useMemo } from 'react'; +import { memo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiPaintBrushBold } from 'react-icons/pi'; export const ToolBrushButton = memo(() => { const { t } = useTranslation(); - const isFiltering = useIsFiltering(); - const isTransforming = useIsTransforming(); - const isStaging = useAppSelector(selectIsStaging); - const selectBrush = useSelectTool('brush'); const isSelected = useToolIsSelected('brush'); - const isDrawingToolAllowed = useAppSelector(selectIsSelectedEntityDrawable); - - const isDisabled = useMemo(() => { - return isTransforming || isFiltering || isStaging || !isDrawingToolAllowed; - }, [isDrawingToolAllowed, isFiltering, isStaging, isTransforming]); + const selectBrush = useSelectTool('brush'); - useHotkeys('b', selectBrush, { enabled: !isDisabled || isSelected }, [isDisabled, isSelected, selectBrush]); + useHotkeys('b', selectBrush, { enabled: !isSelected }, [isSelected, selectBrush]); return ( { tooltip={`${t('controlLayers.tool.brush')} (B)`} icon={} colorScheme={isSelected ? 'invokeBlue' : 'base'} - variant="outline" + variant="solid" onClick={selectBrush} - isDisabled={isDisabled} + isDisabled={isSelected} /> ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolColorPickerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolColorPickerButton.tsx index d418409ffb9..d74dea968b1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolColorPickerButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolColorPickerButton.tsx @@ -1,31 +1,16 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks'; -import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering'; -import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming'; -import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice'; -import { memo, useMemo } from 'react'; +import { memo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiEyedropperBold } from 'react-icons/pi'; export const ToolColorPickerButton = memo(() => { const { t } = useTranslation(); - const isFiltering = useIsFiltering(); - const isTransforming = useIsTransforming(); - const selectColorPicker = useSelectTool('colorPicker'); const isSelected = useToolIsSelected('colorPicker'); - const isStaging = useAppSelector(selectIsStaging); - - const isDisabled = useMemo(() => { - return isTransforming || isFiltering || isStaging; - }, [isFiltering, isStaging, isTransforming]); + const selectColorPicker = useSelectTool('colorPicker'); - useHotkeys('i', selectColorPicker, { enabled: !isDisabled || isSelected }, [ - selectColorPicker, - isSelected, - isDisabled, - ]); + useHotkeys('i', selectColorPicker, { enabled: !isSelected }, [selectColorPicker, isSelected]); return ( { tooltip={`${t('controlLayers.tool.colorPicker')} (I)`} icon={} colorScheme={isSelected ? 'invokeBlue' : 'base'} - variant="outline" + variant="solid" onClick={selectColorPicker} - isDisabled={isDisabled} + isDisabled={isSelected} /> ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserButton.tsx index be3004e7b50..95d8081bdd4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserButton.tsx @@ -1,28 +1,16 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks'; -import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering'; -import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming'; -import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice'; -import { selectIsSelectedEntityDrawable } from 'features/controlLayers/store/selectors'; -import { memo, useMemo } from 'react'; +import { memo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiEraserBold } from 'react-icons/pi'; export const ToolEraserButton = memo(() => { const { t } = useTranslation(); - const isFiltering = useIsFiltering(); - const isTransforming = useIsTransforming(); - const isStaging = useAppSelector(selectIsStaging); - const selectEraser = useSelectTool('eraser'); const isSelected = useToolIsSelected('eraser'); - const isDrawingToolAllowed = useAppSelector(selectIsSelectedEntityDrawable); - const isDisabled = useMemo(() => { - return isTransforming || isFiltering || isStaging || !isDrawingToolAllowed; - }, [isDrawingToolAllowed, isFiltering, isStaging, isTransforming]); + const selectEraser = useSelectTool('eraser'); - useHotkeys('e', selectEraser, { enabled: !isDisabled || isSelected }, [isDisabled, isSelected, selectEraser]); + useHotkeys('e', selectEraser, { enabled: !isSelected }, [isSelected, selectEraser]); return ( { tooltip={`${t('controlLayers.tool.eraser')} (E)`} icon={} colorScheme={isSelected ? 'invokeBlue' : 'base'} - variant="outline" + variant="solid" onClick={selectEraser} - isDisabled={isDisabled} + isDisabled={isSelected} /> ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolMoveButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolMoveButton.tsx index 4c1340e15f6..2506cb03da3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolMoveButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolMoveButton.tsx @@ -1,28 +1,16 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks'; -import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering'; -import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming'; -import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice'; -import { selectIsSelectedEntityDrawable } from 'features/controlLayers/store/selectors'; -import { memo, useMemo } from 'react'; +import { memo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiCursorBold } from 'react-icons/pi'; export const ToolMoveButton = memo(() => { const { t } = useTranslation(); - const isFiltering = useIsFiltering(); - const isTransforming = useIsTransforming(); - const selectMove = useSelectTool('move'); const isSelected = useToolIsSelected('move'); - const isStaging = useAppSelector(selectIsStaging); - const isDrawingToolAllowed = useAppSelector(selectIsSelectedEntityDrawable); - const isDisabled = useMemo(() => { - return isTransforming || isFiltering || isStaging || !isDrawingToolAllowed; - }, [isDrawingToolAllowed, isFiltering, isStaging, isTransforming]); + const selectMove = useSelectTool('move'); - useHotkeys('v', selectMove, { enabled: !isDisabled || isSelected }, [isDisabled, isSelected, selectMove]); + useHotkeys('v', selectMove, { enabled: !isSelected }, [isSelected, selectMove]); return ( { tooltip={`${t('controlLayers.tool.move')} (V)`} icon={} colorScheme={isSelected ? 'invokeBlue' : 'base'} - variant="outline" + variant="solid" onClick={selectMove} - isDisabled={isDisabled} + isDisabled={isSelected} /> ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolRectButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolRectButton.tsx index 801d603b40e..6b58a3526e7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolRectButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolRectButton.tsx @@ -1,29 +1,16 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks'; -import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering'; -import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming'; -import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice'; -import { selectIsSelectedEntityDrawable } from 'features/controlLayers/store/selectors'; -import { memo, useMemo } from 'react'; +import { memo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiRectangleBold } from 'react-icons/pi'; export const ToolRectButton = memo(() => { const { t } = useTranslation(); - const selectRect = useSelectTool('rect'); const isSelected = useToolIsSelected('rect'); - const isFiltering = useIsFiltering(); - const isTransforming = useIsTransforming(); - const isStaging = useAppSelector(selectIsStaging); - const isDrawingToolAllowed = useAppSelector(selectIsSelectedEntityDrawable); - - const isDisabled = useMemo(() => { - return isTransforming || isFiltering || isStaging || !isDrawingToolAllowed; - }, [isDrawingToolAllowed, isFiltering, isStaging, isTransforming]); + const selectRect = useSelectTool('rect'); - useHotkeys('u', selectRect, { enabled: !isDisabled || isSelected }, [isDisabled, isSelected, selectRect]); + useHotkeys('u', selectRect, { enabled: !isSelected }, [isSelected, selectRect]); return ( { tooltip={`${t('controlLayers.tool.rectangle')} (U)`} icon={} colorScheme={isSelected ? 'invokeBlue' : 'base'} - variant="outline" + variant="solid" onClick={selectRect} - isDisabled={isDisabled} + isDisabled={isSelected} /> ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolViewButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolViewButton.tsx index 44ffb635c0e..1479bd61247 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolViewButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolViewButton.tsx @@ -1,26 +1,16 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks'; -import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering'; -import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming'; -import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice'; -import { memo, useMemo } from 'react'; +import { memo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiHandBold } from 'react-icons/pi'; export const ToolViewButton = memo(() => { const { t } = useTranslation(); - const isTransforming = useIsTransforming(); - const isFiltering = useIsFiltering(); - const isStaging = useAppSelector(selectIsStaging); - const selectView = useSelectTool('view'); const isSelected = useToolIsSelected('view'); - const isDisabled = useMemo(() => { - return isTransforming || isFiltering || isStaging; - }, [isFiltering, isStaging, isTransforming]); + const selectView = useSelectTool('view'); - useHotkeys('h', selectView, { enabled: !isDisabled || isSelected }, [selectView, isSelected, isDisabled]); + useHotkeys('h', selectView, { enabled: !isSelected }, [selectView, isSelected]); return ( { tooltip={`${t('controlLayers.tool.view')} (H)`} icon={} colorScheme={isSelected ? 'invokeBlue' : 'base'} - variant="outline" + variant="solid" onClick={selectView} - isDisabled={isDisabled} + isDisabled={isSelected} /> ); }); From f7e68ac6d6e3c2d6c5beba42a6cd150ab62a4870 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 3 Sep 2024 14:26:23 +1000 Subject: [PATCH 621/678] feat(ui): clean up tool preview rendering --- .../controlLayers/konva/CanvasToolModule.ts | 507 ++++++++++-------- .../src/features/controlLayers/konva/util.ts | 13 +- 2 files changed, 284 insertions(+), 236 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts index a3a6ed834ef..86c57ef5757 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts @@ -6,12 +6,13 @@ import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase' import { alignCoordForTool, calculateNewBrushSizeFromWheelDelta, + floorCoord, getIsPrimaryMouseDown, getLastPointOfLine, getPrefixedId, getScaledCursorPosition, + isDistanceMoreThanMin, offsetCoord, - validateCandidatePoint, } from 'features/controlLayers/konva/util'; import type { CanvasControlLayerState, @@ -122,7 +123,13 @@ export class CanvasToolModule extends CanvasModuleBase { } }) ); - this.subscriptions.add(this.$tool.listen(this.render)); + this.subscriptions.add( + this.$tool.listen(() => { + // On tool switch, reset mouse state + this.manager.tool.$isMouseDown.set(false); + this.render(); + }) + ); const cleanupListeners = this.setEventListeners(); @@ -279,6 +286,7 @@ export class CanvasToolModule extends CanvasModuleBase { window.addEventListener('keydown', this.onKeyDown); window.addEventListener('keyup', this.onKeyUp); + window.addEventListener('pointerup', this.onWindowPointerUp); return () => { this.konva.stage.off('mouseenter', this.onStageMouseEnter); @@ -290,136 +298,196 @@ export class CanvasToolModule extends CanvasModuleBase { this.konva.stage.off('wheel', this.onStageMouseWheel); window.removeEventListener('keydown', this.onKeyDown); window.removeEventListener('keyup', this.onKeyUp); + window.removeEventListener('pointerup', this.onWindowPointerUp); }; }; - onStageMouseEnter = (_: KonvaEventObject) => { - this.render(); + onStageMouseEnter = async (_: KonvaEventObject) => { + const cursorPos = this.syncLastCursorPos(); + try { + const isMouseDown = this.$isMouseDown.get(); + const toolState = this.manager.stateApi.getToolState(); + const tool = this.$tool.get(); + const selectedEntity = this.manager.stateApi.getSelectedEntity(); + + if (!cursorPos || !isMouseDown || !selectedEntity?.state.isEnabled || selectedEntity.state.isLocked) { + return; + } + + if (selectedEntity.adapter.renderer.bufferState?.type !== 'rect') { + selectedEntity.adapter.renderer.commitBuffer(); + return; + } + + if (tool === 'brush') { + const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position); + const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); + await selectedEntity.adapter.renderer.setBuffer({ + id: getPrefixedId('brush_line'), + type: 'brush_line', + points: [alignedPoint.x, alignedPoint.y], + strokeWidth: toolState.brush.width, + color: this.manager.stateApi.getCurrentFill(), + clip: this.getClip(selectedEntity.state), + }); + this.$lastAddedPoint.set(alignedPoint); + return; + } + + if (tool === 'eraser') { + const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position); + const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); + if (selectedEntity.adapter.renderer.bufferState) { + selectedEntity.adapter.renderer.commitBuffer(); + } + await selectedEntity.adapter.renderer.setBuffer({ + id: getPrefixedId('eraser_line'), + type: 'eraser_line', + points: [alignedPoint.x, alignedPoint.y], + strokeWidth: toolState.eraser.width, + clip: this.getClip(selectedEntity.state), + }); + this.$lastAddedPoint.set(alignedPoint); + return; + } + } finally { + this.render(); + } }; onStageMouseDown = async (e: KonvaEventObject) => { - this.$isMouseDown.set(true); - const toolState = this.manager.stateApi.getToolState(); - const tool = this.$tool.get(); - const pos = this.syncLastCursorPos(); - const selectedEntity = this.manager.stateApi.getSelectedEntity(); + this.$isMouseDown.set(getIsPrimaryMouseDown(e)); + const cursorPos = this.syncLastCursorPos(); - if (tool === 'colorPicker') { - const color = this.getColorUnderCursor(); - if (color) { - this.$colorUnderCursor.set(color); + try { + const tool = this.$tool.get(); + const toolState = this.manager.stateApi.getToolState(); + + if (tool === 'colorPicker') { + const color = this.getColorUnderCursor(); + if (color) { + this.manager.stateApi.setFill({ ...toolState.fill, ...color }); + } + return; } - if (color) { - this.manager.stateApi.setFill({ ...toolState.fill, ...color }); + + const isMouseDown = this.$isMouseDown.get(); + const selectedEntity = this.manager.stateApi.getSelectedEntity(); + + if (!cursorPos || !isMouseDown || !selectedEntity?.state.isEnabled || selectedEntity?.state.isLocked) { + return; } - this.render(); - } else { - const isDrawable = selectedEntity?.state.isEnabled && !selectedEntity.state.isLocked; - if (pos && isDrawable && !this.manager.stateApi.$spaceKey.get() && getIsPrimaryMouseDown(e)) { - this.$lastMouseDownPos.set(pos); - const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); - - if (tool === 'brush') { - const lastLinePoint = selectedEntity.adapter.getLastPointOfLastLine('brush_line'); - const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); - if (e.evt.shiftKey && lastLinePoint) { - // Create a straight line from the last line point - if (selectedEntity.adapter.renderer.bufferState) { - selectedEntity.adapter.renderer.commitBuffer(); - } - - await selectedEntity.adapter.renderer.setBuffer({ - id: getPrefixedId('brush_line'), - type: 'brush_line', - points: [ - // The last point of the last line is already normalized to the entity's coordinates - lastLinePoint.x, - lastLinePoint.y, - alignedPoint.x, - alignedPoint.y, - ], - strokeWidth: toolState.brush.width, - color: this.manager.stateApi.getCurrentFill(), - clip: this.getClip(selectedEntity.state), - }); - } else { - if (selectedEntity.adapter.renderer.bufferState) { - selectedEntity.adapter.renderer.commitBuffer(); - } - await selectedEntity.adapter.renderer.setBuffer({ - id: getPrefixedId('brush_line'), - type: 'brush_line', - points: [alignedPoint.x, alignedPoint.y], - strokeWidth: toolState.brush.width, - color: this.manager.stateApi.getCurrentFill(), - clip: this.getClip(selectedEntity.state), - }); + + const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position); + + if (tool === 'brush') { + const lastLinePoint = selectedEntity.adapter.getLastPointOfLastLine('brush_line'); + const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); + if (e.evt.shiftKey && lastLinePoint) { + // Create a straight line from the last line point + if (selectedEntity.adapter.renderer.bufferState) { + selectedEntity.adapter.renderer.commitBuffer(); } - this.$lastAddedPoint.set(alignedPoint); - } - if (tool === 'eraser') { - const lastLinePoint = selectedEntity.adapter.getLastPointOfLastLine('eraser_line'); - const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); - if (e.evt.shiftKey && lastLinePoint) { - // Create a straight line from the last line point - if (selectedEntity.adapter.renderer.bufferState) { - selectedEntity.adapter.renderer.commitBuffer(); - } - await selectedEntity.adapter.renderer.setBuffer({ - id: getPrefixedId('eraser_line'), - type: 'eraser_line', - points: [ - // The last point of the last line is already normalized to the entity's coordinates - lastLinePoint.x, - lastLinePoint.y, - alignedPoint.x, - alignedPoint.y, - ], - strokeWidth: toolState.eraser.width, - clip: this.getClip(selectedEntity.state), - }); - } else { - if (selectedEntity.adapter.renderer.bufferState) { - selectedEntity.adapter.renderer.commitBuffer(); - } - await selectedEntity.adapter.renderer.setBuffer({ - id: getPrefixedId('eraser_line'), - type: 'eraser_line', - points: [alignedPoint.x, alignedPoint.y], - strokeWidth: toolState.eraser.width, - clip: this.getClip(selectedEntity.state), - }); + await selectedEntity.adapter.renderer.setBuffer({ + id: getPrefixedId('brush_line'), + type: 'brush_line', + points: [ + // The last point of the last line is already normalized to the entity's coordinates + lastLinePoint.x, + lastLinePoint.y, + alignedPoint.x, + alignedPoint.y, + ], + strokeWidth: toolState.brush.width, + color: this.manager.stateApi.getCurrentFill(), + clip: this.getClip(selectedEntity.state), + }); + } else { + if (selectedEntity.adapter.renderer.bufferState) { + selectedEntity.adapter.renderer.commitBuffer(); } - this.$lastAddedPoint.set(alignedPoint); + await selectedEntity.adapter.renderer.setBuffer({ + id: getPrefixedId('brush_line'), + type: 'brush_line', + points: [alignedPoint.x, alignedPoint.y], + strokeWidth: toolState.brush.width, + color: this.manager.stateApi.getCurrentFill(), + clip: this.getClip(selectedEntity.state), + }); } + this.$lastAddedPoint.set(alignedPoint); + } - if (tool === 'rect') { + if (tool === 'eraser') { + const lastLinePoint = selectedEntity.adapter.getLastPointOfLastLine('eraser_line'); + const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); + if (e.evt.shiftKey && lastLinePoint) { + // Create a straight line from the last line point if (selectedEntity.adapter.renderer.bufferState) { selectedEntity.adapter.renderer.commitBuffer(); } await selectedEntity.adapter.renderer.setBuffer({ - id: getPrefixedId('rect'), - type: 'rect', - rect: { x: Math.round(normalizedPoint.x), y: Math.round(normalizedPoint.y), width: 0, height: 0 }, - color: this.manager.stateApi.getCurrentFill(), + id: getPrefixedId('eraser_line'), + type: 'eraser_line', + points: [ + // The last point of the last line is already normalized to the entity's coordinates + lastLinePoint.x, + lastLinePoint.y, + alignedPoint.x, + alignedPoint.y, + ], + strokeWidth: toolState.eraser.width, + clip: this.getClip(selectedEntity.state), + }); + } else { + if (selectedEntity.adapter.renderer.bufferState) { + selectedEntity.adapter.renderer.commitBuffer(); + } + await selectedEntity.adapter.renderer.setBuffer({ + id: getPrefixedId('eraser_line'), + type: 'eraser_line', + points: [alignedPoint.x, alignedPoint.y], + strokeWidth: toolState.eraser.width, + clip: this.getClip(selectedEntity.state), }); } + this.$lastAddedPoint.set(alignedPoint); + } + + if (tool === 'rect') { + if (selectedEntity.adapter.renderer.bufferState) { + selectedEntity.adapter.renderer.commitBuffer(); + } + await selectedEntity.adapter.renderer.setBuffer({ + id: getPrefixedId('rect'), + type: 'rect', + rect: { x: Math.round(normalizedPoint.x), y: Math.round(normalizedPoint.y), width: 0, height: 0 }, + color: this.manager.stateApi.getCurrentFill(), + }); } + } finally { + this.$lastMouseDownPos.set(cursorPos); + this.render(); } }; onStageMouseUp = (_: KonvaEventObject) => { - this.$isMouseDown.set(false); - const pos = this.$lastCursorPos.get(); - const selectedEntity = this.manager.stateApi.getSelectedEntity(); - const isDrawable = selectedEntity?.state.isEnabled && !selectedEntity.state.isLocked; - const tool = this.$tool.get(); + try { + this.$isMouseDown.set(false); + const cursorPos = this.syncLastCursorPos(); + if (!cursorPos) { + return; + } + const selectedEntity = this.manager.stateApi.getSelectedEntity(); + const isDrawable = selectedEntity?.state.isEnabled && !selectedEntity.state.isLocked; + if (!isDrawable) { + return; + } + const tool = this.$tool.get(); - if (pos && isDrawable && !this.manager.stateApi.$spaceKey.get()) { if (tool === 'brush') { - const drawingBuffer = selectedEntity.adapter.renderer.bufferState; - if (drawingBuffer?.type === 'brush_line') { + if (selectedEntity.adapter.renderer.bufferState?.type === 'brush_line') { selectedEntity.adapter.renderer.commitBuffer(); } else { selectedEntity.adapter.renderer.clearBuffer(); @@ -427,8 +495,7 @@ export class CanvasToolModule extends CanvasModuleBase { } if (tool === 'eraser') { - const drawingBuffer = selectedEntity.adapter.renderer.bufferState; - if (drawingBuffer?.type === 'eraser_line') { + if (selectedEntity.adapter.renderer.bufferState?.type === 'eraser_line') { selectedEntity.adapter.renderer.commitBuffer(); } else { selectedEntity.adapter.renderer.clearBuffer(); @@ -436,153 +503,104 @@ export class CanvasToolModule extends CanvasModuleBase { } if (tool === 'rect') { - const drawingBuffer = selectedEntity.adapter.renderer.bufferState; - if (drawingBuffer?.type === 'rect') { + if (selectedEntity.adapter.renderer.bufferState?.type === 'rect') { selectedEntity.adapter.renderer.commitBuffer(); } else { selectedEntity.adapter.renderer.clearBuffer(); } } - + } finally { this.$lastMouseDownPos.set(null); + this.render(); } - this.render(); }; - onStageMouseMove = async (e: KonvaEventObject) => { - const toolState = this.manager.stateApi.getToolState(); - const pos = this.syncLastCursorPos(); - const selectedEntity = this.manager.stateApi.getSelectedEntity(); - const tool = this.$tool.get(); + onStageMouseMove = async (_: KonvaEventObject) => { + try { + const tool = this.$tool.get(); + const cursorPos = this.syncLastCursorPos(); - if (tool === 'colorPicker') { - const color = this.getColorUnderCursor(); - if (color) { - this.$colorUnderCursor.set(color); + if (tool === 'colorPicker') { + const color = this.getColorUnderCursor(); + if (color) { + this.$colorUnderCursor.set(color); + } + return; } - } else { - const isDrawable = selectedEntity?.state.isEnabled && !selectedEntity.state.isLocked; - if (pos && isDrawable && !this.manager.stateApi.$spaceKey.get() && getIsPrimaryMouseDown(e)) { - if (tool === 'brush') { - const drawingBuffer = selectedEntity.adapter.renderer.bufferState; - if (drawingBuffer) { - if (drawingBuffer.type === 'brush_line') { - const lastPoint = getLastPointOfLine(drawingBuffer.points); - const minDistance = toolState.brush.width * this.config.BRUSH_SPACING_TARGET_SCALE; - if (lastPoint && validateCandidatePoint(pos, lastPoint, minDistance)) { - const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); - const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); - // Do not add duplicate points - if (lastPoint.x !== alignedPoint.x || lastPoint.y !== alignedPoint.y) { - drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); - await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); - this.$lastAddedPoint.set(alignedPoint); - } - } - } else { - selectedEntity.adapter.renderer.clearBuffer(); - } - } else { - if (selectedEntity.adapter.renderer.bufferState) { - selectedEntity.adapter.renderer.commitBuffer(); - } - const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); - const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); - await selectedEntity.adapter.renderer.setBuffer({ - id: getPrefixedId('brush_line'), - type: 'brush_line', - points: [alignedPoint.x, alignedPoint.y], - strokeWidth: toolState.brush.width, - color: this.manager.stateApi.getCurrentFill(), - clip: this.getClip(selectedEntity.state), - }); - this.$lastAddedPoint.set(alignedPoint); - } + + const isMouseDown = this.$isMouseDown.get(); + const selectedEntity = this.manager.stateApi.getSelectedEntity(); + const isDrawable = selectedEntity?.state.isEnabled && !selectedEntity.state.isLocked && cursorPos && isMouseDown; + + if (!isDrawable) { + return; + } + + const bufferState = selectedEntity.adapter.renderer.bufferState; + + if (!bufferState) { + return; + } + + const toolState = this.manager.stateApi.getToolState(); + + if (tool === 'brush' && bufferState.type === 'brush_line') { + const lastPoint = getLastPointOfLine(bufferState.points); + const minDistance = toolState.brush.width * this.config.BRUSH_SPACING_TARGET_SCALE; + if (!lastPoint || !isDistanceMoreThanMin(cursorPos, lastPoint, minDistance)) { + return; } - if (tool === 'eraser') { - const drawingBuffer = selectedEntity.adapter.renderer.bufferState; - if (drawingBuffer) { - if (drawingBuffer.type === 'eraser_line') { - const lastPoint = getLastPointOfLine(drawingBuffer.points); - const minDistance = toolState.eraser.width * this.config.BRUSH_SPACING_TARGET_SCALE; - if (lastPoint && validateCandidatePoint(pos, lastPoint, minDistance)) { - const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); - const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); - // Do not add duplicate points - if (lastPoint.x !== alignedPoint.x || lastPoint.y !== alignedPoint.y) { - drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); - await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); - this.$lastAddedPoint.set(alignedPoint); - } - } - } else { - selectedEntity.adapter.renderer.clearBuffer(); - } - } else { - if (selectedEntity.adapter.renderer.bufferState) { - selectedEntity.adapter.renderer.commitBuffer(); - } - const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); - const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); - await selectedEntity.adapter.renderer.setBuffer({ - id: getPrefixedId('eraser_line'), - type: 'eraser_line', - points: [alignedPoint.x, alignedPoint.y], - strokeWidth: toolState.eraser.width, - clip: this.getClip(selectedEntity.state), - }); - this.$lastAddedPoint.set(alignedPoint); - } + const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position); + const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); + + if (lastPoint.x === alignedPoint.x && lastPoint.y === alignedPoint.y) { + // Do not add duplicate points + return; } - if (tool === 'rect') { - const drawingBuffer = selectedEntity.adapter.renderer.bufferState; - if (drawingBuffer) { - if (drawingBuffer.type === 'rect') { - const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); - drawingBuffer.rect.width = Math.round(normalizedPoint.x - drawingBuffer.rect.x); - drawingBuffer.rect.height = Math.round(normalizedPoint.y - drawingBuffer.rect.y); - await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); - } else { - selectedEntity.adapter.renderer.clearBuffer(); - } - } + bufferState.points.push(alignedPoint.x, alignedPoint.y); + await selectedEntity.adapter.renderer.setBuffer(bufferState); + this.$lastAddedPoint.set(alignedPoint); + } else if (tool === 'eraser' && bufferState.type === 'eraser_line') { + const lastPoint = getLastPointOfLine(bufferState.points); + const minDistance = toolState.eraser.width * this.config.BRUSH_SPACING_TARGET_SCALE; + if (!lastPoint || !isDistanceMoreThanMin(cursorPos, lastPoint, minDistance)) { + return; + } + + const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position); + const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); + + if (lastPoint.x === alignedPoint.x && lastPoint.y === alignedPoint.y) { + // Do not add duplicate points + return; } + + bufferState.points.push(alignedPoint.x, alignedPoint.y); + await selectedEntity.adapter.renderer.setBuffer(bufferState); + this.$lastAddedPoint.set(alignedPoint); + } else if (tool === 'rect' && bufferState.type === 'rect') { + const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position); + const alignedPoint = floorCoord(normalizedPoint); + bufferState.rect.width = Math.round(alignedPoint.x - bufferState.rect.x); + bufferState.rect.height = Math.round(alignedPoint.y - bufferState.rect.y); + await selectedEntity.adapter.renderer.setBuffer(bufferState); + } else { + selectedEntity?.adapter.renderer.clearBuffer(); } + } finally { + this.render(); } - - this.render(); }; - onStageMouseLeave = async (e: KonvaEventObject) => { - const pos = this.syncLastCursorPos(); + onStageMouseLeave = (_: KonvaEventObject) => { this.$lastCursorPos.set(null); this.$lastMouseDownPos.set(null); const selectedEntity = this.manager.stateApi.getSelectedEntity(); - const toolState = this.manager.stateApi.getToolState(); - const isDrawable = selectedEntity?.state.isEnabled && !selectedEntity.state.isLocked; - const tool = this.$tool.get(); - if (pos && isDrawable && !this.manager.stateApi.$spaceKey.get() && getIsPrimaryMouseDown(e)) { - const drawingBuffer = selectedEntity.adapter.renderer.bufferState; - const normalizedPoint = offsetCoord(pos, selectedEntity.state.position); - if (tool === 'brush' && drawingBuffer?.type === 'brush_line') { - const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); - drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); - await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); - selectedEntity.adapter.renderer.commitBuffer(); - } else if (tool === 'eraser' && drawingBuffer?.type === 'eraser_line') { - const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); - drawingBuffer.points.push(alignedPoint.x, alignedPoint.y); - await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); - selectedEntity.adapter.renderer.commitBuffer(); - } else if (tool === 'rect' && drawingBuffer?.type === 'rect') { - drawingBuffer.rect.width = Math.round(normalizedPoint.x - drawingBuffer.rect.x); - drawingBuffer.rect.height = Math.round(normalizedPoint.y - drawingBuffer.rect.y); - await selectedEntity.adapter.renderer.setBuffer(drawingBuffer); - selectedEntity.adapter.renderer.commitBuffer(); - } + if (selectedEntity && selectedEntity.adapter.renderer.bufferState?.type !== 'rect') { + selectedEntity.adapter.renderer.commitBuffer(); } this.render(); @@ -614,13 +632,24 @@ export class CanvasToolModule extends CanvasModuleBase { this.render(); }; + onWindowPointerUp = () => { + this.$isMouseDown.set(false); + const selectedEntity = this.manager.stateApi.getSelectedEntity(); + + if (selectedEntity && selectedEntity.adapter.renderer.hasBuffer()) { + selectedEntity.adapter.renderer.commitBuffer(); + } + }; + onKeyDown = (e: KeyboardEvent) => { if (e.repeat) { return; } + if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { return; } + if (e.key === 'Escape') { // Cancel shape drawing on escape const selectedEntity = this.manager.stateApi.getSelectedEntity(); @@ -628,14 +657,20 @@ export class CanvasToolModule extends CanvasModuleBase { selectedEntity.adapter.renderer.clearBuffer(); this.$lastMouseDownPos.set(null); } - } else if (e.key === ' ') { + return; + } + + if (e.key === ' ') { // Select the view tool on space key down this.$toolBuffer.set(this.$tool.get()); this.$tool.set('view'); this.manager.stateApi.$spaceKey.set(true); this.$lastCursorPos.set(null); this.$lastMouseDownPos.set(null); - } else if (e.key === 'Alt') { + return; + } + + if (e.key === 'Alt') { // Select the color picker on alt key down this.$toolBuffer.set(this.$tool.get()); this.$tool.set('colorPicker'); @@ -646,20 +681,26 @@ export class CanvasToolModule extends CanvasModuleBase { if (e.repeat) { return; } + if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { return; } + if (e.key === ' ') { // Revert the tool to the previous tool on space key up const toolBuffer = this.$toolBuffer.get(); this.$tool.set(toolBuffer ?? 'move'); this.$toolBuffer.set(null); this.manager.stateApi.$spaceKey.set(false); - } else if (e.key === 'Alt') { + return; + } + + if (e.key === 'Alt') { // Revert the tool to the previous tool on alt key up const toolBuffer = this.$toolBuffer.get(); this.$tool.set(toolBuffer ?? 'move'); this.$toolBuffer.set(null); + return; } }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index ba9063f317c..fae74faf2d9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -96,6 +96,13 @@ export const snapPosToStage = (pos: Vector2d, stage: Konva.Stage, snapPx = 10): return snappedPos; }; +export const floorCoord = (coord: Coordinate): Coordinate => { + return { + x: Math.floor(coord.x), + y: Math.floor(coord.y), + }; +}; + /** * Snaps a position to the edge of the given rect if within a threshold of the edge * @param pos The position to snap @@ -170,13 +177,13 @@ export const calculateNewBrushSizeFromWheelDelta = (brushSize: number, delta: nu }; /** - * Validates a candidate point by checking if it is at least `minDistance` away from the last point. + * Checks if a candidate point is at least `minDistance` away from the last point. If there is no last point, returns true. * @param candidatePoint The candidate point * @param lastPoint The last point * @param minDistance The minimum distance between points - * @returns + * @returns Whether the candidate point is at least `minDistance` away from the last point */ -export const validateCandidatePoint = ( +export const isDistanceMoreThanMin = ( candidatePoint: Coordinate, lastPoint: Coordinate | null, minDistance: number From 6161c71aa7db5540af7c4f277d45b2dec1bf6a0a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 3 Sep 2024 15:04:44 +1000 Subject: [PATCH 622/678] build(ui): add csstype dev dependency --- invokeai/frontend/web/package.json | 1 + invokeai/frontend/web/pnpm-lock.yaml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 4e98e287705..bb51844708c 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -136,6 +136,7 @@ "@vitest/coverage-v8": "^1.5.0", "@vitest/ui": "^1.5.0", "concurrently": "^8.2.2", + "csstype": "^3.1.3", "dpdm": "^3.14.0", "eslint": "^8.57.0", "eslint-plugin-i18next": "^6.0.9", diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index 2dad5456f4e..10519c0623c 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -238,6 +238,9 @@ devDependencies: concurrently: specifier: ^8.2.2 version: 8.2.2 + csstype: + specifier: ^3.1.3 + version: 3.1.3 dpdm: specifier: ^3.14.0 version: 3.14.0 From 7c29caa245d9f65662e7ff09f2036e343980d4fa Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 3 Sep 2024 18:29:14 +1000 Subject: [PATCH 623/678] tidy(ui): merge tool slice, `sendToCanvas` into settings slice --- .../listeners/enqueueRequestedLinear.ts | 4 +- invokeai/frontend/web/src/app/store/store.ts | 3 - .../components/CanvasSendToToggle.tsx | 14 +++- .../CanvasSettingsClipToBboxCheckbox.tsx | 4 +- .../CanvasSettingsInvertScrollCheckbox.tsx | 15 ++-- .../components/Tool/ToolBrushWidth.tsx | 6 +- .../components/Tool/ToolEraserWidth.tsx | 6 +- .../components/Tool/ToolFillColorPicker.tsx | 12 +-- .../components/Toolbar/CanvasToolbar.tsx | 4 +- .../konva/CanvasBrushToolPreview.ts | 8 +- .../konva/CanvasColorPickerToolPreview.ts | 4 +- .../konva/CanvasEraserToolPreview.ts | 6 +- .../controlLayers/konva/CanvasManager.ts | 4 +- .../konva/CanvasRenderingModule.ts | 4 +- .../konva/CanvasStateApiModule.ts | 65 +++++++-------- .../controlLayers/konva/CanvasToolModule.ts | 62 +++++++------- .../controlLayers/store/canvasSessionSlice.ts | 10 --- .../store/canvasSettingsSlice.ts | 83 ++++++++++++++++--- .../features/controlLayers/store/toolSlice.ts | 56 ------------- .../nodes/util/graph/generation/addInpaint.ts | 10 +-- .../util/graph/generation/addOutpaint.ts | 10 +-- .../util/graph/generation/buildSD1Graph.ts | 4 +- .../util/graph/generation/buildSDXLGraph.ts | 4 +- 23 files changed, 196 insertions(+), 202 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/store/toolSlice.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index afd9489bc5a..00cf9031882 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -31,7 +31,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) let didStartStaging = false; - if (!state.canvasSession.isStaging && state.canvasSession.sendToCanvas) { + if (!state.canvasSession.isStaging && state.canvasSettings.sendToCanvas) { dispatch(sessionStartedStaging()); didStartStaging = true; } @@ -70,7 +70,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) const { g, noise, posCond } = buildGraphResult.value; - const destination = state.canvasSession.sendToCanvas ? 'canvas' : 'gallery'; + const destination = state.canvasSettings.sendToCanvas ? 'canvas' : 'gallery'; const prepareBatchResult = withResult(() => prepareLinearUIBatch(state, g, prepend, noise, posCond, 'generation', destination) diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 2acdf584fd0..ebd541cb919 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -11,7 +11,6 @@ import { canvasSettingsPersistConfig, canvasSettingsSlice } from 'features/contr import { canvasPersistConfig, canvasSlice, canvasUndoableConfig } from 'features/controlLayers/store/canvasSlice'; import { lorasPersistConfig, lorasSlice } from 'features/controlLayers/store/lorasSlice'; import { paramsPersistConfig, paramsSlice } from 'features/controlLayers/store/paramsSlice'; -import { toolPersistConfig, toolSlice } from 'features/controlLayers/store/toolSlice'; import { deleteImageModalSlice } from 'features/deleteImageModal/store/slice'; import { dynamicPromptsPersistConfig, dynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/gallerySlice'; @@ -63,7 +62,6 @@ const allReducers = { [upscaleSlice.name]: upscaleSlice.reducer, [stylePresetSlice.name]: stylePresetSlice.reducer, [paramsSlice.name]: paramsSlice.reducer, - [toolSlice.name]: toolSlice.reducer, [canvasSettingsSlice.name]: canvasSettingsSlice.reducer, [canvasSessionSlice.name]: canvasSessionSlice.reducer, [lorasSlice.name]: lorasSlice.reducer, @@ -109,7 +107,6 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = { [upscalePersistConfig.name]: upscalePersistConfig, [stylePresetPersistConfig.name]: stylePresetPersistConfig, [paramsPersistConfig.name]: paramsPersistConfig, - [toolPersistConfig.name]: toolPersistConfig, [canvasSettingsPersistConfig.name]: canvasSettingsPersistConfig, [canvasSessionPersistConfig.name]: canvasSessionPersistConfig, [lorasPersistConfig.name]: lorasPersistConfig, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasSendToToggle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasSendToToggle.tsx index 4d572beccb0..ad6311ffb2f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasSendToToggle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasSendToToggle.tsx @@ -1,7 +1,11 @@ import { Flex, Text } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { IconSwitch } from 'common/components/IconSwitch'; -import { selectIsComposing, sessionSendToCanvasChanged } from 'features/controlLayers/store/canvasSessionSlice'; +import { + selectCanvasSettingsSlice, + settingsSendToCanvasChanged, +} from 'features/controlLayers/store/canvasSettingsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiImageBold, PiPaintBrushBold } from 'react-icons/pi'; @@ -32,20 +36,22 @@ const TooltipSendToCanvas = memo(() => { TooltipSendToCanvas.displayName = 'TooltipSendToCanvas'; +const selectSendToCanvas = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.sendToCanvas); + export const CanvasSendToToggle = memo(() => { const dispatch = useAppDispatch(); - const isComposing = useAppSelector(selectIsComposing); + const sendToCanvas = useAppSelector(selectSendToCanvas); const onChange = useCallback( (isChecked: boolean) => { - dispatch(sessionSendToCanvasChanged(isChecked)); + dispatch(settingsSendToCanvasChanged(isChecked)); }, [dispatch] ); return ( } tooltipUnchecked={} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox.tsx index 7795d7d1b3c..a92f400f4cc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox.tsx @@ -1,7 +1,7 @@ import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { clipToBboxChanged, selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice'; +import { selectCanvasSettingsSlice,settingsClipToBboxChanged } from 'features/controlLayers/store/canvasSettingsSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -13,7 +13,7 @@ export const CanvasSettingsClipToBboxCheckbox = memo(() => { const dispatch = useAppDispatch(); const clipToBbox = useAppSelector(selectClipToBbox); const onChange = useCallback( - (e: ChangeEvent) => dispatch(clipToBboxChanged(e.target.checked)), + (e: ChangeEvent) => dispatch(settingsClipToBboxChanged(e.target.checked)), [dispatch] ); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox.tsx index a8f6d2e18a6..9cb327396c0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox.tsx @@ -1,25 +1,30 @@ import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { invertScrollChanged, selectToolSlice } from 'features/controlLayers/store/toolSlice'; +import { selectCanvasSettingsSlice, settingsInvertScrollForToolWidthChanged } from 'features/controlLayers/store/canvasSettingsSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -const selectInvertScroll = createSelector(selectToolSlice, (tool) => tool.invertScroll); +const selectInvertScrollForToolWidth = createSelector( + selectCanvasSettingsSlice, + (settings) => settings.invertScrollForToolWidth +); export const CanvasSettingsInvertScrollCheckbox = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const invertScroll = useAppSelector(selectInvertScroll); + const invertScrollForToolWidth = useAppSelector(selectInvertScrollForToolWidth); const onChange = useCallback( - (e: ChangeEvent) => dispatch(invertScrollChanged(e.target.checked)), + (e: ChangeEvent) => { + dispatch(settingsInvertScrollForToolWidthChanged(e.target.checked)); + }, [dispatch] ); return ( {t('unifiedCanvas.invertBrushSizeScrollDirection')} - + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushWidth.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushWidth.tsx index e7d2e63bd57..ec807a3798d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushWidth.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushWidth.tsx @@ -15,7 +15,7 @@ import { import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useToolIsSelected } from 'features/controlLayers/components/Tool/hooks'; -import { brushWidthChanged, selectToolSlice } from 'features/controlLayers/store/toolSlice'; +import { selectCanvasSettingsSlice, settingsBrushWidthChanged } from 'features/controlLayers/store/canvasSettingsSlice'; import { clamp } from 'lodash-es'; import type { KeyboardEvent } from 'react'; import { memo, useCallback, useEffect, useState } from 'react'; @@ -23,7 +23,7 @@ import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiCaretDownBold } from 'react-icons/pi'; -const selectBrushWidth = createSelector(selectToolSlice, (tool) => tool.brush.width); +const selectBrushWidth = createSelector(selectCanvasSettingsSlice, (settings) => settings.brushWidth); const formatPx = (v: number | string) => `${v} px`; function mapSliderValueToRawValue(value: number) { @@ -73,7 +73,7 @@ export const ToolBrushWidth = memo(() => { const [localValue, setLocalValue] = useState(width); const onChange = useCallback( (v: number) => { - dispatch(brushWidthChanged(clamp(Math.round(v), 1, 600))); + dispatch(settingsBrushWidthChanged(clamp(Math.round(v), 1, 600))); }, [dispatch] ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserWidth.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserWidth.tsx index 188b6488e64..36f43fbaef8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserWidth.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserWidth.tsx @@ -15,7 +15,7 @@ import { import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useToolIsSelected } from 'features/controlLayers/components/Tool/hooks'; -import { eraserWidthChanged, selectToolSlice } from 'features/controlLayers/store/toolSlice'; +import { selectCanvasSettingsSlice, settingsEraserWidthChanged } from 'features/controlLayers/store/canvasSettingsSlice'; import { clamp } from 'lodash-es'; import type { KeyboardEvent } from 'react'; import { memo, useCallback, useEffect, useState } from 'react'; @@ -23,7 +23,7 @@ import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiCaretDownBold } from 'react-icons/pi'; -const selectEraserWidth = createSelector(selectToolSlice, (tool) => tool.eraser.width); +const selectEraserWidth = createSelector(selectCanvasSettingsSlice, (settings) => settings.eraserWidth); const formatPx = (v: number | string) => `${v} px`; function mapSliderValueToRawValue(value: number) { @@ -73,7 +73,7 @@ export const ToolEraserWidth = memo(() => { const [localValue, setLocalValue] = useState(width); const onChange = useCallback( (v: number) => { - dispatch(eraserWidthChanged(clamp(Math.round(v), 1, 600))); + dispatch(settingsEraserWidthChanged(clamp(Math.round(v), 1, 600))); }, [dispatch] ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolFillColorPicker.tsx index d86830a96ed..87d8ff2a3b2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolFillColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolFillColorPicker.tsx @@ -3,20 +3,20 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIColorPicker from 'common/components/IAIColorPicker'; import { rgbaColorToString } from 'common/util/colorCodeTransformers'; -import { fillChanged, selectToolSlice } from 'features/controlLayers/store/toolSlice'; +import { selectCanvasSettingsSlice, settingsColorChanged } from 'features/controlLayers/store/canvasSettingsSlice'; import type { RgbaColor } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -const selectFill = createSelector(selectToolSlice, (tool) => tool.fill); +const selectColor = createSelector(selectCanvasSettingsSlice, (settings) => settings.color); -export const ToolFillColorPicker = memo(() => { +export const ToolColorPicker = memo(() => { const { t } = useTranslation(); - const fill = useAppSelector(selectFill); + const fill = useAppSelector(selectColor); const dispatch = useAppDispatch(); const onChange = useCallback( (color: RgbaColor) => { - dispatch(fillChanged(color)); + dispatch(settingsColorChanged(color)); }, [dispatch] ); @@ -40,4 +40,4 @@ export const ToolFillColorPicker = memo(() => { ); }); -ToolFillColorPicker.displayName = 'ToolFillColorPicker'; +ToolColorPicker.displayName = 'ToolFillColorPicker'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx index d2ea4628a30..6662631873d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx @@ -2,7 +2,7 @@ import { Flex, Spacer } from '@invoke-ai/ui-library'; import { CanvasSettingsPopover } from 'features/controlLayers/components/Settings/CanvasSettingsPopover'; import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser'; -import { ToolFillColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker'; +import { ToolColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker'; import { ToolSettings } from 'features/controlLayers/components/Tool/ToolSettings'; import { CanvasToolbarResetViewButton } from 'features/controlLayers/components/Toolbar/CanvasToolbarResetViewButton'; import { CanvasToolbarSaveToGalleryButton } from 'features/controlLayers/components/Toolbar/CanvasToolbarSaveToGalleryButton'; @@ -35,7 +35,7 @@ export const CanvasToolbar = memo(() => { - + diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushToolPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushToolPreview.ts index 476a62f6cc5..7f3d5230c19 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushToolPreview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushToolPreview.ts @@ -94,10 +94,10 @@ export class CanvasBrushToolPreview extends CanvasModuleBase { return; } - const toolState = this.manager.stateApi.getToolState(); - const brushPreviewFill = this.manager.stateApi.getBrushPreviewFill(); - const alignedCursorPos = alignCoordForTool(cursorPos, toolState.brush.width); - const radius = toolState.brush.width / 2; + const settings = this.manager.stateApi.getSettings(); + const brushPreviewFill = this.manager.stateApi.getBrushPreviewColor(); + const alignedCursorPos = alignCoordForTool(cursorPos, settings.brushWidth); + const radius = settings.brushWidth / 2; // The circle is scaled this.konva.fillCircle.setAttrs({ diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasColorPickerToolPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasColorPickerToolPreview.ts index d9c043e4f84..e877eba3330 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasColorPickerToolPreview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasColorPickerToolPreview.ts @@ -198,7 +198,7 @@ export class CanvasColorPickerToolPreview extends CanvasModuleBase { return; } - const toolState = this.manager.stateApi.getToolState(); + const settings = this.manager.stateApi.getSettings(); const colorUnderCursor = this.parent.$colorUnderCursor.get(); const colorPickerInnerRadius = this.manager.stage.getScaledPixels(this.config.RING_INNER_RADIUS); const colorPickerOuterRadius = this.manager.stage.getScaledPixels(this.config.RING_OUTER_RADIUS); @@ -215,7 +215,7 @@ export class CanvasColorPickerToolPreview extends CanvasModuleBase { this.konva.ringCurrentColor.setAttrs({ x: cursorPos.x, y: cursorPos.y, - fill: rgbColorToString(toolState.fill), + fill: rgbColorToString(settings.color), innerRadius: colorPickerInnerRadius, outerRadius: colorPickerOuterRadius, }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserToolPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserToolPreview.ts index 6fa3376cf49..0e6c2146916 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserToolPreview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserToolPreview.ts @@ -84,9 +84,9 @@ export class CanvasEraserToolPreview extends CanvasModuleBase { return; } - const toolState = this.manager.stateApi.getToolState(); - const alignedCursorPos = alignCoordForTool(cursorPos, toolState.eraser.width); - const radius = toolState.eraser.width / 2; + const settings = this.manager.stateApi.getSettings(); + const alignedCursorPos = alignCoordForTool(cursorPos, settings.eraserWidth); + const radius = settings.eraserWidth / 2; // The circle is scaled this.konva.cutoutCircle.setAttrs({ diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index ab82d29949a..84ad45e9523 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -136,9 +136,9 @@ export class CanvasManager extends CanvasModuleBase { // These atoms require the canvas manager to be set up before we can provide their initial values this.stateApi.$transformingAdapter.set(null); - this.stateApi.$toolState.set(this.stateApi.getToolState()); + this.stateApi.$settingsState.set(this.stateApi.getSettings()); this.stateApi.$selectedEntityIdentifier.set(this.stateApi.getCanvasState().selectedEntityIdentifier); - this.stateApi.$currentFill.set(this.stateApi.getCurrentFill()); + this.stateApi.$currentFill.set(this.stateApi.getCurrentColor()); this.stateApi.$selectedEntity.set(this.stateApi.getSelectedEntity()); this.subscriptions.add(this.store.subscribe(this.renderer.render)); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts index 28de08889b6..db0b888fd38 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts @@ -55,10 +55,10 @@ export class CanvasRenderingModule extends CanvasModuleBase { const prevState = this.state; this.state = state; - this.manager.stateApi.$toolState.set(this.manager.stateApi.getToolState()); + this.manager.stateApi.$settingsState.set(this.manager.stateApi.getSettings()); this.manager.stateApi.$selectedEntityIdentifier.set(state.selectedEntityIdentifier); this.manager.stateApi.$selectedEntity.set(this.manager.stateApi.getSelectedEntity()); - this.manager.stateApi.$currentFill.set(this.manager.stateApi.getCurrentFill()); + this.manager.stateApi.$currentFill.set(this.manager.stateApi.getCurrentColor()); if (prevState === state) { // No changes to state - no need to render diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index fecd9f4296d..0053a0bce64 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -5,6 +5,13 @@ import type { CanvasEntityMaskAdapter } from 'features/controlLayers/konva/Canva import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { getPrefixedId } from 'features/controlLayers/konva/util'; +import type { + CanvasSettingsState} from 'features/controlLayers/store/canvasSettingsSlice'; +import { + settingsBrushWidthChanged, + settingsColorChanged, + settingsEraserWidthChanged, +} from 'features/controlLayers/store/canvasSettingsSlice'; import { bboxChanged, entityBrushLineAdded, @@ -15,12 +22,6 @@ import { entityReset, } from 'features/controlLayers/store/canvasSlice'; import { selectAllRenderableEntities, selectCanvasSlice } from 'features/controlLayers/store/selectors'; -import { - brushWidthChanged, - eraserWidthChanged, - fillChanged, - type ToolState, -} from 'features/controlLayers/store/toolSlice'; import type { CanvasControlLayerState, CanvasEntityIdentifier, @@ -158,21 +159,21 @@ export class CanvasStateApiModule extends CanvasModuleBase { * Sets the brush width, pushing state to redux. */ setBrushWidth = (width: number) => { - this.store.dispatch(brushWidthChanged(width)); + this.store.dispatch(settingsBrushWidthChanged(width)); }; /** * Sets the eraser width, pushing state to redux. */ setEraserWidth = (width: number) => { - this.store.dispatch(eraserWidthChanged(width)); + this.store.dispatch(settingsEraserWidthChanged(width)); }; /** - * Sets the fill color, pushing state to redux. + * Sets the drawing color, pushing state to redux. */ - setFill = (fill: RgbaColor) => { - return this.store.dispatch(fillChanged(fill)); + setColor = (color: RgbaColor) => { + return this.store.dispatch(settingsColorChanged(color)); }; /** @@ -193,13 +194,6 @@ export class CanvasStateApiModule extends CanvasModuleBase { return this.getCanvasState().bbox; }; - /** - * Gets the tool state from redux. - */ - getToolState = () => { - return this.store.getState().tool; - }; - /** * Gets the canvas settings from redux. */ @@ -322,37 +316,36 @@ export class CanvasStateApiModule extends CanvasModuleBase { }; /** - * Gets the current fill color. The fill color is determined by the tool state and the selected entity. + * Gets the current drawing color. * - * The fill color is determined by the tool state, except when the selected entity is a regional guidance or inpaint - * mask. In that case, the fill color is always black. + * The color is determined by the tool state, except when the selected entity is a regional guidance or inpaint mask. + * In that case, the color is always black. * * Regional guidance and inpaint mask entities use a compositing rect to draw with their selected color and texture, - * so the fill color for lines and rects doesn't matter - it is never seen. The only requirement is that it is opaque. - * For consistency with conventional black and white mask images, we use black as the fill color for these entities. + * so the color for lines and rects doesn't matter - it is never seen. The only requirement is that it is opaque. For + * consistency with conventional black and white mask images, we use black as the color for these entities. */ - getCurrentFill = () => { - let currentFill: RgbaColor = this.getToolState().fill; + getCurrentColor = () => { + let color: RgbaColor = this.getSettings().color; const selectedEntity = this.getSelectedEntity(); if (selectedEntity) { // These two entity types use a compositing rect for opacity. Their fill is always a solid color. if (selectedEntity.state.type === 'regional_guidance' || selectedEntity.state.type === 'inpaint_mask') { - currentFill = RGBA_BLACK; + color = RGBA_BLACK; } } - return currentFill; + return color; }; /** - * Gets the brush preview fill color. The brush preview fill color is determined by the tool state and the selected - * entity. + * Gets the brush preview color. The brush preview color is determined by the tool state and the selected entity. * - * The color is the tool state's fill color, except when the selected entity is a regional guidance or inpaint mask. + * The color is the tool state's color, except when the selected entity is a regional guidance or inpaint mask. * - * These entities have their own fill color and texture, so the brush preview should use those instead of the tool - * state's fill color. + * These entities have their own color and texture, so the brush preview should use those instead of the tool state's + * color. */ - getBrushPreviewFill = (): RgbaColor => { + getBrushPreviewColor = (): RgbaColor => { const selectedEntity = this.getSelectedEntity(); if (selectedEntity?.state.type === 'regional_guidance' || selectedEntity?.state.type === 'inpaint_mask') { // TODO(psyche): If we move the brush preview's Konva nodes to the selected entity renderer, we can draw them @@ -361,7 +354,7 @@ export class CanvasStateApiModule extends CanvasModuleBase { // selected entity's fill color with 50% opacity. return { ...selectedEntity.state.fill.color, a: 0.5 }; } else { - return this.getToolState().fill; + return this.getSettings().color; } }; @@ -376,9 +369,9 @@ export class CanvasStateApiModule extends CanvasModuleBase { $isTranforming = computed(this.$transformingAdapter, (transformingAdapter) => Boolean(transformingAdapter)); /** - * A nanostores atom, kept in sync with the redux store's tool state. + * A nanostores atom, kept in sync with the redux store's settings state. */ - $toolState: WritableAtom = atom(); + $settingsState: WritableAtom = atom(); /** * The current fill color, derived from the tool state and the selected entity. diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts index 86c57ef5757..cfbb73c1fcf 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts @@ -112,12 +112,12 @@ export class CanvasToolModule extends CanvasModuleBase { this.subscriptions.add(this.manager.stage.$stageAttrs.listen(this.render)); this.subscriptions.add( - this.manager.stateApi.$toolState.listen((value, oldValue) => { + this.manager.stateApi.$settingsState.listen((settings, prevSettings) => { if ( - value !== oldValue || - value.brush.width !== oldValue.brush.width || - value.eraser.width !== oldValue.eraser.width || - value.fill !== oldValue.fill + settings !== prevSettings || + settings.brushWidth !== prevSettings.brushWidth || + settings.eraserWidth !== prevSettings.eraserWidth || + settings.color !== prevSettings.color ) { this.render(); } @@ -306,7 +306,7 @@ export class CanvasToolModule extends CanvasModuleBase { const cursorPos = this.syncLastCursorPos(); try { const isMouseDown = this.$isMouseDown.get(); - const toolState = this.manager.stateApi.getToolState(); + const settings = this.manager.stateApi.getSettings(); const tool = this.$tool.get(); const selectedEntity = this.manager.stateApi.getSelectedEntity(); @@ -321,13 +321,13 @@ export class CanvasToolModule extends CanvasModuleBase { if (tool === 'brush') { const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position); - const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); + const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth); await selectedEntity.adapter.renderer.setBuffer({ id: getPrefixedId('brush_line'), type: 'brush_line', points: [alignedPoint.x, alignedPoint.y], - strokeWidth: toolState.brush.width, - color: this.manager.stateApi.getCurrentFill(), + strokeWidth: settings.brushWidth, + color: this.manager.stateApi.getCurrentColor(), clip: this.getClip(selectedEntity.state), }); this.$lastAddedPoint.set(alignedPoint); @@ -336,7 +336,7 @@ export class CanvasToolModule extends CanvasModuleBase { if (tool === 'eraser') { const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position); - const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); + const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth); if (selectedEntity.adapter.renderer.bufferState) { selectedEntity.adapter.renderer.commitBuffer(); } @@ -344,7 +344,7 @@ export class CanvasToolModule extends CanvasModuleBase { id: getPrefixedId('eraser_line'), type: 'eraser_line', points: [alignedPoint.x, alignedPoint.y], - strokeWidth: toolState.eraser.width, + strokeWidth: settings.eraserWidth, clip: this.getClip(selectedEntity.state), }); this.$lastAddedPoint.set(alignedPoint); @@ -361,12 +361,12 @@ export class CanvasToolModule extends CanvasModuleBase { try { const tool = this.$tool.get(); - const toolState = this.manager.stateApi.getToolState(); + const settings = this.manager.stateApi.getSettings(); if (tool === 'colorPicker') { const color = this.getColorUnderCursor(); if (color) { - this.manager.stateApi.setFill({ ...toolState.fill, ...color }); + this.manager.stateApi.setColor({ ...settings.color, ...color }); } return; } @@ -382,7 +382,7 @@ export class CanvasToolModule extends CanvasModuleBase { if (tool === 'brush') { const lastLinePoint = selectedEntity.adapter.getLastPointOfLastLine('brush_line'); - const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); + const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth); if (e.evt.shiftKey && lastLinePoint) { // Create a straight line from the last line point if (selectedEntity.adapter.renderer.bufferState) { @@ -399,8 +399,8 @@ export class CanvasToolModule extends CanvasModuleBase { alignedPoint.x, alignedPoint.y, ], - strokeWidth: toolState.brush.width, - color: this.manager.stateApi.getCurrentFill(), + strokeWidth: settings.brushWidth, + color: this.manager.stateApi.getCurrentColor(), clip: this.getClip(selectedEntity.state), }); } else { @@ -411,8 +411,8 @@ export class CanvasToolModule extends CanvasModuleBase { id: getPrefixedId('brush_line'), type: 'brush_line', points: [alignedPoint.x, alignedPoint.y], - strokeWidth: toolState.brush.width, - color: this.manager.stateApi.getCurrentFill(), + strokeWidth: settings.brushWidth, + color: this.manager.stateApi.getCurrentColor(), clip: this.getClip(selectedEntity.state), }); } @@ -421,7 +421,7 @@ export class CanvasToolModule extends CanvasModuleBase { if (tool === 'eraser') { const lastLinePoint = selectedEntity.adapter.getLastPointOfLastLine('eraser_line'); - const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); + const alignedPoint = alignCoordForTool(normalizedPoint, settings.eraserWidth); if (e.evt.shiftKey && lastLinePoint) { // Create a straight line from the last line point if (selectedEntity.adapter.renderer.bufferState) { @@ -437,7 +437,7 @@ export class CanvasToolModule extends CanvasModuleBase { alignedPoint.x, alignedPoint.y, ], - strokeWidth: toolState.eraser.width, + strokeWidth: settings.eraserWidth, clip: this.getClip(selectedEntity.state), }); } else { @@ -448,7 +448,7 @@ export class CanvasToolModule extends CanvasModuleBase { id: getPrefixedId('eraser_line'), type: 'eraser_line', points: [alignedPoint.x, alignedPoint.y], - strokeWidth: toolState.eraser.width, + strokeWidth: settings.eraserWidth, clip: this.getClip(selectedEntity.state), }); } @@ -463,7 +463,7 @@ export class CanvasToolModule extends CanvasModuleBase { id: getPrefixedId('rect'), type: 'rect', rect: { x: Math.round(normalizedPoint.x), y: Math.round(normalizedPoint.y), width: 0, height: 0 }, - color: this.manager.stateApi.getCurrentFill(), + color: this.manager.stateApi.getCurrentColor(), }); } } finally { @@ -542,17 +542,17 @@ export class CanvasToolModule extends CanvasModuleBase { return; } - const toolState = this.manager.stateApi.getToolState(); + const settings = this.manager.stateApi.getSettings(); if (tool === 'brush' && bufferState.type === 'brush_line') { const lastPoint = getLastPointOfLine(bufferState.points); - const minDistance = toolState.brush.width * this.config.BRUSH_SPACING_TARGET_SCALE; + const minDistance = settings.brushWidth * this.config.BRUSH_SPACING_TARGET_SCALE; if (!lastPoint || !isDistanceMoreThanMin(cursorPos, lastPoint, minDistance)) { return; } const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position); - const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width); + const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth); if (lastPoint.x === alignedPoint.x && lastPoint.y === alignedPoint.y) { // Do not add duplicate points @@ -564,13 +564,13 @@ export class CanvasToolModule extends CanvasModuleBase { this.$lastAddedPoint.set(alignedPoint); } else if (tool === 'eraser' && bufferState.type === 'eraser_line') { const lastPoint = getLastPointOfLine(bufferState.points); - const minDistance = toolState.eraser.width * this.config.BRUSH_SPACING_TARGET_SCALE; + const minDistance = settings.eraserWidth * this.config.BRUSH_SPACING_TARGET_SCALE; if (!lastPoint || !isDistanceMoreThanMin(cursorPos, lastPoint, minDistance)) { return; } const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position); - const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width); + const alignedPoint = alignCoordForTool(normalizedPoint, settings.eraserWidth); if (lastPoint.x === alignedPoint.x && lastPoint.y === alignedPoint.y) { // Do not add duplicate points @@ -613,20 +613,20 @@ export class CanvasToolModule extends CanvasModuleBase { return; } - const toolState = this.manager.stateApi.getToolState(); + const settings = this.manager.stateApi.getSettings(); const tool = this.$tool.get(); let delta = e.evt.deltaY; - if (toolState.invertScroll) { + if (settings.invertScrollForToolWidth) { delta = -delta; } // Holding ctrl or meta while scrolling changes the brush size if (tool === 'brush') { - this.manager.stateApi.setBrushWidth(calculateNewBrushSizeFromWheelDelta(toolState.brush.width, delta)); + this.manager.stateApi.setBrushWidth(calculateNewBrushSizeFromWheelDelta(settings.brushWidth, delta)); } else if (tool === 'eraser') { - this.manager.stateApi.setEraserWidth(calculateNewBrushSizeFromWheelDelta(toolState.eraser.width, delta)); + this.manager.stateApi.setEraserWidth(calculateNewBrushSizeFromWheelDelta(settings.eraserWidth, delta)); } this.render(); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSessionSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSessionSlice.ts index e115b6800bc..0826bbf26f7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSessionSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSessionSlice.ts @@ -4,14 +4,12 @@ import { canvasSlice } from 'features/controlLayers/store/canvasSlice'; import type { StagingAreaImage } from 'features/controlLayers/store/types'; export type CanvasSessionState = { - sendToCanvas: boolean; isStaging: boolean; stagedImages: StagingAreaImage[]; selectedStagedImageIndex: number; }; const initialState: CanvasSessionState = { - sendToCanvas: false, isStaging: false, stagedImages: [], selectedStagedImageIndex: 0, @@ -51,9 +49,6 @@ export const canvasSessionSlice = createSlice({ state.stagedImages = []; state.selectedStagedImageIndex = 0; }, - sessionSendToCanvasChanged: (state, action: PayloadAction) => { - state.sendToCanvas = action.payload; - }, }, }); @@ -64,7 +59,6 @@ export const { sessionStagingAreaReset, sessionNextStagedImageSelected, sessionPrevStagedImageSelected, - sessionSendToCanvasChanged, } = canvasSessionSlice.actions; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ @@ -85,7 +79,3 @@ export const sessionStagingAreaImageAccepted = createAction<{ index: number }>( export const selectCanvasSessionSlice = (s: RootState) => s.canvasSession; export const selectIsStaging = createSelector(selectCanvasSessionSlice, (canvasSession) => canvasSession.isStaging); -export const selectIsComposing = createSelector( - selectCanvasSessionSlice, - (canvasSession) => canvasSession.sendToCanvas -); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts index ca6034377b5..bbfe7ac9368 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts @@ -1,32 +1,71 @@ import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; +import type { RgbaColor } from 'features/controlLayers/store/types'; export type CanvasSettingsState = { - imageSmoothing: boolean; + /** + * Whether to show HUD (Heads-Up Display) on the canvas. + */ showHUD: boolean; + /** + * Whether to automatically save canvas generations to the gallery. If in Save to Gallery mode, this setting will be + * ignored, and all generations will be saved. + */ autoSave: boolean; - preserveMaskedArea: boolean; - cropToBboxOnSave: boolean; + /** + * Whether to clip lines and shapes to the generation bounding box. If disabled, lines and shapes will be clipped to + * the canvas bounds. + */ clipToBbox: boolean; + /** + * Whether to show a dynamic grid on the canvas. If disabled, a checkerboard pattern will be shown instead. + */ dynamicGrid: boolean; + /** + * Whether to invert the scroll direction when adjusting the brush or eraser width with the scroll wheel. + */ + invertScrollForToolWidth: boolean; + /** + * The width of the brush tool. + */ + brushWidth: number; + /** + * The width of the eraser tool. + */ + eraserWidth: number; + /** + * The color to use when drawing lines or filling shapes. + */ + color: RgbaColor; + /** + * Whether to send generated images to canvas staging area. When disabled, generated images will be sent directly to + * the gallery. + */ + sendToCanvas: boolean; + + // TODO(psyche): These are copied from old canvas state, need to be implemented + // imageSmoothing: boolean; + // preserveMaskedArea: boolean; + // cropToBboxOnSave: boolean; }; const initialState: CanvasSettingsState = { - // TODO(psyche): These are copied from old canvas state, need to be implemented autoSave: false, - imageSmoothing: true, - preserveMaskedArea: false, showHUD: true, clipToBbox: false, - cropToBboxOnSave: false, dynamicGrid: false, + brushWidth: 50, + eraserWidth: 50, + invertScrollForToolWidth: false, + color: { r: 31, g: 160, b: 224, a: 1 }, // invokeBlue.500 + sendToCanvas: false, }; export const canvasSettingsSlice = createSlice({ name: 'canvasSettings', initialState, reducers: { - clipToBboxChanged: (state, action: PayloadAction) => { + settingsClipToBboxChanged: (state, action: PayloadAction) => { state.clipToBbox = action.payload; }, settingsDynamicGridToggled: (state) => { @@ -38,11 +77,35 @@ export const canvasSettingsSlice = createSlice({ settingsShowHUDToggled: (state) => { state.showHUD = !state.showHUD; }, + settingsBrushWidthChanged: (state, action: PayloadAction) => { + state.brushWidth = Math.round(action.payload); + }, + settingsEraserWidthChanged: (state, action: PayloadAction) => { + state.eraserWidth = Math.round(action.payload); + }, + settingsColorChanged: (state, action: PayloadAction) => { + state.color = action.payload; + }, + settingsInvertScrollForToolWidthChanged: (state, action: PayloadAction) => { + state.invertScrollForToolWidth = action.payload; + }, + settingsSendToCanvasChanged: (state, action: PayloadAction) => { + state.sendToCanvas = action.payload; + }, }, }); -export const { clipToBboxChanged, settingsAutoSaveToggled, settingsDynamicGridToggled, settingsShowHUDToggled } = - canvasSettingsSlice.actions; +export const { + settingsClipToBboxChanged, + settingsAutoSaveToggled, + settingsDynamicGridToggled, + settingsShowHUDToggled, + settingsBrushWidthChanged, + settingsEraserWidthChanged, + settingsColorChanged, + settingsInvertScrollForToolWidthChanged, + settingsSendToCanvasChanged, +} = canvasSettingsSlice.actions; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const migrate = (state: any): any => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/toolSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/toolSlice.ts deleted file mode 100644 index 15096836be3..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/store/toolSlice.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; -import type { PersistConfig, RootState } from 'app/store/store'; -import type { RgbaColor } from 'features/controlLayers/store/types'; - -export type ToolState = { - invertScroll: boolean; - brush: { width: number }; - eraser: { width: number }; - fill: RgbaColor; -}; - -const initialState: ToolState = { - invertScroll: false, - fill: { r: 31, g: 160, b: 224, a: 1 }, // invokeBlue.500 - brush: { - width: 50, - }, - eraser: { - width: 50, - }, -}; - -export const toolSlice = createSlice({ - name: 'tool', - initialState, - reducers: { - brushWidthChanged: (state, action: PayloadAction) => { - state.brush.width = Math.round(action.payload); - }, - eraserWidthChanged: (state, action: PayloadAction) => { - state.eraser.width = Math.round(action.payload); - }, - fillChanged: (state, action: PayloadAction) => { - state.fill = action.payload; - }, - invertScrollChanged: (state, action: PayloadAction) => { - state.invertScroll = action.payload; - }, - }, -}); - -export const { brushWidthChanged, eraserWidthChanged, fillChanged, invertScrollChanged } = toolSlice.actions; - -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrate = (state: any): any => { - return state; -}; - -export const toolPersistConfig: PersistConfig = { - name: toolSlice.name, - initialState, - migrate, - persistDenylist: [], -}; - -export const selectToolSlice = (state: RootState) => state.tool; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts index c69820bd498..bc134a0fc5f 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts @@ -1,7 +1,7 @@ import type { RootState } from 'app/store/store'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import { selectCanvasSessionSlice } from 'features/controlLayers/store/canvasSessionSlice'; +import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice'; import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; import type { Dimensions } from 'features/controlLayers/store/types'; @@ -25,11 +25,11 @@ export const addInpaint = async ( denoise.denoising_start = denoising_start; const params = selectParamsSlice(state); - const canvasSession = selectCanvasSessionSlice(state); + const canvasSettings = selectCanvasSettingsSlice(state); const canvas = selectCanvasSlice(state); const { bbox } = canvas; - const { sendToCanvas: isComposing } = canvasSession; + const { sendToCanvas } = canvasSettings; const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect); const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect); @@ -99,7 +99,7 @@ export const addInpaint = async ( g.addEdge(resizeImageToOriginalSize, 'image', canvasPasteBack, 'generated_image'); g.addEdge(resizeMaskToOriginalSize, 'image', canvasPasteBack, 'mask'); - if (!isComposing) { + if (!sendToCanvas) { canvasPasteBack.source_image = { image_name: initialImage.image_name }; } @@ -143,7 +143,7 @@ export const addInpaint = async ( g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image'); - if (!isComposing) { + if (!sendToCanvas) { canvasPasteBack.source_image = { image_name: initialImage.image_name }; } diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index 80cdf5d53d9..322a339649c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts @@ -1,7 +1,7 @@ import type { RootState } from 'app/store/store'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import { selectCanvasSessionSlice } from 'features/controlLayers/store/canvasSessionSlice'; +import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice'; import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; import type { Dimensions } from 'features/controlLayers/store/types'; @@ -26,11 +26,11 @@ export const addOutpaint = async ( denoise.denoising_start = denoising_start; const params = selectParamsSlice(state); - const canvasSession = selectCanvasSessionSlice(state); + const canvasSettings = selectCanvasSettingsSlice(state); const canvas = selectCanvasSlice(state); const { bbox } = canvas; - const { sendToCanvas: isComposing } = canvasSession; + const { sendToCanvas } = canvasSettings; const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect); const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect); @@ -123,7 +123,7 @@ export const addOutpaint = async ( g.addEdge(resizeOutputImageToOriginalSize, 'image', canvasPasteBack, 'generated_image'); g.addEdge(resizeOutputMaskToOriginalSize, 'image', canvasPasteBack, 'mask'); - if (!isComposing) { + if (!sendToCanvas) { canvasPasteBack.source_image = { image_name: initialImage.image_name }; } @@ -173,7 +173,7 @@ export const addOutpaint = async ( g.addEdge(createGradientMask, 'expanded_mask_area', canvasPasteBack, 'mask'); g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image'); - if (!isComposing) { + if (!sendToCanvas) { canvasPasteBack.source_image = { image_name: initialImage.image_name }; } diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index 693b194e521..7d8886f3af0 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -2,7 +2,6 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import { selectCanvasSessionSlice } from 'features/controlLayers/store/canvasSessionSlice'; import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice'; import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; @@ -36,7 +35,6 @@ export const buildSD1Graph = async ( log.debug({ generationMode }, 'Building SD1/SD2 graph'); const params = selectParamsSlice(state); - const canvasSession = selectCanvasSessionSlice(state); const canvasSettings = selectCanvasSettingsSlice(state); const canvas = selectCanvasSlice(state); @@ -282,7 +280,7 @@ export const buildSD1Graph = async ( canvasOutput = addWatermarker(g, canvasOutput); } - const shouldSaveToGallery = !canvasSession.sendToCanvas || canvasSettings.autoSave; + const shouldSaveToGallery = !canvasSettings.sendToCanvas || canvasSettings.autoSave; g.updateNode(canvasOutput, { id: getPrefixedId('canvas_output'), diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index 3fbf2f2f567..78a0da2828f 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -2,7 +2,6 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import { selectCanvasSessionSlice } from 'features/controlLayers/store/canvasSessionSlice'; import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice'; import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; @@ -36,7 +35,6 @@ export const buildSDXLGraph = async ( log.debug({ generationMode }, 'Building SDXL graph'); const params = selectParamsSlice(state); - const canvasSession = selectCanvasSessionSlice(state); const canvasSettings = selectCanvasSettingsSlice(state); const canvas = selectCanvasSlice(state); @@ -285,7 +283,7 @@ export const buildSDXLGraph = async ( canvasOutput = addWatermarker(g, canvasOutput); } - const shouldSaveToGallery = !canvasSession.sendToCanvas || canvasSettings.autoSave; + const shouldSaveToGallery = !canvasSettings.sendToCanvas || canvasSettings.autoSave; g.updateNode(canvasOutput, { id: getPrefixedId('canvas_output'), From 3ed85d821b8b1fc0c0859850d1f5aece72cf63ed Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 3 Sep 2024 18:42:27 +1000 Subject: [PATCH 624/678] feat(ui): add compositeMaskedRegions setting --- invokeai/frontend/web/public/locales/en.json | 1 + ...SettingsCompositeMaskedRegionsCheckbox.tsx | 33 +++++++++++++++++++ .../Settings/CanvasSettingsPopover.tsx | 2 ++ .../store/canvasSettingsSlice.ts | 18 ++++++++-- .../nodes/util/graph/generation/addInpaint.ts | 5 ++- .../util/graph/generation/addOutpaint.ts | 5 ++- 6 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsCompositeMaskedRegionsCheckbox.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 1c472673a11..d7fcbb21533 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1673,6 +1673,7 @@ "clearCaches": "Clear Caches", "recalculateRects": "Recalculate Rects", "clipToBbox": "Clip Strokes to Bbox", + "compositeMaskedRegions": "Composite Masked Regions", "addLayer": "Add Layer", "duplicate": "Duplicate", "moveToFront": "Move to Front", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsCompositeMaskedRegionsCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsCompositeMaskedRegionsCheckbox.tsx new file mode 100644 index 00000000000..647889b625d --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsCompositeMaskedRegionsCheckbox.tsx @@ -0,0 +1,33 @@ +import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { + selectCanvasSettingsSlice, + settingsCompositeMaskedRegionsChanged, +} from 'features/controlLayers/store/canvasSettingsSlice'; +import type { ChangeEvent } from 'react'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +const selectCompositeMaskedRegions = createSelector( + selectCanvasSettingsSlice, + (canvasSettings) => canvasSettings.compositeMaskedRegions +); + +export const CanvasSettingsCompositeMaskedRegionsCheckbox = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const compositeMaskedRegions = useAppSelector(selectCompositeMaskedRegions); + const onChange = useCallback( + (e: ChangeEvent) => dispatch(settingsCompositeMaskedRegionsChanged(e.target.checked)), + [dispatch] + ); + return ( + + {t('controlLayers.compositeMaskedRegions')} + + + ); +}); + +CanvasSettingsCompositeMaskedRegionsCheckbox.displayName = 'CanvasSettingsCompositeMaskedRegionsCheckbox'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsPopover.tsx index fe1b9a4b27d..50f89748ace 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsPopover.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsPopover.tsx @@ -13,6 +13,7 @@ import { CanvasSettingsAutoSaveCheckbox } from 'features/controlLayers/component import { CanvasSettingsClearCachesButton } from 'features/controlLayers/components/Settings/CanvasSettingsClearCachesButton'; import { CanvasSettingsClearHistoryButton } from 'features/controlLayers/components/Settings/CanvasSettingsClearHistoryButton'; import { CanvasSettingsClipToBboxCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox'; +import { CanvasSettingsCompositeMaskedRegionsCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsCompositeMaskedRegionsCheckbox'; import { CanvasSettingsDynamicGridSwitch } from 'features/controlLayers/components/Settings/CanvasSettingsDynamicGridSwitch'; import { CanvasSettingsInvertScrollCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox'; import { CanvasSettingsLogDebugInfoButton } from 'features/controlLayers/components/Settings/CanvasSettingsLogDebugInfo'; @@ -37,6 +38,7 @@ export const CanvasSettingsPopover = memo(() => { + diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts index bbfe7ac9368..ae39e5a26ca 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts @@ -8,8 +8,9 @@ export type CanvasSettingsState = { */ showHUD: boolean; /** - * Whether to automatically save canvas generations to the gallery. If in Save to Gallery mode, this setting will be - * ignored, and all generations will be saved. + * Whether to automatically save canvas generations to the gallery. + * + * When `sendToCanvas` is disabled, this setting is ignored, and images are always saved to the gallery. */ autoSave: boolean; /** @@ -42,6 +43,14 @@ export type CanvasSettingsState = { * the gallery. */ sendToCanvas: boolean; + /** + * Whether to composite inpainted/outpainted regions back onto the source image when saving canvas generations. + * + * If disabled, inpainted/outpainted regions will be saved with a transparent background. + * + * When `sendToCanvas` is disabled, this setting is ignored, masked regions will always be composited. + */ + compositeMaskedRegions: boolean; // TODO(psyche): These are copied from old canvas state, need to be implemented // imageSmoothing: boolean; @@ -59,6 +68,7 @@ const initialState: CanvasSettingsState = { invertScrollForToolWidth: false, color: { r: 31, g: 160, b: 224, a: 1 }, // invokeBlue.500 sendToCanvas: false, + compositeMaskedRegions: false, }; export const canvasSettingsSlice = createSlice({ @@ -92,6 +102,9 @@ export const canvasSettingsSlice = createSlice({ settingsSendToCanvasChanged: (state, action: PayloadAction) => { state.sendToCanvas = action.payload; }, + settingsCompositeMaskedRegionsChanged: (state, action: PayloadAction) => { + state.compositeMaskedRegions = action.payload; + }, }, }); @@ -105,6 +118,7 @@ export const { settingsColorChanged, settingsInvertScrollForToolWidthChanged, settingsSendToCanvasChanged, + settingsCompositeMaskedRegionsChanged, } = canvasSettingsSlice.actions; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts index bc134a0fc5f..262379fe904 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts @@ -29,7 +29,6 @@ export const addInpaint = async ( const canvas = selectCanvasSlice(state); const { bbox } = canvas; - const { sendToCanvas } = canvasSettings; const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect); const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect); @@ -99,7 +98,7 @@ export const addInpaint = async ( g.addEdge(resizeImageToOriginalSize, 'image', canvasPasteBack, 'generated_image'); g.addEdge(resizeMaskToOriginalSize, 'image', canvasPasteBack, 'mask'); - if (!sendToCanvas) { + if (!canvasSettings.sendToCanvas || canvasSettings.compositeMaskedRegions) { canvasPasteBack.source_image = { image_name: initialImage.image_name }; } @@ -143,7 +142,7 @@ export const addInpaint = async ( g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image'); - if (!sendToCanvas) { + if (!canvasSettings.sendToCanvas || canvasSettings.compositeMaskedRegions) { canvasPasteBack.source_image = { image_name: initialImage.image_name }; } diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index 322a339649c..0f0fb825721 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts @@ -30,7 +30,6 @@ export const addOutpaint = async ( const canvas = selectCanvasSlice(state); const { bbox } = canvas; - const { sendToCanvas } = canvasSettings; const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect); const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect); @@ -123,7 +122,7 @@ export const addOutpaint = async ( g.addEdge(resizeOutputImageToOriginalSize, 'image', canvasPasteBack, 'generated_image'); g.addEdge(resizeOutputMaskToOriginalSize, 'image', canvasPasteBack, 'mask'); - if (!sendToCanvas) { + if (!canvasSettings.sendToCanvas || canvasSettings.compositeMaskedRegions) { canvasPasteBack.source_image = { image_name: initialImage.image_name }; } @@ -173,7 +172,7 @@ export const addOutpaint = async ( g.addEdge(createGradientMask, 'expanded_mask_area', canvasPasteBack, 'mask'); g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image'); - if (!sendToCanvas) { + if (!canvasSettings.sendToCanvas || canvasSettings.compositeMaskedRegions) { canvasPasteBack.source_image = { image_name: initialImage.image_name }; } From 5d089ecc7741fb8b3e78b166e78803db3a24ce9d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 3 Sep 2024 20:16:31 +1000 Subject: [PATCH 625/678] feat(ui): prevent layer interactions when transforming or filtering --- .../ControlLayerMenuItemsControlToRaster.tsx | 4 +- .../RasterLayerMenuItemsRasterToControl.tsx | 4 +- ...uidanceMenuItemsAddPromptsAndIPAdapter.tsx | 10 +- .../common/CanvasEntityMenuItemsArrange.tsx | 18 +++- .../common/CanvasEntityMenuItemsDelete.tsx | 4 +- .../common/CanvasEntityMenuItemsDuplicate.tsx | 4 +- .../common/CanvasEntityMenuItemsFilter.tsx | 6 +- .../common/CanvasEntityMenuItemsTransform.tsx | 8 +- .../controlLayers/hooks/useCanvasIsBusy.ts | 9 ++ .../controlLayers/konva/CanvasBboxModule.ts | 3 +- .../konva/CanvasEntityLayerAdapter.ts | 4 + .../konva/CanvasEntityMaskAdapter.ts | 4 + .../konva/CanvasEntityTransformer.ts | 7 ++ .../controlLayers/konva/CanvasManager.ts | 12 ++- .../konva/CanvasRenderingModule.ts | 1 + .../controlLayers/konva/CanvasStageModule.ts | 5 + .../controlLayers/konva/CanvasToolModule.ts | 91 ++++++++++++------- 17 files changed, 139 insertions(+), 55 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasIsBusy.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsControlToRaster.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsControlToRaster.tsx index 5f1797a0a72..89506de8950 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsControlToRaster.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsControlToRaster.tsx @@ -1,6 +1,7 @@ import { MenuItem } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy'; import { controlLayerConvertedToRasterLayer } from 'features/controlLayers/store/canvasSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,6 +10,7 @@ import { PiLightningBold } from 'react-icons/pi'; export const ControlLayerMenuItemsControlToRaster = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); + const isBusy = useCanvasIsBusy(); const entityIdentifier = useEntityIdentifierContext('control_layer'); const convertControlLayerToRasterLayer = useCallback(() => { @@ -16,7 +18,7 @@ export const ControlLayerMenuItemsControlToRaster = memo(() => { }, [dispatch, entityIdentifier]); return ( - }> + } isDisabled={isBusy}> {t('controlLayers.convertToRasterLayer')} ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl.tsx index 7ca76922769..b480dbd6c63 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl.tsx @@ -1,6 +1,7 @@ import { MenuItem } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy'; import { rasterLayerConvertedToControlLayer } from 'features/controlLayers/store/canvasSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -10,13 +11,14 @@ export const RasterLayerMenuItemsRasterToControl = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const entityIdentifier = useEntityIdentifierContext('raster_layer'); + const isBusy = useCanvasIsBusy(); const convertRasterLayerToControlLayer = useCallback(() => { dispatch(rasterLayerConvertedToControlLayer({ entityIdentifier })); }, [dispatch, entityIdentifier]); return ( - }> + } isDisabled={isBusy}> {t('controlLayers.convertToControlLayer')} ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx index 256f46086de..a7832e26ac3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx @@ -2,6 +2,7 @@ import { MenuItem } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy'; import { rgIPAdapterAdded, rgNegativePromptChanged, @@ -15,6 +16,7 @@ export const RegionalGuidanceMenuItemsAddPromptsAndIPAdapter = memo(() => { const entityIdentifier = useEntityIdentifierContext('regional_guidance'); const { t } = useTranslation(); const dispatch = useAppDispatch(); + const isBusy = useCanvasIsBusy(); const selectValidActions = useMemo( () => createMemoizedSelector(selectCanvasSlice, (canvas) => { @@ -39,13 +41,15 @@ export const RegionalGuidanceMenuItemsAddPromptsAndIPAdapter = memo(() => { return ( <> - + {t('controlLayers.addPositivePrompt')} - + {t('controlLayers.addNegativePrompt')} - {t('controlLayers.addIPAdapter')} + + {t('controlLayers.addIPAdapter')} + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx index 8555b65e647..3d6c4c213e5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx @@ -2,6 +2,7 @@ import { MenuItem } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy'; import { entityArrangedBackwardOne, entityArrangedForwardOne, @@ -55,6 +56,7 @@ export const CanvasEntityMenuItemsArrange = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const entityIdentifier = useEntityIdentifierContext(); + const isBusy = useCanvasIsBusy(); const selectValidActions = useMemo( () => createMemoizedSelector(selectCanvasSlice, (canvas) => { @@ -86,16 +88,24 @@ export const CanvasEntityMenuItemsArrange = memo(() => { return ( <> - }> + }> {t('controlLayers.moveToFront')} - }> + } + > {t('controlLayers.moveForward')} - }> + } + > {t('controlLayers.moveBackward')} - }> + }> {t('controlLayers.moveToBack')} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDelete.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDelete.tsx index 29b6ec5c2f9..dd392b4ac52 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDelete.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDelete.tsx @@ -1,6 +1,7 @@ import { MenuItem } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy'; import { entityDeleted } from 'features/controlLayers/store/canvasSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -10,13 +11,14 @@ export const CanvasEntityMenuItemsDelete = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const entityIdentifier = useEntityIdentifierContext(); + const isBusy = useCanvasIsBusy(); const deleteEntity = useCallback(() => { dispatch(entityDeleted({ entityIdentifier })); }, [dispatch, entityIdentifier]); return ( - } color="error.300"> + } isDestructive isDisabled={isBusy}> {t('common.delete')} ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate.tsx index 36a4f18fd55..9aedd711002 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate.tsx @@ -1,6 +1,7 @@ import { MenuItem } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy'; import { entityDuplicated } from 'features/controlLayers/store/canvasSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -10,13 +11,14 @@ export const CanvasEntityMenuItemsDuplicate = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const entityIdentifier = useEntityIdentifierContext(); + const isBusy = useCanvasIsBusy(); const onClick = useCallback(() => { dispatch(entityDuplicated({ entityIdentifier })); }, [dispatch, entityIdentifier]); return ( - }> + } isDisabled={isBusy}> {t('controlLayers.duplicate')} ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsFilter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsFilter.tsx index 514f3b9bf19..3d9773ce8ab 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsFilter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsFilter.tsx @@ -1,6 +1,7 @@ import { MenuItem } from '@invoke-ai/ui-library'; import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiShootingStarBold } from 'react-icons/pi'; @@ -9,13 +10,14 @@ export const CanvasEntityMenuItemsFilter = memo(() => { const { t } = useTranslation(); const canvasManager = useCanvasManager(); const entityIdentifier = useEntityIdentifierContext(); + const isBusy = useCanvasIsBusy(); const onClick = useCallback(() => { canvasManager.filter.initialize(entityIdentifier); - }, [entityIdentifier, canvasManager.filter]); + }, [canvasManager.filter, entityIdentifier]); return ( - }> + } isDisabled={isBusy}> {t('controlLayers.filter.filter')} ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsTransform.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsTransform.tsx index e3fdac1f721..6d0b5f1900c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsTransform.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsTransform.tsx @@ -1,7 +1,6 @@ import { MenuItem } from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; -import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy'; import { useEntityAdapter } from 'features/controlLayers/hooks/useEntityAdapter'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -10,16 +9,15 @@ import { PiFrameCornersBold } from 'react-icons/pi'; export const CanvasEntityMenuItemsTransform = memo(() => { const { t } = useTranslation(); const entityIdentifier = useEntityIdentifierContext(); - const canvasManager = useCanvasManager(); const adapter = useEntityAdapter(entityIdentifier); - const isTransforming = useStore(canvasManager.stateApi.$isTranforming); + const isBusy = useCanvasIsBusy(); const onClick = useCallback(() => { adapter.transformer.startTransform(); }, [adapter.transformer]); return ( - } isDisabled={isTransforming}> + } isDisabled={isBusy}> {t('controlLayers.transform.transform')} ); diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasIsBusy.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasIsBusy.ts new file mode 100644 index 00000000000..25341ace54d --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasIsBusy.ts @@ -0,0 +1,9 @@ +import { useStore } from '@nanostores/react'; +import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; + +export const useCanvasIsBusy = () => { + const canvasManager = useCanvasManager(); + const isBusy = useStore(canvasManager.$isBusy); + + return isBusy; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts index cd6fedf5d7e..40e99c4ec73 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts @@ -126,7 +126,8 @@ export class CanvasBboxModule extends CanvasModuleBase { this.konva.group.visible(true); // We need to reach up to the preview layer to enable/disable listening so that the bbox can be interacted with. - this.manager.konva.previewLayer.listening(tool === 'bbox'); + // If the mangaer is busy, we disable listening so the bbox cannot be interacted with. + this.manager.konva.previewLayer.listening(tool === 'bbox' && !this.manager.$isBusy.get()); this.konva.proxyRect.setAttrs({ x, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityLayerAdapter.ts index 9d5301315b2..4c881d5f4aa 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityLayerAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityLayerAdapter.ts @@ -207,6 +207,10 @@ export class CanvasEntityLayerAdapter extends CanvasModuleBase { return null; }; + isInteractable = (): boolean => { + return this.state.isEnabled && !this.state.isLocked; + }; + destroy = (): void => { this.log.debug('Destroying module'); this.renderer.destroy(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityMaskAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityMaskAdapter.ts index b9b7f467233..546ce2170e7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityMaskAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityMaskAdapter.ts @@ -170,6 +170,10 @@ export class CanvasEntityMaskAdapter extends CanvasModuleBase { return canvas; }; + isInteractable = (): boolean => { + return this.state.isEnabled && !this.state.isLocked; + }; + destroy = () => { this.log.debug('Destroying module'); this.transformer.destroy(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts index 3e8b1959563..b34015a20bb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts @@ -500,6 +500,13 @@ export class CanvasEntityTransformer extends CanvasModuleBase { syncInteractionState = () => { this.log.trace('Syncing interaction state'); + if (this.manager.$isBusy.get()) { + // If the canvas is busy, we can't interact with the transformer + this.parent.konva.layer.listening(false); + this.setInteractionMode('off'); + return; + } + const pixelRect = this.$pixelRect.get(); const isPendingRectCalculation = this.$isPendingRectCalculation.get(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 84ad45e9523..a7c3b88a296 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -16,7 +16,8 @@ import { CanvasToolModule } from 'features/controlLayers/konva/CanvasToolModule' import { CanvasWorkerModule } from 'features/controlLayers/konva/CanvasWorkerModule.js'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import Konva from 'konva'; -import { atom } from 'nanostores'; +import type { Atom } from 'nanostores'; +import { atom, computed } from 'nanostores'; import type { Logger } from 'roarr'; import { CanvasBackgroundModule } from './CanvasBackgroundModule'; @@ -73,6 +74,11 @@ export class CanvasManager extends CanvasModuleBase { _isDebugging: boolean = false; + /** + * Whether the canvas is currently busy with a transformation or filtering operation. + */ + $isBusy: Atom; + constructor(stage: Konva.Stage, container: HTMLDivElement, store: AppStore, socket: AppSocket) { super(); this.id = getPrefixedId(this.type); @@ -120,6 +126,10 @@ export class CanvasManager extends CanvasModuleBase { this.konva.previewLayer.add(this.progressImage.konva.group); this.konva.previewLayer.add(this.bbox.konva.group); this.konva.previewLayer.add(this.tool.konva.group); + + this.$isBusy = computed([this.filter.$isFiltering, this.stateApi.$isTranforming], (isFiltering, isTransforming) => { + return isFiltering || isTransforming; + }); } enableDebugging() { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts index db0b888fd38..458a6943bf7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts @@ -71,6 +71,7 @@ export class CanvasRenderingModule extends CanvasModuleBase { await this.renderInpaintMasks(state, prevState); await this.renderBbox(state, prevState); this.arrangeEntities(state, prevState); + this.manager.tool.syncCursorStyle(); }; renderSettings = () => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts index 9daae1b5ff1..d35cb0daaa8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts @@ -1,3 +1,4 @@ +import type { Property } from 'csstype'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { getPrefixedId, getRectUnion } from 'features/controlLayers/konva/util'; @@ -306,6 +307,10 @@ export class CanvasStageModule extends CanvasModuleBase { return pixels / this.getScale(); }; + setCursor = (cursor: Property.Cursor) => { + this.container.style.cursor = cursor; + }; + setIsDraggable = (isDraggable: boolean) => { this.konva.stage.draggable(isDraggable); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts index cfbb73c1fcf..1efa56c7560 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts @@ -143,46 +143,29 @@ export class CanvasToolModule extends CanvasModuleBase { }; syncCursorStyle = () => { + this.log.trace('Syncing cursor style'); const stage = this.manager.stage; - const renderedEntityCount = this.manager.stateApi.getRenderedEntityCount(); - const selectedEntity = this.manager.stateApi.getSelectedEntity(); const isMouseDown = this.$isMouseDown.get(); const tool = this.$tool.get(); - const isDrawable = - !!selectedEntity && - selectedEntity.state.isEnabled && - !selectedEntity.state.isLocked && - isDrawableEntity(selectedEntity.state); - - // Update the stage's pointer style - if (this.manager.stateApi.$isTranforming.get() || renderedEntityCount === 0) { - // We are transforming and/or have no layers, so we should not render any tool - stage.container.style.cursor = 'default'; - } else if (tool === 'view') { - // view tool gets a hand - stage.container.style.cursor = isMouseDown ? 'grabbing' : 'grab'; - // Bbox tool gets default - } else if (tool === 'bbox') { - stage.container.style.cursor = 'default'; - } else if (tool === 'colorPicker') { - // Color picker gets none - stage.container.style.cursor = 'none'; - } else if (isDrawable) { - if (tool === 'move') { - // Move gets default arrow - stage.container.style.cursor = 'default'; - } else if (tool === 'rect') { - // Rect gets a crosshair - stage.container.style.cursor = 'crosshair'; - } else if (tool === 'brush' || tool === 'eraser') { - // Hide the native cursor and use the konva-rendered brush preview - stage.container.style.cursor = 'none'; - } + if (tool === 'view') { + stage.setCursor(isMouseDown ? 'grabbing' : 'grab'); + } else if (this.manager.stateApi.getRenderedEntityCount() === 0) { + stage.setCursor('not-allowed'); + } else if (this.manager.stateApi.$isTranforming.get()) { + stage.setCursor('not-allowed'); + } else if (this.manager.filter.$isFiltering.get()) { + stage.setCursor('not-allowed'); + } else if (!this.manager.stateApi.getSelectedEntity()?.adapter.isInteractable()) { + stage.setCursor('not-allowed'); + } else if (tool === 'colorPicker' || tool === 'brush' || tool === 'eraser') { + stage.setCursor('none'); + } else if (tool === 'move' || tool === 'bbox') { + stage.setCursor('default'); + } else if (tool === 'rect') { + stage.setCursor('crosshair'); } else { - // isDrawable === 'false' - // Non-drawable layers don't have tools - stage.container.style.cursor = 'not-allowed'; + stage.setCursor('not-allowed'); } }; @@ -302,7 +285,25 @@ export class CanvasToolModule extends CanvasModuleBase { }; }; + getCanDraw = (): boolean => { + if (this.manager.stateApi.getRenderedEntityCount() === 0) { + return false; + } else if (this.manager.stateApi.$isTranforming.get()) { + return false; + } else if (this.manager.filter.$isFiltering.get()) { + return false; + } else if (!this.manager.stateApi.getSelectedEntity()?.adapter.isInteractable()) { + return false; + } else { + return true; + } + }; + onStageMouseEnter = async (_: KonvaEventObject) => { + if (!this.getCanDraw()) { + return; + } + const cursorPos = this.syncLastCursorPos(); try { const isMouseDown = this.$isMouseDown.get(); @@ -356,6 +357,10 @@ export class CanvasToolModule extends CanvasModuleBase { }; onStageMouseDown = async (e: KonvaEventObject) => { + if (!this.getCanDraw()) { + return; + } + this.$isMouseDown.set(getIsPrimaryMouseDown(e)); const cursorPos = this.syncLastCursorPos(); @@ -473,6 +478,10 @@ export class CanvasToolModule extends CanvasModuleBase { }; onStageMouseUp = (_: KonvaEventObject) => { + if (!this.getCanDraw()) { + return; + } + try { this.$isMouseDown.set(false); const cursorPos = this.syncLastCursorPos(); @@ -516,6 +525,10 @@ export class CanvasToolModule extends CanvasModuleBase { }; onStageMouseMove = async (_: KonvaEventObject) => { + if (!this.getCanDraw()) { + return; + } + try { const tool = this.$tool.get(); const cursorPos = this.syncLastCursorPos(); @@ -595,6 +608,10 @@ export class CanvasToolModule extends CanvasModuleBase { }; onStageMouseLeave = (_: KonvaEventObject) => { + if (!this.getCanDraw()) { + return; + } + this.$lastCursorPos.set(null); this.$lastMouseDownPos.set(null); const selectedEntity = this.manager.stateApi.getSelectedEntity(); @@ -607,6 +624,10 @@ export class CanvasToolModule extends CanvasModuleBase { }; onStageMouseWheel = (e: KonvaEventObject) => { + if (!this.getCanDraw()) { + return; + } + e.evt.preventDefault(); if (!e.evt.ctrlKey && !e.evt.metaKey) { From 376a6264192bad3cfdf5eb5fa32f3aca70d9c517 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 3 Sep 2024 22:09:33 +1000 Subject: [PATCH 626/678] fix(ui): unable to drag while transforming after switching tools --- .../konva/CanvasEntityTransformer.ts | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts index b34015a20bb..8702ff4b514 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts @@ -500,8 +500,15 @@ export class CanvasEntityTransformer extends CanvasModuleBase { syncInteractionState = () => { this.log.trace('Syncing interaction state'); - if (this.manager.$isBusy.get()) { - // If the canvas is busy, we can't interact with the transformer + if (this.manager.filter.$isFiltering.get()) { + // May not interact with the entity when the filter is active + this.parent.konva.layer.listening(false); + this.setInteractionMode('off'); + return; + } + + if (this.manager.stateApi.$isTranforming.get() && !this.$isTransforming.get()) { + // If another entity is being transformed, we can't interact with this transformer this.parent.konva.layer.listening(false); this.setInteractionMode('off'); return; @@ -520,7 +527,7 @@ export class CanvasEntityTransformer extends CanvasModuleBase { const tool = this.manager.tool.$tool.get(); const isSelected = this.manager.stateApi.getIsSelected(this.parent.id); - if (!this.parent.renderer.hasObjects() || this.parent.state.isLocked || !this.parent.state.isEnabled) { + if (!this.parent.renderer.hasObjects() || !this.parent.isInteractable()) { // The layer is totally empty, we can just disable the layer this.parent.konva.layer.listening(false); this.setInteractionMode('off'); @@ -531,15 +538,18 @@ export class CanvasEntityTransformer extends CanvasModuleBase { // We are moving this layer, it must be listening this.parent.konva.layer.listening(true); this.setInteractionMode('drag'); - } else if (isSelected && this.$isTransforming.get()) { + return; + } + + if (isSelected && this.$isTransforming.get()) { // When transforming, we want the stage to still be movable if the view tool is selected. If the transformer is // active, it will interrupt the stage drag events. So we should disable listening when the view tool is selected. - if (tool !== 'view') { - this.parent.konva.layer.listening(true); - this.setInteractionMode('all'); - } else { + if (tool === 'view') { this.parent.konva.layer.listening(false); this.setInteractionMode('off'); + } else { + this.parent.konva.layer.listening(true); + this.setInteractionMode('all'); } } else { // The layer is not selected, or we are using a tool that doesn't need the layer to be listening - disable interaction stuff From 5eb9e2ba043baa0755f2387bba3140e97c427db2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 4 Sep 2024 08:46:15 +1000 Subject: [PATCH 627/678] tidy(ui): disable isDebugging flag on root component --- invokeai/frontend/web/src/main.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/main.tsx b/invokeai/frontend/web/src/main.tsx index 0479a70fbff..acf94917780 100644 --- a/invokeai/frontend/web/src/main.tsx +++ b/invokeai/frontend/web/src/main.tsx @@ -2,4 +2,4 @@ import ReactDOM from 'react-dom/client'; import InvokeAIUI from './app/components/InvokeAIUI'; -ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(); +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(); From 0a22e6b5a1a7269d5a5341edfc04e2215e1d921d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 4 Sep 2024 08:56:19 +1000 Subject: [PATCH 628/678] feat(ui): clean up unused tool module state --- .../konva/CanvasBrushToolPreview.ts | 2 +- .../konva/CanvasColorPickerToolPreview.ts | 2 +- .../konva/CanvasEraserToolPreview.ts | 2 +- .../controlLayers/konva/CanvasToolModule.ts | 33 ++++--------------- 4 files changed, 9 insertions(+), 30 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushToolPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushToolPreview.ts index 7f3d5230c19..caa164a5d36 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushToolPreview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushToolPreview.ts @@ -87,7 +87,7 @@ export class CanvasBrushToolPreview extends CanvasModuleBase { } render = () => { - const cursorPos = this.manager.tool.$lastCursorPos.get(); + const cursorPos = this.manager.tool.$cursorPos.get(); // If the cursor position is not available, do not update the brush preview. The tool module will handle visiblity. if (!cursorPos) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasColorPickerToolPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasColorPickerToolPreview.ts index e877eba3330..e8f4c57a584 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasColorPickerToolPreview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasColorPickerToolPreview.ts @@ -191,7 +191,7 @@ export class CanvasColorPickerToolPreview extends CanvasModuleBase { * Renders the color picker tool preview on the canvas. */ render = () => { - const cursorPos = this.manager.tool.$lastCursorPos.get(); + const cursorPos = this.manager.tool.$cursorPos.get(); // If the cursor position is not available, do not render the preview. The tool module will handle visibility. if (!cursorPos) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserToolPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserToolPreview.ts index 0e6c2146916..6128e79febc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserToolPreview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserToolPreview.ts @@ -78,7 +78,7 @@ export class CanvasEraserToolPreview extends CanvasModuleBase { } render = () => { - const cursorPos = this.manager.tool.$lastCursorPos.get(); + const cursorPos = this.manager.tool.$cursorPos.get(); if (!cursorPos) { return; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts index 1efa56c7560..b99fc5f1508 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts @@ -61,22 +61,14 @@ export class CanvasToolModule extends CanvasModuleBase { * hold-to-activate tools, like the view or color picker tools. */ $toolBuffer = atom(null); - /** - * The last point added to the current entity. - */ - $lastAddedPoint = atom(null); /** * Whether the mouse is currently down. */ $isMouseDown = atom(false); - /** - * The last position where the mouse was down. - */ - $lastMouseDownPos = atom(null); /** * The last cursor position. */ - $lastCursorPos = atom(null); + $cursorPos = atom(null); /** * The color currently under the cursor. Only has a value when the color picker tool is active. */ @@ -152,9 +144,7 @@ export class CanvasToolModule extends CanvasModuleBase { stage.setCursor(isMouseDown ? 'grabbing' : 'grab'); } else if (this.manager.stateApi.getRenderedEntityCount() === 0) { stage.setCursor('not-allowed'); - } else if (this.manager.stateApi.$isTranforming.get()) { - stage.setCursor('not-allowed'); - } else if (this.manager.filter.$isFiltering.get()) { + } else if (this.manager.$isBusy.get()) { stage.setCursor('not-allowed'); } else if (!this.manager.stateApi.getSelectedEntity()?.adapter.isInteractable()) { stage.setCursor('not-allowed'); @@ -173,7 +163,7 @@ export class CanvasToolModule extends CanvasModuleBase { const stage = this.manager.stage; const renderedEntityCount = this.manager.stateApi.getRenderedEntityCount(); const selectedEntity = this.manager.stateApi.getSelectedEntity(); - const cursorPos = this.$lastCursorPos.get(); + const cursorPos = this.$cursorPos.get(); const tool = this.$tool.get(); const isDrawable = @@ -207,7 +197,7 @@ export class CanvasToolModule extends CanvasModuleBase { syncLastCursorPos = (): Coordinate | null => { const pos = getScaledCursorPosition(this.konva.stage); - this.$lastCursorPos.set(pos); + this.$cursorPos.set(pos); return pos; }; @@ -331,7 +321,6 @@ export class CanvasToolModule extends CanvasModuleBase { color: this.manager.stateApi.getCurrentColor(), clip: this.getClip(selectedEntity.state), }); - this.$lastAddedPoint.set(alignedPoint); return; } @@ -348,7 +337,6 @@ export class CanvasToolModule extends CanvasModuleBase { strokeWidth: settings.eraserWidth, clip: this.getClip(selectedEntity.state), }); - this.$lastAddedPoint.set(alignedPoint); return; } } finally { @@ -421,7 +409,6 @@ export class CanvasToolModule extends CanvasModuleBase { clip: this.getClip(selectedEntity.state), }); } - this.$lastAddedPoint.set(alignedPoint); } if (tool === 'eraser') { @@ -457,7 +444,6 @@ export class CanvasToolModule extends CanvasModuleBase { clip: this.getClip(selectedEntity.state), }); } - this.$lastAddedPoint.set(alignedPoint); } if (tool === 'rect') { @@ -472,7 +458,6 @@ export class CanvasToolModule extends CanvasModuleBase { }); } } finally { - this.$lastMouseDownPos.set(cursorPos); this.render(); } }; @@ -519,7 +504,6 @@ export class CanvasToolModule extends CanvasModuleBase { } } } finally { - this.$lastMouseDownPos.set(null); this.render(); } }; @@ -574,7 +558,6 @@ export class CanvasToolModule extends CanvasModuleBase { bufferState.points.push(alignedPoint.x, alignedPoint.y); await selectedEntity.adapter.renderer.setBuffer(bufferState); - this.$lastAddedPoint.set(alignedPoint); } else if (tool === 'eraser' && bufferState.type === 'eraser_line') { const lastPoint = getLastPointOfLine(bufferState.points); const minDistance = settings.eraserWidth * this.config.BRUSH_SPACING_TARGET_SCALE; @@ -592,7 +575,6 @@ export class CanvasToolModule extends CanvasModuleBase { bufferState.points.push(alignedPoint.x, alignedPoint.y); await selectedEntity.adapter.renderer.setBuffer(bufferState); - this.$lastAddedPoint.set(alignedPoint); } else if (tool === 'rect' && bufferState.type === 'rect') { const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position); const alignedPoint = floorCoord(normalizedPoint); @@ -612,8 +594,7 @@ export class CanvasToolModule extends CanvasModuleBase { return; } - this.$lastCursorPos.set(null); - this.$lastMouseDownPos.set(null); + this.$cursorPos.set(null); const selectedEntity = this.manager.stateApi.getSelectedEntity(); if (selectedEntity && selectedEntity.adapter.renderer.bufferState?.type !== 'rect') { @@ -676,7 +657,6 @@ export class CanvasToolModule extends CanvasModuleBase { const selectedEntity = this.manager.stateApi.getSelectedEntity(); if (selectedEntity) { selectedEntity.adapter.renderer.clearBuffer(); - this.$lastMouseDownPos.set(null); } return; } @@ -686,8 +666,7 @@ export class CanvasToolModule extends CanvasModuleBase { this.$toolBuffer.set(this.$tool.get()); this.$tool.set('view'); this.manager.stateApi.$spaceKey.set(true); - this.$lastCursorPos.set(null); - this.$lastMouseDownPos.set(null); + this.$cursorPos.set(null); return; } From ad53169ebafe6d4df113800fc992dc42b9c0ab81 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 4 Sep 2024 15:50:16 +1000 Subject: [PATCH 629/678] tidy(ui): remove extraneous docstrings --- .../konva/CanvasEntityTransformer.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts index 8702ff4b514..04ee00913c0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts @@ -89,8 +89,6 @@ export class CanvasEntityTransformer extends CanvasModuleBase { /** * The rect of the parent, _including_ transparent regions. * It is calculated via Konva's getClientRect method, which is fast but includes transparent regions. - * - * Stored as a nanostores atom for easy reactivity. */ $nodeRect = atom(getEmptyRect()); @@ -99,15 +97,11 @@ export class CanvasEntityTransformer extends CanvasModuleBase { * If the parent's nodes have no possibility of transparent regions, this will be calculated the same way as nodeRect. * If the parent's nodes may have transparent regions, this will be calculated manually by rasterizing the parent and * checking the pixel data. - * - * Stored as a nanostores atom for easy reactivity. */ $pixelRect = atom(getEmptyRect()); /** * Whether the transformer is currently calculating the rect of the parent. - * - * Stored as a nanostores atom for easy reactivity. */ $isPendingRectCalculation = atom(true); @@ -118,8 +112,6 @@ export class CanvasEntityTransformer extends CanvasModuleBase { /** * Whether the transformer is currently transforming the entity. - * - * Stored as a nanostores atom for easy reactivity. */ $isTransforming = atom(false); @@ -128,29 +120,21 @@ export class CanvasEntityTransformer extends CanvasModuleBase { * - 'all': The entity can be moved, resized, and rotated. * - 'drag': The entity can be moved. * - 'off': The transformer is not interactable. - * - * Stored as a nanostores atom for easy reactivity. */ $interactionMode = atom<'all' | 'drag' | 'off'>('off'); /** * Whether dragging is enabled. Dragging is enabled in both 'all' and 'drag' interaction modes. - * - * Stored as a nanostores atom for easy reactivity. */ $isDragEnabled = atom(false); /** * Whether transforming is enabled. Transforming is enabled only in 'all' interaction mode. - * - * Stored as a nanostores atom for easy reactivity. */ $isTransformEnabled = atom(false); /** * Whether the transformer is currently processing (rasterizing and uploading) the transformed entity. - * - * Stored as a nanostores atom for easy reactivity. */ $isProcessing = atom(false); From 736fabe327026d42e4dff92c61b9dfd118fb4880 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 4 Sep 2024 15:51:08 +1000 Subject: [PATCH 630/678] tidy(ui): remove commented code --- .../konva/CanvasEntityTransformer.ts | 38 ------------------- 1 file changed, 38 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts index 04ee00913c0..e04315b7fcd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts @@ -405,44 +405,6 @@ export class CanvasEntityTransformer extends CanvasModuleBase { this.manager.stateApi.setEntityPosition({ entityIdentifier: this.parent.getEntityIdentifier(), position }); }; - // TODO(psyche): These don't work when the entity is rotated, need to do some math to offset the flip after rotation - // flipHorizontal = () => { - // if (!this.isTransforming || this.$isProcessing.get()) { - // return; - // } - - // // Flipping horizontally = flipping across the vertical axis: - // // - Flip by negating the x scale - // // - Restore position by translating the rect rightwards by the width of the rect - // const x = this.konva.proxyRect.x(); - // const width = this.konva.proxyRect.width(); - // const scaleX = this.konva.proxyRect.scaleX(); - // this.konva.proxyRect.setAttrs({ - // scaleX: -scaleX, - // x: x + width * scaleX, - // }); - - // this.syncObjectGroupWithProxyRect(); - // }; - - // flipVertical = () => { - // if (!this.isTransforming || this.$isProcessing.get()) { - // return; - // } - - // // Flipping vertically = flipping across the horizontal axis: - // // - Flip by negating the y scale - // // - Restore position by translating the rect downwards by the height of the rect - // const y = this.konva.proxyRect.y(); - // const height = this.konva.proxyRect.height(); - // const scaleY = this.konva.proxyRect.scaleY(); - // this.konva.proxyRect.setAttrs({ - // scaleY: -scaleY, - // y: y + height * scaleY, - // }); - // this.syncObjectGroupWithProxyRect(); - // }; - syncObjectGroupWithProxyRect = () => { this.parent.renderer.konva.objectGroup.setAttrs({ x: this.konva.proxyRect.x(), From 6ba78e76323f1ec54561a2a7996eb382df29bcf3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 4 Sep 2024 16:05:20 +1000 Subject: [PATCH 631/678] tidy(ui): remove unused id on konva nodes --- .../features/controlLayers/konva/CanvasEntityLayerAdapter.ts | 3 --- .../features/controlLayers/konva/CanvasEntityMaskAdapter.ts | 3 --- 2 files changed, 6 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityLayerAdapter.ts index 4c881d5f4aa..fa846b3cd0d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityLayerAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityLayerAdapter.ts @@ -83,9 +83,6 @@ export class CanvasEntityLayerAdapter extends CanvasModuleBase { this.konva = { layer: new Konva.Layer({ - // We need the ID on the layer to help with building the composite initial image - // See `getCompositeLayerStageClone()` - id: this.id, name: `${this.type}:layer`, listening: false, imageSmoothingEnabled: false, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityMaskAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityMaskAdapter.ts index 546ce2170e7..eb5a4fe5fbe 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityMaskAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityMaskAdapter.ts @@ -54,9 +54,6 @@ export class CanvasEntityMaskAdapter extends CanvasModuleBase { this.konva = { layer: new Konva.Layer({ - // We need the ID on the layer to help with building the composite initial image - // See `getCompositeLayerStageClone()` - id: this.id, name: `${this.type}:layer`, listening: false, imageSmoothingEnabled: false, From 73e8837d4571361635dfc1a0327b86130b8fe4d5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 4 Sep 2024 18:17:08 +1000 Subject: [PATCH 632/678] feat(ui): revise entity rendering flow --- .../components/ControlLayer/ControlLayer.tsx | 6 +- .../components/InpaintMask/InpaintMask.tsx | 6 +- .../components/RasterLayer/RasterLayer.tsx | 6 +- .../RegionalGuidance/RegionalGuidance.tsx | 6 +- .../components/Transform/Transform.tsx | 138 ++++++++------- .../contexts/EntityAdapterContext.tsx | 103 ++++++----- .../controlLayers/hooks/useEntityAdapter.ts | 8 +- .../konva/CanvasControlLayerAdapter.ts | 167 ++++++++++++++++++ .../konva/CanvasEntityRenderer.ts | 29 ++- .../konva/CanvasEntityTransformer.ts | 16 +- .../controlLayers/konva/CanvasFilterModule.ts | 15 +- .../konva/CanvasInpaintMaskAdapter.ts | 163 +++++++++++++++++ .../controlLayers/konva/CanvasManager.ts | 21 ++- .../konva/CanvasRasterLayerAdapter.ts | 163 +++++++++++++++++ .../konva/CanvasRegionalGuidanceAdapter.ts | 163 +++++++++++++++++ .../konva/CanvasRenderingModule.ts | 40 +++-- .../konva/CanvasStateApiModule.ts | 25 ++- .../controlLayers/konva/CanvasToolModule.ts | 5 +- .../src/features/controlLayers/konva/util.ts | 18 +- .../src/features/controlLayers/store/types.ts | 1 + 20 files changed, 927 insertions(+), 172 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlLayerAdapter.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMaskAdapter.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasRasterLayerAdapter.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegionalGuidanceAdapter.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx index fe65a54e6be..eb0b250a225 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx @@ -7,7 +7,7 @@ import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/c import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit'; import { ControlLayerBadges } from 'features/controlLayers/components/ControlLayer/ControlLayerBadges'; import { ControlLayerControlAdapter } from 'features/controlLayers/components/ControlLayer/ControlLayerControlAdapter'; -import { EntityLayerAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext'; +import { ControlLayerAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; @@ -21,7 +21,7 @@ export const ControlLayer = memo(({ id }: Props) => { return ( - + @@ -34,7 +34,7 @@ export const ControlLayer = memo(({ id }: Props) => { - + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx index 6f489a14fc5..b941a20c986 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx @@ -4,7 +4,7 @@ import { CanvasEntityHeader } from 'features/controlLayers/components/common/Can import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions'; import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage'; import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit'; -import { EntityMaskAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext'; +import { InpaintMaskAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; @@ -18,7 +18,7 @@ export const InpaintMask = memo(({ id }: Props) => { return ( - + @@ -27,7 +27,7 @@ export const InpaintMask = memo(({ id }: Props) => { - + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx index 493b3cf373c..4a33ac08c05 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx @@ -4,7 +4,7 @@ import { CanvasEntityHeader } from 'features/controlLayers/components/common/Can import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions'; import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage'; import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit'; -import { EntityLayerAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext'; +import { RasterLayerAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; @@ -18,7 +18,7 @@ export const RasterLayer = memo(({ id }: Props) => { return ( - + @@ -27,7 +27,7 @@ export const RasterLayer = memo(({ id }: Props) => { - + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx index 7f86fcda05c..cb4bbba8db2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx @@ -6,7 +6,7 @@ import { CanvasEntityPreviewImage } from 'features/controlLayers/components/comm import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit'; import { RegionalGuidanceBadges } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges'; import { RegionalGuidanceSettings } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings'; -import { EntityMaskAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext'; +import { RegionalGuidanceAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; @@ -20,7 +20,7 @@ export const RegionalGuidance = memo(({ id }: Props) => { return ( - + @@ -31,7 +31,7 @@ export const RegionalGuidance = memo(({ id }: Props) => { - + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Transform/Transform.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Transform/Transform.tsx index ffcfacebef4..bfb9e8a7d57 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Transform/Transform.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Transform/Transform.tsx @@ -1,74 +1,86 @@ import { Button, ButtonGroup, Flex, Heading, Spacer } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; -import type { CanvasEntityLayerAdapter } from 'features/controlLayers/konva/CanvasEntityLayerAdapter'; -import type { CanvasEntityMaskAdapter } from 'features/controlLayers/konva/CanvasEntityMaskAdapter'; +import type { CanvasControlLayerAdapter } from 'features/controlLayers/konva/CanvasControlLayerAdapter'; +import type { CanvasInpaintMaskAdapter } from 'features/controlLayers/konva/CanvasInpaintMaskAdapter'; +import type { CanvasRasterLayerAdapter } from 'features/controlLayers/konva/CanvasRasterLayerAdapter'; +import type { CanvasRegionalGuidanceAdapter } from 'features/controlLayers/konva/CanvasRegionalGuidanceAdapter'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowsCounterClockwiseBold, PiArrowsOutBold, PiCheckBold, PiXBold } from 'react-icons/pi'; -const TransformBox = memo(({ adapter }: { adapter: CanvasEntityLayerAdapter | CanvasEntityMaskAdapter }) => { - const { t } = useTranslation(); - const isProcessing = useStore(adapter.transformer.$isProcessing); +const TransformBox = memo( + ({ + adapter, + }: { + adapter: + | CanvasRasterLayerAdapter + | CanvasControlLayerAdapter + | CanvasInpaintMaskAdapter + | CanvasRegionalGuidanceAdapter; + }) => { + const { t } = useTranslation(); + const isProcessing = useStore(adapter.transformer.$isProcessing); - return ( - - - {t('controlLayers.transform.transform')} - - - - - - - - - - ); -}); + return ( + + + {t('controlLayers.transform.transform')} + + + + + + + + + + ); + } +); TransformBox.displayName = 'Transform'; diff --git a/invokeai/frontend/web/src/features/controlLayers/contexts/EntityAdapterContext.tsx b/invokeai/frontend/web/src/features/controlLayers/contexts/EntityAdapterContext.tsx index 42f99875a3b..75e0563b46e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/contexts/EntityAdapterContext.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/contexts/EntityAdapterContext.tsx @@ -1,27 +1,24 @@ -import type { SyncableMap } from 'common/util/SyncableMap/SyncableMap'; import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import type { CanvasEntityLayerAdapter } from 'features/controlLayers/konva/CanvasEntityLayerAdapter'; -import type { CanvasEntityMaskAdapter } from 'features/controlLayers/konva/CanvasEntityMaskAdapter'; +import type { CanvasControlLayerAdapter } from 'features/controlLayers/konva/CanvasControlLayerAdapter'; +import type { CanvasInpaintMaskAdapter } from 'features/controlLayers/konva/CanvasInpaintMaskAdapter'; +import type { CanvasRasterLayerAdapter } from 'features/controlLayers/konva/CanvasRasterLayerAdapter'; +import type { CanvasRegionalGuidanceAdapter } from 'features/controlLayers/konva/CanvasRegionalGuidanceAdapter'; import type { PropsWithChildren } from 'react'; import { createContext, memo, useContext, useMemo, useSyncExternalStore } from 'react'; import { assert } from 'tsafe'; -const EntityAdapterContext = createContext(null); +const EntityAdapterContext = createContext< + CanvasRasterLayerAdapter | CanvasControlLayerAdapter | CanvasInpaintMaskAdapter | CanvasRegionalGuidanceAdapter | null +>(null); -export const EntityLayerAdapterGate = memo(({ children }: PropsWithChildren) => { +export const RasterLayerAdapterGate = memo(({ children }: PropsWithChildren) => { const canvasManager = useCanvasManager(); const entityIdentifier = useEntityIdentifierContext(); - const store = useMemo>(() => { - if (entityIdentifier.type === 'raster_layer') { - return canvasManager.adapters.rasterLayers; - } - if (entityIdentifier.type === 'control_layer') { - return canvasManager.adapters.controlLayers; - } - assert(false, 'Unknown entity type'); - }, [canvasManager.adapters.controlLayers, canvasManager.adapters.rasterLayers, entityIdentifier.type]); - const adapters = useSyncExternalStore(store.subscribe, store.getSnapshot); + const adapters = useSyncExternalStore( + canvasManager.adapters.rasterLayers.subscribe, + canvasManager.adapters.rasterLayers.getSnapshot + ); const adapter = useMemo(() => { return adapters.get(entityIdentifier.id) ?? null; }, [adapters, entityIdentifier.id]); @@ -33,28 +30,35 @@ export const EntityLayerAdapterGate = memo(({ children }: PropsWithChildren) => return {children}; }); -EntityLayerAdapterGate.displayName = 'EntityLayerAdapterGate'; +RasterLayerAdapterGate.displayName = 'RasterLayerAdapterGate'; -// export const useEntityLayerAdapter = (): CanvasLayerAdapter => { -// const adapter = useContext(EntityAdapterContext); -// assert(adapter, 'useEntityLayerAdapter must be used within a EntityLayerAdapterGate'); -// assert(adapter.type === 'layer_adapter', 'useEntityLayerAdapter must be used with a layer adapter'); -// return adapter; -// }; +export const ControlLayerAdapterGate = memo(({ children }: PropsWithChildren) => { + const canvasManager = useCanvasManager(); + const entityIdentifier = useEntityIdentifierContext(); + const adapters = useSyncExternalStore( + canvasManager.adapters.controlLayers.subscribe, + canvasManager.adapters.controlLayers.getSnapshot + ); + const adapter = useMemo(() => { + return adapters.get(entityIdentifier.id) ?? null; + }, [adapters, entityIdentifier.id]); + + if (!adapter) { + return null; + } + + return {children}; +}); + +ControlLayerAdapterGate.displayName = 'ControlLayerAdapterGate'; -export const EntityMaskAdapterGate = memo(({ children }: PropsWithChildren) => { +export const InpaintMaskAdapterGate = memo(({ children }: PropsWithChildren) => { const canvasManager = useCanvasManager(); const entityIdentifier = useEntityIdentifierContext(); - const store = useMemo>(() => { - if (entityIdentifier.type === 'inpaint_mask') { - return canvasManager.adapters.inpaintMasks; - } - if (entityIdentifier.type === 'regional_guidance') { - return canvasManager.adapters.regionMasks; - } - assert(false, 'Unknown entity type'); - }, [canvasManager.adapters.inpaintMasks, canvasManager.adapters.regionMasks, entityIdentifier.type]); - const adapters = useSyncExternalStore(store.subscribe, store.getSnapshot); + const adapters = useSyncExternalStore( + canvasManager.adapters.inpaintMasks.subscribe, + canvasManager.adapters.inpaintMasks.getSnapshot + ); const adapter = useMemo(() => { return adapters.get(entityIdentifier.id) ?? null; }, [adapters, entityIdentifier.id]); @@ -66,16 +70,33 @@ export const EntityMaskAdapterGate = memo(({ children }: PropsWithChildren) => { return {children}; }); -EntityMaskAdapterGate.displayName = 'EntityMaskAdapterGate'; +InpaintMaskAdapterGate.displayName = 'InpaintMaskAdapterGate'; + +export const RegionalGuidanceAdapterGate = memo(({ children }: PropsWithChildren) => { + const canvasManager = useCanvasManager(); + const entityIdentifier = useEntityIdentifierContext(); + const adapters = useSyncExternalStore( + canvasManager.adapters.regionMasks.subscribe, + canvasManager.adapters.regionMasks.getSnapshot + ); + const adapter = useMemo(() => { + return adapters.get(entityIdentifier.id) ?? null; + }, [adapters, entityIdentifier.id]); + + if (!adapter) { + return null; + } + + return {children}; +}); -// export const useEntityMaskAdapter = (): CanvasMaskAdapter => { -// const adapter = useContext(EntityAdapterContext); -// assert(adapter, 'useEntityMaskAdapter must be used within a CanvasMaskAdapterGate'); -// assert(adapter.type === 'mask_adapter', 'useEntityMaskAdapter must be used with a mask adapter'); -// return adapter; -// }; +RegionalGuidanceAdapterGate.displayName = 'RegionalGuidanceAdapterGate'; -export const useEntityAdapter = (): CanvasEntityLayerAdapter | CanvasEntityMaskAdapter => { +export const useEntityAdapter = (): + | CanvasRasterLayerAdapter + | CanvasControlLayerAdapter + | CanvasInpaintMaskAdapter + | CanvasRegionalGuidanceAdapter => { const adapter = useContext(EntityAdapterContext); assert(adapter, 'useEntityAdapter must be used within a CanvasRasterLayerAdapterGate'); return adapter; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityAdapter.ts index 246156e8eed..24c689fc9ef 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityAdapter.ts @@ -1,13 +1,15 @@ import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; -import type { CanvasEntityLayerAdapter } from 'features/controlLayers/konva/CanvasEntityLayerAdapter'; -import type { CanvasEntityMaskAdapter } from 'features/controlLayers/konva/CanvasEntityMaskAdapter'; +import type { CanvasControlLayerAdapter } from 'features/controlLayers/konva/CanvasControlLayerAdapter'; +import type { CanvasInpaintMaskAdapter } from 'features/controlLayers/konva/CanvasInpaintMaskAdapter'; +import type { CanvasRasterLayerAdapter } from 'features/controlLayers/konva/CanvasRasterLayerAdapter'; +import type { CanvasRegionalGuidanceAdapter } from 'features/controlLayers/konva/CanvasRegionalGuidanceAdapter'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; import { assert } from 'tsafe'; export const useEntityAdapter = ( entityIdentifier: CanvasEntityIdentifier -): CanvasEntityLayerAdapter | CanvasEntityMaskAdapter => { +): CanvasRasterLayerAdapter | CanvasControlLayerAdapter | CanvasInpaintMaskAdapter | CanvasRegionalGuidanceAdapter => { const canvasManager = useCanvasManager(); const adapter = useMemo(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlLayerAdapter.ts new file mode 100644 index 00000000000..576b5928548 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlLayerAdapter.ts @@ -0,0 +1,167 @@ +import type { SerializableObject } from 'common/types'; +import { deepClone } from 'common/util/deepClone'; +import { CanvasEntityRenderer } from 'features/controlLayers/konva/CanvasEntityRenderer'; +import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntityTransformer'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; +import type { CanvasControlLayerState, CanvasEntityIdentifier, Rect } from 'features/controlLayers/store/types'; +import Konva from 'konva'; +import type { GroupConfig } from 'konva/lib/Group'; +import { omit } from 'lodash-es'; +import type { Logger } from 'roarr'; +import stableHash from 'stable-hash'; +import { assert } from 'tsafe'; + +export class CanvasControlLayerAdapter extends CanvasModuleBase { + readonly type = 'control_layer_adapter'; + readonly id: string; + readonly path: string[]; + readonly manager: CanvasManager; + readonly parent: CanvasManager; + readonly log: Logger; + + entityIdentifier: CanvasEntityIdentifier<'control_layer'>; + + /** + * The last known state of the entity. + */ + private _state: CanvasControlLayerState | null = null; + + /** + * The Konva nodes that make up the entity layer: + * - A layer to hold the everything + * + * Note that the transformer and object renderer have their own Konva nodes, but they are not stored here. + */ + konva: { + layer: Konva.Layer; + }; + + /** + * The transformer for this entity layer. + */ + transformer: CanvasEntityTransformer; + + /** + * The renderer for this entity layer. + */ + renderer: CanvasEntityRenderer; + + constructor(entityIdentifier: CanvasEntityIdentifier<'control_layer'>, manager: CanvasManager) { + super(); + this.id = entityIdentifier.id; + this.entityIdentifier = entityIdentifier; + this.manager = manager; + this.parent = manager; + this.path = this.manager.buildPath(this); + this.log = this.manager.buildLogger(this); + + this.log.debug('Creating module'); + + this.konva = { + layer: new Konva.Layer({ + name: `${this.type}:layer`, + listening: false, + imageSmoothingEnabled: false, + }), + }; + + this.renderer = new CanvasEntityRenderer(this); + this.transformer = new CanvasEntityTransformer(this); + } + + get state(): CanvasControlLayerState { + if (this._state) { + return this._state; + } + const state = this.manager.stateApi.getControlLayersState().entities.find((layer) => layer.id === this.id); + assert(state, `State not found for ${this.id}`); + return state; + } + + set state(state: CanvasControlLayerState) { + const prevState = this._state; + this._state = state; + this.render(state, prevState); + } + + private render = async (state: CanvasControlLayerState, prevState: CanvasControlLayerState | null): Promise => { + if (prevState && prevState === state) { + this.log.trace('State unchanged, skipping update'); + return; + } + + if (!prevState || state.isEnabled !== prevState.isEnabled) { + this.log.trace('Updating visibility'); + this.konva.layer.visible(state.isEnabled); + this.renderer.syncCache(state.isEnabled); + } + if (!prevState || state.isLocked !== prevState.isLocked) { + this.transformer.syncInteractionState(); + } + if (!prevState || state.objects !== prevState.objects) { + const didRender = await this.renderer.render(this.state.objects); + if (didRender) { + this.transformer.requestRectCalculation(); + } + } + if (!prevState || state.position !== prevState.position) { + this.transformer.updatePosition(); + } + if (!prevState || state.opacity !== prevState.opacity) { + this.renderer.updateOpacity(state.opacity); + } + if (!prevState || state.withTransparencyEffect !== prevState.withTransparencyEffect) { + this.renderer.updateTransparencyEffect(state.withTransparencyEffect); + } + + if (!prevState) { + // First render + this.transformer.updateBbox(); + } + }; + + getCanvas = (rect?: Rect): HTMLCanvasElement => { + this.log.trace({ rect }, 'Getting canvas'); + // The opacity may have been changed in response to user selecting a different entity category, so we must restore + // the original opacity before rendering the canvas + const attrs: GroupConfig = { opacity: this.state.opacity }; + const canvas = this.renderer.getCanvas(rect, attrs); + return canvas; + }; + + getHashableState = (): SerializableObject => { + const keysToOmit: (keyof CanvasControlLayerState)[] = ['name', 'controlAdapter', 'withTransparencyEffect']; + return omit(this.state, keysToOmit); + }; + + isInteractable = (): boolean => { + return this.state.isEnabled && !this.state.isLocked; + }; + + hash = (extra?: SerializableObject): string => { + const arg = { + state: this.getHashableState(), + extra, + }; + return stableHash(arg); + }; + + destroy = (): void => { + this.log.debug('Destroying module'); + this.renderer.destroy(); + this.transformer.destroy(); + this.konva.layer.destroy(); + }; + + repr = () => { + return { + id: this.id, + type: this.type, + path: this.path, + state: deepClone(this.state), + transformer: this.transformer.repr(), + renderer: this.renderer.repr(), + }; + }; +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts index f03ab1f527a..8c9f8e12d9c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts @@ -1,12 +1,14 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; -import type { CanvasEntityLayerAdapter } from 'features/controlLayers/konva/CanvasEntityLayerAdapter'; -import type { CanvasEntityMaskAdapter } from 'features/controlLayers/konva/CanvasEntityMaskAdapter'; +import type { CanvasControlLayerAdapter } from 'features/controlLayers/konva/CanvasControlLayerAdapter'; +import type { CanvasInpaintMaskAdapter } from 'features/controlLayers/konva/CanvasInpaintMaskAdapter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { CanvasObjectBrushLineRenderer } from 'features/controlLayers/konva/CanvasObjectBrushLineRenderer'; import { CanvasObjectEraserLineRenderer } from 'features/controlLayers/konva/CanvasObjectEraserLineRenderer'; import { CanvasObjectImageRenderer } from 'features/controlLayers/konva/CanvasObjectImageRenderer'; import { CanvasObjectRectRenderer } from 'features/controlLayers/konva/CanvasObjectRectRenderer'; +import type { CanvasRasterLayerAdapter } from 'features/controlLayers/konva/CanvasRasterLayerAdapter'; +import type { CanvasRegionalGuidanceAdapter } from 'features/controlLayers/konva/CanvasRegionalGuidanceAdapter'; import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; import { getPatternSVG } from 'features/controlLayers/konva/patterns/getPatternSVG'; import { @@ -64,7 +66,11 @@ export class CanvasEntityRenderer extends CanvasModuleBase { readonly type = 'entity_renderer'; readonly id: string; readonly path: string[]; - readonly parent: CanvasEntityLayerAdapter | CanvasEntityMaskAdapter; + readonly parent: + | CanvasRasterLayerAdapter + | CanvasControlLayerAdapter + | CanvasInpaintMaskAdapter + | CanvasRegionalGuidanceAdapter; readonly manager: CanvasManager; readonly log: Logger; @@ -135,7 +141,13 @@ export class CanvasEntityRenderer extends CanvasModuleBase { */ $canvasCache = atom<{ canvas: HTMLCanvasElement; rect: Rect } | null>(null); - constructor(parent: CanvasEntityLayerAdapter | CanvasEntityMaskAdapter) { + constructor( + parent: + | CanvasRasterLayerAdapter + | CanvasControlLayerAdapter + | CanvasInpaintMaskAdapter + | CanvasRegionalGuidanceAdapter + ) { super(); this.id = getPrefixedId(this.type); this.parent = parent; @@ -183,7 +195,10 @@ export class CanvasEntityRenderer extends CanvasModuleBase { // need to update the compositing rect to match the stage. this.subscriptions.add( this.manager.stage.$stageAttrs.listen(() => { - if (this.konva.compositing && this.parent.type === 'entity_mask_adapter') { + if ( + this.konva.compositing && + (this.parent.type === 'inpaint_mask_adapter' || this.parent.type === 'regional_guidance_adapter') + ) { this.updateCompositingRectSize(); } }) @@ -449,7 +464,7 @@ export class CanvasEntityRenderer extends CanvasModuleBase { this.renderers.set(this.bufferState.id, this.bufferRenderer); if (pushToState) { - const entityIdentifier = this.parent.getEntityIdentifier(); + const entityIdentifier = this.parent.entityIdentifier; if (this.bufferState.type === 'brush_line') { this.manager.stateApi.addBrushLine({ entityIdentifier, brushLine: this.bufferState }); } else if (this.bufferState.type === 'eraser_line') { @@ -546,7 +561,7 @@ export class CanvasEntityRenderer extends CanvasModuleBase { this.commitBuffer({ pushToState: false }); } this.manager.stateApi.rasterizeEntity({ - entityIdentifier: this.parent.getEntityIdentifier(), + entityIdentifier: this.parent.entityIdentifier, imageObject, rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: imageDTO.width, height: imageDTO.height }, replaceObjects, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts index e04315b7fcd..7c755624ad8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts @@ -1,7 +1,9 @@ -import type { CanvasEntityLayerAdapter } from 'features/controlLayers/konva/CanvasEntityLayerAdapter'; -import type { CanvasEntityMaskAdapter } from 'features/controlLayers/konva/CanvasEntityMaskAdapter'; +import type { CanvasControlLayerAdapter } from 'features/controlLayers/konva/CanvasControlLayerAdapter'; +import type { CanvasInpaintMaskAdapter } from 'features/controlLayers/konva/CanvasInpaintMaskAdapter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; +import type { CanvasRasterLayerAdapter } from 'features/controlLayers/konva/CanvasRasterLayerAdapter'; +import type { CanvasRegionalGuidanceAdapter } from 'features/controlLayers/konva/CanvasRegionalGuidanceAdapter'; import { canvasToImageData, getEmptyRect, getPrefixedId } from 'features/controlLayers/konva/util'; import type { Coordinate, Rect, RectWithRotation } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -80,7 +82,11 @@ export class CanvasEntityTransformer extends CanvasModuleBase { readonly type = 'entity_transformer'; readonly id: string; readonly path: string[]; - readonly parent: CanvasEntityLayerAdapter | CanvasEntityMaskAdapter; + readonly parent: + | CanvasRasterLayerAdapter + | CanvasControlLayerAdapter + | CanvasInpaintMaskAdapter + | CanvasRegionalGuidanceAdapter; readonly manager: CanvasManager; readonly log: Logger; @@ -402,7 +408,7 @@ export class CanvasEntityTransformer extends CanvasModuleBase { }; this.log.trace({ position }, 'Position changed'); - this.manager.stateApi.setEntityPosition({ entityIdentifier: this.parent.getEntityIdentifier(), position }); + this.manager.stateApi.setEntityPosition({ entityIdentifier: this.parent.entityIdentifier, position }); }; syncObjectGroupWithProxyRect = () => { @@ -653,7 +659,7 @@ export class CanvasEntityTransformer extends CanvasModuleBase { // If the layer already has no objects, we don't need to reset the entity state. This would cause a push to the // undo stack and clear the redo stack. if (this.parent.renderer.hasObjects()) { - this.manager.stateApi.resetEntity({ entityIdentifier: this.parent.getEntityIdentifier() }); + this.manager.stateApi.resetEntity({ entityIdentifier: this.parent.entityIdentifier }); this.syncInteractionState(); } } else { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts index 19155dfaba9..382ac92b8b1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts @@ -1,7 +1,10 @@ import type { SerializableObject } from 'common/types'; -import type { CanvasEntityLayerAdapter } from 'features/controlLayers/konva/CanvasEntityLayerAdapter'; +import type { CanvasControlLayerAdapter } from 'features/controlLayers/konva/CanvasControlLayerAdapter'; +import type { CanvasInpaintMaskAdapter } from 'features/controlLayers/konva/CanvasInpaintMaskAdapter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; +import type { CanvasRasterLayerAdapter } from 'features/controlLayers/konva/CanvasRasterLayerAdapter'; +import type { CanvasRegionalGuidanceAdapter } from 'features/controlLayers/konva/CanvasRegionalGuidanceAdapter'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { CanvasEntityIdentifier, CanvasImageState, FilterConfig } from 'features/controlLayers/store/types'; import { IMAGE_FILTERS, imageDTOToImageObject } from 'features/controlLayers/store/types'; @@ -21,7 +24,13 @@ export class CanvasFilterModule extends CanvasModuleBase { imageState: CanvasImageState | null = null; - $adapter = atom(null); + $adapter = atom< + | CanvasRasterLayerAdapter + | CanvasControlLayerAdapter + | CanvasInpaintMaskAdapter + | CanvasRegionalGuidanceAdapter + | null + >(null); $isFiltering = computed(this.$adapter, (adapter) => Boolean(adapter)); $isProcessing = atom(false); $config = atom(IMAGE_FILTERS.canny_image_processor.buildDefaults()); @@ -112,7 +121,7 @@ export class CanvasFilterModule extends CanvasModuleBase { adapter.renderer.commitBuffer(); const rect = adapter.transformer.getRelativeRect(); this.manager.stateApi.rasterizeEntity({ - entityIdentifier: adapter.getEntityIdentifier(), + entityIdentifier: adapter.entityIdentifier, imageObject: imageState, rect: { x: Math.round(rect.x), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMaskAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMaskAdapter.ts new file mode 100644 index 00000000000..1a02b8077c7 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMaskAdapter.ts @@ -0,0 +1,163 @@ +import type { SerializableObject } from 'common/types'; +import { deepClone } from 'common/util/deepClone'; +import { CanvasEntityRenderer } from 'features/controlLayers/konva/CanvasEntityRenderer'; +import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntityTransformer'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; +import type { CanvasEntityIdentifier, CanvasInpaintMaskState, Rect } from 'features/controlLayers/store/types'; +import { getEntityIdentifier } from 'features/controlLayers/store/types'; +import Konva from 'konva'; +import type { GroupConfig } from 'konva/lib/Group'; +import { omit } from 'lodash-es'; +import type { Logger } from 'roarr'; +import stableHash from 'stable-hash'; +import { assert } from 'tsafe'; + +export class CanvasInpaintMaskAdapter extends CanvasModuleBase { + readonly type = 'inpaint_mask_adapter'; + readonly id: string; + readonly path: string[]; + readonly parent: CanvasManager; + readonly manager: CanvasManager; + readonly log: Logger; + + entityIdentifier: CanvasEntityIdentifier<'inpaint_mask'>; + + /** + * The last known state of the entity. + */ + private _state: CanvasInpaintMaskState | null = null; + + transformer: CanvasEntityTransformer; + renderer: CanvasEntityRenderer; + + konva: { + layer: Konva.Layer; + }; + + constructor(entityIdentifier: CanvasEntityIdentifier<'inpaint_mask'>, manager: CanvasManager) { + super(); + this.id = entityIdentifier.id; + this.entityIdentifier = entityIdentifier; + this.parent = manager; + this.manager = manager; + this.path = this.manager.buildPath(this); + this.log = this.manager.buildLogger(this); + + this.log.debug('Creating module'); + + this.konva = { + layer: new Konva.Layer({ + name: `${this.type}:layer`, + listening: false, + imageSmoothingEnabled: false, + }), + }; + + this.renderer = new CanvasEntityRenderer(this); + this.transformer = new CanvasEntityTransformer(this); + } + + get state(): CanvasInpaintMaskState { + if (this._state) { + return this._state; + } + const state = this.manager.stateApi.getInpaintMasksState().entities.find((layer) => layer.id === this.id); + assert(state, `State not found for ${this.id}`); + return state; + } + + set state(state: CanvasInpaintMaskState) { + this._state = state; + } + + /** + * Get this entity's entity identifier + */ + getEntityIdentifier = (): CanvasEntityIdentifier => { + return getEntityIdentifier(this.state); + }; + + update = async (state: CanvasInpaintMaskState) => { + const prevState = this.state; + this.state = state; + + if (prevState && prevState === state) { + this.log.trace('State unchanged, skipping update'); + return; + } + + if (!prevState || state.isEnabled !== prevState.isEnabled) { + this.log.trace('Updating visibility'); + this.konva.layer.visible(state.isEnabled); + this.renderer.syncCache(state.isEnabled); + } + if (!prevState || state.objects !== prevState.objects) { + const didRender = await this.renderer.render(this.state.objects); + if (didRender) { + this.transformer.requestRectCalculation(); + } + } + if (!prevState || state.position !== prevState.position) { + this.transformer.updatePosition(); + } + if (!prevState || state.opacity !== prevState.opacity) { + this.renderer.updateOpacity(state.opacity); + } + if (!prevState || state.isLocked !== prevState.isLocked) { + this.transformer.syncInteractionState(); + } + if (!prevState || state.fill !== prevState.fill) { + this.renderer.updateCompositingRectFill(state.fill); + } + + if (!prevState) { + this.renderer.updateCompositingRectSize(); + } + + if (!prevState) { + this.transformer.updateBbox(); + } + }; + + getHashableState = (): SerializableObject => { + const keysToOmit: (keyof CanvasInpaintMaskState)[] = ['fill', 'name', 'opacity']; + return omit(this.state, keysToOmit); + }; + + hash = (extra?: SerializableObject): string => { + const arg = { + state: this.getHashableState(), + extra, + }; + return stableHash(arg); + }; + + getCanvas = (rect?: Rect): HTMLCanvasElement => { + // The opacity may have been changed in response to user selecting a different entity category, and the mask regions + // should be fully opaque - set opacity to 1 before rendering the canvas + const attrs: GroupConfig = { opacity: 1 }; + const canvas = this.renderer.getCanvas(rect, attrs); + return canvas; + }; + + isInteractable = (): boolean => { + return this.state.isEnabled && !this.state.isLocked; + }; + + destroy = () => { + this.log.debug('Destroying module'); + this.transformer.destroy(); + this.renderer.destroy(); + this.konva.layer.destroy(); + }; + + repr = () => { + return { + id: this.id, + type: this.type, + path: this.path, + state: deepClone(this.state), + }; + }; +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index a7c3b88a296..0593b22e038 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -6,9 +6,13 @@ import { SyncableMap } from 'common/util/SyncableMap/SyncableMap'; import { CanvasBboxModule } from 'features/controlLayers/konva/CanvasBboxModule'; import { CanvasCacheModule } from 'features/controlLayers/konva/CanvasCacheModule'; import { CanvasCompositorModule } from 'features/controlLayers/konva/CanvasCompositorModule'; +import type { CanvasControlLayerAdapter } from 'features/controlLayers/konva/CanvasControlLayerAdapter'; import { CanvasFilterModule } from 'features/controlLayers/konva/CanvasFilterModule'; +import type { CanvasInpaintMaskAdapter } from 'features/controlLayers/konva/CanvasInpaintMaskAdapter'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { CanvasProgressImageModule } from 'features/controlLayers/konva/CanvasProgressImageModule'; +import type { CanvasRasterLayerAdapter } from 'features/controlLayers/konva/CanvasRasterLayerAdapter'; +import type { CanvasRegionalGuidanceAdapter } from 'features/controlLayers/konva/CanvasRegionalGuidanceAdapter'; import { CanvasRenderingModule } from 'features/controlLayers/konva/CanvasRenderingModule'; import { CanvasStageModule } from 'features/controlLayers/konva/CanvasStageModule'; import { CanvasStagingAreaModule } from 'features/controlLayers/konva/CanvasStagingAreaModule'; @@ -21,8 +25,6 @@ import { atom, computed } from 'nanostores'; import type { Logger } from 'roarr'; import { CanvasBackgroundModule } from './CanvasBackgroundModule'; -import type { CanvasEntityLayerAdapter } from './CanvasEntityLayerAdapter'; -import type { CanvasEntityMaskAdapter } from './CanvasEntityMaskAdapter'; import { CanvasStateApiModule } from './CanvasStateApiModule'; export const $canvasManager = atom(null); @@ -41,11 +43,16 @@ export class CanvasManager extends CanvasModuleBase { subscriptions = new Set<() => void>(); adapters = { - rasterLayers: new SyncableMap(), - controlLayers: new SyncableMap(), - regionMasks: new SyncableMap(), - inpaintMasks: new SyncableMap(), - getAll: (): (CanvasEntityLayerAdapter | CanvasEntityMaskAdapter)[] => { + rasterLayers: new SyncableMap(), + controlLayers: new SyncableMap(), + regionMasks: new SyncableMap(), + inpaintMasks: new SyncableMap(), + getAll: (): ( + | CanvasRasterLayerAdapter + | CanvasControlLayerAdapter + | CanvasRegionalGuidanceAdapter + | CanvasInpaintMaskAdapter + )[] => { return [ ...this.adapters.rasterLayers.values(), ...this.adapters.controlLayers.values(), diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRasterLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRasterLayerAdapter.ts new file mode 100644 index 00000000000..e1e8bbeecc5 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRasterLayerAdapter.ts @@ -0,0 +1,163 @@ +import type { SerializableObject } from 'common/types'; +import { deepClone } from 'common/util/deepClone'; +import { CanvasEntityRenderer } from 'features/controlLayers/konva/CanvasEntityRenderer'; +import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntityTransformer'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; +import type { CanvasEntityIdentifier, CanvasRasterLayerState, Rect } from 'features/controlLayers/store/types'; +import Konva from 'konva'; +import type { GroupConfig } from 'konva/lib/Group'; +import { omit } from 'lodash-es'; +import type { Logger } from 'roarr'; +import stableHash from 'stable-hash'; +import { assert } from 'tsafe'; + +export class CanvasRasterLayerAdapter extends CanvasModuleBase { + readonly type = 'raster_layer_adapter'; + readonly id: string; + readonly path: string[]; + readonly manager: CanvasManager; + readonly parent: CanvasManager; + readonly log: Logger; + + entityIdentifier: CanvasEntityIdentifier<'raster_layer'>; + + /** + * The last known state of the entity. + */ + private _state: CanvasRasterLayerState | null = null; + + /** + * The Konva nodes that make up the entity layer: + * - A layer to hold the everything + * + * Note that the transformer and object renderer have their own Konva nodes, but they are not stored here. + */ + konva: { + layer: Konva.Layer; + }; + + /** + * The transformer for this entity layer. + */ + transformer: CanvasEntityTransformer; + + /** + * The renderer for this entity layer. + */ + renderer: CanvasEntityRenderer; + + constructor(entityIdentifier: CanvasEntityIdentifier<'raster_layer'>, manager: CanvasManager) { + super(); + this.id = entityIdentifier.id; + this.entityIdentifier = entityIdentifier; + this.manager = manager; + this.parent = manager; + this.path = this.manager.buildPath(this); + this.log = this.manager.buildLogger(this); + + this.log.debug('Creating module'); + + this.konva = { + layer: new Konva.Layer({ + name: `${this.type}:layer`, + listening: false, + imageSmoothingEnabled: false, + }), + }; + + this.renderer = new CanvasEntityRenderer(this); + this.transformer = new CanvasEntityTransformer(this); + } + + get state(): CanvasRasterLayerState { + if (this._state) { + return this._state; + } + const state = this.manager.stateApi.getRasterLayersState().entities.find((layer) => layer.id === this.id); + assert(state, `State not found for ${this.id}`); + return state; + } + + set state(state: CanvasRasterLayerState) { + const prevState = this._state; + this._state = state; + this.render(state, prevState); + } + + private render = async (state: CanvasRasterLayerState, prevState: CanvasRasterLayerState | null): Promise => { + if (prevState && prevState === state) { + this.log.trace('State unchanged, skipping update'); + return; + } + + if (!prevState || state.isEnabled !== prevState.isEnabled) { + this.log.trace('Updating visibility'); + this.konva.layer.visible(state.isEnabled); + this.renderer.syncCache(state.isEnabled); + } + if (!prevState || state.isLocked !== prevState.isLocked) { + this.transformer.syncInteractionState(); + } + if (!prevState || state.objects !== prevState.objects) { + const didRender = await this.renderer.render(this.state.objects); + if (didRender) { + this.transformer.requestRectCalculation(); + } + } + if (!prevState || state.position !== prevState.position) { + this.transformer.updatePosition(); + } + if (!prevState || state.opacity !== prevState.opacity) { + this.renderer.updateOpacity(state.opacity); + } + if (!prevState) { + // First render + this.transformer.updateBbox(); + } + }; + + getCanvas = (rect?: Rect): HTMLCanvasElement => { + this.log.trace({ rect }, 'Getting canvas'); + // The opacity may have been changed in response to user selecting a different entity category, so we must restore + // the original opacity before rendering the canvas + const attrs: GroupConfig = { opacity: this.state.opacity }; + const canvas = this.renderer.getCanvas(rect, attrs); + return canvas; + }; + + isInteractable = (): boolean => { + return this.state.isEnabled && !this.state.isLocked; + }; + + getHashableState = (): SerializableObject => { + const keysToOmit: (keyof CanvasRasterLayerState)[] = ['name']; + return omit(this.state, keysToOmit); + }; + + hash = (extra?: SerializableObject): string => { + const arg = { + state: this.getHashableState(), + extra, + }; + return stableHash(arg); + }; + + destroy = (): void => { + this.log.debug('Destroying module'); + this.renderer.destroy(); + this.transformer.destroy(); + this.konva.layer.destroy(); + }; + + repr = () => { + return { + id: this.id, + type: this.type, + path: this.path, + state: deepClone(this.state), + transformer: this.transformer.repr(), + renderer: this.renderer.repr(), + }; + }; +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegionalGuidanceAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegionalGuidanceAdapter.ts new file mode 100644 index 00000000000..e72dbfc174f --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegionalGuidanceAdapter.ts @@ -0,0 +1,163 @@ +import type { SerializableObject } from 'common/types'; +import { deepClone } from 'common/util/deepClone'; +import { CanvasEntityRenderer } from 'features/controlLayers/konva/CanvasEntityRenderer'; +import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntityTransformer'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; +import type { CanvasEntityIdentifier, CanvasRegionalGuidanceState, Rect } from 'features/controlLayers/store/types'; +import { getEntityIdentifier } from 'features/controlLayers/store/types'; +import Konva from 'konva'; +import type { GroupConfig } from 'konva/lib/Group'; +import { omit } from 'lodash-es'; +import type { Logger } from 'roarr'; +import stableHash from 'stable-hash'; +import { assert } from 'tsafe'; + +export class CanvasRegionalGuidanceAdapter extends CanvasModuleBase { + readonly type = 'regional_guidance_adapter'; + readonly id: string; + readonly path: string[]; + readonly parent: CanvasManager; + readonly manager: CanvasManager; + readonly log: Logger; + + entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>; + + /** + * The last known state of the entity. + */ + private _state: CanvasRegionalGuidanceState | null = null; + + transformer: CanvasEntityTransformer; + renderer: CanvasEntityRenderer; + + konva: { + layer: Konva.Layer; + }; + + constructor(entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>, manager: CanvasManager) { + super(); + this.id = entityIdentifier.id; + this.entityIdentifier = entityIdentifier; + this.parent = manager; + this.manager = manager; + this.path = this.manager.buildPath(this); + this.log = this.manager.buildLogger(this); + + this.log.debug('Creating module'); + + this.konva = { + layer: new Konva.Layer({ + name: `${this.type}:layer`, + listening: false, + imageSmoothingEnabled: false, + }), + }; + + this.renderer = new CanvasEntityRenderer(this); + this.transformer = new CanvasEntityTransformer(this); + } + + get state(): CanvasRegionalGuidanceState { + if (this._state) { + return this._state; + } + const state = this.manager.stateApi.getRegionsState().entities.find((layer) => layer.id === this.id); + assert(state, `State not found for ${this.id}`); + return state; + } + + set state(state: CanvasRegionalGuidanceState) { + this._state = state; + } + + /** + * Get this entity's entity identifier + */ + getEntityIdentifier = (): CanvasEntityIdentifier => { + return getEntityIdentifier(this.state); + }; + + update = async (state: CanvasRegionalGuidanceState) => { + const prevState = this.state; + this.state = state; + + if (prevState && prevState === state) { + this.log.trace('State unchanged, skipping update'); + return; + } + + if (!prevState || state.isEnabled !== prevState.isEnabled) { + this.log.trace('Updating visibility'); + this.konva.layer.visible(state.isEnabled); + this.renderer.syncCache(state.isEnabled); + } + if (!prevState || state.objects !== prevState.objects) { + const didRender = await this.renderer.render(this.state.objects); + if (didRender) { + this.transformer.requestRectCalculation(); + } + } + if (!prevState || state.position !== prevState.position) { + this.transformer.updatePosition(); + } + if (!prevState || state.opacity !== prevState.opacity) { + this.renderer.updateOpacity(state.opacity); + } + if (!prevState || state.isLocked !== prevState.isLocked) { + this.transformer.syncInteractionState(); + } + if (!prevState || state.fill !== prevState.fill) { + this.renderer.updateCompositingRectFill(state.fill); + } + + if (!prevState) { + this.renderer.updateCompositingRectSize(); + } + + if (!prevState) { + this.transformer.updateBbox(); + } + }; + + getHashableState = (): SerializableObject => { + const keysToOmit: (keyof CanvasRegionalGuidanceState)[] = ['fill', 'name', 'opacity']; + return omit(this.state, keysToOmit); + }; + + hash = (extra?: SerializableObject): string => { + const arg = { + state: this.getHashableState(), + extra, + }; + return stableHash(arg); + }; + + getCanvas = (rect?: Rect): HTMLCanvasElement => { + // The opacity may have been changed in response to user selecting a different entity category, and the mask regions + // should be fully opaque - set opacity to 1 before rendering the canvas + const attrs: GroupConfig = { opacity: 1 }; + const canvas = this.renderer.getCanvas(rect, attrs); + return canvas; + }; + + isInteractable = (): boolean => { + return this.state.isEnabled && !this.state.isLocked; + }; + + destroy = () => { + this.log.debug('Destroying module'); + this.transformer.destroy(); + this.renderer.destroy(); + this.konva.layer.destroy(); + }; + + repr = () => { + return { + id: this.id, + type: this.type, + path: this.path, + state: deepClone(this.state), + }; + }; +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts index 458a6943bf7..a1024458a7e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts @@ -1,11 +1,13 @@ -import { CanvasEntityLayerAdapter } from 'features/controlLayers/konva/CanvasEntityLayerAdapter'; -import { CanvasEntityMaskAdapter } from 'features/controlLayers/konva/CanvasEntityMaskAdapter'; +import { CanvasControlLayerAdapter } from 'features/controlLayers/konva/CanvasControlLayerAdapter'; +import { CanvasInpaintMaskAdapter } from 'features/controlLayers/konva/CanvasInpaintMaskAdapter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; +import { CanvasRasterLayerAdapter } from 'features/controlLayers/konva/CanvasRasterLayerAdapter'; +import { CanvasRegionalGuidanceAdapter } from 'features/controlLayers/konva/CanvasRegionalGuidanceAdapter'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { CanvasSessionState } from 'features/controlLayers/store/canvasSessionSlice'; import type { CanvasSettingsState } from 'features/controlLayers/store/canvasSettingsSlice'; -import type { CanvasState } from 'features/controlLayers/store/types'; +import { type CanvasState, getEntityIdentifier } from 'features/controlLayers/store/types'; import type { Logger } from 'roarr'; export class CanvasRenderingModule extends CanvasModuleBase { @@ -49,7 +51,7 @@ export class CanvasRenderingModule extends CanvasModuleBase { } }; - renderCanvas = async () => { + renderCanvas = () => { const state = this.manager.stateApi.getCanvasState(); const prevState = this.state; @@ -65,11 +67,11 @@ export class CanvasRenderingModule extends CanvasModuleBase { return; } - await this.renderRasterLayers(state, prevState); - await this.renderControlLayers(prevState, state); - await this.renderRegionalGuidance(prevState, state); - await this.renderInpaintMasks(state, prevState); - await this.renderBbox(state, prevState); + this.renderRasterLayers(state, prevState); + this.renderControlLayers(prevState, state); + this.renderRegionalGuidance(prevState, state); + this.renderInpaintMasks(state, prevState); + this.renderBbox(state, prevState); this.arrangeEntities(state, prevState); this.manager.tool.syncCursorStyle(); }; @@ -136,11 +138,11 @@ export class CanvasRenderingModule extends CanvasModuleBase { for (const entityState of state.rasterLayers.entities) { let adapter = adapterMap.get(entityState.id); if (!adapter) { - adapter = new CanvasEntityLayerAdapter(entityState, this.manager); + adapter = new CanvasRasterLayerAdapter(getEntityIdentifier(entityState), this.manager); adapterMap.set(adapter.id, adapter); this.manager.stage.addLayer(adapter.konva.layer); } - await adapter.update({ state: entityState }); + adapter.state = entityState; } } }; @@ -165,16 +167,16 @@ export class CanvasRenderingModule extends CanvasModuleBase { for (const entityState of state.controlLayers.entities) { let adapter = adapterMap.get(entityState.id); if (!adapter) { - adapter = new CanvasEntityLayerAdapter(entityState, this.manager); + adapter = new CanvasControlLayerAdapter(getEntityIdentifier(entityState), this.manager); adapterMap.set(adapter.id, adapter); this.manager.stage.addLayer(adapter.konva.layer); } - await adapter.update({ state: entityState }); + adapter.state = entityState; } } }; - renderRegionalGuidance = async (prevState: CanvasState | null, state: CanvasState) => { + renderRegionalGuidance = (prevState: CanvasState | null, state: CanvasState) => { const adapterMap = this.manager.adapters.regionMasks; if (!prevState || state.regions.isHidden !== prevState.regions.isHidden) { @@ -199,16 +201,16 @@ export class CanvasRenderingModule extends CanvasModuleBase { for (const entityState of state.regions.entities) { let adapter = adapterMap.get(entityState.id); if (!adapter) { - adapter = new CanvasEntityMaskAdapter(entityState, this.manager); + adapter = new CanvasRegionalGuidanceAdapter(getEntityIdentifier(entityState), this.manager); adapterMap.set(adapter.id, adapter); this.manager.stage.addLayer(adapter.konva.layer); } - await adapter.update({ state: entityState }); + adapter.state = entityState; } } }; - renderInpaintMasks = async (state: CanvasState, prevState: CanvasState | null) => { + renderInpaintMasks = (state: CanvasState, prevState: CanvasState | null) => { const adapterMap = this.manager.adapters.inpaintMasks; if (!prevState || state.inpaintMasks.isHidden !== prevState.inpaintMasks.isHidden) { @@ -233,11 +235,11 @@ export class CanvasRenderingModule extends CanvasModuleBase { for (const entityState of state.inpaintMasks.entities) { let adapter = adapterMap.get(entityState.id); if (!adapter) { - adapter = new CanvasEntityMaskAdapter(entityState, this.manager); + adapter = new CanvasInpaintMaskAdapter(getEntityIdentifier(entityState), this.manager); adapterMap.set(adapter.id, adapter); this.manager.stage.addLayer(adapter.konva.layer); } - await adapter.update({ state: entityState }); + adapter.state = entityState; } } }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index 0053a0bce64..0dd85b3819f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -1,12 +1,13 @@ import { $alt, $ctrl, $meta, $shift } from '@invoke-ai/ui-library'; import type { AppStore } from 'app/store/store'; -import type { CanvasEntityLayerAdapter } from 'features/controlLayers/konva/CanvasEntityLayerAdapter'; -import type { CanvasEntityMaskAdapter } from 'features/controlLayers/konva/CanvasEntityMaskAdapter'; +import type { CanvasControlLayerAdapter } from 'features/controlLayers/konva/CanvasControlLayerAdapter'; +import type { CanvasInpaintMaskAdapter } from 'features/controlLayers/konva/CanvasInpaintMaskAdapter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; +import type { CanvasRasterLayerAdapter } from 'features/controlLayers/konva/CanvasRasterLayerAdapter'; +import type { CanvasRegionalGuidanceAdapter } from 'features/controlLayers/konva/CanvasRegionalGuidanceAdapter'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import type { - CanvasSettingsState} from 'features/controlLayers/store/canvasSettingsSlice'; +import type { CanvasSettingsState } from 'features/controlLayers/store/canvasSettingsSlice'; import { settingsBrushWidthChanged, settingsColorChanged, @@ -50,25 +51,25 @@ type EntityStateAndAdapter = id: string; type: CanvasRasterLayerState['type']; state: CanvasRasterLayerState; - adapter: CanvasEntityLayerAdapter; + adapter: CanvasRasterLayerAdapter; } | { id: string; type: CanvasControlLayerState['type']; state: CanvasControlLayerState; - adapter: CanvasEntityLayerAdapter; + adapter: CanvasControlLayerAdapter; } | { id: string; type: CanvasInpaintMaskState['type']; state: CanvasInpaintMaskState; - adapter: CanvasEntityMaskAdapter; + adapter: CanvasInpaintMaskAdapter; } | { id: string; type: CanvasRegionalGuidanceState['type']; state: CanvasRegionalGuidanceState; - adapter: CanvasEntityMaskAdapter; + adapter: CanvasRegionalGuidanceAdapter; }; export class CanvasStateApiModule extends CanvasModuleBase { @@ -361,7 +362,13 @@ export class CanvasStateApiModule extends CanvasModuleBase { /** * The entity adapter being transformed, if any. */ - $transformingAdapter = atom(null); + $transformingAdapter = atom< + | CanvasRasterLayerAdapter + | CanvasControlLayerAdapter + | CanvasInpaintMaskAdapter + | CanvasRegionalGuidanceAdapter + | null + >(null); /** * Whether an entity is currently being transformed. Derived from `$transformingAdapter`. diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts index b99fc5f1508..0d3b01c91af 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts @@ -8,6 +8,7 @@ import { calculateNewBrushSizeFromWheelDelta, floorCoord, getIsPrimaryMouseDown, + getLastPointOfLastLine, getLastPointOfLine, getPrefixedId, getScaledCursorPosition, @@ -374,7 +375,7 @@ export class CanvasToolModule extends CanvasModuleBase { const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position); if (tool === 'brush') { - const lastLinePoint = selectedEntity.adapter.getLastPointOfLastLine('brush_line'); + const lastLinePoint = getLastPointOfLastLine(selectedEntity.state.objects, 'brush_line'); const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth); if (e.evt.shiftKey && lastLinePoint) { // Create a straight line from the last line point @@ -412,7 +413,7 @@ export class CanvasToolModule extends CanvasModuleBase { } if (tool === 'eraser') { - const lastLinePoint = selectedEntity.adapter.getLastPointOfLastLine('eraser_line'); + const lastLinePoint = getLastPointOfLastLine(selectedEntity.state.objects, 'eraser_line'); const alignedPoint = alignCoordForTool(normalizedPoint, settings.eraserWidth); if (e.evt.shiftKey && lastLinePoint) { // Create a straight line from the last line point diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index fae74faf2d9..f419db81795 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -1,4 +1,4 @@ -import type { CanvasEntityIdentifier, Coordinate, Rect } from 'features/controlLayers/store/types'; +import type { CanvasEntityIdentifier, CanvasObjectState, Coordinate, Rect } from 'features/controlLayers/store/types'; import type Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; import type { Vector2d } from 'konva/lib/types'; @@ -464,3 +464,19 @@ export const getRectUnion = (...rects: Rect[]): Rect => { export const exhaustiveCheck = (value: never): never => { assert(false, `Unhandled value: ${value}`); }; + +export const getLastPointOfLastLine = ( + objects: CanvasObjectState[], + type: 'brush_line' | 'eraser_line' +): Coordinate | null => { + const lastObject = objects[objects.length - 1]; + if (!lastObject) { + return null; + } + + if (lastObject.type === type) { + return getLastPointOfLine(lastObject.points); + } + + return null; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 0661259fc25..048af3b7d5c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -523,6 +523,7 @@ const zCanvasObjectState = z.discriminatedUnion('type', [ zCanvasEraserLineState, zCanvasRectState, ]); +export type CanvasObjectState = z.infer; const zIPAdapterConfig = z.object({ image: zImageWithDims.nullable(), From 5a30ff6045b32079a300cdbc685e53f4c7532386 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 4 Sep 2024 19:06:49 +1000 Subject: [PATCH 633/678] feat(ui): abstract out CanvasEntityAdapterBase Things were getting to complex to reason about & classes a bit complicated. Trying to simplify... --- ...tityListActionBarSelectedEntityOpacity.tsx | 4 +- .../components/Transform/Transform.tsx | 137 +++++------ .../konva/CanvasControlLayerAdapter.ts | 94 +------- .../konva/CanvasEntityAdapterBase.ts | 105 ++++++++ .../konva/CanvasEntityLayerAdapter.ts | 228 ------------------ .../konva/CanvasEntityMaskAdapter.ts | 189 --------------- .../konva/CanvasEntityRenderer.ts | 19 +- .../konva/CanvasEntityTransformer.ts | 11 +- .../konva/CanvasInpaintMaskAdapter.ts | 83 +------ .../konva/CanvasRasterLayerAdapter.ts | 91 +------ .../konva/CanvasRegionalGuidanceAdapter.ts | 90 +------ .../konva/CanvasStateApiModule.ts | 9 +- .../controlLayers/konva/CanvasToolModule.ts | 4 +- .../controlLayers/store/canvasSlice.ts | 14 +- .../src/features/controlLayers/store/types.ts | 14 +- 15 files changed, 214 insertions(+), 878 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterBase.ts delete mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityLayerAdapter.ts delete mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityMaskAdapter.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityOpacity.tsx index fed4fcf8163..c514e4ee8be 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityOpacity.tsx @@ -22,7 +22,7 @@ import { selectEntity, selectSelectedEntityIdentifier, } from 'features/controlLayers/store/selectors'; -import { isDrawableEntity } from 'features/controlLayers/store/types'; +import { isRenderableEntity } from 'features/controlLayers/store/types'; import { clamp, round } from 'lodash-es'; import type { KeyboardEvent } from 'react'; import { memo, useCallback, useEffect, useState } from 'react'; @@ -70,7 +70,7 @@ const selectOpacity = createSelector(selectCanvasSlice, (canvas) => { if (!selectedEntity) { return 1; // fallback to 100% opacity } - if (!isDrawableEntity(selectedEntity)) { + if (!isRenderableEntity(selectedEntity)) { return 1; // fallback to 100% opacity } // Opacity is a float from 0-1, but we want to display it as a percentage diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Transform/Transform.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Transform/Transform.tsx index bfb9e8a7d57..d9608788cf2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Transform/Transform.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Transform/Transform.tsx @@ -1,86 +1,73 @@ import { Button, ButtonGroup, Flex, Heading, Spacer } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; -import type { CanvasControlLayerAdapter } from 'features/controlLayers/konva/CanvasControlLayerAdapter'; -import type { CanvasInpaintMaskAdapter } from 'features/controlLayers/konva/CanvasInpaintMaskAdapter'; -import type { CanvasRasterLayerAdapter } from 'features/controlLayers/konva/CanvasRasterLayerAdapter'; -import type { CanvasRegionalGuidanceAdapter } from 'features/controlLayers/konva/CanvasRegionalGuidanceAdapter'; +import type { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapterBase'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowsCounterClockwiseBold, PiArrowsOutBold, PiCheckBold, PiXBold } from 'react-icons/pi'; -const TransformBox = memo( - ({ - adapter, - }: { - adapter: - | CanvasRasterLayerAdapter - | CanvasControlLayerAdapter - | CanvasInpaintMaskAdapter - | CanvasRegionalGuidanceAdapter; - }) => { - const { t } = useTranslation(); - const isProcessing = useStore(adapter.transformer.$isProcessing); +const TransformBox = memo(({ adapter }: { adapter: CanvasEntityAdapterBase }) => { + const { t } = useTranslation(); + const isProcessing = useStore(adapter.transformer.$isProcessing); - return ( - - - {t('controlLayers.transform.transform')} - - - - - - - - - - ); - } -); + return ( + + + {t('controlLayers.transform.transform')} + + + + + + + + + + ); +}); TransformBox.displayName = 'Transform'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlLayerAdapter.ts index 576b5928548..d9e200b3bfa 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlLayerAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlLayerAdapter.ts @@ -1,73 +1,17 @@ import type { SerializableObject } from 'common/types'; -import { deepClone } from 'common/util/deepClone'; -import { CanvasEntityRenderer } from 'features/controlLayers/konva/CanvasEntityRenderer'; -import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntityTransformer'; +import { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapterBase'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import type { CanvasControlLayerState, CanvasEntityIdentifier, Rect } from 'features/controlLayers/store/types'; -import Konva from 'konva'; import type { GroupConfig } from 'konva/lib/Group'; import { omit } from 'lodash-es'; -import type { Logger } from 'roarr'; -import stableHash from 'stable-hash'; import { assert } from 'tsafe'; -export class CanvasControlLayerAdapter extends CanvasModuleBase { - readonly type = 'control_layer_adapter'; - readonly id: string; - readonly path: string[]; - readonly manager: CanvasManager; - readonly parent: CanvasManager; - readonly log: Logger; - - entityIdentifier: CanvasEntityIdentifier<'control_layer'>; - - /** - * The last known state of the entity. - */ +export class CanvasControlLayerAdapter extends CanvasEntityAdapterBase { + static TYPE = 'control_layer_adapter'; private _state: CanvasControlLayerState | null = null; - /** - * The Konva nodes that make up the entity layer: - * - A layer to hold the everything - * - * Note that the transformer and object renderer have their own Konva nodes, but they are not stored here. - */ - konva: { - layer: Konva.Layer; - }; - - /** - * The transformer for this entity layer. - */ - transformer: CanvasEntityTransformer; - - /** - * The renderer for this entity layer. - */ - renderer: CanvasEntityRenderer; - constructor(entityIdentifier: CanvasEntityIdentifier<'control_layer'>, manager: CanvasManager) { - super(); - this.id = entityIdentifier.id; - this.entityIdentifier = entityIdentifier; - this.manager = manager; - this.parent = manager; - this.path = this.manager.buildPath(this); - this.log = this.manager.buildLogger(this); - - this.log.debug('Creating module'); - - this.konva = { - layer: new Konva.Layer({ - name: `${this.type}:layer`, - listening: false, - imageSmoothingEnabled: false, - }), - }; - - this.renderer = new CanvasEntityRenderer(this); - this.transformer = new CanvasEntityTransformer(this); + super(entityIdentifier, manager, CanvasControlLayerAdapter.TYPE); } get state(): CanvasControlLayerState { @@ -134,34 +78,4 @@ export class CanvasControlLayerAdapter extends CanvasModuleBase { const keysToOmit: (keyof CanvasControlLayerState)[] = ['name', 'controlAdapter', 'withTransparencyEffect']; return omit(this.state, keysToOmit); }; - - isInteractable = (): boolean => { - return this.state.isEnabled && !this.state.isLocked; - }; - - hash = (extra?: SerializableObject): string => { - const arg = { - state: this.getHashableState(), - extra, - }; - return stableHash(arg); - }; - - destroy = (): void => { - this.log.debug('Destroying module'); - this.renderer.destroy(); - this.transformer.destroy(); - this.konva.layer.destroy(); - }; - - repr = () => { - return { - id: this.id, - type: this.type, - path: this.path, - state: deepClone(this.state), - transformer: this.transformer.repr(), - renderer: this.renderer.repr(), - }; - }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterBase.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterBase.ts new file mode 100644 index 00000000000..7a155b77732 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterBase.ts @@ -0,0 +1,105 @@ +import type { SerializableObject } from 'common/types'; +import { deepClone } from 'common/util/deepClone'; +import { CanvasEntityRenderer } from 'features/controlLayers/konva/CanvasEntityRenderer'; +import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntityTransformer'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; +import type { CanvasEntityIdentifier, CanvasRenderableEntityState, Rect } from 'features/controlLayers/store/types'; +import Konva from 'konva'; +import type { Logger } from 'roarr'; +import stableHash from 'stable-hash'; + +export abstract class CanvasEntityAdapterBase< + T extends CanvasRenderableEntityState = CanvasRenderableEntityState, +> extends CanvasModuleBase { + readonly type: string; + readonly id: string; + readonly path: string[]; + readonly manager: CanvasManager; + readonly parent: CanvasManager; + readonly log: Logger; + + readonly entityIdentifier: CanvasEntityIdentifier; + + /** + * The Konva nodes that make up the entity adapter: + * - A Konva.Layer to hold the everything + * + * Note that the transformer and object renderer have their own Konva nodes, but they are not stored here. + */ + konva: { + layer: Konva.Layer; + }; + + /** + * The transformer for this entity adapter. + */ + transformer: CanvasEntityTransformer; + + /** + * The renderer for this entity adapter. + */ + renderer: CanvasEntityRenderer; + + constructor(entityIdentifier: CanvasEntityIdentifier, manager: CanvasManager, adapterType: string) { + super(); + this.type = adapterType; + this.id = entityIdentifier.id; + this.entityIdentifier = entityIdentifier; + this.manager = manager; + this.parent = manager; + this.path = this.manager.buildPath(this); + this.log = this.manager.buildLogger(this); + + this.log.debug('Creating module'); + + this.konva = { + layer: new Konva.Layer({ + name: `${this.type}:layer`, + listening: false, + imageSmoothingEnabled: false, + }), + }; + + this.renderer = new CanvasEntityRenderer(this); + this.transformer = new CanvasEntityTransformer(this); + } + + abstract get state(): T; + + abstract set state(state: T); + + abstract getCanvas: (rect?: Rect) => HTMLCanvasElement; + + abstract getHashableState: () => SerializableObject; + + isInteractable = (): boolean => { + return this.state.isEnabled && !this.state.isLocked; + }; + + hash = (extra?: SerializableObject): string => { + const arg = { + state: this.getHashableState(), + extra, + }; + return stableHash(arg); + }; + + destroy = (): void => { + this.log.debug('Destroying module'); + this.renderer.destroy(); + this.transformer.destroy(); + this.konva.layer.destroy(); + }; + + repr = () => { + return { + id: this.id, + type: this.type, + path: this.path, + state: deepClone(this.state), + transformer: this.transformer.repr(), + renderer: this.renderer.repr(), + }; + }; +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityLayerAdapter.ts deleted file mode 100644 index fa846b3cd0d..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityLayerAdapter.ts +++ /dev/null @@ -1,228 +0,0 @@ -import type { SerializableObject } from 'common/types'; -import { deepClone } from 'common/util/deepClone'; -import { CanvasEntityRenderer } from 'features/controlLayers/konva/CanvasEntityRenderer'; -import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntityTransformer'; -import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; -import { getLastPointOfLine } from 'features/controlLayers/konva/util'; -import type { - CanvasBrushLineState, - CanvasControlLayerState, - CanvasEntityIdentifier, - CanvasEraserLineState, - CanvasRasterLayerState, - Coordinate, - Rect, -} from 'features/controlLayers/store/types'; -import { getEntityIdentifier } from 'features/controlLayers/store/types'; -import Konva from 'konva'; -import type { GroupConfig } from 'konva/lib/Group'; -import { get, omit } from 'lodash-es'; -import type { Logger } from 'roarr'; -import stableHash from 'stable-hash'; -import { assert } from 'tsafe'; - -/** - * Handles the rendering for a single raster or control layer entity. - * - * This module has two main components: - * - A transformer, which handles the positioning and interaction state of the layer - * - A renderer, which handles the rendering of the layer's objects - * - * The canvas rendering module interacts with this module to coordinate the rendering of all raster and control layers. - */ -export class CanvasEntityLayerAdapter extends CanvasModuleBase { - readonly type = 'entity_layer_adapter'; - readonly id: string; - readonly path: string[]; - readonly manager: CanvasManager; - readonly parent: CanvasManager; - readonly log: Logger; - - /** - * The last known state of the entity. - */ - state: CanvasRasterLayerState | CanvasControlLayerState; - - /** - * The Konva nodes that make up the entity layer: - * - A layer to hold the everything - * - * Note that the transformer and object renderer have their own Konva nodes, but they are not stored here. - */ - konva: { - layer: Konva.Layer; - }; - - /** - * The transformer for this entity layer. - */ - transformer: CanvasEntityTransformer; - - /** - * The renderer for this entity layer. - */ - renderer: CanvasEntityRenderer; - - /** - * Whether this is the first render of the entity layer. - */ - isFirstRender: boolean = true; - - constructor(state: CanvasEntityLayerAdapter['state'], manager: CanvasEntityLayerAdapter['manager']) { - super(); - this.id = state.id; - this.manager = manager; - this.parent = manager; - this.path = this.manager.buildPath(this); - this.log = this.manager.buildLogger(this); - - this.log.debug('Creating module'); - - this.state = state; - - this.konva = { - layer: new Konva.Layer({ - name: `${this.type}:layer`, - listening: false, - imageSmoothingEnabled: false, - }), - }; - - this.renderer = new CanvasEntityRenderer(this); - this.transformer = new CanvasEntityTransformer(this); - } - - /** - * Get this entity's entity identifier - */ - getEntityIdentifier = (): CanvasEntityIdentifier => { - return getEntityIdentifier(this.state); - }; - - update = async (arg?: { state: CanvasEntityLayerAdapter['state'] }): Promise => { - const state = get(arg, 'state', this.state); - - const prevState = this.state; - this.state = state; - - if (!this.isFirstRender && prevState === state) { - this.log.trace('State unchanged, skipping update'); - return; - } - - this.log.debug('Updating'); - const { position, objects, opacity, isEnabled, isLocked } = state; - - if (this.isFirstRender || isEnabled !== prevState.isEnabled) { - this.updateVisibility({ isEnabled }); - } - if (this.isFirstRender || isLocked !== prevState.isLocked) { - this.transformer.syncInteractionState(); - } - if (this.isFirstRender || objects !== prevState.objects) { - await this.updateObjects({ objects }); - } - if (this.isFirstRender || position !== prevState.position) { - this.transformer.updatePosition({ position }); - } - if (this.isFirstRender || opacity !== prevState.opacity) { - this.renderer.updateOpacity(opacity); - } - - if (state.type === 'control_layer' && prevState.type === 'control_layer') { - if (this.isFirstRender || state.withTransparencyEffect !== prevState.withTransparencyEffect) { - this.renderer.updateTransparencyEffect(state.withTransparencyEffect); - } - } - - if (this.isFirstRender) { - this.transformer.updateBbox(); - } - - this.isFirstRender = false; - }; - - updateVisibility = (arg?: { isEnabled: boolean }) => { - this.log.trace('Updating visibility'); - const isEnabled = get(arg, 'isEnabled', this.state.isEnabled); - this.konva.layer.visible(isEnabled); - this.renderer.syncCache(isEnabled); - }; - - updateObjects = async (arg?: { objects: CanvasRasterLayerState['objects'] }) => { - this.log.trace('Updating objects'); - - const objects = get(arg, 'objects', this.state.objects); - - const didUpdate = await this.renderer.render(objects); - - if (didUpdate) { - this.transformer.requestRectCalculation(); - } - }; - - getCanvas = (rect?: Rect): HTMLCanvasElement => { - this.log.trace({ rect }, 'Getting canvas'); - // The opacity may have been changed in response to user selecting a different entity category, so we must restore - // the original opacity before rendering the canvas - const attrs: GroupConfig = { opacity: this.state.opacity }; - const canvas = this.renderer.getCanvas(rect, attrs); - return canvas; - }; - - getHashableState = (): SerializableObject => { - if (this.state.type === 'control_layer') { - const keysToOmit: (keyof CanvasControlLayerState)[] = ['name', 'controlAdapter', 'withTransparencyEffect']; - return omit(this.state, keysToOmit); - } else if (this.state.type === 'raster_layer') { - const keysToOmit: (keyof CanvasRasterLayerState)[] = ['name']; - return omit(this.state, keysToOmit); - } else { - assert(false, 'Unexpected layer type'); - } - }; - - hash = (extra?: SerializableObject): string => { - const arg = { - state: this.getHashableState(), - extra, - }; - return stableHash(arg); - }; - - getLastPointOfLastLine = (type: CanvasBrushLineState['type'] | CanvasEraserLineState['type']): Coordinate | null => { - const lastObject = this.state.objects[this.state.objects.length - 1]; - if (!lastObject) { - return null; - } - - if (lastObject.type === type) { - return getLastPointOfLine(lastObject.points); - } - - return null; - }; - - isInteractable = (): boolean => { - return this.state.isEnabled && !this.state.isLocked; - }; - - destroy = (): void => { - this.log.debug('Destroying module'); - this.renderer.destroy(); - this.transformer.destroy(); - this.konva.layer.destroy(); - }; - - repr = () => { - return { - id: this.id, - type: this.type, - path: this.path, - state: deepClone(this.state), - transformer: this.transformer.repr(), - renderer: this.renderer.repr(), - }; - }; -} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityMaskAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityMaskAdapter.ts deleted file mode 100644 index eb5a4fe5fbe..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityMaskAdapter.ts +++ /dev/null @@ -1,189 +0,0 @@ -import type { SerializableObject } from 'common/types'; -import { deepClone } from 'common/util/deepClone'; -import { CanvasEntityRenderer } from 'features/controlLayers/konva/CanvasEntityRenderer'; -import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntityTransformer'; -import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; -import { getLastPointOfLine } from 'features/controlLayers/konva/util'; -import type { - CanvasBrushLineState, - CanvasEntityIdentifier, - CanvasEraserLineState, - CanvasInpaintMaskState, - CanvasRegionalGuidanceState, - Coordinate, - Rect, -} from 'features/controlLayers/store/types'; -import { getEntityIdentifier } from 'features/controlLayers/store/types'; -import Konva from 'konva'; -import type { GroupConfig } from 'konva/lib/Group'; -import { get, omit } from 'lodash-es'; -import type { Logger } from 'roarr'; -import stableHash from 'stable-hash'; - -export class CanvasEntityMaskAdapter extends CanvasModuleBase { - readonly type = 'entity_mask_adapter'; - readonly id: string; - readonly path: string[]; - readonly parent: CanvasManager; - readonly manager: CanvasManager; - readonly log: Logger; - - state: CanvasInpaintMaskState | CanvasRegionalGuidanceState; - - transformer: CanvasEntityTransformer; - renderer: CanvasEntityRenderer; - - isFirstRender: boolean = true; - - konva: { - layer: Konva.Layer; - }; - - constructor(state: CanvasEntityMaskAdapter['state'], manager: CanvasEntityMaskAdapter['manager']) { - super(); - this.id = state.id; - this.parent = manager; - this.manager = manager; - this.path = this.manager.buildPath(this); - this.log = this.manager.buildLogger(this); - - this.log.debug('Creating module'); - - this.state = state; - - this.konva = { - layer: new Konva.Layer({ - name: `${this.type}:layer`, - listening: false, - imageSmoothingEnabled: false, - }), - }; - - this.renderer = new CanvasEntityRenderer(this); - this.transformer = new CanvasEntityTransformer(this); - } - - /** - * Get this entity's entity identifier - */ - getEntityIdentifier = (): CanvasEntityIdentifier => { - return getEntityIdentifier(this.state); - }; - - update = async (arg?: { state: CanvasEntityMaskAdapter['state'] }) => { - const state = get(arg, 'state', this.state); - - const prevState = this.state; - this.state = state; - - if (!this.isFirstRender && prevState === state && prevState.fill === state.fill) { - this.log.trace('State unchanged, skipping update'); - return; - } - - this.log.debug('Updating'); - const { position, objects, isEnabled, isLocked, opacity } = state; - - if (this.isFirstRender || objects !== prevState.objects) { - await this.updateObjects({ objects }); - } - if (this.isFirstRender || position !== prevState.position) { - this.transformer.updatePosition({ position }); - } - if (this.isFirstRender || opacity !== prevState.opacity) { - this.renderer.updateOpacity(opacity); - } - if (this.isFirstRender || isEnabled !== prevState.isEnabled) { - this.updateVisibility({ isEnabled }); - } - if (this.isFirstRender || isLocked !== prevState.isLocked) { - this.transformer.syncInteractionState(); - } - if (this.isFirstRender || state.fill !== prevState.fill) { - this.renderer.updateCompositingRectFill(state.fill); - } - - if (this.isFirstRender) { - this.renderer.updateCompositingRectSize(); - } - - if (this.isFirstRender) { - this.transformer.updateBbox(); - } - - this.isFirstRender = false; - }; - - updateObjects = async (arg?: { objects: CanvasInpaintMaskState['objects'] }) => { - this.log.trace('Updating objects'); - - const objects = get(arg, 'objects', this.state.objects); - - const didUpdate = await this.renderer.render(objects); - - if (didUpdate) { - this.transformer.requestRectCalculation(); - } - }; - - updateVisibility = (arg?: { isEnabled: boolean }) => { - this.log.trace('Updating visibility'); - const isEnabled = get(arg, 'isEnabled', this.state.isEnabled); - this.konva.layer.visible(isEnabled); - }; - - getLastPointOfLastLine = (type: CanvasBrushLineState['type'] | CanvasEraserLineState['type']): Coordinate | null => { - const lastObject = this.state.objects[this.state.objects.length - 1]; - if (!lastObject) { - return null; - } - - if (lastObject.type === type) { - return getLastPointOfLine(lastObject.points); - } - - return null; - }; - - getHashableState = (): SerializableObject => { - const keysToOmit: (keyof CanvasEntityMaskAdapter['state'])[] = ['fill', 'name', 'opacity']; - return omit(this.state, keysToOmit); - }; - - hash = (extra?: SerializableObject): string => { - const arg = { - state: this.getHashableState(), - extra, - }; - return stableHash(arg); - }; - - getCanvas = (rect?: Rect): HTMLCanvasElement => { - // The opacity may have been changed in response to user selecting a different entity category, and the mask regions - // should be fully opaque - set opacity to 1 before rendering the canvas - const attrs: GroupConfig = { opacity: 1 }; - const canvas = this.renderer.getCanvas(rect, attrs); - return canvas; - }; - - isInteractable = (): boolean => { - return this.state.isEnabled && !this.state.isLocked; - }; - - destroy = () => { - this.log.debug('Destroying module'); - this.transformer.destroy(); - this.renderer.destroy(); - this.konva.layer.destroy(); - }; - - repr = () => { - return { - id: this.id, - type: this.type, - path: this.path, - state: deepClone(this.state), - }; - }; -} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts index 8c9f8e12d9c..ee28a35ac41 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts @@ -1,14 +1,11 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; -import type { CanvasControlLayerAdapter } from 'features/controlLayers/konva/CanvasControlLayerAdapter'; -import type { CanvasInpaintMaskAdapter } from 'features/controlLayers/konva/CanvasInpaintMaskAdapter'; +import type { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapterBase'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { CanvasObjectBrushLineRenderer } from 'features/controlLayers/konva/CanvasObjectBrushLineRenderer'; import { CanvasObjectEraserLineRenderer } from 'features/controlLayers/konva/CanvasObjectEraserLineRenderer'; import { CanvasObjectImageRenderer } from 'features/controlLayers/konva/CanvasObjectImageRenderer'; import { CanvasObjectRectRenderer } from 'features/controlLayers/konva/CanvasObjectRectRenderer'; -import type { CanvasRasterLayerAdapter } from 'features/controlLayers/konva/CanvasRasterLayerAdapter'; -import type { CanvasRegionalGuidanceAdapter } from 'features/controlLayers/konva/CanvasRegionalGuidanceAdapter'; import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; import { getPatternSVG } from 'features/controlLayers/konva/patterns/getPatternSVG'; import { @@ -66,11 +63,7 @@ export class CanvasEntityRenderer extends CanvasModuleBase { readonly type = 'entity_renderer'; readonly id: string; readonly path: string[]; - readonly parent: - | CanvasRasterLayerAdapter - | CanvasControlLayerAdapter - | CanvasInpaintMaskAdapter - | CanvasRegionalGuidanceAdapter; + readonly parent: CanvasEntityAdapterBase; readonly manager: CanvasManager; readonly log: Logger; @@ -141,13 +134,7 @@ export class CanvasEntityRenderer extends CanvasModuleBase { */ $canvasCache = atom<{ canvas: HTMLCanvasElement; rect: Rect } | null>(null); - constructor( - parent: - | CanvasRasterLayerAdapter - | CanvasControlLayerAdapter - | CanvasInpaintMaskAdapter - | CanvasRegionalGuidanceAdapter - ) { + constructor(parent: CanvasEntityAdapterBase) { super(); this.id = getPrefixedId(this.type); this.parent = parent; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts index 7c755624ad8..0c639010003 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityTransformer.ts @@ -1,9 +1,6 @@ -import type { CanvasControlLayerAdapter } from 'features/controlLayers/konva/CanvasControlLayerAdapter'; -import type { CanvasInpaintMaskAdapter } from 'features/controlLayers/konva/CanvasInpaintMaskAdapter'; +import type { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapterBase'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; -import type { CanvasRasterLayerAdapter } from 'features/controlLayers/konva/CanvasRasterLayerAdapter'; -import type { CanvasRegionalGuidanceAdapter } from 'features/controlLayers/konva/CanvasRegionalGuidanceAdapter'; import { canvasToImageData, getEmptyRect, getPrefixedId } from 'features/controlLayers/konva/util'; import type { Coordinate, Rect, RectWithRotation } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -82,11 +79,7 @@ export class CanvasEntityTransformer extends CanvasModuleBase { readonly type = 'entity_transformer'; readonly id: string; readonly path: string[]; - readonly parent: - | CanvasRasterLayerAdapter - | CanvasControlLayerAdapter - | CanvasInpaintMaskAdapter - | CanvasRegionalGuidanceAdapter; + readonly parent: CanvasEntityAdapterBase; readonly manager: CanvasManager; readonly log: Logger; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMaskAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMaskAdapter.ts index 1a02b8077c7..0a0028b830c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMaskAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMaskAdapter.ts @@ -1,61 +1,21 @@ import type { SerializableObject } from 'common/types'; -import { deepClone } from 'common/util/deepClone'; -import { CanvasEntityRenderer } from 'features/controlLayers/konva/CanvasEntityRenderer'; -import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntityTransformer'; +import { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapterBase'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import type { CanvasEntityIdentifier, CanvasInpaintMaskState, Rect } from 'features/controlLayers/store/types'; -import { getEntityIdentifier } from 'features/controlLayers/store/types'; -import Konva from 'konva'; import type { GroupConfig } from 'konva/lib/Group'; import { omit } from 'lodash-es'; -import type { Logger } from 'roarr'; -import stableHash from 'stable-hash'; import { assert } from 'tsafe'; -export class CanvasInpaintMaskAdapter extends CanvasModuleBase { - readonly type = 'inpaint_mask_adapter'; - readonly id: string; - readonly path: string[]; - readonly parent: CanvasManager; - readonly manager: CanvasManager; - readonly log: Logger; - - entityIdentifier: CanvasEntityIdentifier<'inpaint_mask'>; +export class CanvasInpaintMaskAdapter extends CanvasEntityAdapterBase { + static TYPE = 'inpaint_mask_adapter'; /** * The last known state of the entity. */ private _state: CanvasInpaintMaskState | null = null; - transformer: CanvasEntityTransformer; - renderer: CanvasEntityRenderer; - - konva: { - layer: Konva.Layer; - }; - constructor(entityIdentifier: CanvasEntityIdentifier<'inpaint_mask'>, manager: CanvasManager) { - super(); - this.id = entityIdentifier.id; - this.entityIdentifier = entityIdentifier; - this.parent = manager; - this.manager = manager; - this.path = this.manager.buildPath(this); - this.log = this.manager.buildLogger(this); - - this.log.debug('Creating module'); - - this.konva = { - layer: new Konva.Layer({ - name: `${this.type}:layer`, - listening: false, - imageSmoothingEnabled: false, - }), - }; - - this.renderer = new CanvasEntityRenderer(this); - this.transformer = new CanvasEntityTransformer(this); + super(entityIdentifier, manager, CanvasInpaintMaskAdapter.TYPE); } get state(): CanvasInpaintMaskState { @@ -71,13 +31,6 @@ export class CanvasInpaintMaskAdapter extends CanvasModuleBase { this._state = state; } - /** - * Get this entity's entity identifier - */ - getEntityIdentifier = (): CanvasEntityIdentifier => { - return getEntityIdentifier(this.state); - }; - update = async (state: CanvasInpaintMaskState) => { const prevState = this.state; this.state = state; @@ -125,14 +78,6 @@ export class CanvasInpaintMaskAdapter extends CanvasModuleBase { return omit(this.state, keysToOmit); }; - hash = (extra?: SerializableObject): string => { - const arg = { - state: this.getHashableState(), - extra, - }; - return stableHash(arg); - }; - getCanvas = (rect?: Rect): HTMLCanvasElement => { // The opacity may have been changed in response to user selecting a different entity category, and the mask regions // should be fully opaque - set opacity to 1 before rendering the canvas @@ -140,24 +85,4 @@ export class CanvasInpaintMaskAdapter extends CanvasModuleBase { const canvas = this.renderer.getCanvas(rect, attrs); return canvas; }; - - isInteractable = (): boolean => { - return this.state.isEnabled && !this.state.isLocked; - }; - - destroy = () => { - this.log.debug('Destroying module'); - this.transformer.destroy(); - this.renderer.destroy(); - this.konva.layer.destroy(); - }; - - repr = () => { - return { - id: this.id, - type: this.type, - path: this.path, - state: deepClone(this.state), - }; - }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRasterLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRasterLayerAdapter.ts index e1e8bbeecc5..c3a9da71d3f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRasterLayerAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRasterLayerAdapter.ts @@ -1,73 +1,20 @@ import type { SerializableObject } from 'common/types'; -import { deepClone } from 'common/util/deepClone'; -import { CanvasEntityRenderer } from 'features/controlLayers/konva/CanvasEntityRenderer'; -import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntityTransformer'; +import { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapterBase'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import type { CanvasEntityIdentifier, CanvasRasterLayerState, Rect } from 'features/controlLayers/store/types'; -import Konva from 'konva'; import type { GroupConfig } from 'konva/lib/Group'; import { omit } from 'lodash-es'; -import type { Logger } from 'roarr'; -import stableHash from 'stable-hash'; import { assert } from 'tsafe'; -export class CanvasRasterLayerAdapter extends CanvasModuleBase { - readonly type = 'raster_layer_adapter'; - readonly id: string; - readonly path: string[]; - readonly manager: CanvasManager; - readonly parent: CanvasManager; - readonly log: Logger; - - entityIdentifier: CanvasEntityIdentifier<'raster_layer'>; - +export class CanvasRasterLayerAdapter extends CanvasEntityAdapterBase { + static TYPE = 'raster_layer_adapter'; /** * The last known state of the entity. */ private _state: CanvasRasterLayerState | null = null; - /** - * The Konva nodes that make up the entity layer: - * - A layer to hold the everything - * - * Note that the transformer and object renderer have their own Konva nodes, but they are not stored here. - */ - konva: { - layer: Konva.Layer; - }; - - /** - * The transformer for this entity layer. - */ - transformer: CanvasEntityTransformer; - - /** - * The renderer for this entity layer. - */ - renderer: CanvasEntityRenderer; - constructor(entityIdentifier: CanvasEntityIdentifier<'raster_layer'>, manager: CanvasManager) { - super(); - this.id = entityIdentifier.id; - this.entityIdentifier = entityIdentifier; - this.manager = manager; - this.parent = manager; - this.path = this.manager.buildPath(this); - this.log = this.manager.buildLogger(this); - - this.log.debug('Creating module'); - - this.konva = { - layer: new Konva.Layer({ - name: `${this.type}:layer`, - listening: false, - imageSmoothingEnabled: false, - }), - }; - - this.renderer = new CanvasEntityRenderer(this); - this.transformer = new CanvasEntityTransformer(this); + super(entityIdentifier, manager, CanvasRasterLayerAdapter.TYPE); } get state(): CanvasRasterLayerState { @@ -126,38 +73,8 @@ export class CanvasRasterLayerAdapter extends CanvasModuleBase { return canvas; }; - isInteractable = (): boolean => { - return this.state.isEnabled && !this.state.isLocked; - }; - getHashableState = (): SerializableObject => { const keysToOmit: (keyof CanvasRasterLayerState)[] = ['name']; return omit(this.state, keysToOmit); }; - - hash = (extra?: SerializableObject): string => { - const arg = { - state: this.getHashableState(), - extra, - }; - return stableHash(arg); - }; - - destroy = (): void => { - this.log.debug('Destroying module'); - this.renderer.destroy(); - this.transformer.destroy(); - this.konva.layer.destroy(); - }; - - repr = () => { - return { - id: this.id, - type: this.type, - path: this.path, - state: deepClone(this.state), - transformer: this.transformer.repr(), - renderer: this.renderer.repr(), - }; - }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegionalGuidanceAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegionalGuidanceAdapter.ts index e72dbfc174f..5ce08fa7897 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegionalGuidanceAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegionalGuidanceAdapter.ts @@ -1,61 +1,21 @@ import type { SerializableObject } from 'common/types'; -import { deepClone } from 'common/util/deepClone'; -import { CanvasEntityRenderer } from 'features/controlLayers/konva/CanvasEntityRenderer'; -import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntityTransformer'; +import { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapterBase'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import type { CanvasEntityIdentifier, CanvasRegionalGuidanceState, Rect } from 'features/controlLayers/store/types'; -import { getEntityIdentifier } from 'features/controlLayers/store/types'; -import Konva from 'konva'; import type { GroupConfig } from 'konva/lib/Group'; import { omit } from 'lodash-es'; -import type { Logger } from 'roarr'; -import stableHash from 'stable-hash'; import { assert } from 'tsafe'; -export class CanvasRegionalGuidanceAdapter extends CanvasModuleBase { - readonly type = 'regional_guidance_adapter'; - readonly id: string; - readonly path: string[]; - readonly parent: CanvasManager; - readonly manager: CanvasManager; - readonly log: Logger; - - entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>; +export class CanvasRegionalGuidanceAdapter extends CanvasEntityAdapterBase { + static TYPE = 'regional_guidance_adapter'; /** * The last known state of the entity. */ private _state: CanvasRegionalGuidanceState | null = null; - transformer: CanvasEntityTransformer; - renderer: CanvasEntityRenderer; - - konva: { - layer: Konva.Layer; - }; - constructor(entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>, manager: CanvasManager) { - super(); - this.id = entityIdentifier.id; - this.entityIdentifier = entityIdentifier; - this.parent = manager; - this.manager = manager; - this.path = this.manager.buildPath(this); - this.log = this.manager.buildLogger(this); - - this.log.debug('Creating module'); - - this.konva = { - layer: new Konva.Layer({ - name: `${this.type}:layer`, - listening: false, - imageSmoothingEnabled: false, - }), - }; - - this.renderer = new CanvasEntityRenderer(this); - this.transformer = new CanvasEntityTransformer(this); + super(entityIdentifier, manager, CanvasRegionalGuidanceAdapter.TYPE); } get state(): CanvasRegionalGuidanceState { @@ -68,20 +28,12 @@ export class CanvasRegionalGuidanceAdapter extends CanvasModuleBase { } set state(state: CanvasRegionalGuidanceState) { + const prevState = this._state; this._state = state; + this.render(state, prevState); } - /** - * Get this entity's entity identifier - */ - getEntityIdentifier = (): CanvasEntityIdentifier => { - return getEntityIdentifier(this.state); - }; - - update = async (state: CanvasRegionalGuidanceState) => { - const prevState = this.state; - this.state = state; - + render = async (state: CanvasRegionalGuidanceState, prevState: CanvasRegionalGuidanceState | null) => { if (prevState && prevState === state) { this.log.trace('State unchanged, skipping update'); return; @@ -125,14 +77,6 @@ export class CanvasRegionalGuidanceAdapter extends CanvasModuleBase { return omit(this.state, keysToOmit); }; - hash = (extra?: SerializableObject): string => { - const arg = { - state: this.getHashableState(), - extra, - }; - return stableHash(arg); - }; - getCanvas = (rect?: Rect): HTMLCanvasElement => { // The opacity may have been changed in response to user selecting a different entity category, and the mask regions // should be fully opaque - set opacity to 1 before rendering the canvas @@ -140,24 +84,4 @@ export class CanvasRegionalGuidanceAdapter extends CanvasModuleBase { const canvas = this.renderer.getCanvas(rect, attrs); return canvas; }; - - isInteractable = (): boolean => { - return this.state.isEnabled && !this.state.isLocked; - }; - - destroy = () => { - this.log.debug('Destroying module'); - this.transformer.destroy(); - this.renderer.destroy(); - this.konva.layer.destroy(); - }; - - repr = () => { - return { - id: this.id, - type: this.type, - path: this.path, - state: deepClone(this.state), - }; - }; } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index 0dd85b3819f..b3fb0913652 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -1,6 +1,7 @@ import { $alt, $ctrl, $meta, $shift } from '@invoke-ai/ui-library'; import type { AppStore } from 'app/store/store'; import type { CanvasControlLayerAdapter } from 'features/controlLayers/konva/CanvasControlLayerAdapter'; +import type { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapterBase'; import type { CanvasInpaintMaskAdapter } from 'features/controlLayers/konva/CanvasInpaintMaskAdapter'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; @@ -362,13 +363,7 @@ export class CanvasStateApiModule extends CanvasModuleBase { /** * The entity adapter being transformed, if any. */ - $transformingAdapter = atom< - | CanvasRasterLayerAdapter - | CanvasControlLayerAdapter - | CanvasInpaintMaskAdapter - | CanvasRegionalGuidanceAdapter - | null - >(null); + $transformingAdapter = atom(null); /** * Whether an entity is currently being transformed. Derived from `$transformingAdapter`. diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts index 0d3b01c91af..308ee5f7ed6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasToolModule.ts @@ -24,7 +24,7 @@ import type { RgbColor, Tool, } from 'features/controlLayers/store/types'; -import { isDrawableEntity, RGBA_BLACK } from 'features/controlLayers/store/types'; +import { isRenderableEntity, RGBA_BLACK } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; import { atom } from 'nanostores'; @@ -171,7 +171,7 @@ export class CanvasToolModule extends CanvasModuleBase { !!selectedEntity && selectedEntity.state.isEnabled && !selectedEntity.state.isLocked && - isDrawableEntity(selectedEntity.state); + isRenderableEntity(selectedEntity.state); this.syncCursorStyle(); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index ba62226c3ca..5987553f264 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -56,7 +56,7 @@ import { imageDTOToImageWithDims, initialControlNet, initialIPAdapter, - isDrawableEntity, + isRenderableEntity, } from './types'; const DEFAULT_MASK_COLORS: RgbColor[] = [ @@ -818,7 +818,7 @@ export const canvasSlice = createSlice({ const entity = selectEntity(state, entityIdentifier); if (!entity) { return; - } else if (isDrawableEntity(entity)) { + } else if (isRenderableEntity(entity)) { entity.isEnabled = true; entity.objects = []; entity.position = { x: 0, y: 0 }; @@ -907,7 +907,7 @@ export const canvasSlice = createSlice({ return; } - if (isDrawableEntity(entity)) { + if (isRenderableEntity(entity)) { entity.position = position; } }, @@ -918,7 +918,7 @@ export const canvasSlice = createSlice({ return; } - if (isDrawableEntity(entity)) { + if (isRenderableEntity(entity)) { if (replaceObjects) { entity.objects = [imageObject]; entity.position = { x: rect.x, y: rect.y }; @@ -932,7 +932,7 @@ export const canvasSlice = createSlice({ return; } - if (!isDrawableEntity(entity)) { + if (!isRenderableEntity(entity)) { assert(false, `Cannot add a brush line to a non-drawable entity of type ${entity.type}`); } @@ -947,7 +947,7 @@ export const canvasSlice = createSlice({ return; } - if (!isDrawableEntity(entity)) { + if (!isRenderableEntity(entity)) { assert(false, `Cannot add a eraser line to a non-drawable entity of type ${entity.type}`); } @@ -962,7 +962,7 @@ export const canvasSlice = createSlice({ return; } - if (!isDrawableEntity(entity)) { + if (!isRenderableEntity(entity)) { assert(false, `Cannot add a rect to a non-drawable entity of type ${entity.type}`); } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 048af3b7d5c..e7ce9e7a6f0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -675,6 +675,12 @@ export type CanvasEntityState = | CanvasInpaintMaskState | CanvasIPAdapterState; +export type CanvasRenderableEntityState = + | CanvasRasterLayerState + | CanvasControlLayerState + | CanvasRegionalGuidanceState + | CanvasInpaintMaskState; + export type CanvasEntityType = CanvasEntityState['type']; export type CanvasEntityIdentifier = { id: string; type: T }; @@ -773,7 +779,9 @@ export type EntityRasterizedPayload = EntityIdentifierPayload<{ export type GenerationMode = 'txt2img' | 'img2img' | 'inpaint' | 'outpaint'; -export function isDrawableEntityType(entityType: CanvasEntityState['type']) { +export function isDrawableEntityType( + entityType: CanvasEntityState['type'] +): entityType is CanvasRenderableEntityState['type'] { return ( entityType === 'raster_layer' || entityType === 'control_layer' || @@ -782,9 +790,7 @@ export function isDrawableEntityType(entityType: CanvasEntityState['type']) { ); } -export function isDrawableEntity( - entity: CanvasEntityState -): entity is CanvasRasterLayerState | CanvasControlLayerState | CanvasRegionalGuidanceState | CanvasInpaintMaskState { +export function isRenderableEntity(entity: CanvasEntityState): entity is CanvasRenderableEntityState { return isDrawableEntityType(entity.type); } From 200dbbbf550f2354365be8b3fe993d47e403aeb7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 4 Sep 2024 19:09:32 +1000 Subject: [PATCH 634/678] tidy(ui): rename some classes to better represent their responsibilities --- .../konva/CanvasEntityAdapterBase.ts | 6 +- ...derer.ts => CanvasEntityObjectRenderer.ts} | 58 +++++++++---------- ...neRenderer.ts => CanvasObjectBrushLine.ts} | 10 ++-- ...eRenderer.ts => CanvasObjectEraserLine.ts} | 10 ++-- ...tImageRenderer.ts => CanvasObjectImage.ts} | 10 ++-- ...ectRectRenderer.ts => CanvasObjectRect.ts} | 10 ++-- .../konva/CanvasStagingAreaModule.ts | 6 +- 7 files changed, 55 insertions(+), 55 deletions(-) rename invokeai/frontend/web/src/features/controlLayers/konva/{CanvasEntityRenderer.ts => CanvasEntityObjectRenderer.ts} (91%) rename invokeai/frontend/web/src/features/controlLayers/konva/{CanvasObjectBrushLineRenderer.ts => CanvasObjectBrushLine.ts} (87%) rename invokeai/frontend/web/src/features/controlLayers/konva/{CanvasObjectEraserLineRenderer.ts => CanvasObjectEraserLine.ts} (89%) rename invokeai/frontend/web/src/features/controlLayers/konva/{CanvasObjectImageRenderer.ts => CanvasObjectImage.ts} (93%) rename invokeai/frontend/web/src/features/controlLayers/konva/{CanvasObjectRectRenderer.ts => CanvasObjectRect.ts} (86%) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterBase.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterBase.ts index 7a155b77732..dea9af3b3ca 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterBase.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityAdapterBase.ts @@ -1,6 +1,6 @@ import type { SerializableObject } from 'common/types'; import { deepClone } from 'common/util/deepClone'; -import { CanvasEntityRenderer } from 'features/controlLayers/konva/CanvasEntityRenderer'; +import { CanvasEntityObjectRenderer } from 'features/controlLayers/konva/CanvasEntityObjectRenderer'; import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntityTransformer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; @@ -39,7 +39,7 @@ export abstract class CanvasEntityAdapterBase< /** * The renderer for this entity adapter. */ - renderer: CanvasEntityRenderer; + renderer: CanvasEntityObjectRenderer; constructor(entityIdentifier: CanvasEntityIdentifier, manager: CanvasManager, adapterType: string) { super(); @@ -61,7 +61,7 @@ export abstract class CanvasEntityAdapterBase< }), }; - this.renderer = new CanvasEntityRenderer(this); + this.renderer = new CanvasEntityObjectRenderer(this); this.transformer = new CanvasEntityTransformer(this); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityObjectRenderer.ts similarity index 91% rename from invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityObjectRenderer.ts index ee28a35ac41..92834febff7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityObjectRenderer.ts @@ -2,10 +2,10 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers'; import type { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntityAdapterBase'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; -import { CanvasObjectBrushLineRenderer } from 'features/controlLayers/konva/CanvasObjectBrushLineRenderer'; -import { CanvasObjectEraserLineRenderer } from 'features/controlLayers/konva/CanvasObjectEraserLineRenderer'; -import { CanvasObjectImageRenderer } from 'features/controlLayers/konva/CanvasObjectImageRenderer'; -import { CanvasObjectRectRenderer } from 'features/controlLayers/konva/CanvasObjectRectRenderer'; +import { CanvasObjectBrushLine } from 'features/controlLayers/konva/CanvasObjectBrushLine'; +import { CanvasObjectEraserLine } from 'features/controlLayers/konva/CanvasObjectEraserLine'; +import { CanvasObjectImage } from 'features/controlLayers/konva/CanvasObjectImage'; +import { CanvasObjectRect } from 'features/controlLayers/konva/CanvasObjectRect'; import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; import { getPatternSVG } from 'features/controlLayers/konva/patterns/getPatternSVG'; import { @@ -47,10 +47,10 @@ function setFillPatternImage(shape: Konva.Shape, ...args: Parameters { let needsPixelBbox = false; for (const renderer of this.renderers.values()) { - const isEraserLine = renderer instanceof CanvasObjectEraserLineRenderer; - const isImage = renderer instanceof CanvasObjectImageRenderer; - const hasClip = renderer instanceof CanvasObjectBrushLineRenderer && renderer.state.clip; + const isEraserLine = renderer instanceof CanvasObjectEraserLine; + const isImage = renderer instanceof CanvasObjectImage; + const hasClip = renderer instanceof CanvasObjectBrushLine && renderer.state.clip; if (isEraserLine || hasClip || isImage) { needsPixelBbox = true; break; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectBrushLineRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectBrushLine.ts similarity index 87% rename from invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectBrushLineRenderer.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectBrushLine.ts index 0d831b9112e..eee77083fd7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectBrushLineRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectBrushLine.ts @@ -1,17 +1,17 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; -import type { CanvasEntityRenderer } from 'features/controlLayers/konva/CanvasEntityRenderer'; +import type { CanvasEntityObjectRenderer } from 'features/controlLayers/konva/CanvasEntityObjectRenderer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import type { CanvasBrushLineState } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; -export class CanvasObjectBrushLineRenderer extends CanvasModuleBase { - readonly type = 'object_brush_line_renderer'; +export class CanvasObjectBrushLine extends CanvasModuleBase { + readonly type = 'object_brush_line'; readonly id: string; readonly path: string[]; - readonly parent: CanvasEntityRenderer; + readonly parent: CanvasEntityObjectRenderer; readonly manager: CanvasManager; readonly log: Logger; @@ -21,7 +21,7 @@ export class CanvasObjectBrushLineRenderer extends CanvasModuleBase { line: Konva.Line; }; - constructor(state: CanvasBrushLineState, parent: CanvasEntityRenderer) { + constructor(state: CanvasBrushLineState, parent: CanvasEntityObjectRenderer) { super(); const { id, clip } = state; this.id = id; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectEraserLineRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectEraserLine.ts similarity index 89% rename from invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectEraserLineRenderer.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectEraserLine.ts index 5eb6ee4d9d8..e4de8522bda 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectEraserLineRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectEraserLine.ts @@ -1,16 +1,16 @@ import { deepClone } from 'common/util/deepClone'; -import type { CanvasEntityRenderer } from 'features/controlLayers/konva/CanvasEntityRenderer'; +import type { CanvasEntityObjectRenderer } from 'features/controlLayers/konva/CanvasEntityObjectRenderer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import type { CanvasEraserLineState } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; -export class CanvasObjectEraserLineRenderer extends CanvasModuleBase { - readonly type = 'object_eraser_line_renderer'; +export class CanvasObjectEraserLine extends CanvasModuleBase { + readonly type = 'object_eraser_line'; readonly id: string; readonly path: string[]; - readonly parent: CanvasEntityRenderer; + readonly parent: CanvasEntityObjectRenderer; readonly manager: CanvasManager; readonly log: Logger; @@ -20,7 +20,7 @@ export class CanvasObjectEraserLineRenderer extends CanvasModuleBase { line: Konva.Line; }; - constructor(state: CanvasEraserLineState, parent: CanvasEntityRenderer) { + constructor(state: CanvasEraserLineState, parent: CanvasEntityObjectRenderer) { super(); this.id = state.id; this.parent = parent; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectImageRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectImage.ts similarity index 93% rename from invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectImageRenderer.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectImage.ts index 3c4a6c6e5d3..effd103f09e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectImageRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectImage.ts @@ -1,6 +1,6 @@ import { Mutex } from 'async-mutex'; import { deepClone } from 'common/util/deepClone'; -import type { CanvasEntityRenderer } from 'features/controlLayers/konva/CanvasEntityRenderer'; +import type { CanvasEntityObjectRenderer } from 'features/controlLayers/konva/CanvasEntityObjectRenderer'; import type { CanvasFilterModule } from 'features/controlLayers/konva/CanvasFilterModule'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; @@ -12,11 +12,11 @@ import Konva from 'konva'; import type { Logger } from 'roarr'; import { getImageDTO } from 'services/api/endpoints/images'; -export class CanvasObjectImageRenderer extends CanvasModuleBase { - readonly type = 'object_image_renderer'; +export class CanvasObjectImage extends CanvasModuleBase { + readonly type = 'object_image'; readonly id: string; readonly path: string[]; - readonly parent: CanvasEntityRenderer | CanvasStagingAreaModule | CanvasFilterModule; + readonly parent: CanvasEntityObjectRenderer | CanvasStagingAreaModule | CanvasFilterModule; readonly manager: CanvasManager; readonly log: Logger; @@ -31,7 +31,7 @@ export class CanvasObjectImageRenderer extends CanvasModuleBase { isError: boolean = false; mutex = new Mutex(); - constructor(state: CanvasImageState, parent: CanvasEntityRenderer | CanvasStagingAreaModule | CanvasFilterModule) { + constructor(state: CanvasImageState, parent: CanvasEntityObjectRenderer | CanvasStagingAreaModule | CanvasFilterModule) { super(); this.id = state.id; this.parent = parent; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRect.ts similarity index 86% rename from invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRectRenderer.ts rename to invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRect.ts index 68ee51000f8..d8224c25bd3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRect.ts @@ -1,17 +1,17 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { deepClone } from 'common/util/deepClone'; -import type { CanvasEntityRenderer } from 'features/controlLayers/konva/CanvasEntityRenderer'; +import type { CanvasEntityObjectRenderer } from 'features/controlLayers/konva/CanvasEntityObjectRenderer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import type { CanvasRectState } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { Logger } from 'roarr'; -export class CanvasObjectRectRenderer extends CanvasModuleBase { - readonly type = 'object_rect_renderer'; +export class CanvasObjectRect extends CanvasModuleBase { + readonly type = 'object_rect'; readonly id: string; readonly path: string[]; - readonly parent: CanvasEntityRenderer; + readonly parent: CanvasEntityObjectRenderer; readonly manager: CanvasManager; readonly log: Logger; @@ -22,7 +22,7 @@ export class CanvasObjectRectRenderer extends CanvasModuleBase { }; isFirstRender: boolean = false; - constructor(state: CanvasRectState, parent: CanvasEntityRenderer) { + constructor(state: CanvasRectState, parent: CanvasEntityObjectRenderer) { super(); this.id = state.id; this.parent = parent; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts index 2446cc3f13c..3bc00efdf15 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts @@ -1,6 +1,6 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; -import { CanvasObjectImageRenderer } from 'features/controlLayers/konva/CanvasObjectImageRenderer'; +import { CanvasObjectImage } from 'features/controlLayers/konva/CanvasObjectImage'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { imageDTOToImageWithDims, type StagingAreaImage } from 'features/controlLayers/store/types'; import Konva from 'konva'; @@ -17,7 +17,7 @@ export class CanvasStagingAreaModule extends CanvasModuleBase { subscriptions: Set<() => void> = new Set(); konva: { group: Konva.Group }; - image: CanvasObjectImageRenderer | null; + image: CanvasObjectImage | null; selectedImage: StagingAreaImage | null; $shouldShowStagedImage = atom(true); @@ -53,7 +53,7 @@ export class CanvasStagingAreaModule extends CanvasModuleBase { if (!this.image) { const { image_name } = imageDTO; - this.image = new CanvasObjectImageRenderer( + this.image = new CanvasObjectImage( { id: 'staging-area-image', type: 'image', From 34554f8555795a057bb895277c4c5019deddf1a3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 4 Sep 2024 19:17:47 +1000 Subject: [PATCH 635/678] tidy(ui): remove unnecessary awaits in rendering module --- .../features/controlLayers/konva/CanvasRenderingModule.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts index a1024458a7e..d8d21be0e82 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts @@ -35,14 +35,14 @@ export class CanvasRenderingModule extends CanvasModuleBase { this.log.debug('Creating module'); } - render = async () => { + render = () => { if (!this.state || !this.settings || !this.session) { this.log.trace('First render'); } - await this.renderCanvas(); + this.renderCanvas(); this.renderSettings(); - await this.renderSession(); + this.renderSession(); // We have no prev state for the first render if (this.isFirstRender) { From 8266c10c534525502ccf4d6403ca77424e49a10b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 4 Sep 2024 19:18:01 +1000 Subject: [PATCH 636/678] feat(ui): restore size of invoke button --- .../web/src/features/queue/components/InvokeQueueBackButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx b/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx index 68356c28376..1d58c878cf1 100644 --- a/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx +++ b/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx @@ -15,7 +15,7 @@ export const InvokeQueueBackButton = memo(() => { const isLoadingDynamicPrompts = useAppSelector(selectDynamicPromptsIsLoading); return ( - + +