From cdd8b60fd04123bb81c2a22ed1a5b1b88e272b71 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] 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 5d8d2ee11b..05157b577b 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 5f3bcab13c..ec7bcdaa25 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 45e51f59fc..ed4bcb38b3 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 377c15cebb..c9c20892ec 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<CanvasEntityIdentifier | null>(null);
+  $isProcessingTransform = atom<boolean>(false);
 
   $toolState: WritableAtom<CanvasV2State['tool']> = atom();
   $currentFill: WritableAtom<RgbaColor> = atom();
@@ -253,17 +245,23 @@ export class CanvasStateApiModule {
   $colorUnderCursor: WritableAtom<RgbColor> = atom(RGBA_BLACK);
 
   // Read-write state, ephemeral interaction state
-  $isDrawing = $isDrawing;
-  $isMouseDown = $isMouseDown;
-  $lastAddedPoint = $lastAddedPoint;
-  $lastMouseDownPos = $lastMouseDownPos;
-  $lastCursorPos = $lastCursorPos;
+  $isDrawing = atom<boolean>(false);
+  $isMouseDown = atom<boolean>(false);
+  $lastAddedPoint = atom<Coordinate | null>(null);
+  $lastMouseDownPos = atom<Coordinate | null>(null);
+  $lastCursorPos = atom<Coordinate | null>(null);
   $lastCanvasProgressEvent = $lastCanvasProgressEvent;
-  $spaceKey = $spaceKey;
+  $spaceKey = atom<boolean>(false);
   $altKey = $alt;
   $ctrlKey = $ctrl;
   $metaKey = $meta;
   $shiftKey = $shift;
-  $shouldShowStagedImage = $shouldShowStagedImage;
-  $stageAttrs = $stageAttrs;
+  $shouldShowStagedImage = atom(true);
+  $stageAttrs = atom<StageAttrs>({
+    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 611cf49622..81b688c950 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<StageAttrs>({
-  x: 0,
-  y: 0,
-  width: 0,
-  height: 0,
-  scale: 0,
-});
-export const $shouldShowStagedImage = atom(true);
-export const $isDrawing = atom<boolean>(false);
-export const $isMouseDown = atom<boolean>(false);
-export const $lastAddedPoint = atom<Coordinate | null>(null);
-export const $lastMouseDownPos = atom<Coordinate | null>(null);
-export const $lastCursorPos = atom<Coordinate | null>(null);
-export const $spaceKey = atom<boolean>(false);
-export const $transformingEntity = atom<CanvasEntityIdentifier | null>(null);
-export const $isProcessingTransform = atom<boolean>(false);
-
 export const canvasV2PersistConfig: PersistConfig<CanvasV2State> = {
   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 dad4fdc215..85905142d3 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]
   );