diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index 0cbb7fcf45..a99f1366ad 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 3b897b8679..63abe4d799 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 4d898f31bd..48333909b6 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 4170b87ffe..d0e611e859 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 43db3554ee..4945af0706 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 d34d48063c..2606a26e73 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 b6033416cd..4bb255317e 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 56b188c3bf..b901cc494e 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 (