diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx index 97fe0a5c53..f286b75711 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx @@ -15,7 +15,7 @@ import { layerTranslated, selectRegionalPromptsSlice, } from 'features/regionalPrompts/store/regionalPromptsSlice'; -import { debouncedRenderers, renderers } from 'features/regionalPrompts/util/renderers'; +import { debouncedRenderers, renderers as normalRenderers } from 'features/regionalPrompts/util/renderers'; import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; import type { MutableRefObject } from 'react'; @@ -52,20 +52,8 @@ const useStageRenderer = ( const lastMouseDownPos = useStore($lastMouseDownPos); const isMouseOver = useStore($isMouseOver); const selectedLayerIdColor = useAppSelector(selectSelectedLayerColor); - - const renderLayers = useMemo( - () => (asPreview ? debouncedRenderers.renderLayers : renderers.renderLayers), - [asPreview] - ); - const renderToolPreview = useMemo( - () => (asPreview ? debouncedRenderers.renderToolPreview : renderers.renderToolPreview), - [asPreview] - ); - const renderBbox = useMemo(() => (asPreview ? debouncedRenderers.renderBbox : renderers.renderBbox), [asPreview]); - const renderBackground = useMemo( - () => (asPreview ? debouncedRenderers.renderBackground : renderers.renderBackground), - [asPreview] - ); + const layerIds = useMemo(() => state.layers.map((l) => l.id), [state.layers]); + const renderers = useMemo(() => (asPreview ? debouncedRenderers : normalRenderers), [asPreview]); const onLayerPosChanged = useCallback( (layerId: string, x: number, y: number) => { @@ -152,11 +140,12 @@ const useStageRenderer = ( }, [stageRef, width, height, wrapper]); useLayoutEffect(() => { - log.trace('Rendering brush preview'); + log.trace('Rendering tool preview'); if (asPreview) { + // Preview should not display tool return; } - renderToolPreview( + renderers.renderToolPreview( stageRef.current, tool, selectedLayerIdColor, @@ -176,29 +165,36 @@ const useStageRenderer = ( lastMouseDownPos, isMouseOver, state.brushSize, - renderToolPreview, + renderers, ]); useLayoutEffect(() => { log.trace('Rendering layers'); - renderLayers(stageRef.current, state.layers, state.globalMaskLayerOpacity, tool, onLayerPosChanged); - }, [stageRef, state.layers, state.globalMaskLayerOpacity, tool, onLayerPosChanged, renderLayers]); + renderers.renderLayers(stageRef.current, state.layers, state.globalMaskLayerOpacity, tool, onLayerPosChanged); + }, [stageRef, state.layers, state.globalMaskLayerOpacity, tool, onLayerPosChanged, renderers]); useLayoutEffect(() => { log.trace('Rendering bbox'); if (asPreview) { + // Preview should not display bboxes return; } - renderBbox(stageRef.current, state.layers, state.selectedLayerId, tool, onBboxChanged, onBboxMouseDown); - }, [stageRef, asPreview, state.layers, state.selectedLayerId, tool, onBboxChanged, onBboxMouseDown, renderBbox]); + renderers.renderBbox(stageRef.current, state.layers, state.selectedLayerId, tool, onBboxChanged, onBboxMouseDown); + }, [stageRef, asPreview, state.layers, state.selectedLayerId, tool, onBboxChanged, onBboxMouseDown, renderers]); useLayoutEffect(() => { log.trace('Rendering background'); if (asPreview) { + // The preview should not have a background return; } - renderBackground(stageRef.current, width, height); - }, [stageRef, asPreview, width, height, renderBackground]); + renderers.renderBackground(stageRef.current, width, height); + }, [stageRef, asPreview, width, height, renderers]); + + useLayoutEffect(() => { + log.trace('Arranging layers'); + renderers.arrangeLayers(stageRef.current, layerIds); + }, [stageRef, layerIds, renderers]); }; type Props = { diff --git a/invokeai/frontend/web/src/features/regionalPrompts/util/renderers.ts b/invokeai/frontend/web/src/features/regionalPrompts/util/renderers.ts index 76c9bb4f93..4e999fd60b 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/util/renderers.ts +++ b/invokeai/frontend/web/src/features/regionalPrompts/util/renderers.ts @@ -267,9 +267,6 @@ const createVectorMaskLayer = ( stage.add(konvaLayer); - // When a layer is added, it ends up on top of the brush preview - we need to move the preview back to the top. - stage.findOne(`#${TOOL_PREVIEW_LAYER_ID}`)?.moveToTop(); - return konvaLayer; }; @@ -326,7 +323,6 @@ const createVectorMaskRect = (reduxObject: VectorMaskRect, konvaGroup: Konva.Gro const renderVectorMaskLayer = ( stage: Konva.Stage, reduxLayer: VectorMaskLayer, - reduxLayerIndex: number, globalMaskLayerOpacity: number, tool: Tool, onLayerPosChanged?: (layerId: string, x: number, y: number) => void @@ -339,10 +335,6 @@ const renderVectorMaskLayer = ( listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events x: Math.floor(reduxLayer.x), y: Math.floor(reduxLayer.y), - // We have a konva layer for each redux layer, plus a brush preview layer, which should always be on top. We can - // therefore use the index of the redux layer as the zIndex for konva layers. If more layers are added to the - // stage, this may no longer be work. - zIndex: reduxLayerIndex, }); // Convert the color to a string, stripping the alpha - the object group will handle opacity. @@ -433,11 +425,9 @@ const renderLayers = ( } } - for (let layerIndex = 0; layerIndex < reduxLayers.length; layerIndex++) { - const reduxLayer = reduxLayers[layerIndex]; - assert(reduxLayer, `Layer at index ${layerIndex} is undefined`); + for (const reduxLayer of reduxLayers) { if (isVectorMaskLayer(reduxLayer)) { - renderVectorMaskLayer(stage, reduxLayer, layerIndex, globalMaskLayerOpacity, tool, onLayerPosChanged); + renderVectorMaskLayer(stage, reduxLayer, globalMaskLayerOpacity, tool, onLayerPosChanged); } } }; @@ -593,11 +583,29 @@ const renderBackground = (stage: Konva.Stage, width: number, height: number) => 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 + */ +export 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++); +}; + export const renderers = { renderToolPreview, renderLayers, renderBbox, renderBackground, + arrangeLayers, }; const DEBOUNCE_MS = 300; @@ -607,4 +615,5 @@ export const debouncedRenderers = { renderLayers: debounce(renderLayers, DEBOUNCE_MS), renderBbox: debounce(renderBbox, DEBOUNCE_MS), renderBackground: debounce(renderBackground, DEBOUNCE_MS), + arrangeLayers: debounce(arrangeLayers, DEBOUNCE_MS), };