Compare commits

..

19 Commits

Author SHA1 Message Date
psychedelicious
434afdd21d chore(ui): lint 2024-08-26 22:57:56 +10:00
psychedelicious
ea267c0e09 chore: release v4.2.9.dev4
Canvas dev build.
2024-08-26 22:46:07 +10:00
psychedelicious
35b483b83a feat(ui): rough out undo/redo on canvas UNDO ME? 2024-08-26 22:45:11 +10:00
psychedelicious
217b2759c3 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.
2024-08-26 22:44:47 +10:00
psychedelicious
7d528dc03d feat(ui): split out loras state from canvas rendering state 2024-08-26 22:29:28 +10:00
psychedelicious
bc40c1a99f feat(ui): split out session state from canvas rendering state 2024-08-26 22:20:04 +10:00
psychedelicious
a2b508016e feat(ui): split out settings state from canvas rendering state 2024-08-26 22:02:56 +10:00
psychedelicious
3ce8294379 feat(ui): split out tool state from canvas rendering state 2024-08-26 21:52:43 +10:00
psychedelicious
9c3da8de8e 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.
2024-08-26 21:41:47 +10:00
psychedelicious
ccba597e58 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.
2024-08-26 21:12:31 +10:00
psychedelicious
8028943cdd feat(ui): move selected tool and tool buffer out of redux
This ephemeral state can live in the canvas classes.
2024-08-26 19:59:06 +10:00
psychedelicious
cdd8b60fd0 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);
```
2024-08-26 19:14:56 +10:00
psychedelicious
c624754404 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.
2024-08-26 18:52:28 +10:00
psychedelicious
0505634296 feat(ui): move events into modules who care about them 2024-08-26 10:34:59 +10:00
psychedelicious
e06ea5d595 fix(ui): color picker resets brush opacity 2024-08-26 08:58:16 +10:00
psychedelicious
838a0574a5 fix(ui): scaled bbox loses sync 2024-08-26 08:55:33 +10:00
psychedelicious
ed3581c70c feat(ui): add context menu to entity list 2024-08-24 19:50:36 +10:00
psychedelicious
522cae6a42 chore(ui): bump @invoke-ai/ui-library 2024-08-24 19:49:43 +10:00
psychedelicious
a9032a34f2 fix(ui): missing vae precision in graph builders 2024-08-24 18:35:30 +10:00
214 changed files with 3187 additions and 2573 deletions

View File

@@ -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';
/**

View File

@@ -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",

View File

@@ -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):

View File

@@ -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,10 +55,10 @@ 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;
const { x, y } = state.canvasV2.present.bbox.rect;
const { imageDTO, offsetX, offsetY } = stagingAreaImage;
const imageObject = imageDTOToImageObject(imageDTO);

View File

@@ -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

View File

@@ -16,7 +16,7 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS
const { nodes, canvasV2 } = getState();
deleted_images.forEach((image_name) => {
const imageUsage = getImageUsage(nodes.present, canvasV2, image_name);
const imageUsage = getImageUsage(nodes.present, canvasV2.present, image_name);
if (imageUsage.isNodesImage && !wasNodeEditorReset) {
dispatch(nodeEditorReset());

View File

@@ -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';
@@ -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();
@@ -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());
}
};

View File

@@ -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,

View File

@@ -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';
@@ -39,7 +40,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.canvasV2.present.controlAdapters.entities.forEach(({ id, imageObject, processedImageObject }) => {
// if (
// imageObject?.image.image_name === imageDTO.image_name ||
// processedImageObject?.image.image_name === imageDTO.image_name
@@ -51,15 +52,15 @@ 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.present.ipAdapters.entities.forEach((entity) => {
if (entity.ipAdapter.image?.image_name === imageDTO.image_name) {
dispatch(ipaImageChanged({ entityIdentifier: getEntityIdentifier(entity), imageDTO: null }));
}
});
};
const deleteLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
state.canvasV2.rasterLayers.entities.forEach(({ id, objects }) => {
state.canvasV2.present.rasterLayers.entities.forEach(({ id, objects }) => {
let shouldDelete = false;
for (const obj of objects) {
if (obj.type === 'image' && obj.image.image_name === imageDTO.image_name) {

View File

@@ -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;
}
@@ -77,7 +85,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 } = getState().canvasV2.present.bbox.rect;
const overrides: Partial<CanvasRasterLayerState> = {
objects: [imageObject],
position: { x, y },
@@ -95,7 +103,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 } = getState().canvasV2.present.bbox.rect;
const overrides: Partial<CanvasControlLayerState> = {
objects: [imageObject],
position: { x, y },

View File

@@ -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;
}

View File

@@ -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/lorasSlice';
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,14 +24,14 @@ 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
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;
@@ -38,14 +39,14 @@ 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;
}
// handle incompatible controlnets
// state.canvasV2.controlAdapters.entities.forEach((ca) => {
// state.canvasV2.present.controlAdapters.entities.forEach((ca) => {
// if (ca.model?.base !== newBaseModel) {
// modelsCleared += 1;
// if (ca.isEnabled) {
@@ -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 }));
},
});
};

View File

@@ -7,12 +7,11 @@ import {
bboxWidthChanged,
controlLayerModelChanged,
ipaModelChanged,
loraDeleted,
modelChanged,
refinerModelChanged,
rgIPAdapterModelChanged,
vaeSelected,
} 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';
import { postProcessingModelChanged, upscaleModelChanged } from 'features/parameters/store/upscaleSlice';
import { zParameterModel, zParameterVAEModel } from 'features/parameters/types/parameterSchemas';
@@ -62,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
@@ -84,11 +83,17 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => {
dispatch(modelChanged({ model: defaultModelInList, previousModel: currentModel }));
const optimalDimension = getOptimalDimension(defaultModelInList);
if (getIsSizeOptimal(state.canvasV2.bbox.rect.width, state.canvasV2.bbox.rect.height, optimalDimension)) {
if (
getIsSizeOptimal(
state.canvasV2.present.bbox.rect.width,
state.canvasV2.present.bbox.rect.height,
optimalDimension
)
) {
return;
}
const { width, height } = calculateNewSize(
state.canvasV2.bbox.aspectRatio.value,
state.canvasV2.present.bbox.aspectRatio.value,
optimalDimension * optimalDimension
);
@@ -109,7 +114,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
@@ -128,7 +133,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"
@@ -162,7 +167,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;
@@ -173,32 +178,34 @@ 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) => {
state.canvasV2.present.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 }));
dispatch(controlLayerModelChanged({ entityIdentifier: getEntityIdentifier(entity), modelConfig: null }));
});
};
const handleIPAdapterModels: ModelHandler = (models, state, dispatch, _log) => {
const ipaModels = models.filter(isIPAdapterModelConfig);
state.canvasV2.ipAdapters.entities.forEach((entity) => {
state.canvasV2.present.ipAdapters.entities.forEach((entity) => {
const isModelAvailable = ipaModels.some((m) => m.key === entity.ipAdapter.model?.key);
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.present.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 })
);
});
});
};

View File

@@ -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,

View File

@@ -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;

View File

@@ -6,7 +6,12 @@ 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 { 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';
@@ -53,10 +58,15 @@ const allReducers = {
[queueSlice.name]: queueSlice.reducer,
[workflowSlice.name]: workflowSlice.reducer,
[hrfSlice.name]: hrfSlice.reducer,
[canvasV2Slice.name]: canvasV2Slice.reducer,
[canvasV2Slice.name]: undoable(canvasV2Slice.reducer),
[workflowSettingsSlice.name]: workflowSettingsSlice.reducer,
[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,
};
const rootReducer = combineReducers(allReducers);
@@ -98,6 +108,11 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
[workflowSettingsPersistConfig.name]: workflowSettingsPersistConfig,
[upscalePersistConfig.name]: upscalePersistConfig,
[stylePresetPersistConfig.name]: stylePresetPersistConfig,
[paramsPersistConfig.name]: paramsPersistConfig,
[toolPersistConfig.name]: toolPersistConfig,
[canvasSettingsPersistConfig.name]: canvasSettingsPersistConfig,
[canvasSessionPersistConfig.name]: canvasSessionPersistConfig,
[lorasPersistConfig.name]: lorasPersistConfig,
};
const unserialize: UnserializeFunction = (data, key) => {

View File

@@ -32,7 +32,7 @@ export const useGroupedModelCombobox = <T extends AnyModelConfig>(
arg: UseGroupedModelComboboxArg<T>
): 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<GroupBase<ComboboxOption>[]>(() => {
if (!modelConfigs) {

View File

@@ -2,7 +2,8 @@ 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 { 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';
import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice';
@@ -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 }[] = [];

View File

@@ -11,7 +11,7 @@ import { memo } from 'react';
export const CanvasEntityList = memo(() => {
return (
<ScrollableContent>
<Flex flexDir="column" gap={4} pt={2} data-testid="control-layers-layer-list">
<Flex flexDir="column" gap={4} pt={2} data-testid="control-layers-layer-list" w="full" h="full">
<CanvasEntityOpacity />
<InpaintMaskList />
<RegionalGuidanceEntityList />

View File

@@ -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 (
<Menu>
<MenuButton
as={IconButton}
aria-label={t('accessibility.menu')}
icon={<PiDotsThreeOutlineFill />}
variant="link"
data-testid="control-layers-add-layer-menu-button"
alignSelf="stretch"
/>
<MenuList>
<CanvasEntityListMenuItems />
</MenuList>
</Menu>
);
});
CanvasEntityListMenuButton.displayName = 'CanvasEntityListMenuButton';

View File

@@ -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 (
<>
<MenuItem icon={<PiPlusBold />} onClick={addInpaintMask}>
{t('controlLayers.inpaintMask', { count: 1 })}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addRegionalGuidance}>
{t('controlLayers.regionalGuidance', { count: 1 })}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addRasterLayer}>
{t('controlLayers.rasterLayer', { count: 1 })}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addControlLayer}>
{t('controlLayers.controlLayer', { count: 1 })}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addIPAdapter}>
{t('controlLayers.ipAdapter', { count: 1 })}
</MenuItem>
<MenuDivider />
<MenuItem onClick={deleteAll} icon={<PiTrashSimpleBold />} color="error.300" isDisabled={!hasEntities}>
{t('controlLayers.deleteAll', { count: 1 })}
</MenuItem>
</>
);
});
CanvasEntityListMenuItems.displayName = 'CanvasEntityListMenu';

View File

@@ -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 (
<Menu>
<MenuButton
as={IconButton}
aria-label={t('accessibility.menu')}
icon={<PiDotsThreeOutlineFill />}
variant="link"
data-testid="control-layers-add-layer-menu-button"
alignSelf="stretch"
/>
<MenuList>
<MenuItem icon={<PiPlusBold />} onClick={addInpaintMask}>
{t('controlLayers.inpaintMask', { count: 1 })}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addRegionalGuidance}>
{t('controlLayers.regionalGuidance', { count: 1 })}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addRasterLayer}>
{t('controlLayers.rasterLayer', { count: 1 })}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addControlLayer}>
{t('controlLayers.controlLayer', { count: 1 })}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addIPAdapter}>
{t('controlLayers.ipAdapter', { count: 1 })}
</MenuItem>
<MenuDivider />
<MenuItem onClick={deleteAll} icon={<PiTrashSimpleBold />} color="error.300" isDisabled={!hasEntities}>
{t('controlLayers.deleteAll', { count: 1 })}
</MenuItem>
</MenuList>
</Menu>
);
});
CanvasEntityListMenu.displayName = 'CanvasEntityListMenu';

View File

@@ -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]);

View File

@@ -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(
() => (
<MenuList>
<CanvasEntityListMenuItems />
</MenuList>
),
[]
);
return (
<CanvasManagerProviderGate>
{!hasEntities && <CanvasAddEntityButtons />}
{hasEntities && <CanvasEntityList />}
<ContextMenu<HTMLDivElement> renderMenu={renderMenu}>
{(ref) => (
<Box ref={ref} w="full" h="full">
{!hasEntities && <CanvasAddEntityButtons />}
{hasEntities && <CanvasEntityList />}
</Box>
)}
</ContextMenu>
</CanvasManagerProviderGate>
);
});

View File

@@ -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(

View File

@@ -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.present, entityIdentifier).withTransparencyEffect
);
return (

View File

@@ -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 (

View File

@@ -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]);

View File

@@ -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) => {
@@ -11,7 +11,9 @@ const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) =
});
export const ControlLayerEntityList = memo(() => {
const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'control_layer'));
const isSelected = useAppSelector((s) =>
Boolean(s.canvasV2.present.selectedEntityIdentifier?.type === 'control_layer')
);
const layerIds = useAppSelector(selectEntityIds);
if (layerIds.length === 0) {

View File

@@ -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 (
<MenuItem onClick={convertControlLayerToRasterLayer} icon={<PiLightningBold />}>

View File

@@ -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 (

View File

@@ -33,9 +33,11 @@ export const CanvasEditor = memo(() => {
<ControlLayersToolbar />
<StageComponent />
<Flex position="absolute" bottom={16} gap={2} align="center" justify="center">
<StagingAreaIsStagingGate>
<StagingAreaToolbar />
</StagingAreaIsStagingGate>
<CanvasManagerProviderGate>
<StagingAreaIsStagingGate>
<StagingAreaToolbar />
</StagingAreaIsStagingGate>
</CanvasManagerProviderGate>
</Flex>
<Flex position="absolute" bottom={16}>
<CanvasManagerProviderGate>

View File

@@ -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 (
<CanvasManagerProviderGate>
<Flex w="full" gap={2} alignItems="center">
<ToggleProgressButton />
<ToolChooser />
<Spacer />
{tool === 'brush' && <ToolBrushWidth />}
{tool === 'eraser' && <ToolEraserWidth />}
<ToolSettings />
<Spacer />
<CanvasScale />
<CanvasResetViewButton />

View File

@@ -1,25 +1,19 @@
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 bbox = useAppSelector((s) => s.canvasV2.bbox);
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.present.bbox);
return (
<Flex flexDir="column" bg="blackAlpha.400" borderBottomEndRadius="base" p={2} minW={64} gap={2}>

View File

@@ -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) => {
@@ -12,7 +12,7 @@ const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) =
});
export const IPAdapterList = memo(() => {
const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'ip_adapter'));
const isSelected = useAppSelector((s) => Boolean(s.canvasV2.present.selectedEntityIdentifier?.type === 'ip_adapter'));
const ipaIds = useAppSelector(selectEntityIds);
if (ipaIds.length === 0) {

View File

@@ -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]);

View File

@@ -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.present, 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<IPAImageDropData>(() => ({ actionType: 'SET_IPA_IMAGE', context: { id }, id }), [id]);
const postUploadAction = useMemo<IPALayerImagePostUploadAction>(() => ({ type: 'SET_IPA_IMAGE', id }), [id]);
const droppableData = useMemo<IPAImageDropData>(
() => ({ actionType: 'SET_IPA_IMAGE', context: { id: entityIdentifier.id }, id: entityIdentifier.id }),
[entityIdentifier.id]
);
const postUploadAction = useMemo<IPALayerImagePostUploadAction>(
() => ({ type: 'SET_IPA_IMAGE', id: entityIdentifier.id }),
[entityIdentifier.id]
);
return (
<CanvasEntitySettingsWrapper>
@@ -95,7 +101,7 @@ export const IPAdapterSettings = memo(() => {
<IPAdapterImagePreview
image={ipAdapter.image ?? null}
onChangeImage={onChangeImage}
ipAdapterId={id}
ipAdapterId={entityIdentifier.id}
droppableData={droppableData}
postUploadAction={postUploadAction}
/>

View File

@@ -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) => {
@@ -11,7 +11,9 @@ const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) =
});
export const InpaintMaskList = memo(() => {
const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'inpaint_mask'));
const isSelected = useAppSelector((s) =>
Boolean(s.canvasV2.present.selectedEntityIdentifier?.type === 'inpaint_mask')
);
const entityIds = useAppSelector(selectEntityIds);
if (entityIds.length === 0) {

View File

@@ -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.present, entityIdentifier).fill);
const onChangeFillColor = useCallback(
(color: RgbColor) => {

View File

@@ -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) => {
@@ -11,7 +11,9 @@ const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) =
});
export const RasterLayerEntityList = memo(() => {
const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'raster_layer'));
const isSelected = useAppSelector((s) =>
Boolean(s.canvasV2.present.selectedEntityIdentifier?.type === 'raster_layer')
);
const layerIds = useAppSelector(selectEntityIds);
if (layerIds.length === 0) {

View File

@@ -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 (
<MenuItem onClick={convertRasterLayerToControlLayer} icon={<PiLightningBold />}>

View File

@@ -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 (
<Flex w="full" p={2} justifyContent="space-between">

View File

@@ -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.present, entityIdentifier).autoNegative);
return (
<>

View File

@@ -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) => {
@@ -11,7 +11,9 @@ const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) =
});
export const RegionalGuidanceEntityList = memo(() => {
const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'regional_guidance'));
const isSelected = useAppSelector((s) =>
Boolean(s.canvasV2.present.selectedEntityIdentifier?.type === 'regional_guidance')
);
const rgIds = useAppSelector(selectEntityIds);
if (rgIds.length === 0) {

View File

@@ -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.present, 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<RGIPAdapterImageDropData>(
() => ({ 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<RGIPAdapterImagePostUploadAction>(
() => ({ type: 'SET_RG_IP_ADAPTER_IMAGE', id, ipAdapterId }),
[ipAdapterId, id]
() => ({ type: 'SET_RG_IP_ADAPTER_IMAGE', id: entityIdentifier.id, ipAdapterId }),
[entityIdentifier.id, ipAdapterId]
);
return (

View File

@@ -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) => (
<Fragment key={ipAdapterId}>
{index > 0 && <Divider />}
<RegionalGuidanceIPAdapterSettings id={id} ipAdapterId={ipAdapterId} ipAdapterNumber={index + 1} />
<RegionalGuidanceIPAdapterSettings ipAdapterId={ipAdapterId} ipAdapterNumber={index + 1} />
</Fragment>
))}
</>

View File

@@ -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.present, 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 (
<Popover isLazy>

View File

@@ -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 (
<>

View File

@@ -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.present, entityIdentifier).autoNegative);
const onClick = useCallback(() => {
dispatch(rgAutoNegativeToggled({ id }));
}, [dispatch, id]);
dispatch(rgAutoNegativeToggled({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
return (
<MenuItem icon={<PiSelectionInverseBold />} onClick={onClick}>

View File

@@ -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.present, entityIdentifier).negativePrompt ?? '');
const dispatch = useAppDispatch();
const textareaRef = useRef<HTMLTextAreaElement>(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,

View File

@@ -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.present, entityIdentifier).positivePrompt ?? '');
const dispatch = useAppDispatch();
const textareaRef = useRef<HTMLTextAreaElement>(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,

View File

@@ -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,33 @@ 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.present, entityIdentifier).positivePrompt !== null
);
const hasNegativePrompt = useAppSelector(
(s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).negativePrompt !== null
(s) => selectEntityOrThrow(s.canvasV2.present, entityIdentifier).negativePrompt !== null
);
const hasIPAdapters = useAppSelector(
(s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).ipAdapters.length > 0
(s) => selectEntityOrThrow(s.canvasV2.present, entityIdentifier).ipAdapters.length > 0
);
return (
<CanvasEntitySettingsWrapper>
{!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && (
<RegionalGuidanceAddPromptsIPAdapterButtons id={id} />
)}
{!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && <RegionalGuidanceAddPromptsIPAdapterButtons />}
{hasPositivePrompt && (
<>
<RegionalGuidancePositivePrompt id={id} />
<RegionalGuidancePositivePrompt />
{(hasNegativePrompt || hasIPAdapters) && <Divider />}
</>
)}
{hasNegativePrompt && (
<>
<RegionalGuidanceNegativePrompt id={id} />
<RegionalGuidanceNegativePrompt />
{hasIPAdapters && <Divider />}
</>
)}
{hasIPAdapters && <RegionalGuidanceIPAdapters id={id} />}
{hasIPAdapters && <RegionalGuidanceIPAdapters />}
</CanvasEntitySettingsWrapper>
);
});

View File

@@ -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 (
<FormControl w="full">

View File

@@ -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<HTMLInputElement>) => dispatch(clipToBboxChanged(e.target.checked)),
[dispatch]

View File

@@ -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]);

View File

@@ -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<HTMLInputElement>) => dispatch(invertScrollChanged(e.target.checked)),
[dispatch]

View File

@@ -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(() => {
@@ -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(
() =>

View File

@@ -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;

View File

@@ -2,14 +2,14 @@ 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,
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,8 +27,9 @@ 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 session = useAppSelector((s) => s.canvasSession);
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) {

View File

@@ -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 isStaging = useAppSelector((s) => s.canvasSession.isStaging);
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 (
<IconButton
@@ -32,7 +28,7 @@ export const ToolBboxButton = memo(() => {
icon={<PiBoundingBoxBold />}
colorScheme={isSelected ? 'invokeBlue' : 'base'}
variant="outline"
onClick={onClick}
onClick={selectBbox}
isDisabled={isDisabled}
/>
);

View File

@@ -1,37 +1,33 @@
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 isStaging = useAppSelector((s) => s.canvasSession.isStaging);
const selectBrush = useSelectTool('brush');
const isSelected = useToolIsSelected('brush');
const isDrawingToolAllowed = useAppSelector((s) => {
if (!s.canvasV2.selectedEntityIdentifier?.type) {
if (!s.canvasV2.present.selectedEntityIdentifier?.type) {
return false;
}
return isDrawableEntityType(s.canvasV2.selectedEntityIdentifier.type);
return isDrawableEntityType(s.canvasV2.present.selectedEntityIdentifier.type);
});
const isDisabled = useMemo(() => {
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 (
<IconButton
@@ -40,7 +36,7 @@ export const ToolBrushButton = memo(() => {
icon={<PiPaintBrushBold />}
colorScheme={isSelected ? 'invokeBlue' : 'base'}
variant="outline"
onClick={onClick}
onClick={selectBrush}
isDisabled={isDisabled}
/>
);

View File

@@ -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)));

View File

@@ -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 isStaging = useAppSelector((s) => s.canvasV2.session.isStaging);
const selectColorPicker = useSelectTool('colorPicker');
const isSelected = useToolIsSelected('colorPicker');
const isStaging = useAppSelector((s) => s.canvasSession.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 (
<IconButton
@@ -33,7 +33,7 @@ export const ToolColorPickerButton = memo(() => {
icon={<PiEyedropperBold />}
colorScheme={isSelected ? 'invokeBlue' : 'base'}
variant="outline"
onClick={onClick}
onClick={selectColorPicker}
isDisabled={isDisabled}
/>
);

View File

@@ -1,36 +1,32 @@
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 isStaging = useAppSelector((s) => s.canvasSession.isStaging);
const selectEraser = useSelectTool('eraser');
const isSelected = useToolIsSelected('eraser');
const isDrawingToolAllowed = useAppSelector((s) => {
if (!s.canvasV2.selectedEntityIdentifier?.type) {
if (!s.canvasV2.present.selectedEntityIdentifier?.type) {
return false;
}
return isDrawableEntityType(s.canvasV2.selectedEntityIdentifier.type);
return isDrawableEntityType(s.canvasV2.present.selectedEntityIdentifier.type);
});
const isDisabled = useMemo(() => {
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 (
<IconButton
@@ -39,7 +35,7 @@ export const ToolEraserButton = memo(() => {
icon={<PiEraserBold />}
colorScheme={isSelected ? 'invokeBlue' : 'base'}
variant="outline"
onClick={onClick}
onClick={selectEraser}
isDisabled={isDisabled}
/>
);

View File

@@ -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)));

View File

@@ -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) => {

View File

@@ -1,36 +1,32 @@
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 isStaging = useAppSelector((s) => s.canvasV2.session.isStaging);
const selectMove = useSelectTool('move');
const isSelected = useToolIsSelected('move');
const isStaging = useAppSelector((s) => s.canvasSession.isStaging);
const isDrawingToolAllowed = useAppSelector((s) => {
if (!s.canvasV2.selectedEntityIdentifier?.type) {
if (!s.canvasV2.present.selectedEntityIdentifier?.type) {
return false;
}
return isDrawableEntityType(s.canvasV2.selectedEntityIdentifier.type);
return isDrawableEntityType(s.canvasV2.present.selectedEntityIdentifier.type);
});
const isDisabled = useMemo(() => {
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 (
<IconButton
@@ -39,7 +35,7 @@ export const ToolMoveButton = memo(() => {
icon={<PiCursorBold />}
colorScheme={isSelected ? 'invokeBlue' : 'base'}
variant="outline"
onClick={onClick}
onClick={selectMove}
isDisabled={isDisabled}
/>
);

View File

@@ -1,37 +1,33 @@
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);
const isStaging = useAppSelector((s) => s.canvasSession.isStaging);
const isDrawingToolAllowed = useAppSelector((s) => {
if (!s.canvasV2.selectedEntityIdentifier?.type) {
if (!s.canvasV2.present.selectedEntityIdentifier?.type) {
return false;
}
return isDrawableEntityType(s.canvasV2.selectedEntityIdentifier.type);
return isDrawableEntityType(s.canvasV2.present.selectedEntityIdentifier.type);
});
const isDisabled = useMemo(() => {
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 (
<IconButton
@@ -40,7 +36,7 @@ export const ToolRectButton = memo(() => {
icon={<PiRectangleBold />}
colorScheme={isSelected ? 'invokeBlue' : 'base'}
variant="outline"
onClick={onClick}
onClick={selectRect}
isDisabled={isDisabled}
/>
);

View File

@@ -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 <ToolBrushWidth />;
}
if (tool === 'eraser') {
return <ToolEraserWidth />;
}
return null;
});
ToolSettings.displayName = 'ToolSettings';

View File

@@ -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 isStaging = useAppSelector((s) => s.canvasSession.isStaging);
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 (
<IconButton
@@ -31,7 +28,7 @@ export const ToolViewButton = memo(() => {
icon={<PiHandBold />}
colorScheme={isSelected ? 'invokeBlue' : 'base'}
variant="outline"
onClick={onClick}
onClick={selectView}
isDisabled={isDisabled}
/>
);

View File

@@ -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;
};

View File

@@ -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,

View File

@@ -56,7 +56,7 @@ export const CanvasEntityHeader = memo(({ children, ...rest }: FlexProps) => {
}, [entityIdentifier]);
return (
<ContextMenu renderMenu={renderMenu}>
<ContextMenu renderMenu={renderMenu} stopImmediatePropagation>
{(ref) => (
<Flex ref={ref} gap={2} alignItems="center" p={2} {...rest}>
{children}

View File

@@ -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';

View File

@@ -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';
@@ -58,13 +59,13 @@ 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((s) => s.canvasV2.present.selectedEntityIdentifier);
const opacity = useAppSelector((s) => {
const selectedEntityIdentifier = s.canvasV2.selectedEntityIdentifier;
const selectedEntityIdentifier = s.canvasV2.present.selectedEntityIdentifier;
if (!selectedEntityIdentifier) {
return null;
}
const selectedEntity = selectEntity(s.canvasV2, selectedEntityIdentifier);
const selectedEntity = selectEntity(s.canvasV2.present, selectedEntityIdentifier);
if (!selectedEntity) {
return null;
}

View File

@@ -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';

View File

@@ -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<CanvasEntityIdentifier | null>(null);
export const useEntityIdentifierContext = (): CanvasEntityIdentifier => {
export const useEntityIdentifierContext = <T extends CanvasEntityType | undefined = CanvasEntityType>(
type?: T
): CanvasEntityIdentifier<T extends undefined ? CanvasEntityType : T> => {
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<T extends undefined ? CanvasEntityType : T>;
};

View File

@@ -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';
@@ -14,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) {

View File

@@ -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';

View File

@@ -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';

View File

@@ -3,7 +3,7 @@ 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 selectedEntityIdentifier = useAppSelector((s) => s.canvasV2.present.selectedEntityIdentifier);
const isSelected = useMemo(() => {
return selectedEntityIdentifier?.id === entityIdentifier.id;
}, [selectedEntityIdentifier, entityIdentifier.id]);

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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]
@@ -31,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));
@@ -52,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));

View File

@@ -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('.') };
};
}

View File

@@ -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,14 +23,16 @@ 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;
subscriptions: Set<() => void> = new Set();
parent: CanvasPreviewModule;
konva: {
group: Konva.Group;
@@ -38,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.
@@ -228,17 +232,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() {
this.log.trace('Rendering generation bbox');
render = () => {
this.log.trace('Rendering bbox module');
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,15 +252,29 @@ 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,
});
}
};
repr = () => {
return {
id: this.id,
type: this.type,
path: this.path,
};
};
destroy = () => {
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('.') };
};
}

View File

@@ -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('.') };
};
}

View File

@@ -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<string, string>({ max: 100 });
canvasElementCache = new LRUCache<string, HTMLCanvasElement>({ max: 32 });
generationModeCache = new LRUCache<string, GenerationMode>({ 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('.') };
};
}

View File

@@ -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('.') };
};
}

View File

@@ -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('.') };
};
}

View File

@@ -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<FilterConfig>(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) => {
@@ -47,7 +49,7 @@ export class CanvasFilterModule {
return;
}
this.$adapter.set(entity.adapter);
this.manager.stateApi.setTool('view');
this.manager.stateApi.$tool.set('view');
};
previewFilter = async () => {
@@ -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('.') };
};
}

View File

@@ -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('.') };
};
}

View File

@@ -1,13 +1,17 @@
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';
import type {
CanvasBrushLineState,
CanvasControlLayerState,
CanvasEntityIdentifier,
CanvasEraserLineState,
CanvasRasterLayerState,
CanvasV2State,
Coordinate,
Rect,
} from 'features/controlLayers/store/types';
import { getEntityIdentifier } from 'features/controlLayers/store/types';
@@ -18,12 +22,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;
@@ -37,11 +42,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 = {
@@ -67,18 +75,14 @@ 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();
};
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) {
@@ -140,6 +144,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(),
@@ -180,6 +185,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(),

View File

@@ -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';
@@ -19,20 +20,22 @@ 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<CanvasManager | null>(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<string, CanvasLayerAdapter>(),
controlLayers: new SyncableMap<string, CanvasLayerAdapter>(),
@@ -61,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;
@@ -81,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();
@@ -101,37 +107,61 @@ 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);
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());
const cleanupEventHandlers = setStageEventHandlers(this);
const cleanupStage = this.stage.initialize();
const cleanupStore = this.store.subscribe(this.renderer.render);
this.subscriptions.add(this.store.subscribe(this.renderer.render));
this.stage.initialize();
};
return () => {
this.log.debug('Cleaning up canvas manager');
for (const adapter of this.adapters.getAll()) {
adapter.destroy();
}
this.background.destroy();
this.preview.destroy();
cleanupStore();
cleanupEventHandlers();
cleanupStage();
};
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('.'),
@@ -153,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;
}

View File

@@ -1,13 +1,17 @@
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';
import type {
CanvasBrushLineState,
CanvasEntityIdentifier,
CanvasEraserLineState,
CanvasInpaintMaskState,
CanvasRegionalGuidanceState,
CanvasV2State,
Coordinate,
Rect,
} from 'features/controlLayers/store/types';
import { getEntityIdentifier } from 'features/controlLayers/store/types';
@@ -17,13 +21,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;
@@ -37,11 +42,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 = {
@@ -67,18 +75,14 @@ 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();
};
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) {
@@ -136,10 +140,24 @@ 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,
type: this.type,
path: this.path,
state: deepClone(this.state),
};
};
@@ -165,7 +183,8 @@ export class CanvasMaskAdapter {
const canvas = this.renderer.getCanvas(rect, attrs);
return canvas;
};
getLoggingContext = (): SerializableObject => {
getLoggingContext = () => {
return { ...this.manager.getLoggingContext(), path: this.path.join('.') };
};
}

View File

@@ -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;
};
}

View File

@@ -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';
@@ -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';
@@ -55,8 +56,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 +124,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 }),
@@ -156,11 +158,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();
})
);
@@ -548,17 +551,22 @@ export class CanvasObjectRenderer {
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);
@@ -596,11 +604,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();
}
@@ -615,13 +620,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('.') };
};
}

Some files were not shown because too many files have changed in this diff Show More