diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx index 4b4a9842a2..afaafe69f2 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 0892d3da09..d3e7d54a92 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 9b9d624a2b..6c8d908be6 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 4411a37394..7c04a0a89b 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 ec813fa275..fd48383b86 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 d3b7dd40d9..d9f9a8d3e7 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;