Compare commits

..

609 Commits

Author SHA1 Message Date
psychedelicious
2c5abd44a7 chore: release v4.2.9.dev10 2024-08-30 23:10:59 +10:00
psychedelicious
765d99ac2f feat(ui): remove entity list context menu (again)
stupid events
2024-08-30 23:10:36 +10:00
psychedelicious
ac9a66a628 fix(ui): entity groups not collapsing 2024-08-30 23:10:15 +10:00
psychedelicious
0ea88dc170 chore: release v4.2.9.dev9 2024-08-30 22:24:08 +10:00
psychedelicious
8369826d22 fix(ui): entity opacity number input focus prevents slider from opening 2024-08-30 22:20:49 +10:00
psychedelicious
0e354f5164 feat(ui): add merge visible for raster and inpaint mask layers
I don't think it makes sense to merge control layers or regional guidance layers because they have additional state.
2024-08-30 22:20:49 +10:00
psychedelicious
41f2ee2633 fix(ui): save to gallery rect too large
Was including all layer types in the rect - only want the raster layers.
2024-08-30 22:20:49 +10:00
psychedelicious
4e74006c5f fix(ui): canvasToBlob not raising error correctly 2024-08-30 22:20:49 +10:00
psychedelicious
48edb6e023 feat(ui): add save to gallery button 2024-08-30 22:20:49 +10:00
psychedelicious
aeae6af0a1 fix(ui): fix getRectUnion util, add some tests 2024-08-30 22:20:49 +10:00
psychedelicious
ab11d9af8e fix(ui): modals not staying open
TBH not sure exactly why this broke. Fixed by rollback back the use of a render prop in favor of global state. Also revised the API of `useBoolean` and `buildUseBoolean`.
2024-08-30 22:20:49 +10:00
psychedelicious
2e84327ca4 fix(ui): correct labels for generation tab origin 2024-08-30 22:20:49 +10:00
psychedelicious
fa6842121c fix(ui): context menu doesn't work for new entities
I do not understand why this fixes the issue, doesn't seem like it should. But it does.
2024-08-30 22:20:49 +10:00
psychedelicious
c402aa397d tidy(ui): organise tool module 2024-08-30 22:20:49 +10:00
psychedelicious
a58c8adc38 fix(ui): staging hotkeys enabled at wrong times 2024-08-30 22:20:49 +10:00
psychedelicious
d43e2d690e fix(ui): incorrect batch origin preventing progress/staging 2024-08-30 22:20:49 +10:00
psychedelicious
284f768810 feat(ui): restore minimal HUD 2024-08-30 22:20:49 +10:00
psychedelicious
e933d1ae2b feat(ui): remove unused asPreview for StageComponent 2024-08-30 22:20:49 +10:00
psychedelicious
1e134de771 chore(ui): lint 2024-08-30 22:20:49 +10:00
psychedelicious
29c47c8be5 chore: release v4.2.9.dev8 2024-08-30 22:20:49 +10:00
psychedelicious
e1122c541d feat(ui): revise generation mode logic
- Canvas generation mode is replace with a boolean `sendToCanvas` flag. When off, images generated on the canvas go to the gallery. When on, they get added to the staging area.
- When an image result is received, if its destination is the canvas, staging is automatically started.
- Updated queue list to show the destination column.
- Added `IconSwitch` component to represent binary choices, used for the new `sendToCanvas` flag and image viewer toggle.
- Remove the queue actions menu in `QueueControls`. Move the queue count badge to the cancel button.
- Redo layout of `QueueControls` to prevent duplicate queue count badges.
- Fix issue where gallery and options panels could show thru transparent regions of queue tab.
- Disable panel hotkeys when on mm/queue tabs.
2024-08-30 22:20:49 +10:00
psychedelicious
2f81d1ac83 chore(ui): typegen 2024-08-30 22:20:49 +10:00
psychedelicious
56fbe751db feat(app): add destination column to session_queue
The frontend needs to know where queue items came from (i.e. which tab), and where results are going to (i.e. send images to gallery or canvas). The `origin` column is not quite enough to represent this cleanly.

A `destination` column provides the frontend what it needs to handle incoming generations.
2024-08-30 22:20:49 +10:00
psychedelicious
93f1d67fbf tidy(ui): ViewerToggleMenu -> ViewerToggle 2024-08-30 22:20:49 +10:00
psychedelicious
9467b937ff feat(ui): alt quick switches to color picker 2024-08-30 22:20:49 +10:00
psychedelicious
4242e6e6c2 feat(ui): tweak add entity button layout 2024-08-30 22:20:49 +10:00
psychedelicious
9b39452b3e feat(ui): restore context menu for entity list 2024-08-30 22:20:49 +10:00
psychedelicious
85b23784cf feat(ui): add delete button to each layer 2024-08-30 22:20:49 +10:00
psychedelicious
085cc82926 feat(ui): add + buttons to entity categories 2024-08-30 22:20:49 +10:00
psychedelicious
0098c33f81 feat(ui): tweak brush fill UI 2024-08-30 22:20:49 +10:00
psychedelicious
292e00ab68 feat(ui): do not select layer on staging accept 2024-08-30 22:20:49 +10:00
psychedelicious
6c1fb2d06e fix(ui): more fiddly queue count layout stuff 2024-08-30 22:20:49 +10:00
psychedelicious
d60605fcd8 fix(ui): floating params panel invoke button loading state 2024-08-30 22:20:49 +10:00
psychedelicious
38ed720ff2 feat(ui): move canvas undo/redo to hook 2024-08-30 22:20:49 +10:00
psychedelicious
22203b8eb0 fix(ui): queue count badge positioning 2024-08-30 22:20:49 +10:00
psychedelicious
cf5fa792a1 fix(ui): add node cmdk only enabled on workflows tab 2024-08-30 22:20:49 +10:00
psychedelicious
c636633a8e chore: release v4.2.9.dev7 2024-08-30 22:20:49 +10:00
psychedelicious
55fe1ebc53 fix(ui): pending node connection stuck 2024-08-30 22:20:49 +10:00
psychedelicious
3c2fa6b475 chore(ui): lint 2024-08-30 22:20:49 +10:00
psychedelicious
9b927de2e0 chore: release v4.2.9.dev6 2024-08-30 22:20:49 +10:00
psychedelicious
6a62854e7d feat(ui): migrate add node popover to cmdk
Put this together as a way to figure out the library before moving on to the full app cmdk. Works great.
2024-08-30 22:20:49 +10:00
psychedelicious
312093cbb0 fix(ui): schema parsing now that node_pack is guaranteed to be present 2024-08-30 22:20:49 +10:00
psychedelicious
06fe14e1fc chore(ui): typegen 2024-08-30 22:20:49 +10:00
psychedelicious
1b54e58726 fix(app): node_pack not added to openapi schema correctly 2024-08-30 22:20:49 +10:00
psychedelicious
219d7c9611 fix(ui): unnecessary z-index on invoke button 2024-08-30 22:20:49 +10:00
psychedelicious
9f742a669e feat(ui): split settings modal 2024-08-30 22:20:49 +10:00
psychedelicious
41e324fd51 perf(ui): disable useInert on modals
This hook forcibly updates _all_ portals with `data-hidden=true` when the modal opens - then reverts it when the modal closes. It's intended to help screen readers. Unfortunately, this absolutely tanks performance because we have many portals. React needs to do alot of layout calculations (not re-renders).

IMO this behaviour is a bug in chakra. The modals which generated the portals are hidden by default, so this data attr should really be set by default. Dunno why it isn't.
2024-08-30 22:20:36 +10:00
psychedelicious
ce55a96125 feat(ui): fix queue item count badge positioning
Previously this badge, floating over the queue menu button next to the invoke button, was rendered within the existing layout. When I initially positioned it, the app layout interfered - it would extend into an area reserved for a flex gap, which cut off the badge.

As a (bad) workaround, I had shifted the whole app down a few pixels to make room for it. What I should have done is what I've done in this commit - render the badge in a portal to take it out of the layout so we don't need that extra vertical padding.

Sleekified some styling a bit too.
2024-08-30 22:20:36 +10:00
psychedelicious
64e60a7fde fix(ui): transparency effect not updating 2024-08-30 22:20:36 +10:00
psychedelicious
972f03960a feat(ui): tidy canvas toolbar buttons 2024-08-30 22:20:36 +10:00
psychedelicious
5a403f087d feat(ui): revised viewer toggle @joshistoast 2024-08-30 22:20:36 +10:00
psychedelicious
fe59d7f3b0 fix(ui): opacity reset value incorrect 2024-08-30 22:20:36 +10:00
psychedelicious
b2b2b73aed revert(ui): roll back flip, doesn't work with rotate yet 2024-08-30 22:20:36 +10:00
psychedelicious
20b563c4cb fix(ui): disable opacity slider fully when no valid entity selected 2024-08-30 22:20:36 +10:00
psychedelicious
263a0ef5b4 fix(ui): layer preview image sometimes not rendering
The canvas size was dynamic based on the container div's size. When the div was hidden (e.g. when selecting another tab), the container's effective size is 0. This resulted in the preview image canvas being drawn at a scale of 0.

Fixed by using an absolute size for the canvas container.
2024-08-30 22:20:36 +10:00
psychedelicious
e8723b7cd3 feat(ui): tweak regional prompt box styles 2024-08-30 22:20:36 +10:00
psychedelicious
03e05b2068 feat(ui): tweak enabled/locked toggle styles 2024-08-30 22:20:36 +10:00
psychedelicious
6c0482a71d feat(ui): tweak filter styling 2024-08-30 22:20:36 +10:00
psychedelicious
e6153e6fa4 feat(ui): add flip & reset to transform 2024-08-30 22:20:36 +10:00
psychedelicious
6d209c6cc3 tidy(ui): use helper to sync scaled bbox size on model change 2024-08-30 22:20:36 +10:00
psychedelicious
beb4e823dc fix(ui): randomize seed toggle linked to prompt concat 2024-08-30 22:20:36 +10:00
psychedelicious
61ba4c606b chore: release v4.2.9.dev5 2024-08-30 22:20:36 +10:00
psychedelicious
af840cedf3 chore(ui): lint 2024-08-30 22:20:36 +10:00
psychedelicious
0bf0bca03f feat(ui): generalize mask fill, add to action bar 2024-08-30 22:20:36 +10:00
psychedelicious
e470eaf8f3 feat(ui): implement interaction locking on layers 2024-08-30 22:20:36 +10:00
psychedelicious
377db3f726 feat(ui): iterate on layer actions
- Add lock toggle
- Tweak lock and enabled styles
- Update entity list action bar w/ delete & delete all
- Move add layer menu to action bar
- Adjust opacity slider style
2024-08-30 22:20:36 +10:00
psychedelicious
77f020a997 feat(ui): collapsible entity groups 2024-08-30 22:20:36 +10:00
psychedelicious
34e2eda625 tidy(ui): rename some classes to be consistent 2024-08-30 22:20:36 +10:00
psychedelicious
e1d559db69 feat(ui): tuned canvas undo/redo
- Throttle pushing to history for actions of the same type, starting with 1000ms throttle.
- History has a limit of 64 items, same as workflow editor
- Add clear history button
- Fix an issue where entity transformers would reset the entity state when the entity is fully transparent, resetting the redo stack. This could happen when you undo to the starting state of a layer
2024-08-30 22:20:36 +10:00
psychedelicious
23a98e2ed6 tidy(ui): move all undoable reducers back to canvas slice 2024-08-30 22:20:36 +10:00
psychedelicious
fe3b2ed357 fix(ui): dnd image count 2024-08-30 22:20:36 +10:00
psychedelicious
eedf81dcc5 fix(ui): canvas entity opacity scale 2024-08-30 22:20:36 +10:00
psychedelicious
dbef1a9e06 perf(ui): optimize all selectors 2
Mostly selector optimization. Still a few places to tidy up but I'll get to that later.
2024-08-30 22:20:36 +10:00
psychedelicious
a41406ca9a perf(ui): optimize all selectors 1
I learned that the inline selector syntax recreates the selector function on every render:

```ts
const val = useAppSelector((s) => s.slice.val)
```

Not good! Better is to create a selector outside the function and use it. Doing that for all selectors now, most of the way through now. Feels snappier.
2024-08-30 22:20:12 +10:00
psychedelicious
f126a61f66 feat(ui): rough out undo/redo on canvas 2024-08-30 22:20:12 +10:00
psychedelicious
89c79276f3 chore: release v4.2.9.dev4
Canvas dev build.
2024-08-30 22:20:12 +10:00
psychedelicious
423e463b95 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-30 22:20:12 +10:00
psychedelicious
52202e45de feat(ui): split out loras state from canvas rendering state 2024-08-30 22:20:12 +10:00
psychedelicious
100832c66d feat(ui): split out session state from canvas rendering state 2024-08-30 22:20:12 +10:00
psychedelicious
a58b91b221 feat(ui): split out settings state from canvas rendering state 2024-08-30 22:20:12 +10:00
psychedelicious
3af6d79852 feat(ui): split out tool state from canvas rendering state 2024-08-30 22:20:12 +10:00
psychedelicious
1303e18e93 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-30 22:20:12 +10:00
psychedelicious
301da97670 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-30 22:20:12 +10:00
psychedelicious
17e76981bb feat(ui): move selected tool and tool buffer out of redux
This ephemeral state can live in the canvas classes.
2024-08-30 22:20:12 +10:00
psychedelicious
9c1732e2bb 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-30 22:20:12 +10:00
psychedelicious
a3179e7a3f 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-30 22:20:12 +10:00
psychedelicious
f86b50d18a feat(ui): move events into modules who care about them 2024-08-30 22:20:12 +10:00
psychedelicious
307885f505 fix(ui): color picker resets brush opacity 2024-08-30 22:20:12 +10:00
psychedelicious
4b49c1dd6b fix(ui): scaled bbox loses sync 2024-08-30 22:20:12 +10:00
psychedelicious
f917cefa84 feat(ui): add context menu to entity list 2024-08-30 22:20:12 +10:00
psychedelicious
bea98438fc chore(ui): bump @invoke-ai/ui-library 2024-08-30 22:20:12 +10:00
psychedelicious
17d3275086 fix(ui): missing vae precision in graph builders 2024-08-30 22:20:12 +10:00
psychedelicious
059b7a0fcf chore: release v4.2.9.dev3
Instead of using dates, just going to increment.
2024-08-30 22:20:12 +10:00
psychedelicious
05d3a989f6 feat(ui): use new Result utils for enqueueing 2024-08-30 22:20:12 +10:00
psychedelicious
590ae70c12 fix(ui): graph building issue w/ controlnet 2024-08-30 22:20:12 +10:00
psychedelicious
5240ec6e6f feat(ui): add Result type & helpers
Wrappers to capture errors and turn into results:
- `withResult` wraps a sync function
- `withResultAsync` wraps an async function

Comments, tests.
2024-08-30 22:20:12 +10:00
psychedelicious
04772b642c chore: release v4.2.9.dev20240824 2024-08-30 22:20:12 +10:00
psychedelicious
65f6cb416f fix(ui): lint & fix issues with adding regional ip adapters 2024-08-30 22:20:12 +10:00
psychedelicious
24c2028739 feat(ui): add knipignore tag
I'm not ready to delete some things but still want to build the app.
2024-08-30 22:20:12 +10:00
psychedelicious
b0db9a3f56 feat(ui): duplicate entity 2024-08-30 22:20:12 +10:00
psychedelicious
3ea83574c0 feat(ui): autocomplete on getPrefixeId 2024-08-30 22:20:12 +10:00
psychedelicious
05252a9bfc feat(ui): paste canvas gens back on source in generate mode 2024-08-30 22:20:12 +10:00
psychedelicious
ce854f086e chore(ui): typegen 2024-08-30 22:20:12 +10:00
psychedelicious
ff0c16978c feat(nodes): CanvasV2MaskAndCropInvocation can paste generated image back on source
This is needed for `Generate` mode.
2024-08-30 22:20:12 +10:00
psychedelicious
41cc650031 fix(ui): extraneous entity preview updates 2024-08-30 22:20:12 +10:00
psychedelicious
c3f7554053 fix(ui): newly-added entities are selected 2024-08-30 22:20:12 +10:00
psychedelicious
3f597a1c60 feat(ui): add crosshair to color picker 2024-08-30 22:20:12 +10:00
psychedelicious
ccffdf1878 fix(ui): color picker ignores alpha 2024-08-30 22:20:12 +10:00
psychedelicious
474089e892 fix(ui): calculate renderable entities correctly in tool module 2024-08-30 22:20:12 +10:00
psychedelicious
778e8ad161 feat(ui): better color picker 2024-08-30 22:20:12 +10:00
psychedelicious
9f29892c24 feat(ui): colored mask preview image 2024-08-30 22:20:12 +10:00
psychedelicious
56fd46a069 fix(ui): new rectangles don't trigger rerender 2024-08-30 22:20:12 +10:00
psychedelicious
579e594861 chore: bump version v4.2.9.dev20240823 2024-08-30 22:20:12 +10:00
psychedelicious
af3440fbe3 feat(ui): disable most interaction while filtering 2024-08-30 22:19:54 +10:00
psychedelicious
cc101f55c4 fix(ui): filter preview offset 2024-08-30 22:19:54 +10:00
psychedelicious
ef1adf07f5 feat(ui): tweak layout of staging area toolbar 2024-08-30 22:19:54 +10:00
psychedelicious
625c05d9be chore(ui): typegen 2024-08-30 22:19:54 +10:00
psychedelicious
8ad3d8f738 tidy(app): clean up app changes for canvas v2 2024-08-30 22:19:54 +10:00
psychedelicious
4759875733 feat(ui): use singleton for clear q confirm dialog 2024-08-30 22:19:54 +10:00
psychedelicious
768e6a3c55 fix(ui): rip out broken recall logic, NO TS ERRORS 2024-08-30 22:19:54 +10:00
psychedelicious
45bd85c039 chore(ui): lint 2024-08-30 22:19:54 +10:00
psychedelicious
9f94c5a8bd fix(ui): staging area interaction scopes 2024-08-30 22:19:54 +10:00
psychedelicious
23fdd65961 fix(ui): staging area actions 2024-08-30 22:19:54 +10:00
psychedelicious
8034195c30 tidy(ui): more cleanup 2024-08-30 22:19:54 +10:00
psychedelicious
08761127c9 fix(ui): upscale tab graph 2024-08-30 22:19:54 +10:00
psychedelicious
4a10010b6c fix(ui): sdxl graph builder 2024-08-30 22:19:54 +10:00
psychedelicious
14cc5e2453 fix(ui): select next entity in the list when deleting 2024-08-30 22:19:54 +10:00
psychedelicious
3d87adea60 feat(ui): fix delete layer hotkey 2024-08-30 22:19:54 +10:00
psychedelicious
36e8232ab6 tidy(ui): "eye dropper" -> "color picker" 2024-08-30 22:19:54 +10:00
psychedelicious
72722a73be tidy(ui): regional guidance buttons 2024-08-30 22:19:54 +10:00
psychedelicious
a09aa232a9 feat(ui): update entity list menu 2024-08-30 22:19:54 +10:00
psychedelicious
7ae8b64699 feat(ui): add log debug button 2024-08-30 22:19:54 +10:00
psychedelicious
60e0d17f34 chore(ui): lint 2024-08-30 22:19:54 +10:00
psychedelicious
bf8bef2f00 chore(ui): prettier 2024-08-30 22:19:54 +10:00
psychedelicious
b586d67bac chore(ui): eslint 2024-08-30 22:19:54 +10:00
psychedelicious
31e5e5af13 tidy(ui): remove unused stuff 4 2024-08-30 22:19:35 +10:00
psychedelicious
94871e88cd tidy(ui): remove unused stuff 3 2024-08-30 22:18:50 +10:00
psychedelicious
00e56d1968 tidy(ui): remove unused pkg @chakra-ui/react-use-size 2024-08-30 22:18:50 +10:00
psychedelicious
43672a53ab feat(ui): revise graph building for control layers, fix issues w/ invocation complete events 2024-08-30 22:18:50 +10:00
psychedelicious
45097ed2a6 feat(ui): use unique id for metadata in Graph class 2024-08-30 22:18:50 +10:00
psychedelicious
871f6b9f95 tidy(ui): remove unused stuff 2 2024-08-30 22:18:50 +10:00
psychedelicious
e6476e3c75 tidy(ui): remove unused stuff 2024-08-30 22:18:50 +10:00
psychedelicious
ac9b5f246d tidy(ui): reduce use of parseify util 2024-08-30 22:18:50 +10:00
psychedelicious
8bc72a2744 feat(ui): refine canvas entity list items & menus 2024-08-30 22:18:50 +10:00
psychedelicious
f76f1d89d7 feat(ui): canvas layer preview, revised reactivity for adapters 2024-08-30 22:18:50 +10:00
psychedelicious
7b54762b5e feat(ui): add SyncableMap
Can be used with useSyncExternal store to make a `Map` reactive.
2024-08-30 22:18:50 +10:00
psychedelicious
bc6faf6a6d tidy(ui): removed unused transform methods from canvasmanager 2024-08-30 22:18:50 +10:00
psychedelicious
e7ae1ac9b2 feat(ui): transform tool ux 2024-08-30 22:18:50 +10:00
psychedelicious
dcb436adb1 feat(ui): rough out canvas mode 2024-08-30 22:18:50 +10:00
psychedelicious
80f0441905 feat(ui): add canvas autosave checkbox 2024-08-30 22:18:50 +10:00
psychedelicious
8cde803654 fix(ui): memory leak when getting image DTO
must unsubscribe!
2024-08-30 22:18:50 +10:00
psychedelicious
62445680ad feat(ui): rework settings menu 2024-08-30 22:18:50 +10:00
psychedelicious
7685e36886 feat(ui): no entities fallback buttons 2024-08-30 22:18:50 +10:00
psychedelicious
4c196844bd perf(ui): optimize gallery image delete button rendering 2024-08-30 22:18:50 +10:00
psychedelicious
b36159bda4 feat(ui): remove "solid" background option 2024-08-30 22:18:50 +10:00
psychedelicious
b02948d49a tidy(ui): organise files and classes 2024-08-30 22:18:50 +10:00
psychedelicious
f442d206be tidy(ui): abstract compositing logic to module 2024-08-30 22:18:50 +10:00
psychedelicious
21ed6bccd8 fix(ui): fix canvas cache property access 2024-08-30 22:18:50 +10:00
psychedelicious
143ce7f00b tidy(ui): clean up CanvasFilter class 2024-08-30 22:18:50 +10:00
psychedelicious
28e716139b tidy(ui): clean up a few bits and bobs 2024-08-30 22:18:50 +10:00
psychedelicious
80a7c0c521 tidy(ui): abstract canvas rendering logic to module 2024-08-30 22:18:50 +10:00
psychedelicious
255ad3d2ad tidy(ui): abstract caching logic to module 2024-08-30 22:18:50 +10:00
psychedelicious
089bc9c7d8 tidy(ui): abstract worker logic to module 2024-08-30 22:18:50 +10:00
psychedelicious
ee7dafaf57 tidy(ui): abstract stage logic into module 2024-08-30 22:18:50 +10:00
psychedelicious
516ecdb0ee feat(ui): add entity group hiding 2024-08-30 22:18:50 +10:00
psychedelicious
b77675f74d feat(ui): move all caching out of redux
While we lose the benefit of the caches persisting across reloads, this is a much simpler way to handle things. If we need a persistent cache, we can explore it in the future.
2024-08-30 22:18:50 +10:00
psychedelicious
eea5c8efad feat(ui): revised rasterization caching
- use `stable-hash` to generate stable, non-crypto hashes for cache entries, instead of using deep object comparisons
- use an object to store image name caches
2024-08-30 22:18:50 +10:00
psychedelicious
09f1aac3a3 feat(ui): revise filter implementation 2024-08-30 22:18:50 +10:00
psychedelicious
dd1dcb5eba fix(ui): add button to delete inpaint mask 2024-08-30 22:18:50 +10:00
psychedelicious
757bd62ebe feat(ui): add contexts/hooks to access entity adapters directly 2024-08-30 22:18:50 +10:00
psychedelicious
5a3127949b feat(ui): add CanvasManagerProviderGate
This context waits to render its children its until the canvas manager is available. Then its children have access to the manager directly via hook.
2024-08-30 22:18:50 +10:00
psychedelicious
ced934c0a3 feat(ui) do not set $canvasManager until ready 2024-08-30 22:18:50 +10:00
psychedelicious
c32445084f fix(ui): inpaint mask naming 2024-08-30 22:18:50 +10:00
psychedelicious
9f1af0cdaa feat(ui): efficient canvas compositing
Also solves issue of exporting layers at different opacities than what is visible
2024-08-30 22:18:50 +10:00
psychedelicious
0d26cab400 feat(ui): allow multiple inpaint masks
This is easier than making it a nullable singleton
2024-08-30 22:18:50 +10:00
psychedelicious
c8de2da3fc fix(ui): missing rasterization cache invalidations 2024-08-30 22:18:50 +10:00
psychedelicious
ca089a105e feat(ui): iterate on filter UI, flow 2024-08-30 22:18:50 +10:00
psychedelicious
22000918d6 fix(ui): rehydration data loss 2024-08-30 22:18:50 +10:00
psychedelicious
6affc28da4 feat(ui): sort log namespaces 2024-08-30 22:18:50 +10:00
psychedelicious
f659995e1c fix(ui): do not merge arrays by index during rehydration 2024-08-30 22:18:50 +10:00
psychedelicious
56fb3e738f fix(ui): clone parsed data during state rehydration
Without this, the objects and arrays in `parsed` could be mutated, and the log statment would show the mutated data.
2024-08-30 22:18:50 +10:00
psychedelicious
56d450a907 fix(ui): fix logger filter
was accidetnally replacing the filter instead of appending to it.
2024-08-30 22:18:50 +10:00
psychedelicious
d3cdcef36b fix(ui): race condition queue status
Sequence of events causing the race condition:
- Enqueue batch
- Invalidate `SessionQueueStatus` tag
- Request updated queue status via HTTP - batch still processing at this point
- Batch completes
- Event emitted saying so
- Optimistically update the queue status cache, it is correct
- HTTP request makes it back and overwrites the optimistic update, indicating the batch is still in progress

FIxed by not invalidating the cache.
2024-08-30 22:18:50 +10:00
psychedelicious
19434e73b4 fix(ui): handle opacity for masks 2024-08-30 22:18:50 +10:00
psychedelicious
f7b3df9583 feat(ui): default background to checkerboard 2024-08-30 22:18:50 +10:00
psychedelicious
4da4b3bd50 feat(ui): clean up logging namespaces, allow skipping namespaces 2024-08-30 22:18:50 +10:00
psychedelicious
e83513882a chore(ui): bump ui library 2024-08-30 22:18:50 +10:00
psychedelicious
5adc784b6b fix(ui): do not allow drawing if layer disabled 2024-08-30 22:18:50 +10:00
psychedelicious
f177513523 fix(ui): stale state causing race conditions & extraneous renders 2024-08-30 22:18:50 +10:00
psychedelicious
8ebcf79b1a fix(ui): do not clear buffer when rendering "real" objects 2024-08-30 22:18:50 +10:00
psychedelicious
c7e5f24704 tidy(ui): remove "filter" from CanvasImageState 2024-08-30 22:18:50 +10:00
psychedelicious
ab3eb32ec8 feat(ui): better editable title 2024-08-30 22:18:50 +10:00
psychedelicious
d76509e5cb fix(ui): stroke eraserline 2024-08-30 22:18:50 +10:00
psychedelicious
04f56aab82 feat(ui): restore transparency effect for control layers 2024-08-30 22:18:50 +10:00
psychedelicious
c7913cbbbb feat(ui): use text cursor for entity title 2024-08-30 22:18:50 +10:00
psychedelicious
0556468518 tidy(ui): remove extraneous logging in CanvasStateApi 2024-08-30 22:18:49 +10:00
psychedelicious
1c7ef827b6 feat(ui): better buffer commit logic 2024-08-30 22:18:49 +10:00
psychedelicious
5720ed4d64 feat(ui): render buffer separately from "real" objects 2024-08-30 22:18:49 +10:00
psychedelicious
7f05af4a68 fix(ui): pixelRect should always be integer 2024-08-30 22:18:49 +10:00
psychedelicious
6db615ed5a fix(ui): only update stage attrs when stage itself is dragged 2024-08-30 22:18:49 +10:00
psychedelicious
465f020c86 feat(ui): add line simplification
This fixes some awkward issues where line segments stack up.
2024-08-30 22:18:49 +10:00
psychedelicious
f05b77088f fix(ui): various things listening when they need not listen 2024-08-30 22:18:49 +10:00
psychedelicious
80a5abf1ad feat(ui): layer opacity via caching 2024-08-30 22:18:49 +10:00
psychedelicious
7a6e8de60f feat(ui): reset view fits all visible objects 2024-08-30 22:18:49 +10:00
psychedelicious
8364fa74cf fix(ui): rerenders when changing canvas scale 2024-08-30 22:18:49 +10:00
psychedelicious
14f4566dd0 fix(ui): do not render rasterized layer unless renderObjects=true 2024-08-30 22:18:49 +10:00
psychedelicious
6145378923 feat(ui): revise app layout strategy, add interaction scopes for hotkeys 2024-08-30 22:18:49 +10:00
psychedelicious
68e2606427 feat(ui): tweak mask patterns 2024-08-30 22:18:49 +10:00
psychedelicious
0f3eb04d1a fix(ui): dynamic prompts recalcs when presets are loaded 2024-08-30 22:18:49 +10:00
psychedelicious
4a355323b2 fix(ui): use style preset prompts correctly 2024-08-30 22:18:49 +10:00
psychedelicious
8601fbb4ea fix(ui): discard selected staging image not all other images 2024-08-30 22:18:49 +10:00
psychedelicious
db885aa180 fix(ui): respect image size in staging preview 2024-08-30 22:18:49 +10:00
psychedelicious
c18fb980a2 tidy(ui): cleanup after events change 2024-08-30 22:18:49 +10:00
psychedelicious
b630dbdf20 feat(ui): move socket event handling out of redux
Download events and invocation status events (including progress images) are very frequent. There's no real need for these to pass through redux. Handling them outside redux is a significant performance win - far fewer store subscription calls, far fewer trips through middleware.

All event handling is moved outside middleware. Cleanup of unused actions and listeners to follow.
2024-08-30 22:18:49 +10:00
psychedelicious
29ac1b5e01 fix(ui): rebase conflicts 2024-08-30 22:18:49 +10:00
psychedelicious
506d3b079e fix(ui): update compositing rect when fill changes 2024-08-30 22:18:49 +10:00
psychedelicious
0670e6b53a feat(ui): add canvas background style 2024-08-30 22:18:49 +10:00
psychedelicious
76124ea35b feat(ui): mask layers choose own opacity 2024-08-30 22:18:49 +10:00
psychedelicious
6eae3470cd feat(ui): mask fill patterns 2024-08-30 22:18:49 +10:00
psychedelicious
c7ba7ac876 build(ui): add vite types to tsconfig 2024-08-30 22:18:49 +10:00
psychedelicious
edc733abd9 fix(ui): do not smooth pixel data when using eyeDropper 2024-08-30 22:18:49 +10:00
psychedelicious
a56ded664e tidy(ui): tool components & translations 2024-08-30 22:18:49 +10:00
psychedelicious
31ace5fb0c feat(ui): rough out eyedropper tool
It's a bit slow bc we are converting the stage to canvas on every mouse move. Also need to improve the visual but it works.
2024-08-30 22:18:49 +10:00
psychedelicious
11010236b3 fix(ui): ip adapters work 2024-08-30 22:18:49 +10:00
psychedelicious
5f061ac1e2 feat(ui): rename layers 2024-08-30 22:18:49 +10:00
psychedelicious
72919fa34e feat(ui): revise entity menus 2024-08-30 22:18:49 +10:00
psychedelicious
d5ca99fc3c feat(ui): split control layers from raster layers for UI and internal state, same rendering as raster layers 2024-08-30 22:18:49 +10:00
psychedelicious
e49b72ee4e feat(ui): implement cache for image rasterization, rip out some old controladapters code 2024-08-30 22:18:49 +10:00
psychedelicious
abe8db8154 feat(ui, app): use layer as control (wip) 2024-08-30 22:18:49 +10:00
psychedelicious
e0e5941384 feat(ui): add contextmenu for canvas entities 2024-08-30 22:18:49 +10:00
psychedelicious
86e1f4e8b0 feat(ui): more better logging & naming 2024-08-30 22:18:49 +10:00
psychedelicious
447d873ef0 feat(ui): better logging w/ path 2024-08-30 22:18:49 +10:00
psychedelicious
b21d613ce4 feat(ui): always show marks on canvas scale slider 2024-08-30 22:18:49 +10:00
psychedelicious
fc91adb32f fix(ui): do not import button from chakra 2024-08-30 22:18:49 +10:00
psychedelicious
71885db5fd fix(ui): scaled bbox preview 2024-08-30 22:18:49 +10:00
psychedelicious
b88d14b3df feat(ui): tidy up atoms 2024-08-30 22:18:49 +10:00
psychedelicious
d98d35a8a8 feat(ui): convert all my pubsubs to atoms
its the same but better
2024-08-30 22:18:49 +10:00
psychedelicious
87bc0ebd73 feat(ui): add trnalsation 2024-08-30 22:18:49 +10:00
psychedelicious
7b6ba3f690 fix(ui): give up on thumbnail loading, causes flash during transformer 2024-08-30 22:18:49 +10:00
psychedelicious
b0d8948428 fix(ui): depth anything v2 2024-08-30 22:18:49 +10:00
psychedelicious
b32d681cee tidy(ui): remove unused code, comments 2024-08-30 22:18:49 +10:00
psychedelicious
11a66d1d09 fix(ui): staging area works 2024-08-30 22:18:49 +10:00
psychedelicious
e41987f08c feat(nodes): temp disable canvas output crop 2024-08-30 22:18:49 +10:00
psychedelicious
34b57ec188 fix(ui): max scale 1 when reset view 2024-08-30 22:18:49 +10:00
psychedelicious
d74843be31 feat(ui): better scale changer component, reset view functionality 2024-08-30 22:18:49 +10:00
psychedelicious
1216c6f9c9 fix(ui): img2img 2024-08-30 22:18:49 +10:00
psychedelicious
865b6017d3 feat(ui): add manual scale controls 2024-08-30 22:18:49 +10:00
psychedelicious
922a021821 fix(ui): do not await clearBuffer 2024-08-30 22:18:49 +10:00
psychedelicious
0b5f4cac57 feat(ui): dnd image into layer 2024-08-30 22:18:49 +10:00
psychedelicious
c988c58c63 fix(ui): do not await commitBuffer 2024-08-30 22:18:49 +10:00
psychedelicious
ceb8cbf59e fix(ui): properly destroy entities in manager cleanup 2024-08-30 22:18:49 +10:00
psychedelicious
52e9f43c46 tidy(ui): clearer component names for regional guidance 2024-08-30 22:18:49 +10:00
psychedelicious
4e5e7761fc tidy(ui): clearer component names for ip adapter 2024-08-30 22:18:49 +10:00
psychedelicious
9879999a65 tidy(ui): clearer component names for inpaint mask 2024-08-30 22:18:49 +10:00
psychedelicious
bedaca70a3 tidy(ui): clearer component names for control adapters 2024-08-30 22:18:49 +10:00
psychedelicious
2dd2225d2e feat(ui): simplify canvas list item headers 2024-08-30 22:18:49 +10:00
psychedelicious
d82031eec1 fix(ui): ip adapter list item 2024-08-30 22:18:49 +10:00
psychedelicious
e5f2860b74 tidy(ui): clean up unused logic 2024-08-30 22:18:49 +10:00
psychedelicious
fa3560bb61 feat(ui): clean up state, add mutex for image loading, add thumbnail loading 2024-08-30 22:18:49 +10:00
psychedelicious
9b23f6ce30 chore(ui): add async-mutex dep 2024-08-30 22:18:49 +10:00
psychedelicious
5d6aa6cfd5 feat(ui): txt2img, img2img, inpaint & outpaint working 2024-08-30 22:18:49 +10:00
psychedelicious
7d1819335f feat(ui): no padding on transformer outlines 2024-08-30 22:18:49 +10:00
psychedelicious
539e7a3f2d feat(ui): restore object count to layer titles 2024-08-30 22:18:49 +10:00
psychedelicious
1686924ac8 tidy(ui): "useIsEntitySelected" -> "useEntityIsSelected" 2024-08-30 22:18:49 +10:00
psychedelicious
556c1dc67b tidy(ui): move transformer statics into class 2024-08-30 22:18:49 +10:00
psychedelicious
00f7093e65 tidy(ui): massive cleanup
- create a context for entity identifiers, massively simplifying UI for each entity int he list
- consolidate common redux actions
- remove now-unused code
2024-08-30 22:18:49 +10:00
psychedelicious
79eb11dce9 perf(ui): do not add duplicate points to lines 2024-08-30 22:18:49 +10:00
psychedelicious
0bf48c0d41 feat(ui): up line tension to 0.3 2024-08-30 22:18:49 +10:00
psychedelicious
3f33e5f770 perf(ui): disable stroke, perfect draw on compositing rect 2024-08-30 22:18:49 +10:00
psychedelicious
da3888ba9e tidy(ui): remove unused code, initial image 2024-08-30 22:18:49 +10:00
psychedelicious
a2f91b1055 tidy(ui): remove unused state & actions 2024-08-30 22:18:49 +10:00
psychedelicious
d26095dfa1 feat(ui): region mask rendering 2024-08-30 22:18:49 +10:00
psychedelicious
83e786bd1e feat(ui): esc cancels drawing buffer
maybe this is not wanted? we'll see
2024-08-30 22:18:49 +10:00
psychedelicious
4cae12a507 fix(ui): render transformer over objects, fix issue w/ inpaint rect color 2024-08-30 22:18:49 +10:00
psychedelicious
d8e3708e0f fix(ui): brush preview fill for inpaint/region 2024-08-30 22:18:49 +10:00
psychedelicious
f4de2fd3b1 fix(ui): no objects rendered until vis toggled 2024-08-30 22:18:49 +10:00
psychedelicious
e1cb30bbb4 feat(ui): inpaint mask transform 2024-08-30 22:18:49 +10:00
psychedelicious
97e0edc549 fix(ui): layer accidental early set isFirstRender=false 2024-08-30 22:18:49 +10:00
psychedelicious
f4e66bf14f fix(ui): inpaint mask rendering 2024-08-30 22:18:49 +10:00
psychedelicious
a6a7fe8aba feat(ui): wip inpaint mask uses new API 2024-08-30 22:18:49 +10:00
psychedelicious
a273f72560 feat(ui): move updatePosition to transformer 2024-08-30 22:18:49 +10:00
psychedelicious
b5126f45d6 feat(ui): move resetScale to transformer 2024-08-30 22:18:49 +10:00
psychedelicious
ba3bb7cbf3 tidy(ui): more imperative naming 2024-08-30 22:18:49 +10:00
psychedelicious
608279487b tidy(ui): use imperative names for setters in stateapi 2024-08-30 22:18:49 +10:00
psychedelicious
72b5374916 fix(ui): commit drawing buffer on tool change, fixing bbox not calculating 2024-08-30 22:18:49 +10:00
psychedelicious
08b03212ca fix(ui): sync transformer when requesting bbox calc 2024-08-30 22:18:49 +10:00
psychedelicious
7e341a05a1 tidy(ui): rename union CanvasEntity -> CanvasEntityState 2024-08-30 22:18:49 +10:00
psychedelicious
e665d08ee1 fix(ui): request rect calc immediately on transform, hiding rect 2024-08-30 22:18:49 +10:00
psychedelicious
ba6362dc9d feat(ui): move bbox calculation to transformer 2024-08-30 22:18:49 +10:00
psychedelicious
48f0797c43 feat(ui): use set for transformer subscriptions 2024-08-30 22:18:49 +10:00
psychedelicious
640b0c4939 tidy(ui): clean up worker tasks when complete 2024-08-30 22:18:49 +10:00
psychedelicious
287c61e277 tidy(ui): remove unused code in CanvasTool 2024-08-30 22:18:49 +10:00
psychedelicious
f7b2516109 feat(ui): use pubsub for isTransforming on manager 2024-08-30 22:18:49 +10:00
psychedelicious
b530eb49d4 docs(ui): update transformer docstrings 2024-08-30 22:18:49 +10:00
psychedelicious
fa94979ab6 feat(ui): revised event pubsub, transformer logic split out 2024-08-30 22:18:49 +10:00
psychedelicious
54f2acf5b9 feat(ui): add simple pubsub 2024-08-30 22:18:49 +10:00
psychedelicious
b6d845a4d0 feat(ui): document & clean up object renderer 2024-08-30 22:18:49 +10:00
psychedelicious
1095b7c37f feat(ui): split out object renderer 2024-08-30 22:18:49 +10:00
psychedelicious
136ffd97ca fix(ui): unable to hold shit while transforming to retain ratio 2024-08-30 22:18:49 +10:00
psychedelicious
80163d0af2 tidy(ui): rename canvas stuff 2024-08-30 22:18:49 +10:00
psychedelicious
e1c6e926e7 tidy(ui): consolidate getLoggingContext builders 2024-08-30 22:18:49 +10:00
psychedelicious
2bb74abf31 fix(ui): align all tools to 1px grid
- Offset brush tool by 0.5px when width is odd, ensuring each stroke edge is exactly on a pixel boundary
- Round the rect tool also
2024-08-30 22:18:49 +10:00
psychedelicious
0d4b91afe0 feat(ui): disable image smoothing on layers 2024-08-30 22:18:49 +10:00
psychedelicious
6c688d6878 fix(ui): round position when rasterizing layer 2024-08-30 22:18:49 +10:00
psychedelicious
243feecef9 feat(ui): continue modularizing transform 2024-08-30 22:18:49 +10:00
psychedelicious
abd22ba087 feat(ui): fix a few things that didn't unsubscribe correctly, add helper to manage subscriptions 2024-08-30 22:18:49 +10:00
psychedelicious
ab25546e97 feat(ui): merge bbox outline into transformer 2024-08-30 22:18:49 +10:00
psychedelicious
925f0fca2a fix(ui): update parent's pos not transformers 2024-08-30 22:18:49 +10:00
psychedelicious
066366d885 feat(ui): merge interaction rect into transformer class 2024-08-30 22:18:49 +10:00
psychedelicious
61d52e96b7 feat(ui): prepare staging area 2024-08-30 22:18:49 +10:00
psychedelicious
051e88ca90 feat(ui): typing for logging context 2024-08-30 22:18:49 +10:00
psychedelicious
e873b69850 feat(ui): remove inheritance of CanvasObject
JS is terrible
2024-08-30 22:18:49 +10:00
psychedelicious
661fd55556 feat(ui): split & document transformer logic, iterate on class structures 2024-08-30 22:18:49 +10:00
psychedelicious
402f5a4717 feat(ui): rotation snap to nearest 45deg when holding shift 2024-08-30 22:18:49 +10:00
psychedelicious
81bf52ef37 feat(ui): expose subscribe method for nanostores 2024-08-30 22:18:49 +10:00
psychedelicious
8ff92796df tidy(ui): remove layer scaling reducers 2024-08-30 22:18:49 +10:00
psychedelicious
68af60e12e fix(ui): pixel-perfect transforms 2024-08-30 22:18:49 +10:00
psychedelicious
cce6bf9428 fix(ui): layer visibility toggle 2024-08-30 22:18:49 +10:00
psychedelicious
078908fbea fix(nodes): fix canvas mask erode
it wasn't eroding enough and caused incorrect transparency in result images
2024-08-30 22:18:49 +10:00
psychedelicious
7275caaf5b fix(ui): do not reset layer on first render 2024-08-30 22:18:49 +10:00
psychedelicious
d9487c1df4 feat(ui): revised logging and naming setup, fix staging area 2024-08-30 22:18:49 +10:00
psychedelicious
3a9f955388 feat(ui): add repr methods to layer and object classes 2024-08-30 22:18:49 +10:00
psychedelicious
e46c7acd2e feat(ui): use nanoid(10) instead of uuidv4 for canvas
Shorter ids makes it much more readable
2024-08-30 22:18:49 +10:00
psychedelicious
b771664851 build(ui): add nanoid as explicit dep 2024-08-30 22:18:49 +10:00
psychedelicious
7c21819d20 fix(ui): move CanvasImage's konva image to correct object 2024-08-30 22:18:49 +10:00
psychedelicious
a57e618d47 fix(ui): prevent flash when applying transform 2024-08-30 22:18:49 +10:00
psychedelicious
c9849a79ea build(ui): add eslint rules for async stuff 2024-08-30 22:18:49 +10:00
psychedelicious
f1643fec08 feat(ui): trying to fix flicker after transform 2024-08-30 22:18:49 +10:00
psychedelicious
951e63ca87 feat(ui): transform cleanup 2024-08-30 22:18:49 +10:00
psychedelicious
8e539c8a8c feat(ui): fix transform when rotated 2024-08-30 22:18:49 +10:00
psychedelicious
1e689a4902 fix(ui): use pixel bbox when image is in layer 2024-08-30 22:18:49 +10:00
psychedelicious
7bbd25b5ec fix(ui): transforming when axes flipped 2024-08-30 22:18:49 +10:00
psychedelicious
b1c7236117 feat(ui): hallelujah (???) 2024-08-30 22:18:49 +10:00
psychedelicious
ae3e473024 feat(ui): add debug button 2024-08-30 22:18:49 +10:00
psychedelicious
fd616f247c fix(ui): transformer padding 2024-08-30 22:18:48 +10:00
psychedelicious
45dca2c821 feat(ui): wip transform mode 2 2024-08-30 22:18:48 +10:00
psychedelicious
40dc108c84 feat(ui): wip transform mode 2024-08-30 22:18:48 +10:00
psychedelicious
a421c25952 feat(ui): wip transform mode 2024-08-30 22:18:48 +10:00
psychedelicious
562d0afdbb fix(ui): dnd to canvas broke 2024-08-30 22:18:48 +10:00
psychedelicious
2ce4698eef fix(ui): conflicts after rebasing 2024-08-30 22:18:48 +10:00
psychedelicious
cb53108041 fix(ui): imageDropped listener 2024-08-30 22:18:48 +10:00
psychedelicious
5fa65e5cc6 wip 2024-08-30 22:18:48 +10:00
psychedelicious
e8b0b6cef5 fix(ui): transform tool seems to be working 2024-08-30 22:18:48 +10:00
psychedelicious
eca2712828 fix(ui): move tool fixes, add transform tool 2024-08-30 22:18:48 +10:00
psychedelicious
2804c0aede feat(ui): move tool now only moves 2024-08-30 22:18:48 +10:00
psychedelicious
0429f0480d feat(ui): layer bbox calc in worker 2024-08-30 22:18:48 +10:00
psychedelicious
024759a0fc feat(ui): tweaked entity & group selection styles 2024-08-30 22:18:48 +10:00
psychedelicious
9a94aef2b0 feat(ui): canvas entity list headers 2024-08-30 22:18:48 +10:00
psychedelicious
e329cb45cd tidy(ui): CanvasRegion 2024-08-30 22:18:48 +10:00
psychedelicious
0dc38bd684 tidy(ui): CanvasRect 2024-08-30 22:18:48 +10:00
psychedelicious
98ebca5f8c tidy(ui): CanvasLayer 2024-08-30 22:18:48 +10:00
psychedelicious
05cb3e03cf tidy(ui): CanvasInpaintMask 2024-08-30 22:18:48 +10:00
psychedelicious
181132c149 tidy(ui): CanvasInitialImage 2024-08-30 22:18:48 +10:00
psychedelicious
a69aa00155 tidy(ui): CanvasImage 2024-08-30 22:18:48 +10:00
psychedelicious
47d415e31c tidy(ui): CanvasEraserLine 2024-08-30 22:18:48 +10:00
psychedelicious
667a156817 tidy(ui): CanvasControlAdapter 2024-08-30 22:18:48 +10:00
psychedelicious
00f39b977e tidy(ui): CanvasBrushLine 2024-08-30 22:18:48 +10:00
psychedelicious
e5776e2bd6 tidy(ui): CanvasBbox 2024-08-30 22:18:48 +10:00
psychedelicious
2b21f54897 tidy(ui): CanvasBackground 2024-08-30 22:18:48 +10:00
psychedelicious
678d12fcd5 tidy(ui): update canvas classes, organise location of konva nodes 2024-08-30 22:18:48 +10:00
psychedelicious
03f06f611e feat(ui): add names to all konva objects
Makes troubleshooting much simpler
2024-08-30 22:18:48 +10:00
psychedelicious
6571e0f814 fix(ui): do not await creating new canvas image
If you await this, it causes a race condition where multiple images are created.
2024-08-30 22:18:48 +10:00
psychedelicious
44f91026e1 feat(ui): use position and dimensions instead of separate x,y,width,height attrs 2024-08-30 22:18:48 +10:00
psychedelicious
56237328f1 fix(ui): remove weird rtkq hook wrapper
I do not understand why I did that initially but it doesn't work with TS.
2024-08-30 22:18:48 +10:00
psychedelicious
ff68901e89 feat(ui): rename types size and position to dimensions and coordinate 2024-08-30 22:18:48 +10:00
psychedelicious
e0e7adb2b2 tidy(ui): hide layer settings by default 2024-08-30 22:18:48 +10:00
psychedelicious
0923a5b128 fix(ui): layer rendering when starting as disabled 2024-08-30 22:18:48 +10:00
psychedelicious
75f8a84c79 feat(invocation): reduce canvas v2 mask & crop mask dilation 2024-08-30 22:18:48 +10:00
psychedelicious
af815cf7eb feat(ui): de-jank staging area and progress images 2024-08-30 22:18:48 +10:00
psychedelicious
ef4d6c26f6 feat(ui): update staging handling to work w/ cropped mask 2024-08-30 22:18:48 +10:00
psychedelicious
5087b306c0 chore(ui): typegen 2024-08-30 22:18:48 +10:00
psychedelicious
a5708eaefe feat(app): update CanvasV2MaskAndCropInvocation 2024-08-30 22:18:48 +10:00
psychedelicious
389bfc9e31 feat(ui): use new canvas output node 2024-08-30 22:18:48 +10:00
psychedelicious
fd269e91e0 chore(ui): typegen 2024-08-30 22:18:48 +10:00
psychedelicious
80136b0dfc feat(app): add CanvasV2MaskAndCropInvocation & CanvasV2MaskAndCropOutput
This handles some masking and cropping that the canvas needs.
2024-08-30 22:18:48 +10:00
psychedelicious
9595eff1f9 fix(ui): restore nodes output tracking 2024-08-30 22:18:48 +10:00
psychedelicious
c3c95754f7 feat(ui): rip out document size
barely knew ye
2024-08-30 22:18:48 +10:00
psychedelicious
22ab63fe8d feat(ui): convert initial image to layer when starting canvas session 2024-08-30 22:18:48 +10:00
psychedelicious
5fefcab475 fix(ui): fix layer transparency calculation 2024-08-30 22:18:48 +10:00
psychedelicious
771a05b894 fix(ui): reset initial image when resetting canvas 2024-08-30 22:18:48 +10:00
psychedelicious
e2d8aaa923 fix(ui): reset node executions states when loading workflow 2024-08-30 22:18:48 +10:00
psychedelicious
0951aecb13 fix(ui): entity display list 2024-08-30 22:18:48 +10:00
psychedelicious
b1fe6f9853 feat(ui): img2img working 2024-08-30 22:18:48 +10:00
psychedelicious
551dd393aa feat(ui): rough out img2img on canvas 2024-08-30 22:18:48 +10:00
psychedelicious
78b4562184 UNDO ME WIP 2024-08-30 22:18:48 +10:00
psychedelicious
c49b90e621 feat(ui): log invocation source id on socket event 2024-08-30 22:18:48 +10:00
psychedelicious
89e6233fbf feat(ui): restore document size overlay renderer 2024-08-30 22:18:48 +10:00
psychedelicious
3f9496c237 feat(ui): make documnet size a rect 2024-08-30 22:18:48 +10:00
psychedelicious
36e94af598 refactor(ui): remove modular imagesize components
This is no longer necessary with canvas v2 and added a ton of extraneous redux actions when changing the image size. Also renamed to document size
2024-08-30 22:18:48 +10:00
psychedelicious
a181a684f5 feat(ui): initialState is for generation mode 2024-08-30 22:18:48 +10:00
psychedelicious
bb712b3b3f feat(ui): split out canvas entity list component 2024-08-30 22:18:48 +10:00
psychedelicious
e795de5647 feat(ui): hide bbox button when no canvas session active 2024-08-30 22:18:48 +10:00
psychedelicious
bdc428cdd8 tidy(ui): remove unused naming objects/utils
The canvas manager means we don't need to worry about konva node names as we never directly select konva nodes.
2024-08-30 22:18:48 +10:00
psychedelicious
e4376e21dd feat(ui): split up tool chooser buttons
Prep for distinct toolbars for generation vs canvas modes
2024-08-30 22:18:48 +10:00
psychedelicious
77acc7baed feat(ui): add useAssertSingleton util hook
This simple hook asserts that it is only ever called once. Particularly useful for things like hotkeys hooks.
2024-08-30 22:18:48 +10:00
psychedelicious
9db1556c4d feat(ui): "stagingArea" -> "session" 2024-08-30 22:18:48 +10:00
psychedelicious
65de8b329b feat(ui): add reset button to canvas 2024-08-30 22:18:48 +10:00
psychedelicious
08dae5b047 feat(ui): add snapToRect util 2024-08-30 22:18:48 +10:00
psychedelicious
8d2f056407 fix(ui): fiddle with control adapter filters
some jank still
2024-08-30 22:18:48 +10:00
psychedelicious
e66ef2e25e feat(ui): temp disable doc size overlay 2024-08-30 22:18:48 +10:00
psychedelicious
4d3ee7e082 feat(ui): no animation on layer selection
Felt sluggish
2024-08-30 22:18:48 +10:00
psychedelicious
fe48fda2f3 feat(ui): use canvas as source for control images (wip) 2024-08-30 22:18:48 +10:00
psychedelicious
0f66753aa1 fix(ui): control adapter translate & scale 2024-08-30 22:18:48 +10:00
psychedelicious
a18878474b tidy(ui): removed unused state related to non-buffered drawing 2024-08-30 22:18:48 +10:00
psychedelicious
0aa4568fd4 feat(ui): control adapter image rendering 2024-08-30 22:18:48 +10:00
psychedelicious
1de7e5760a fix(ui): do not floor bbox calc, it cuts off the last pixels 2024-08-30 22:18:48 +10:00
psychedelicious
135d6f2763 feat(ui): fix issue where creating line needs 2 points 2024-08-30 22:18:48 +10:00
psychedelicious
061767ede3 fix(ui): edge cases when holding shift and drawing lines 2024-08-30 22:18:48 +10:00
psychedelicious
7204844bcb fix(ui): set buffered rect color to full alpha 2024-08-30 22:18:48 +10:00
psychedelicious
f2279ecadd fix(ui): handle mouseup correctly 2024-08-30 22:18:48 +10:00
psychedelicious
75694869d2 feat(ui): buffered rect drawing 2024-08-30 22:18:48 +10:00
psychedelicious
d029680ac1 fix(ui): buffered drawing edge cases 2024-08-30 22:18:48 +10:00
psychedelicious
41c195d936 perf(ui): do not use stage.find 2024-08-30 22:18:48 +10:00
psychedelicious
03ea005e9c perf(ui): object groups do not listen 2024-08-30 22:18:48 +10:00
psychedelicious
6d936a7c44 perf(ui): buffered drawing (wip) 2024-08-30 22:18:48 +10:00
psychedelicious
fba17b93a6 tidy(ui): organise files 2024-08-30 22:18:48 +10:00
psychedelicious
73a7a27ea1 tidy(ui): organise files 2024-08-30 22:18:48 +10:00
psychedelicious
79287c2d16 tidy(ui): organise files 2024-08-30 22:18:48 +10:00
psychedelicious
662c5f4b77 fix(ui): background rendering 2024-08-30 22:18:48 +10:00
psychedelicious
7728ca6843 pkg(ui): remove unused deps react-konva & use-image 2024-08-30 22:18:48 +10:00
psychedelicious
9607372f89 feat(ui): organize konva state and files 2024-08-30 22:18:48 +10:00
psychedelicious
d27f948b78 fix(ui): merge conflicts in image deletion listener 2024-08-30 22:18:48 +10:00
psychedelicious
b7aab81717 fix(ui): region rendering 2024-08-30 22:18:48 +10:00
psychedelicious
2998287f61 fix(ui): inpaint mask rendering 2024-08-30 22:18:48 +10:00
psychedelicious
55d7f0ff5b fix(ui): staging area rendering 2024-08-30 22:18:48 +10:00
psychedelicious
4564f36d4a fix(ui): stale selected entity 2024-08-30 22:18:48 +10:00
psychedelicious
319de5c4e9 fix(ui): staging area image offset 2024-08-30 22:18:48 +10:00
psychedelicious
eee499faa3 feat(ui): tweak layer ui component 2024-08-30 22:18:48 +10:00
psychedelicious
63c5e42f2a fix(ui): resetting layer resets position 2024-08-30 22:18:48 +10:00
psychedelicious
bd16dc4479 feat(ui): updated layer list component styling 2024-08-30 22:18:48 +10:00
psychedelicious
49371ddec9 feat(ui): transformable layers 2024-08-30 22:18:48 +10:00
psychedelicious
6a10d31b19 feat(ui): move tool icon is pointer like in other apps 2024-08-30 22:18:48 +10:00
psychedelicious
c951e733d3 feat(ui): do not floor cursor position 2024-08-30 22:18:48 +10:00
psychedelicious
7ed24cf847 feat(ui): disable gallery hotkeys while staging 2024-08-30 22:18:48 +10:00
psychedelicious
821b7a0435 feat(ui): revised canvas progress & staging image handling 2024-08-30 22:18:48 +10:00
psychedelicious
1b0344c412 feat(ui): show queue item origin in queue list 2024-08-30 22:18:48 +10:00
psychedelicious
03ca3c4b3d chore(ui): typegen 2024-08-30 22:18:48 +10:00
psychedelicious
b939192b16 feat(app): add origin to session queue
The origin is an optional field indicating the queue item's origin. For example, "canvas" when the queue item originated from the canvas or "workflows" when the queue item originated from the workflows tab. If omitted, we assume the queue item originated from the API directly.

- Add migration to add the nullable column to the `session_queue` table.
- Update relevant event payloads with the new field.
- Add `cancel_by_origin` method to `session_queue` service and corresponding route. This is required for the canvas to bail out early when staging images.
- Add `origin` to both `SessionQueueItem` and `Batch` - it needs to be provided initially via the batch and then passed onto the queue item.
-
2024-08-30 22:18:48 +10:00
psychedelicious
7ccf559a06 fix(ui): denoise start on outpainting 2024-08-30 22:18:48 +10:00
psychedelicious
9eb091f873 feat(ui): add redux events for queue cleared & batch enqueued socket events 2024-08-30 22:18:48 +10:00
psychedelicious
3bd5521641 feat(ui): canvas staging area works 2024-08-30 22:18:48 +10:00
psychedelicious
ced748e419 feat(ui): switch to view tool when staging 2024-08-30 22:18:48 +10:00
psychedelicious
fbd137da9f tidy(ui): disable preview images on every enqueue 2024-08-30 22:18:48 +10:00
psychedelicious
03baebced6 feat(ui): rough out save staging image 2024-08-30 22:18:48 +10:00
psychedelicious
cb19c1c370 feat(ui): staging area image visibility toggle 2024-08-30 22:18:47 +10:00
psychedelicious
788bad61d0 fix(ui): batch building after removing canvas files 2024-08-30 22:18:47 +10:00
psychedelicious
8f5f9bd44e feat(ui): make Graph class's getMetadataNode public 2024-08-30 22:18:47 +10:00
psychedelicious
2873e3e084 tidy(ui): remove old canvas graphs 2024-08-30 22:18:47 +10:00
psychedelicious
b004f17ae3 fix(ui): do not select already-selected entity 2024-08-30 22:18:47 +10:00
psychedelicious
bea1e8c99b tidy(ui): naming things 2024-08-30 22:18:47 +10:00
psychedelicious
111493223f tidy(ui): file organisation 2024-08-30 22:18:47 +10:00
psychedelicious
0a5ac2baec fix(ui): reset cursor pos when fitting document 2024-08-30 22:18:47 +10:00
psychedelicious
eec3c3b884 feat(ui): staging area works more better 2024-08-30 22:18:47 +10:00
psychedelicious
07b72c3d70 feat(ui): staging area barely works 2024-08-30 22:18:47 +10:00
psychedelicious
766e8c4eb0 feat(ui): consolidate konva API 2024-08-30 22:18:47 +10:00
psychedelicious
57c257d10d feat(ui): consolidate konva API 2024-08-30 22:18:47 +10:00
psychedelicious
d497da0e61 feat(ui): staging area (rendering wip) 2024-08-30 22:18:47 +10:00
psychedelicious
62310e7929 tidy(ui): type "Dimensions" -> "Size" 2024-08-30 22:18:47 +10:00
psychedelicious
d79aa173a6 feat(ui): add updateNode to Graph 2024-08-30 22:18:47 +10:00
psychedelicious
fbfdd3e003 feat(ui): sdxl graphs 2024-08-30 22:18:47 +10:00
psychedelicious
a62b4a26ef feat(ui): sd1 outpaint graph 2024-08-30 22:18:47 +10:00
psychedelicious
817d4168c6 tests(ui): add missing tests for Graph class 2024-08-30 22:18:47 +10:00
psychedelicious
7e0a6d1538 feat(ui): add Graph.getid() util 2024-08-30 22:18:47 +10:00
psychedelicious
ebc498ad19 feat(ui): outpaint graph, organize builder a bit 2024-08-30 22:18:47 +10:00
psychedelicious
b97b8c6ce6 feat(ui): inpaint sd1 graph 2024-08-30 22:18:47 +10:00
psychedelicious
b8abff65a1 feat(ui): temp disable image caching while testing 2024-08-30 22:18:47 +10:00
psychedelicious
a953dc1dbd feat(ui): txt2img & img2img graphs 2024-08-30 22:18:47 +10:00
psychedelicious
a7c9848e99 feat(ui): minor change to canvas bbox state type 2024-08-30 22:18:47 +10:00
psychedelicious
73a1449eaf feat(ui): simplified konva node to blob/imagedata utils 2024-08-30 22:18:47 +10:00
psychedelicious
59f57ff542 feat(ui): node manager getter/setter 2024-08-30 22:18:47 +10:00
psychedelicious
e9204b87e3 feat(ui): generation mode calculation, fudged graphs 2024-08-30 22:18:47 +10:00
psychedelicious
7dd11bd60a feat(ui): add utils for getting images from canvas 2024-08-30 22:18:47 +10:00
psychedelicious
275fc2ccf9 feat(ui): even more simplified API - lean on the konva node manager to abstract imperative state API & rendering 2024-08-30 22:18:47 +10:00
psychedelicious
a2ef8d9d47 feat(ui): revised docstrings for renderers & simplified api 2024-08-30 22:18:47 +10:00
psychedelicious
196779ff19 feat(ui): inpaint mask UI components 2024-08-30 22:18:47 +10:00
psychedelicious
aee3147365 feat(ui): inpaint mask rendering (wip) 2024-08-30 22:18:47 +10:00
psychedelicious
eaca940956 fix(ui): models loaded handler 2024-08-30 22:18:47 +10:00
psychedelicious
06006733e2 feat(ui): internal state for inpaint mask 2024-08-30 22:18:47 +10:00
psychedelicious
14d0bfbef6 refactor(ui): divvy up canvas state a bit 2024-08-30 22:18:47 +10:00
psychedelicious
0c9cf73702 feat(ui): get region and base layer canvas to blob logic working 2024-08-30 22:18:47 +10:00
psychedelicious
3b864921ac refactor(ui): node manager handles more tedious annoying stuff 2024-08-30 22:18:47 +10:00
psychedelicious
f41539532f feat(ui): use node manager for addRegions 2024-08-30 22:18:47 +10:00
psychedelicious
657009c254 feat(ui): persist bbox 2024-08-30 22:18:47 +10:00
psychedelicious
c47e02c309 fix(ui): fix generation graphs 2024-08-30 22:18:47 +10:00
psychedelicious
ce8a7bc178 feat(ui): add toggle for clipToBbox 2024-08-30 22:18:47 +10:00
psychedelicious
488ca87787 feat(ui): rename konva node manager 2024-08-30 22:18:47 +10:00
psychedelicious
d965df8ca9 refactor(ui): create classes to abstract mgmt of konva nodes 2024-08-30 22:18:47 +10:00
psychedelicious
995c26751e tidy(ui): organise renderers 2024-08-30 22:18:47 +10:00
psychedelicious
dd09723a2a refactor(ui): create entity to konva node map abstraction (wip)
Instead of chaining konva `find` and `findOne` methods, all konva nodes are added to a mapping object. Finding and manipulating them is much simpler.

Done for regions and layers, wip for control adapters.
2024-08-30 22:18:47 +10:00
psychedelicious
5ff5af3ba2 perf(ui): fix lag w/ region rendering
Needed to memoize these selectors
2024-08-30 22:18:47 +10:00
psychedelicious
4cb85404c0 feat(ui): move canvas fill color picker to right 2024-08-30 22:18:47 +10:00
psychedelicious
50bc2f100d refactor(ui): remove unused ellipse & polygon objects 2024-08-30 22:18:47 +10:00
psychedelicious
f65ce6a019 fix(ui): incorrect rect/brush/eraser positions 2024-08-30 22:18:47 +10:00
psychedelicious
c28b635f2d refactor(ui): enable global debugging flag 2024-08-30 22:18:47 +10:00
psychedelicious
e55896240d refactor(ui): disable the preview renderer for now 2024-08-30 22:18:47 +10:00
psychedelicious
2b478ee7e1 tweak(ui): canvas editor layout 2024-08-30 22:18:47 +10:00
psychedelicious
69912a35ea perf(ui): memoize layeractionsmenu valid actions 2024-08-30 22:18:47 +10:00
psychedelicious
9f1bd98c7e refactor(ui): decouple konva renderer from react
Subscribe to redux store directly, skipping all the react overhead.

With react in dev mode, a typical frame while using the brush tool on almost-empty canvas is reduced from ~7.5ms to ~3.5ms. All things considered, this still feels slow, but it's a massive improvement.
2024-08-30 22:18:47 +10:00
psychedelicious
b531d6b7f0 feat(ui): clip lines to bbox 2024-08-30 22:18:47 +10:00
psychedelicious
8aa963fb81 fix(ui): document fit positioning 2024-08-30 22:18:47 +10:00
psychedelicious
b76e0ab4e4 feat(ui): document bounds overlay 2024-08-30 22:18:47 +10:00
psychedelicious
aea03b4e92 tidy(ui): background layer 2024-08-30 22:18:47 +10:00
psychedelicious
b39e95966c refactor(ui): use "entity" instead of "data" for canvas 2024-08-30 22:18:47 +10:00
psychedelicious
d53e5e0158 feat(ui): brush size border radius = 1 2024-08-30 22:18:47 +10:00
psychedelicious
0368dd651b fix(ui): canvas HUD doesn't interrupt tool 2024-08-30 22:18:47 +10:00
psychedelicious
84a4a1024e refactor(ui): split up canvas entity renderers, temp disable preview 2024-08-30 22:18:47 +10:00
psychedelicious
af4f258489 fix(ui): delete all layers button 2024-08-30 22:18:47 +10:00
psychedelicious
ddfc8785b4 fix(ui): ignore keyboard shortcuts in input/textarea elements 2024-08-30 22:18:47 +10:00
psychedelicious
d8515b6efc fix(ui): canvas entity ids getting clobbered 2024-08-30 22:18:47 +10:00
psychedelicious
6a07f007a4 fix(ui): move lora followup fixes 2024-08-30 22:18:47 +10:00
psychedelicious
7a5a0c8075 chore(ui): lint 2024-08-30 22:18:47 +10:00
psychedelicious
5ed2e9b0fc refactor(ui): move loras to canvas slice 2024-08-30 22:18:47 +10:00
psychedelicious
aeb0a45eb6 fix(ui): layer is selected when added 2024-08-30 22:18:47 +10:00
psychedelicious
21e814d766 feat(ui): r to center & fit stage on document 2024-08-30 22:18:47 +10:00
psychedelicious
cafc1839e2 feat(ui): better HUD 2024-08-30 22:18:47 +10:00
psychedelicious
e937aa831f fix(ui): always use current brush width when making straight lines 2024-08-30 22:18:47 +10:00
psychedelicious
890e6a95ed feat(ui): hold shift w/ brush to draw straight line 2024-08-30 22:18:47 +10:00
psychedelicious
a5b7274359 fix(ui): update bg on canvas resize 2024-08-30 22:18:47 +10:00
psychedelicious
172acf2cf5 refactor(ui): better hud 2024-08-30 22:18:47 +10:00
psychedelicious
b49fdf6407 refactor(ui): scaled tool preview border 2024-08-30 22:18:47 +10:00
psychedelicious
5184d05bc2 refactor(ui): port remaining canvasV1 rendering logic to V2, remove old code 2024-08-30 22:18:47 +10:00
psychedelicious
7ef4553fc9 refactor(ui): fix more types 2024-08-30 22:18:47 +10:00
psychedelicious
d6bd1e4a49 refactor(ui): metadata recall (wip)
just enough let the app run
2024-08-30 22:18:47 +10:00
psychedelicious
29413f20a7 refactor(ui): undo/redo button temp fix 2024-08-30 22:18:47 +10:00
psychedelicious
04a44c8ea7 refactor(ui): fix renderer stuff 2024-08-30 22:18:47 +10:00
psychedelicious
426f1b6f9a refactor(ui): fix misc types 2024-08-30 22:18:47 +10:00
psychedelicious
9c7f5ed321 refactor(ui): fix gallery stuff 2024-08-30 22:18:47 +10:00
psychedelicious
4c37c7f280 refactor(ui): fix delete image stuff 2024-08-30 22:18:47 +10:00
psychedelicious
a2d13cacbf refactor(ui): fix useIsReadyToEnqueue for new adapterType field 2024-08-30 22:18:47 +10:00
psychedelicious
aa127b83a3 refactor(ui): update generation tab graphs 2024-08-30 22:18:47 +10:00
psychedelicious
e55192ae2a refactor(ui): add adapterType to ControlAdapterData 2024-08-30 22:18:47 +10:00
psychedelicious
5159fcbc33 refactor(ui): update components & logic to use new unified slice (again) 2024-08-30 22:18:47 +10:00
psychedelicious
02ad7a0f93 refactor(ui): update components & logic to use new unified slice 2024-08-30 22:18:47 +10:00
psychedelicious
bfa496e37f refactor(ui): merge compositing, params into canvasV2 slice 2024-08-30 22:18:47 +10:00
psychedelicious
fdf347af26 refactor(ui): add scaled bbox state 2024-08-30 22:18:47 +10:00
psychedelicious
0833dbb19d refactor(ui): update dnd/image upload 2024-08-30 22:18:47 +10:00
psychedelicious
1b6bf58e58 refactor(ui): update size/prompts state 2024-08-30 22:18:47 +10:00
psychedelicious
5ead7bc7b4 refactor(ui): rip out old control adapter implementation 2024-08-30 22:18:47 +10:00
psychedelicious
f326d17856 refactor(ui): canvas v2 (wip)
fix entity count select
2024-08-30 22:18:47 +10:00
psychedelicious
908aa9beea refactor(ui): canvas v2 (wip)
delete unused file
2024-08-30 22:18:47 +10:00
psychedelicious
4071e96245 refactor(ui): canvas v2 (wip)
merge all canvas state reducers into one big slice (but with the logic split across files so it's not hell)
2024-08-30 22:18:47 +10:00
psychedelicious
b4daf29bd8 refactor(ui): canvas v2 (wip)
Fix a few more components
2024-08-30 22:18:47 +10:00
psychedelicious
bf185339c2 refactor(ui): canvas v2 (wip)
missed a spot
2024-08-30 22:18:47 +10:00
psychedelicious
df3abc75c2 refactor(ui): canvas v2 (wip)
Redo all UI components for different canvas entity types
2024-08-30 22:18:47 +10:00
psychedelicious
28fc9a387c refactor(ui): canvas v2 (wip) 2024-08-30 22:18:47 +10:00
psychedelicious
8533f207dc refactor(ui): canvas v2 (wip) 2024-08-30 22:18:47 +10:00
psychedelicious
d135c48319 refactor(ui): canvas v2 (wip) 2024-08-30 22:18:47 +10:00
psychedelicious
ca9090d070 refactor(ui): canvas v2 (wip) 2024-08-30 22:18:47 +10:00
psychedelicious
93b185dc3b feat(ui): bbox tool 2024-08-30 22:18:47 +10:00
psychedelicious
98e5efa895 fix(ui): rect tool preview 2024-08-30 22:18:47 +10:00
psychedelicious
c6774b829d fix(ui): multiple stages 2024-08-30 22:18:47 +10:00
psychedelicious
22925f92bd feat(ui): decouple konva logic from nanostores 2024-08-30 22:18:47 +10:00
psychedelicious
302efcf6e8 feat(ui): store all stage attrs in nanostores 2024-08-30 22:18:47 +10:00
psychedelicious
76f9f90f0a feat(ui): round stage scale 2024-08-30 22:18:47 +10:00
psychedelicious
5ba338e471 chore(ui): bump konva 2024-08-30 22:18:47 +10:00
psychedelicious
01f101c6f2 feat(ui): generation bbox transformation working
whew
2024-08-30 22:18:47 +10:00
psychedelicious
5606aec78d feat(ui): wip generation bbox 2024-08-30 22:18:47 +10:00
psychedelicious
db90e1fe8b feat(ui): wip generation bbox 2024-08-30 22:18:47 +10:00
psychedelicious
ae96c479f2 feat(ui): CL zoom and pan, some rendering optimizations 2024-08-30 22:18:47 +10:00
psychedelicious
344ed2c83e Revert "feat(ui): add x,y,scaleX,scaleY,rotation to objects"
This reverts commit 53318b396c967c72326a7e4dea09667b2ab20bdd.
2024-08-30 22:18:47 +10:00
psychedelicious
1985944659 feat(ui): layers manage their own bbox 2024-08-30 22:18:47 +10:00
psychedelicious
915357a6c1 docs(ui): konva image object docstrings 2024-08-30 22:18:47 +10:00
psychedelicious
63c34e78d7 feat(ui): add x,y,scaleX,scaleY,rotation to objects 2024-08-30 22:18:47 +10:00
psychedelicious
366c460c1f fix(ui): show color picker when using rect tool 2024-08-30 22:18:47 +10:00
psychedelicious
40cab08133 feat(ui): image loading fallback for raster layers 2024-08-30 22:18:47 +10:00
psychedelicious
51de25122a feat(ui): bbox calc for raster layers 2024-08-30 22:18:47 +10:00
psychedelicious
90313091db feat(ui): do not fill brush preview when drawing 2024-08-30 22:18:47 +10:00
psychedelicious
9982219d18 fix(ui): brush spacing handling 2024-08-30 22:18:47 +10:00
psychedelicious
b3fe03b8f9 fix(ui): jank when starting a shape when not already focused on stage 2024-08-30 22:18:47 +10:00
psychedelicious
6edd15d68a feat(ui): wip raster layers
I meant to split this up into smaller commits and undo some of it, but I committed afterwards and it's tedious to undo.
2024-08-30 22:18:47 +10:00
psychedelicious
0e2b328c88 feat(ui): support image objects on raster layers
Just the UI and internal state, not rendering yet.
2024-08-30 22:18:47 +10:00
psychedelicious
25d7f9c316 tidy(ui): clean up event handlers
Separate logic for each tool in preparation for ellipse and polygon tools.
2024-08-30 22:18:47 +10:00
psychedelicious
3870ebdf29 feat(ui): raster layer reset, object group util 2024-08-30 22:18:47 +10:00
psychedelicious
7595d05191 feat(ui): rect shape preview now has fill 2024-08-30 22:18:47 +10:00
psychedelicious
21af727d79 feat(ui): cancel shape drawing on esc 2024-08-30 22:18:47 +10:00
psychedelicious
5691829de6 feat(ui): temp disable history on CL 2024-08-30 22:18:47 +10:00
psychedelicious
20e6a57cf1 feat(ui): raster layer logic
- Deduplicate shared logic
- Split up giant renderers file into separate cohesive files
- Tons of cleanup
- Progress on raster layer functionality
2024-08-30 22:18:47 +10:00
psychedelicious
d0c40a8b5b feat(ui): add raster layer rendering and interaction (WIP) 2024-08-30 22:18:46 +10:00
psychedelicious
f663215f25 feat(ui): scaffold out raster layers
Raster layers may have images, lines and shapes. These will replace initial image layers and provide sketching functionality like we have on canvas.
2024-08-30 22:18:46 +10:00
psychedelicious
7c5dea6d12 refactor(ui): revise types for line and rect objects
- Create separate object types for brush and eraser lines, instead of a single type that has a `tool` field.
- Create new object type for rect shapes.
- Add logic to schemas to migrate old object types to new.
- Update renderers & reducers.
2024-08-30 22:18:46 +10:00
Ryan Dick
87261bdbc9 FLUX memory management improvements (#6791)
## Summary

This PR contains several improvements to memory management for FLUX
workflows.

It is now possible to achieve better FLUX model caching performance, but
this still requires users to manually configure their `ram`/`vram`
settings. E.g. a `vram` setting of 16.0 should allow for all quantized
FLUX models to be kept in memory on the GPU.

Changes:
- Check the size of a model on disk and free the requisite space in the
model cache before loading it. (This behaviour existed previously, but
was removed in https://github.com/invoke-ai/InvokeAI/pull/6072/files.
The removal did not seem to be intentional).
- Removed the hack to free 24GB of space in the cache before loading the
FLUX model.
- Split the T5 embedding and CLIP embedding steps into separate
functions so that the two models don't both have to be held in RAM at
the same time.
- Fix a bug in `InvokeLinear8bitLt` that was causing some tensors to be
left on the GPU when the model was offloaded to the CPU. (This class is
getting very messy due to the non-standard state_dict handling in
`bnb.nn.Linear8bitLt`. )
- Tidy up some dtype handling in FluxTextToImageInvocation to avoid
situations where we hold references to two copies of the same tensor
unnecessarily.
- (minor) Misc cleanup of ModelCache: improve docs and remove unused
vars.

Future:
We should revisit our default ram/vram configs. The current defaults are
very conservative, and users could see major performance improvements
from tuning these values.

## QA Instructions

I tested the FLUX workflow with the following configurations and
verified that the cache hit rates and memory usage matched the expected
behaviour:
- `ram = 16` and `vram = 16`
- `ram = 16` and `vram = 1`
- `ram = 1` and `vram = 1`

Note that the changes in this PR are not isolated to FLUX. Since we now
check the size of models on disk, we may see slight changes in model
cache offload patterns for other models as well.

## Checklist

- [x] _The PR has a short but descriptive title, suitable for a
changelog_
- [x] _Tests added / updated (if applicable)_
- [x] _Documentation added / updated (if applicable)_
2024-08-29 15:17:45 -04:00
Ryan Dick
4e4b6c6dbc Tidy variable management and dtype handling in FluxTextToImageInvocation. 2024-08-29 19:08:18 +00:00
Ryan Dick
5e8cf9fb6a Remove hack to clear cache from the FluxTextToImageInvocation. We now clear the cache based on the on-disk model size. 2024-08-29 19:08:18 +00:00
Ryan Dick
c738fe051f Split T5 encoding and CLIP encoding into separate functions to ensure that all model references are locally-scoped so that the two models don't have to be help in memory at the same time. 2024-08-29 19:08:18 +00:00
Ryan Dick
29fe1533f2 Fix bug in InvokeLinear8bitLt that was causing old state information to persist after loading from a state dict. This manifested as state tensors being left on the GPU even when a model had been offloaded to the CPU cache. 2024-08-29 19:08:18 +00:00
Ryan Dick
77090070bd Check the size of a model on disk and make room for it in the cache before loading it. 2024-08-29 19:08:18 +00:00
Ryan Dick
6ba9b1b6b0 Tidy up GIG -> GB and remove unused GIG constant. 2024-08-29 19:08:18 +00:00
Ryan Dick
c578b8df1e Improve ModelCache docs. 2024-08-29 19:08:18 +00:00
Ryan Dick
cad9a41433 Remove unused MOdelCache.exists(...) function. 2024-08-29 19:08:18 +00:00
Ryan Dick
5fefb3b0f4 Remove unused param from ModelCache. 2024-08-29 19:08:18 +00:00
Ryan Dick
5284a870b0 Remove unused constructor params from ModelCache. 2024-08-29 19:08:18 +00:00
Ryan Dick
e064377c05 Remove default model cache sizes from model_cache_default.py. These defaults were misleading, because the config defaults take precedence over them. 2024-08-29 19:08:18 +00:00
Mary Hipp
3e569c8312 feat(ui): add fields for CLIP embed models and Flux VAE models in workflows 2024-08-29 11:52:51 -04:00
maryhipp
16825ee6e9 feat(nodes): bump version of flux model node, update default workflow 2024-08-29 11:52:51 -04:00
Mary Hipp
3f5340fa53 feat(nodes): add submodels as inputs to FLUX main model node instead of hardcoded names 2024-08-29 11:52:51 -04:00
chainchompa
f2a1a39b33 Add selectedStylePreset to app parameters (#6787)
## Summary
- Add selectedStylePreset to app parameters
<!--A description of the changes in this PR. Include the kind of change
(fix, feature, docs, etc), the "why" and the "how". Screenshots or
videos are useful for frontend changes.-->

## Related Issues / Discussions

<!--WHEN APPLICABLE: List any related issues or discussions on github or
discord. If this PR closes an issue, please use the "Closes #1234"
format, so that the issue will be automatically closed when the PR
merges.-->

## QA Instructions

<!--WHEN APPLICABLE: Describe how you have tested the changes in this
PR. Provide enough detail that a reviewer can reproduce your tests.-->

## Merge Plan

<!--WHEN APPLICABLE: Large PRs, or PRs that touch sensitive things like
DB schemas, may need some care when merging. For example, a careful
rebase by the change author, timing to not interfere with a pending
release, or a message to contributors on discord after merging.-->

## Checklist

- [ ] _The PR has a short but descriptive title, suitable for a
changelog_
- [ ] _Tests added / updated (if applicable)_
- [ ] _Documentation added / updated (if applicable)_
2024-08-28 10:53:07 -04:00
chainchompa
326de55d3e remove api changes and only preselect style preset 2024-08-28 09:53:29 -04:00
chainchompa
b2df909570 added selectedStylePreset to preload presets when app loads 2024-08-28 09:50:44 -04:00
chainchompa
026ac36b06 Revert "added selectedStylePreset to preload presets when app loads"
This reverts commit e97fd85904.
2024-08-28 09:44:08 -04:00
chainchompa
92125e5fd2 bug fixes 2024-08-27 16:13:38 -04:00
chainchompa
c0c139da88 formatting ruff 2024-08-27 15:46:51 -04:00
chainchompa
404ad6a7fd cleanup 2024-08-27 15:42:42 -04:00
chainchompa
fc39086fb4 call stylePresetSelected 2024-08-27 15:34:31 -04:00
chainchompa
cd215700fe added route for selecting style preset 2024-08-27 15:34:07 -04:00
chainchompa
e97fd85904 added selectedStylePreset to preload presets when app loads 2024-08-27 15:33:24 -04:00
Brandon Rising
0a263fa5b1 chore: bump version to v4.2.9rc1 2024-08-27 12:09:27 -04:00
Mary Hipp
fae3836a8d fix CLIP 2024-08-27 10:29:10 -04:00
Mary Hipp
b3d2eb4178 add translations for new model types in MM, remove clip vision from filter since its not displayed in list 2024-08-27 10:29:10 -04:00
psychedelicious
576f1cbb75 build: remove broken scripts
These two scripts are broken and can cause data loss. Remove them.

They are not in the launcher script, but _are_ available to users in the terminal/file browser.

Hopefully, when we removing them here, `pip` will delete them on next installation of the package...
2024-08-27 22:01:45 +10:00
118 changed files with 2047 additions and 1185 deletions

View File

@@ -45,11 +45,13 @@ class UIType(str, Enum, metaclass=MetaEnum):
SDXLRefinerModel = "SDXLRefinerModelField"
ONNXModel = "ONNXModelField"
VAEModel = "VAEModelField"
FluxVAEModel = "FluxVAEModelField"
LoRAModel = "LoRAModelField"
ControlNetModel = "ControlNetModelField"
IPAdapterModel = "IPAdapterModelField"
T2IAdapterModel = "T2IAdapterModelField"
T5EncoderModel = "T5EncoderModelField"
CLIPEmbedModel = "CLIPEmbedModelField"
SpandrelImageToImageModel = "SpandrelImageToImageModelField"
# endregion
@@ -128,6 +130,7 @@ class FieldDescriptions:
noise = "Noise tensor"
clip = "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count"
t5_encoder = "T5 tokenizer and text encoder"
clip_embed_model = "CLIP Embed loader"
unet = "UNet (scheduler, LoRAs)"
transformer = "Transformer"
vae = "VAE"

View File

@@ -40,7 +40,10 @@ class FluxTextEncoderInvocation(BaseInvocation):
@torch.no_grad()
def invoke(self, context: InvocationContext) -> FluxConditioningOutput:
t5_embeddings, clip_embeddings = self._encode_prompt(context)
# Note: The T5 and CLIP encoding are done in separate functions to ensure that all model references are locally
# scoped. This ensures that the T5 model can be freed and gc'd before loading the CLIP model (if necessary).
t5_embeddings = self._t5_encode(context)
clip_embeddings = self._clip_encode(context)
conditioning_data = ConditioningFieldData(
conditionings=[FLUXConditioningInfo(clip_embeds=clip_embeddings, t5_embeds=t5_embeddings)]
)
@@ -48,12 +51,7 @@ class FluxTextEncoderInvocation(BaseInvocation):
conditioning_name = context.conditioning.save(conditioning_data)
return FluxConditioningOutput.build(conditioning_name)
def _encode_prompt(self, context: InvocationContext) -> tuple[torch.Tensor, torch.Tensor]:
# Load CLIP.
clip_tokenizer_info = context.models.load(self.clip.tokenizer)
clip_text_encoder_info = context.models.load(self.clip.text_encoder)
# Load T5.
def _t5_encode(self, context: InvocationContext) -> torch.Tensor:
t5_tokenizer_info = context.models.load(self.t5_encoder.tokenizer)
t5_text_encoder_info = context.models.load(self.t5_encoder.text_encoder)
@@ -70,6 +68,15 @@ class FluxTextEncoderInvocation(BaseInvocation):
prompt_embeds = t5_encoder(prompt)
assert isinstance(prompt_embeds, torch.Tensor)
return prompt_embeds
def _clip_encode(self, context: InvocationContext) -> torch.Tensor:
clip_tokenizer_info = context.models.load(self.clip.tokenizer)
clip_text_encoder_info = context.models.load(self.clip.text_encoder)
prompt = [self.prompt]
with (
clip_text_encoder_info as clip_text_encoder,
clip_tokenizer_info as clip_tokenizer,
@@ -81,6 +88,5 @@ class FluxTextEncoderInvocation(BaseInvocation):
pooled_prompt_embeds = clip_encoder(prompt)
assert isinstance(prompt_embeds, torch.Tensor)
assert isinstance(pooled_prompt_embeds, torch.Tensor)
return prompt_embeds, pooled_prompt_embeds
return pooled_prompt_embeds

View File

@@ -58,13 +58,7 @@ class FluxTextToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
@torch.no_grad()
def invoke(self, context: InvocationContext) -> ImageOutput:
# Load the conditioning data.
cond_data = context.conditioning.load(self.positive_text_conditioning.conditioning_name)
assert len(cond_data.conditionings) == 1
flux_conditioning = cond_data.conditionings[0]
assert isinstance(flux_conditioning, FLUXConditioningInfo)
latents = self._run_diffusion(context, flux_conditioning.clip_embeds, flux_conditioning.t5_embeds)
latents = self._run_diffusion(context)
image = self._run_vae_decoding(context, latents)
image_dto = context.images.save(image=image)
return ImageOutput.build(image_dto)
@@ -72,12 +66,20 @@ class FluxTextToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
def _run_diffusion(
self,
context: InvocationContext,
clip_embeddings: torch.Tensor,
t5_embeddings: torch.Tensor,
):
transformer_info = context.models.load(self.transformer.transformer)
inference_dtype = torch.bfloat16
# Load the conditioning data.
cond_data = context.conditioning.load(self.positive_text_conditioning.conditioning_name)
assert len(cond_data.conditionings) == 1
flux_conditioning = cond_data.conditionings[0]
assert isinstance(flux_conditioning, FLUXConditioningInfo)
flux_conditioning = flux_conditioning.to(dtype=inference_dtype)
t5_embeddings = flux_conditioning.t5_embeds
clip_embeddings = flux_conditioning.clip_embeds
transformer_info = context.models.load(self.transformer.transformer)
# Prepare input noise.
x = get_noise(
num_samples=1,
@@ -88,24 +90,19 @@ class FluxTextToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
seed=self.seed,
)
img, img_ids = prepare_latent_img_patches(x)
x, img_ids = prepare_latent_img_patches(x)
is_schnell = "schnell" in transformer_info.config.config_path
timesteps = get_schedule(
num_steps=self.num_steps,
image_seq_len=img.shape[1],
image_seq_len=x.shape[1],
shift=not is_schnell,
)
bs, t5_seq_len, _ = t5_embeddings.shape
txt_ids = torch.zeros(bs, t5_seq_len, 3, dtype=inference_dtype, device=TorchDevice.choose_torch_device())
# HACK(ryand): Manually empty the cache. Currently we don't check the size of the model before loading it from
# disk. Since the transformer model is large (24GB), there's a good chance that it will OOM on 32GB RAM systems
# if the cache is not empty.
context.models._services.model_manager.load.ram_cache.make_room(24 * 2**30)
with transformer_info as transformer:
assert isinstance(transformer, Flux)
@@ -140,7 +137,7 @@ class FluxTextToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
x = denoise(
model=transformer,
img=img,
img=x,
img_ids=img_ids,
txt=t5_embeddings,
txt_ids=txt_ids,

View File

@@ -157,7 +157,7 @@ class FluxModelLoaderOutput(BaseInvocationOutput):
title="Flux Main Model",
tags=["model", "flux"],
category="model",
version="1.0.3",
version="1.0.4",
classification=Classification.Prototype,
)
class FluxModelLoaderInvocation(BaseInvocation):
@@ -169,23 +169,35 @@ class FluxModelLoaderInvocation(BaseInvocation):
input=Input.Direct,
)
t5_encoder: ModelIdentifierField = InputField(
description=FieldDescriptions.t5_encoder,
ui_type=UIType.T5EncoderModel,
t5_encoder_model: ModelIdentifierField = InputField(
description=FieldDescriptions.t5_encoder, ui_type=UIType.T5EncoderModel, input=Input.Direct, title="T5 Encoder"
)
clip_embed_model: ModelIdentifierField = InputField(
description=FieldDescriptions.clip_embed_model,
ui_type=UIType.CLIPEmbedModel,
input=Input.Direct,
title="CLIP Embed",
)
vae_model: ModelIdentifierField = InputField(
description=FieldDescriptions.vae_model, ui_type=UIType.FluxVAEModel, title="VAE"
)
def invoke(self, context: InvocationContext) -> FluxModelLoaderOutput:
model_key = self.model.key
for key in [self.model.key, self.t5_encoder_model.key, self.clip_embed_model.key, self.vae_model.key]:
if not context.models.exists(key):
raise ValueError(f"Unknown model: {key}")
transformer = self.model.model_copy(update={"submodel_type": SubModelType.Transformer})
vae = self.vae_model.model_copy(update={"submodel_type": SubModelType.VAE})
tokenizer = self.clip_embed_model.model_copy(update={"submodel_type": SubModelType.Tokenizer})
clip_encoder = self.clip_embed_model.model_copy(update={"submodel_type": SubModelType.TextEncoder})
tokenizer2 = self.t5_encoder_model.model_copy(update={"submodel_type": SubModelType.Tokenizer2})
t5_encoder = self.t5_encoder_model.model_copy(update={"submodel_type": SubModelType.TextEncoder2})
if not context.models.exists(model_key):
raise ValueError(f"Unknown model: {model_key}")
transformer = self._get_model(context, SubModelType.Transformer)
tokenizer = self._get_model(context, SubModelType.Tokenizer)
tokenizer2 = self._get_model(context, SubModelType.Tokenizer2)
clip_encoder = self._get_model(context, SubModelType.TextEncoder)
t5_encoder = self._get_model(context, SubModelType.TextEncoder2)
vae = self._get_model(context, SubModelType.VAE)
transformer_config = context.models.get_config(transformer)
assert isinstance(transformer_config, CheckpointConfigBase)
@@ -197,52 +209,6 @@ class FluxModelLoaderInvocation(BaseInvocation):
max_seq_len=max_seq_lengths[transformer_config.config_path],
)
def _get_model(self, context: InvocationContext, submodel: SubModelType) -> ModelIdentifierField:
match submodel:
case SubModelType.Transformer:
return self.model.model_copy(update={"submodel_type": SubModelType.Transformer})
case SubModelType.VAE:
return self._pull_model_from_mm(
context,
SubModelType.VAE,
"FLUX.1-schnell_ae",
ModelType.VAE,
BaseModelType.Flux,
)
case submodel if submodel in [SubModelType.Tokenizer, SubModelType.TextEncoder]:
return self._pull_model_from_mm(
context,
submodel,
"clip-vit-large-patch14",
ModelType.CLIPEmbed,
BaseModelType.Any,
)
case submodel if submodel in [SubModelType.Tokenizer2, SubModelType.TextEncoder2]:
return self._pull_model_from_mm(
context,
submodel,
self.t5_encoder.name,
ModelType.T5Encoder,
BaseModelType.Any,
)
case _:
raise Exception(f"{submodel.value} is not a supported submodule for a flux model")
def _pull_model_from_mm(
self,
context: InvocationContext,
submodel: SubModelType,
name: str,
type: ModelType,
base: BaseModelType,
):
if models := context.models.search_by_attrs(name=name, base=base, type=type):
if len(models) != 1:
raise Exception(f"Multiple models detected for selected model with name {name}")
return ModelIdentifierField.from_config(models[0]).model_copy(update={"submodel_type": submodel})
else:
raise ValueError(f"Please install the {base}:{type} model named {name} via starter models")
@invocation(
"main_model_loader",

View File

@@ -88,7 +88,8 @@ class QueueItemEventBase(QueueEventBase):
item_id: int = Field(description="The ID of the queue item")
batch_id: str = Field(description="The ID of the queue batch")
origin: str | None = Field(default=None, description="The origin of the batch")
origin: str | None = Field(default=None, description="The origin of the queue item")
destination: str | None = Field(default=None, description="The destination of the queue item")
class InvocationEventBase(QueueItemEventBase):
@@ -114,6 +115,7 @@ class InvocationStartedEvent(InvocationEventBase):
item_id=queue_item.item_id,
batch_id=queue_item.batch_id,
origin=queue_item.origin,
destination=queue_item.destination,
session_id=queue_item.session_id,
invocation=invocation,
invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id],
@@ -148,6 +150,7 @@ class InvocationDenoiseProgressEvent(InvocationEventBase):
item_id=queue_item.item_id,
batch_id=queue_item.batch_id,
origin=queue_item.origin,
destination=queue_item.destination,
session_id=queue_item.session_id,
invocation=invocation,
invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id],
@@ -186,6 +189,7 @@ class InvocationCompleteEvent(InvocationEventBase):
item_id=queue_item.item_id,
batch_id=queue_item.batch_id,
origin=queue_item.origin,
destination=queue_item.destination,
session_id=queue_item.session_id,
invocation=invocation,
invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id],
@@ -219,6 +223,7 @@ class InvocationErrorEvent(InvocationEventBase):
item_id=queue_item.item_id,
batch_id=queue_item.batch_id,
origin=queue_item.origin,
destination=queue_item.destination,
session_id=queue_item.session_id,
invocation=invocation,
invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id],
@@ -257,6 +262,7 @@ class QueueItemStatusChangedEvent(QueueItemEventBase):
item_id=queue_item.item_id,
batch_id=queue_item.batch_id,
origin=queue_item.origin,
destination=queue_item.destination,
session_id=queue_item.session_id,
status=queue_item.status,
error_type=queue_item.error_type,

View File

@@ -77,7 +77,14 @@ BatchDataCollection: TypeAlias = list[list[BatchDatum]]
class Batch(BaseModel):
batch_id: str = Field(default_factory=uuid_string, description="The ID of the batch")
origin: str | None = Field(default=None, description="The origin of this batch.")
origin: str | None = Field(
default=None,
description="The origin of this queue item. This data is used by the frontend to determine how to handle results.",
)
destination: str | None = Field(
default=None,
description="The origin of this queue item. This data is used by the frontend to determine how to handle results",
)
data: Optional[BatchDataCollection] = Field(default=None, description="The batch data collection.")
graph: Graph = Field(description="The graph to initialize the session with")
workflow: Optional[WorkflowWithoutID] = Field(
@@ -196,7 +203,14 @@ class SessionQueueItemWithoutGraph(BaseModel):
status: QUEUE_ITEM_STATUS = Field(default="pending", description="The status of this queue item")
priority: int = Field(default=0, description="The priority of this queue item")
batch_id: str = Field(description="The ID of the batch associated with this queue item")
origin: str | None = Field(default=None, description="The origin of this queue item. ")
origin: str | None = Field(
default=None,
description="The origin of this queue item. This data is used by the frontend to determine how to handle results.",
)
destination: str | None = Field(
default=None,
description="The origin of this queue item. This data is used by the frontend to determine how to handle results",
)
session_id: str = Field(
description="The ID of the session associated with this queue item. The session doesn't exist in graph_executions until the queue item is executed."
)
@@ -297,6 +311,7 @@ class BatchStatus(BaseModel):
queue_id: str = Field(..., description="The ID of the queue")
batch_id: str = Field(..., description="The ID of the batch")
origin: str | None = Field(..., description="The origin of the batch")
destination: str | None = Field(..., description="The destination of the batch")
pending: int = Field(..., description="Number of queue items with status 'pending'")
in_progress: int = Field(..., description="Number of queue items with status 'in_progress'")
completed: int = Field(..., description="Number of queue items with status 'complete'")
@@ -443,6 +458,7 @@ class SessionQueueValueToInsert(NamedTuple):
priority: int # priority
workflow: Optional[str] # workflow json
origin: str | None
destination: str | None
ValuesToInsert: TypeAlias = list[SessionQueueValueToInsert]
@@ -464,6 +480,7 @@ def prepare_values_to_insert(queue_id: str, batch: Batch, priority: int, max_new
priority, # priority
json.dumps(workflow, default=to_jsonable_python) if workflow else None, # workflow (json)
batch.origin, # origin
batch.destination, # destination
)
)
return values_to_insert

View File

@@ -128,8 +128,8 @@ class SqliteSessionQueue(SessionQueueBase):
self.__cursor.executemany(
"""--sql
INSERT INTO session_queue (queue_id, session, session_id, batch_id, field_values, priority, workflow, origin)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
INSERT INTO session_queue (queue_id, session, session_id, batch_id, field_values, priority, workflow, origin, destination)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
values_to_insert,
)
@@ -579,7 +579,8 @@ class SqliteSessionQueue(SessionQueueBase):
session_id,
batch_id,
queue_id,
origin
origin,
destination
FROM session_queue
WHERE queue_id = ?
"""
@@ -659,7 +660,7 @@ class SqliteSessionQueue(SessionQueueBase):
self.__lock.acquire()
self.__cursor.execute(
"""--sql
SELECT status, count(*), origin
SELECT status, count(*), origin, destination
FROM session_queue
WHERE
queue_id = ?
@@ -672,6 +673,7 @@ class SqliteSessionQueue(SessionQueueBase):
total = sum(row[1] for row in result)
counts: dict[str, int] = {row[0]: row[1] for row in result}
origin = result[0]["origin"] if result else None
destination = result[0]["destination"] if result else None
except Exception:
self.__conn.rollback()
raise
@@ -681,6 +683,7 @@ class SqliteSessionQueue(SessionQueueBase):
return BatchStatus(
batch_id=batch_id,
origin=origin,
destination=destination,
queue_id=queue_id,
pending=counts.get("pending", 0),
in_progress=counts.get("in_progress", 0),

View File

@@ -10,9 +10,11 @@ class Migration15Callback:
def _add_origin_col(self, cursor: sqlite3.Cursor) -> None:
"""
- Adds `origin` column to the session queue table.
- Adds `destination` column to the session queue table.
"""
cursor.execute("ALTER TABLE session_queue ADD COLUMN origin TEXT;")
cursor.execute("ALTER TABLE session_queue ADD COLUMN destination TEXT;")
def build_migration_15() -> Migration:
@@ -21,6 +23,7 @@ def build_migration_15() -> Migration:
This migration does the following:
- Adds `origin` column to the session queue table.
- Adds `destination` column to the session queue table.
"""
migration_15 = Migration(
from_version=14,

View File

@@ -2,13 +2,13 @@
"name": "FLUX Text to Image",
"author": "InvokeAI",
"description": "A simple text-to-image workflow using FLUX dev or schnell models. Prerequisite model downloads: T5 Encoder, CLIP-L Encoder, and FLUX VAE. Quantized and un-quantized versions can be found in the starter models tab within your Model Manager. We recommend 4 steps for FLUX schnell models and 30 steps for FLUX dev models.",
"version": "1.0.0",
"version": "1.0.4",
"contact": "",
"tags": "text2image, flux",
"notes": "Prerequisite model downloads: T5 Encoder, CLIP-L Encoder, and FLUX VAE. Quantized and un-quantized versions can be found in the starter models tab within your Model Manager. We recommend 4 steps for FLUX schnell models and 30 steps for FLUX dev models.",
"exposedFields": [
{
"nodeId": "4f0207c2-ff40-41fd-b047-ad33fbb1c33a",
"nodeId": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
"fieldName": "model"
},
{
@@ -20,8 +20,8 @@
"fieldName": "num_steps"
},
{
"nodeId": "4f0207c2-ff40-41fd-b047-ad33fbb1c33a",
"fieldName": "t5_encoder"
"nodeId": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
"fieldName": "t5_encoder_model"
}
],
"meta": {
@@ -30,12 +30,12 @@
},
"nodes": [
{
"id": "4f0207c2-ff40-41fd-b047-ad33fbb1c33a",
"id": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
"type": "invocation",
"data": {
"id": "4f0207c2-ff40-41fd-b047-ad33fbb1c33a",
"id": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
"type": "flux_model_loader",
"version": "1.0.3",
"version": "1.0.4",
"label": "",
"notes": "",
"isOpen": true,
@@ -44,31 +44,25 @@
"inputs": {
"model": {
"name": "model",
"label": "Model (Starter Models can be found in Model Manager)",
"value": {
"key": "f04a7a2f-c74d-4538-8d5e-879a53501662",
"hash": "random:4875da7a9508444ffa706f61961c260d0c6729f6181a86b31fad06df1277b850",
"name": "FLUX Dev (Quantized)",
"base": "flux",
"type": "main"
}
"label": ""
},
"t5_encoder": {
"name": "t5_encoder",
"label": "T 5 Encoder (Starter Models can be found in Model Manager)",
"value": {
"key": "20dcd9ec-5fbb-4012-8401-049e707da5e5",
"hash": "random:f986be43ff3502169e4adbdcee158afb0e0a65a1edc4cab16ae59963630cfd8f",
"name": "t5_bnb_int8_quantized_encoder",
"base": "any",
"type": "t5_encoder"
}
"t5_encoder_model": {
"name": "t5_encoder_model",
"label": ""
},
"clip_embed_model": {
"name": "clip_embed_model",
"label": ""
},
"vae_model": {
"name": "vae_model",
"label": ""
}
}
},
"position": {
"x": 337.09365228062825,
"y": 40.63469521079861
"x": 381.1882713063478,
"y": -95.89663532854017
}
},
{
@@ -207,45 +201,45 @@
],
"edges": [
{
"id": "reactflow__edge-4f0207c2-ff40-41fd-b047-ad33fbb1c33amax_seq_len-01f674f8-b3d1-4df1-acac-6cb8e0bfb63ct5_max_seq_len",
"id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90max_seq_len-01f674f8-b3d1-4df1-acac-6cb8e0bfb63ct5_max_seq_len",
"type": "default",
"source": "4f0207c2-ff40-41fd-b047-ad33fbb1c33a",
"source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
"target": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
"sourceHandle": "max_seq_len",
"targetHandle": "t5_max_seq_len"
},
{
"id": "reactflow__edge-4f0207c2-ff40-41fd-b047-ad33fbb1c33avae-159bdf1b-79e7-4174-b86e-d40e646964c8vae",
"id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90vae-159bdf1b-79e7-4174-b86e-d40e646964c8vae",
"type": "default",
"source": "4f0207c2-ff40-41fd-b047-ad33fbb1c33a",
"source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
"target": "159bdf1b-79e7-4174-b86e-d40e646964c8",
"sourceHandle": "vae",
"targetHandle": "vae"
},
{
"id": "reactflow__edge-4f0207c2-ff40-41fd-b047-ad33fbb1c33atransformer-159bdf1b-79e7-4174-b86e-d40e646964c8transformer",
"id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90t5_encoder-01f674f8-b3d1-4df1-acac-6cb8e0bfb63ct5_encoder",
"type": "default",
"source": "4f0207c2-ff40-41fd-b047-ad33fbb1c33a",
"target": "159bdf1b-79e7-4174-b86e-d40e646964c8",
"sourceHandle": "transformer",
"targetHandle": "transformer"
},
{
"id": "reactflow__edge-4f0207c2-ff40-41fd-b047-ad33fbb1c33at5_encoder-01f674f8-b3d1-4df1-acac-6cb8e0bfb63ct5_encoder",
"type": "default",
"source": "4f0207c2-ff40-41fd-b047-ad33fbb1c33a",
"source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
"target": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
"sourceHandle": "t5_encoder",
"targetHandle": "t5_encoder"
},
{
"id": "reactflow__edge-4f0207c2-ff40-41fd-b047-ad33fbb1c33aclip-01f674f8-b3d1-4df1-acac-6cb8e0bfb63cclip",
"id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90clip-01f674f8-b3d1-4df1-acac-6cb8e0bfb63cclip",
"type": "default",
"source": "4f0207c2-ff40-41fd-b047-ad33fbb1c33a",
"source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
"target": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
"sourceHandle": "clip",
"targetHandle": "clip"
},
{
"id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90transformer-159bdf1b-79e7-4174-b86e-d40e646964c8transformer",
"type": "default",
"source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
"target": "159bdf1b-79e7-4174-b86e-d40e646964c8",
"sourceHandle": "transformer",
"targetHandle": "transformer"
},
{
"id": "reactflow__edge-01f674f8-b3d1-4df1-acac-6cb8e0bfb63cconditioning-159bdf1b-79e7-4174-b86e-d40e646964c8positive_text_conditioning",
"type": "default",

View File

@@ -111,16 +111,7 @@ def denoise(
step_callback: Callable[[], None],
guidance: float = 4.0,
):
dtype = model.txt_in.bias.dtype
# TODO(ryand): This shouldn't be necessary if we manage the dtypes properly in the caller.
img = img.to(dtype=dtype)
img_ids = img_ids.to(dtype=dtype)
txt = txt.to(dtype=dtype)
txt_ids = txt_ids.to(dtype=dtype)
vec = vec.to(dtype=dtype)
# this is ignored for schnell
# guidance_vec is ignored for schnell.
guidance_vec = torch.full((img.shape[0],), guidance, device=img.device, dtype=img.dtype)
for t_curr, t_prev in tqdm(list(zip(timesteps[:-1], timesteps[1:], strict=True))):
t_vec = torch.full((img.shape[0],), t_curr, dtype=img.dtype, device=img.device)
@@ -168,9 +159,9 @@ def prepare_latent_img_patches(latent_img: torch.Tensor) -> tuple[torch.Tensor,
img = repeat(img, "1 ... -> bs ...", bs=bs)
# Generate patch position ids.
img_ids = torch.zeros(h // 2, w // 2, 3, device=img.device)
img_ids[..., 1] = img_ids[..., 1] + torch.arange(h // 2, device=img.device)[:, None]
img_ids[..., 2] = img_ids[..., 2] + torch.arange(w // 2, device=img.device)[None, :]
img_ids = torch.zeros(h // 2, w // 2, 3, device=img.device, dtype=img.dtype)
img_ids[..., 1] = img_ids[..., 1] + torch.arange(h // 2, device=img.device, dtype=img.dtype)[:, None]
img_ids[..., 2] = img_ids[..., 2] + torch.arange(w // 2, device=img.device, dtype=img.dtype)[None, :]
img_ids = repeat(img_ids, "h w c -> b (h w) c", b=bs)
return img, img_ids

View File

@@ -72,6 +72,7 @@ class ModelLoader(ModelLoaderBase):
pass
config.path = str(self._get_model_path(config))
self._ram_cache.make_room(self.get_size_fs(config, Path(config.path), submodel_type))
loaded_model = self._load_model(config, submodel_type)
self._ram_cache.put(

View File

@@ -193,15 +193,6 @@ class ModelCacheBase(ABC, Generic[T]):
"""
pass
@abstractmethod
def exists(
self,
key: str,
submodel_type: Optional[SubModelType] = None,
) -> bool:
"""Return true if the model identified by key and submodel_type is in the cache."""
pass
@abstractmethod
def cache_size(self) -> int:
"""Get the total size of the models currently cached."""

View File

@@ -1,22 +1,6 @@
# Copyright (c) 2024 Lincoln D. Stein and the InvokeAI Development team
# TODO: Add Stalker's proper name to copyright
"""
Manage a RAM cache of diffusion/transformer models for fast switching.
They are moved between GPU VRAM and CPU RAM as necessary. If the cache
grows larger than a preset maximum, then the least recently used
model will be cleared and (re)loaded from disk when next needed.
The cache returns context manager generators designed to load the
model into the GPU within the context, and unload outside the
context. Use like this:
cache = ModelCache(max_cache_size=7.5)
with cache.get_model('runwayml/stable-diffusion-1-5') as SD1,
cache.get_model('stabilityai/stable-diffusion-2') as SD2:
do_something_in_GPU(SD1,SD2)
"""
""" """
import gc
import math
@@ -40,45 +24,64 @@ from invokeai.backend.model_manager.load.model_util import calc_model_size_by_da
from invokeai.backend.util.devices import TorchDevice
from invokeai.backend.util.logging import InvokeAILogger
# Maximum size of the cache, in gigs
# Default is roughly enough to hold three fp16 diffusers models in RAM simultaneously
DEFAULT_MAX_CACHE_SIZE = 6.0
# amount of GPU memory to hold in reserve for use by generations (GB)
DEFAULT_MAX_VRAM_CACHE_SIZE = 2.75
# actual size of a gig
GIG = 1073741824
# Size of a GB in bytes.
GB = 2**30
# Size of a MB in bytes.
MB = 2**20
class ModelCache(ModelCacheBase[AnyModel]):
"""Implementation of ModelCacheBase."""
"""A cache for managing models in memory.
The cache is based on two levels of model storage:
- execution_device: The device where most models are executed (typically "cuda", "mps", or "cpu").
- storage_device: The device where models are offloaded when not in active use (typically "cpu").
The model cache is based on the following assumptions:
- storage_device_mem_size > execution_device_mem_size
- disk_to_storage_device_transfer_time >> storage_device_to_execution_device_transfer_time
A copy of all models in the cache is always kept on the storage_device. A subset of the models also have a copy on
the execution_device.
Models are moved between the storage_device and the execution_device as necessary. Cache size limits are enforced
on both the storage_device and the execution_device. The execution_device cache uses a smallest-first offload
policy. The storage_device cache uses a least-recently-used (LRU) offload policy.
Note: Neither of these offload policies has really been compared against alternatives. It's likely that different
policies would be better, although the optimal policies are likely heavily dependent on usage patterns and HW
configuration.
The cache returns context manager generators designed to load the model into the execution device (often GPU) within
the context, and unload outside the context.
Example usage:
```
cache = ModelCache(max_cache_size=7.5, max_vram_cache_size=6.0)
with cache.get_model('runwayml/stable-diffusion-1-5') as SD1:
do_something_on_gpu(SD1)
```
"""
def __init__(
self,
max_cache_size: float = DEFAULT_MAX_CACHE_SIZE,
max_vram_cache_size: float = DEFAULT_MAX_VRAM_CACHE_SIZE,
max_cache_size: float,
max_vram_cache_size: float,
execution_device: torch.device = torch.device("cuda"),
storage_device: torch.device = torch.device("cpu"),
precision: torch.dtype = torch.float16,
sequential_offload: bool = False,
lazy_offloading: bool = True,
sha_chunksize: int = 16777216,
log_memory_usage: bool = False,
logger: Optional[Logger] = None,
):
"""
Initialize the model RAM cache.
:param max_cache_size: Maximum size of the RAM cache [6.0 GB]
:param max_cache_size: Maximum size of the storage_device cache in GBs.
:param max_vram_cache_size: Maximum size of the execution_device cache in GBs.
:param execution_device: Torch device to load active model into [torch.device('cuda')]
:param storage_device: Torch device to save inactive model in [torch.device('cpu')]
:param precision: Precision for loaded models [torch.float16]
:param lazy_offloading: Keep model in VRAM until another model needs to be loaded
:param sequential_offload: Conserve VRAM by loading and unloading each stage of the pipeline sequentially
:param lazy_offloading: Keep model in VRAM until another model needs to be loaded.
:param log_memory_usage: If True, a memory snapshot will be captured before and after every model cache
operation, and the result will be logged (at debug level). There is a time cost to capturing the memory
snapshots, so it is recommended to disable this feature unless you are actively inspecting the model cache's
@@ -86,7 +89,6 @@ class ModelCache(ModelCacheBase[AnyModel]):
"""
# allow lazy offloading only when vram cache enabled
self._lazy_offloading = lazy_offloading and max_vram_cache_size > 0
self._precision: torch.dtype = precision
self._max_cache_size: float = max_cache_size
self._max_vram_cache_size: float = max_vram_cache_size
self._execution_device: torch.device = execution_device
@@ -145,15 +147,6 @@ class ModelCache(ModelCacheBase[AnyModel]):
total += cache_record.size
return total
def exists(
self,
key: str,
submodel_type: Optional[SubModelType] = None,
) -> bool:
"""Return true if the model identified by key and submodel_type is in the cache."""
key = self._make_cache_key(key, submodel_type)
return key in self._cached_models
def put(
self,
key: str,
@@ -203,7 +196,7 @@ class ModelCache(ModelCacheBase[AnyModel]):
# more stats
if self.stats:
stats_name = stats_name or key
self.stats.cache_size = int(self._max_cache_size * GIG)
self.stats.cache_size = int(self._max_cache_size * GB)
self.stats.high_watermark = max(self.stats.high_watermark, self.cache_size())
self.stats.in_cache = len(self._cached_models)
self.stats.loaded_model_sizes[stats_name] = max(
@@ -231,10 +224,13 @@ class ModelCache(ModelCacheBase[AnyModel]):
return model_key
def offload_unlocked_models(self, size_required: int) -> None:
"""Move any unused models from VRAM."""
reserved = self._max_vram_cache_size * GIG
"""Offload models from the execution_device to make room for size_required.
:param size_required: The amount of space to clear in the execution_device cache, in bytes.
"""
reserved = self._max_vram_cache_size * GB
vram_in_use = torch.cuda.memory_allocated() + size_required
self.logger.debug(f"{(vram_in_use/GIG):.2f}GB VRAM needed for models; max allowed={(reserved/GIG):.2f}GB")
self.logger.debug(f"{(vram_in_use/GB):.2f}GB VRAM needed for models; max allowed={(reserved/GB):.2f}GB")
for _, cache_entry in sorted(self._cached_models.items(), key=lambda x: x[1].size):
if vram_in_use <= reserved:
break
@@ -245,7 +241,7 @@ class ModelCache(ModelCacheBase[AnyModel]):
cache_entry.loaded = False
vram_in_use = torch.cuda.memory_allocated() + size_required
self.logger.debug(
f"Removing {cache_entry.key} from VRAM to free {(cache_entry.size/GIG):.2f}GB; vram free = {(torch.cuda.memory_allocated()/GIG):.2f}GB"
f"Removing {cache_entry.key} from VRAM to free {(cache_entry.size/GB):.2f}GB; vram free = {(torch.cuda.memory_allocated()/GB):.2f}GB"
)
TorchDevice.empty_cache()
@@ -303,7 +299,7 @@ class ModelCache(ModelCacheBase[AnyModel]):
self.logger.debug(
f"Moved model '{cache_entry.key}' from {source_device} to"
f" {target_device} in {(end_model_to_time-start_model_to_time):.2f}s."
f"Estimated model size: {(cache_entry.size/GIG):.3f} GB."
f"Estimated model size: {(cache_entry.size/GB):.3f} GB."
f"{get_pretty_snapshot_diff(snapshot_before, snapshot_after)}"
)
@@ -326,14 +322,14 @@ class ModelCache(ModelCacheBase[AnyModel]):
f"Moving model '{cache_entry.key}' from {source_device} to"
f" {target_device} caused an unexpected change in VRAM usage. The model's"
" estimated size may be incorrect. Estimated model size:"
f" {(cache_entry.size/GIG):.3f} GB.\n"
f" {(cache_entry.size/GB):.3f} GB.\n"
f"{get_pretty_snapshot_diff(snapshot_before, snapshot_after)}"
)
def print_cuda_stats(self) -> None:
"""Log CUDA diagnostics."""
vram = "%4.2fG" % (torch.cuda.memory_allocated() / GIG)
ram = "%4.2fG" % (self.cache_size() / GIG)
vram = "%4.2fG" % (torch.cuda.memory_allocated() / GB)
ram = "%4.2fG" % (self.cache_size() / GB)
in_ram_models = 0
in_vram_models = 0
@@ -353,17 +349,20 @@ class ModelCache(ModelCacheBase[AnyModel]):
)
def make_room(self, size: int) -> None:
"""Make enough room in the cache to accommodate a new model of indicated size."""
# calculate how much memory this model will require
# multiplier = 2 if self.precision==torch.float32 else 1
"""Make enough room in the cache to accommodate a new model of indicated size.
Note: This function deletes all of the cache's internal references to a model in order to free it. If there are
external references to the model, there's nothing that the cache can do about it, and those models will not be
garbage-collected.
"""
bytes_needed = size
maximum_size = self.max_cache_size * GIG # stored in GB, convert to bytes
maximum_size = self.max_cache_size * GB # stored in GB, convert to bytes
current_size = self.cache_size()
if current_size + bytes_needed > maximum_size:
self.logger.debug(
f"Max cache size exceeded: {(current_size/GIG):.2f}/{self.max_cache_size:.2f} GB, need an additional"
f" {(bytes_needed/GIG):.2f} GB"
f"Max cache size exceeded: {(current_size/GB):.2f}/{self.max_cache_size:.2f} GB, need an additional"
f" {(bytes_needed/GB):.2f} GB"
)
self.logger.debug(f"Before making_room: cached_models={len(self._cached_models)}")
@@ -380,7 +379,7 @@ class ModelCache(ModelCacheBase[AnyModel]):
if not cache_entry.locked:
self.logger.debug(
f"Removing {model_key} from RAM cache to free at least {(size/GIG):.2f} GB (-{(cache_entry.size/GIG):.2f} GB)"
f"Removing {model_key} from RAM cache to free at least {(size/GB):.2f} GB (-{(cache_entry.size/GB):.2f} GB)"
)
current_size -= cache_entry.size
models_cleared += 1

View File

@@ -54,8 +54,10 @@ class InvokeLinear8bitLt(bnb.nn.Linear8bitLt):
# See `bnb.nn.Linear8bitLt._save_to_state_dict()` for the serialization logic of SCB and weight_format.
scb = state_dict.pop(prefix + "SCB", None)
# weight_format is unused, but we pop it so we can validate that there are no unexpected keys.
_weight_format = state_dict.pop(prefix + "weight_format", None)
# Currently, we only support weight_format=0.
weight_format = state_dict.pop(prefix + "weight_format", None)
assert weight_format == 0
# TODO(ryand): Technically, we should be using `strict`, `missing_keys`, `unexpected_keys`, and `error_msgs`
# rather than raising an exception to correctly implement this API.
@@ -89,6 +91,14 @@ class InvokeLinear8bitLt(bnb.nn.Linear8bitLt):
)
self.bias = bias if bias is None else torch.nn.Parameter(bias)
# Reset the state. The persisted fields are based on the initialization behaviour in
# `bnb.nn.Linear8bitLt.__init__()`.
new_state = bnb.MatmulLtState()
new_state.threshold = self.state.threshold
new_state.has_fp16_weights = False
new_state.use_pool = self.state.use_pool
self.state = new_state
def _convert_linear_layers_to_llm_8bit(
module: torch.nn.Module, ignore_modules: set[str], outlier_threshold: float, prefix: str = ""

View File

@@ -43,6 +43,11 @@ class FLUXConditioningInfo:
clip_embeds: torch.Tensor
t5_embeds: torch.Tensor
def to(self, device: torch.device | None = None, dtype: torch.dtype | None = None):
self.clip_embeds = self.clip_embeds.to(device=device, dtype=dtype)
self.t5_embeds = self.t5_embeds.to(device=device, dtype=dtype)
return self
@dataclass
class ConditioningFieldData:

View File

@@ -3,10 +3,9 @@ Initialization file for invokeai.backend.util
"""
from invokeai.backend.util.logging import InvokeAILogger
from invokeai.backend.util.util import GIG, Chdir, directory_size
from invokeai.backend.util.util import Chdir, directory_size
__all__ = [
"GIG",
"directory_size",
"Chdir",
"InvokeAILogger",

View File

@@ -7,9 +7,6 @@ from pathlib import Path
from PIL import Image
# actual size of a gig
GIG = 1073741824
def slugify(value: str, allow_unicode: bool = False) -> str:
"""

View File

@@ -164,10 +164,10 @@
"alpha": "Alpha",
"selected": "Selected",
"tab": "Tab",
"viewing": "Viewing",
"viewingDesc": "Review images in a large gallery view",
"editing": "Editing",
"editingDesc": "Edit on the Control Layers canvas",
"view": "View",
"viewDesc": "Review images in a large gallery view",
"edit": "Edit",
"editDesc": "Edit on the Canvas",
"comparing": "Comparing",
"comparingDesc": "Comparing two images",
"enabled": "Enabled",
@@ -328,9 +328,13 @@
"completedIn": "Completed in",
"batch": "Batch",
"origin": "Origin",
"originCanvas": "Canvas",
"originWorkflows": "Workflows",
"originOther": "Other",
"destination": "Destination",
"upscaling": "Upscaling",
"canvas": "Canvas",
"generation": "Generation",
"workflows": "Workflows",
"other": "Other",
"gallery": "Gallery",
"batchFieldValues": "Batch Field Values",
"item": "Item",
"session": "Session",
@@ -702,6 +706,8 @@
"availableModels": "Available Models",
"baseModel": "Base Model",
"cancel": "Cancel",
"clipEmbed": "CLIP Embed",
"clipVision": "CLIP Vision",
"config": "Config",
"convert": "Convert",
"convertingModelBegin": "Converting Model. Please wait.",
@@ -789,6 +795,7 @@
"settings": "Settings",
"simpleModelPlaceholder": "URL or path to a local file or diffusers folder",
"source": "Source",
"spandrelImageToImage": "Image to Image (Spandrel)",
"starterModels": "Starter Models",
"starterModelsInModelManager": "Starter Models can be found in Model Manager",
"syncModels": "Sync Models",
@@ -797,6 +804,7 @@
"loraTriggerPhrases": "LoRA Trigger Phrases",
"mainModelTriggerPhrases": "Main Model Trigger Phrases",
"typePhraseHere": "Type phrase here",
"t5Encoder": "T5 Encoder",
"upcastAttention": "Upcast Attention",
"uploadImage": "Upload Image",
"urlOrLocalPath": "URL or Local Path",
@@ -1646,6 +1654,13 @@
"storeNotInitialized": "Store is not initialized"
},
"controlLayers": {
"saveCanvasToGallery": "Save Canvas To Gallery",
"saveBboxToGallery": "Save Bbox To Gallery",
"savedToGalleryOk": "Saved to Gallery",
"savedToGalleryError": "Error saving to gallery",
"mergeVisible": "Merge Visible",
"mergeVisibleOk": "Merged visible layers",
"mergeVisibleError": "Error merging visible layers",
"clearHistory": "Clear History",
"generateMode": "Generate",
"generateModeDesc": "Create individual images. Generated images are added directly to the gallery.",
@@ -1675,32 +1690,44 @@
"deletePrompt": "Delete Prompt",
"resetRegion": "Reset Region",
"debugLayers": "Debug Layers",
"showHUD": "Show HUD",
"rectangle": "Rectangle",
"maskFill": "Mask Fill",
"addPositivePrompt": "Add $t(common.positivePrompt)",
"addNegativePrompt": "Add $t(common.negativePrompt)",
"addIPAdapter": "Add $t(common.ipAdapter)",
"addRasterLayer": "Add $t(controlLayers.rasterLayer)",
"addControlLayer": "Add $t(controlLayers.controlLayer)",
"addInpaintMask": "Add $t(controlLayers.inpaintMask)",
"addRegionalGuidance": "Add $t(controlLayers.regionalGuidance)",
"regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)",
"raster": "Raster",
"rasterLayer_one": "Raster Layer",
"controlLayer_one": "Control Layer",
"inpaintMask_one": "Inpaint Mask",
"regionalGuidance_one": "Regional Guidance",
"ipAdapter_one": "IP Adapter",
"rasterLayer_other": "Raster Layers",
"controlLayer_other": "Control Layers",
"inpaintMask_other": "Inpaint Masks",
"regionalGuidance_other": "Regional Guidance",
"ipAdapter_other": "IP Adapters",
"rasterLayer": "Raster Layer",
"controlLayer": "Control Layer",
"inpaintMask": "Inpaint Mask",
"regionalGuidance": "Regional Guidance",
"ipAdapter": "IP Adapter",
"sendToGallery": "Send To Gallery",
"sendToGalleryDesc": "Generations will be sent to the gallery.",
"sendToCanvas": "Send To Canvas",
"sendToCanvasDesc": "Generations will be staged onto the canvas.",
"rasterLayer_withCount_one": "$t(controlLayers.rasterLayer)",
"controlLayer_withCount_one": "$t(controlLayers.controlLayer)",
"inpaintMask_withCount_one": "$t(controlLayers.inpaintMask)",
"regionalGuidance_withCount_one": "$t(controlLayers.regionalGuidance)",
"ipAdapter_withCount_one": "$t(controlLayers.ipAdapter)",
"rasterLayer_withCount_other": "Raster Layers",
"controlLayer_withCount_other": "Control Layers",
"inpaintMask_withCount_other": "Inpaint Masks",
"regionalGuidance_withCount_other": "Regional Guidance",
"ipAdapter_withCount_other": "IP Adapters",
"opacity": "Opacity",
"regionalGuidance_withCount_hidden": "Regional Guidance ({{count}} hidden)",
"controlAdapters_withCount_hidden": "Control Adapters ({{count}} hidden)",
"controlLayers_withCount_hidden": "Control Layers ({{count}} hidden)",
"rasterLayers_withCount_hidden": "Raster Layers ({{count}} hidden)",
"ipAdapters_withCount_hidden": "IP Adapters ({{count}} hidden)",
"inpaintMasks_withCount_hidden": "Inpaint Masks ({{count}} hidden)",
"regionalGuidance_withCount_visible": "Regional Guidance ({{count}})",
"controlAdapters_withCount_visible": "Control Adapters ({{count}})",
"controlLayers_withCount_visible": "Control Layers ({{count}})",
"rasterLayers_withCount_visible": "Raster Layers ({{count}})",
"ipAdapters_withCount_visible": "IP Adapters ({{count}})",
@@ -1737,6 +1764,7 @@
"flipHorizontal": "Flip Horizontal",
"flipVertical": "Flip Vertical",
"fill": {
"fillColor": "Fill Color",
"fillStyle": "Fill Style",
"solid": "Solid",
"grid": "Grid",

View File

@@ -16,6 +16,7 @@ import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicP
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal';
import { activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice';
import RefreshAfterResetModal from 'features/system/components/SettingsModal/RefreshAfterResetModal';
import SettingsModal from 'features/system/components/SettingsModal/SettingsModal';
import { configChanged } from 'features/system/store/configSlice';
@@ -43,10 +44,17 @@ interface Props {
action: 'sendToImg2Img' | 'sendToCanvas' | 'useAllParameters';
};
selectedWorkflowId?: string;
destination?: TabName | undefined;
selectedStylePresetId?: string;
destination?: TabName;
}
const App = ({ config = DEFAULT_CONFIG, selectedImage, selectedWorkflowId, destination }: Props) => {
const App = ({
config = DEFAULT_CONFIG,
selectedImage,
selectedWorkflowId,
selectedStylePresetId,
destination,
}: Props) => {
const language = useAppSelector(selectLanguage);
const logger = useLogger('system');
const dispatch = useAppDispatch();
@@ -85,6 +93,12 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage, selectedWorkflowId, desti
}
}, [selectedWorkflowId, getAndLoadWorkflow]);
useEffect(() => {
if (selectedStylePresetId) {
dispatch(activeStylePresetIdChanged(selectedStylePresetId));
}
}, [dispatch, selectedStylePresetId]);
useEffect(() => {
if (destination) {
dispatch(setActiveTab(destination));

View File

@@ -45,6 +45,7 @@ interface Props extends PropsWithChildren {
action: 'sendToImg2Img' | 'sendToCanvas' | 'useAllParameters';
};
selectedWorkflowId?: string;
selectedStylePresetId?: string;
destination?: TabName;
customStarUi?: CustomStarUi;
socketOptions?: Partial<ManagerOptions & SocketOptions>;
@@ -66,6 +67,7 @@ const InvokeAIUI = ({
queueId,
selectedImage,
selectedWorkflowId,
selectedStylePresetId,
destination,
customStarUi,
socketOptions,
@@ -227,6 +229,7 @@ const InvokeAIUI = ({
config={config}
selectedImage={selectedImage}
selectedWorkflowId={selectedWorkflowId}
selectedStylePresetId={selectedStylePresetId}
destination={destination}
/>
</AppDndContext>

View File

@@ -68,7 +68,7 @@ export const addStagingListeners = (startAppListening: AppStartListening) => {
objects: [imageObject],
};
api.dispatch(rasterLayerAdded({ overrides, isSelected: true }));
api.dispatch(rasterLayerAdded({ overrides, isSelected: false }));
api.dispatch(sessionStagingAreaReset());
},
});

View File

@@ -31,7 +31,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
let didStartStaging = false;
if (!state.canvasSession.isStaging && state.canvasSession.mode === 'compose') {
if (!state.canvasSession.isStaging && state.canvasSession.sendToCanvas) {
dispatch(sessionStartedStaging());
didStartStaging = true;
}
@@ -70,7 +70,11 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
const { g, noise, posCond } = buildGraphResult.value;
const prepareBatchResult = withResult(() => prepareLinearUIBatch(state, g, prepend, noise, posCond));
const destination = state.canvasSession.sendToCanvas ? 'canvas' : 'gallery';
const prepareBatchResult = withResult(() =>
prepareLinearUIBatch(state, g, prepend, noise, posCond, 'generation', destination)
);
if (isErr(prepareBatchResult)) {
log.error({ error: serializeError(prepareBatchResult.error) }, 'Failed to prepare batch');

View File

@@ -32,6 +32,7 @@ export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) =
workflow: builtWorkflow,
runs: state.params.iterations,
origin: 'workflows',
destination: 'gallery',
},
prepend: action.payload.prepend,
};

View File

@@ -16,7 +16,7 @@ export const addEnqueueRequestedUpscale = (startAppListening: AppStartListening)
const { g, noise, posCond } = await buildMultidiffusionUpscaleGraph(state);
const batchConfig = prepareLinearUIBatch(state, g, prepend, noise, posCond);
const batchConfig = prepareLinearUIBatch(state, g, prepend, noise, posCond, 'upscaling', 'gallery');
const req = dispatch(
queueApi.endpoints.enqueueBatch.initiate(batchConfig, {

View File

@@ -0,0 +1,104 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, Flex, IconButton, Tooltip, useToken } from '@invoke-ai/ui-library';
import type { ReactElement, ReactNode } from 'react';
import { memo, useCallback, useMemo } from 'react';
type IconSwitchProps = {
isChecked: boolean;
onChange: (checked: boolean) => void;
iconChecked: ReactElement;
tooltipChecked?: ReactNode;
iconUnchecked: ReactElement;
tooltipUnchecked?: ReactNode;
ariaLabel: string;
};
const getSx = (padding: string | number): SystemStyleObject => ({
transition: 'left 0.1s ease-in-out, transform 0.1s ease-in-out',
'&[data-checked="true"]': {
left: `calc(100% - ${padding})`,
transform: 'translateX(-100%)',
},
'&[data-checked="false"]': {
left: padding,
transform: 'translateX(0)',
},
});
export const IconSwitch = memo(
({
isChecked,
onChange,
iconChecked,
tooltipChecked,
iconUnchecked,
tooltipUnchecked,
ariaLabel,
}: IconSwitchProps) => {
const onUncheck = useCallback(() => {
onChange(false);
}, [onChange]);
const onCheck = useCallback(() => {
onChange(true);
}, [onChange]);
const gap = useToken('space', 1.5);
const sx = useMemo(() => getSx(gap), [gap]);
return (
<Flex
position="relative"
bg="base.800"
borderRadius="base"
alignItems="center"
justifyContent="center"
h="full"
p={gap}
gap={gap}
>
<Box
position="absolute"
borderRadius="base"
bg="invokeBlue.400"
w={12}
top={gap}
bottom={gap}
data-checked={isChecked}
sx={sx}
/>
<Tooltip hasArrow label={tooltipUnchecked}>
<IconButton
size="sm"
fontSize={16}
icon={iconUnchecked}
onClick={onUncheck}
variant={!isChecked ? 'solid' : 'ghost'}
colorScheme={!isChecked ? 'invokeBlue' : 'base'}
aria-label={ariaLabel}
data-checked={!isChecked}
w={12}
alignSelf="stretch"
h="auto"
/>
</Tooltip>
<Tooltip hasArrow label={tooltipChecked}>
<IconButton
size="sm"
fontSize={16}
icon={iconChecked}
onClick={onCheck}
variant={isChecked ? 'solid' : 'ghost'}
colorScheme={isChecked ? 'invokeBlue' : 'base'}
aria-label={ariaLabel}
data-checked={isChecked}
w={12}
alignSelf="stretch"
h="auto"
/>
</Tooltip>
</Flex>
);
}
);
IconSwitch.displayName = 'IconSwitch';

View File

@@ -1,52 +1,74 @@
import { useStore } from '@nanostores/react';
import type { WritableAtom } from 'nanostores';
import { useCallback, useMemo, useState } from 'react';
import { atom } from 'nanostores';
import { useCallback, useState } from 'react';
export const useBoolean = (initialValue: boolean) => {
const [isTrue, set] = useState(initialValue);
const setTrue = useCallback(() => set(true), []);
const setFalse = useCallback(() => set(false), []);
const toggle = useCallback(() => set((v) => !v), []);
type UseBoolean = {
isTrue: boolean;
setTrue: () => void;
setFalse: () => void;
set: (value: boolean) => void;
toggle: () => void;
};
const api = useMemo(
() => ({
/**
* Creates a hook to manage a boolean state. The boolean is stored in a nanostores atom.
* Returns a tuple containing the hook and the atom. Use this for global boolean state.
* @param initialValue Initial value of the boolean
*/
export const buildUseBoolean = (initialValue: boolean): [() => UseBoolean, WritableAtom<boolean>] => {
const $boolean = atom(initialValue);
const setTrue = () => {
$boolean.set(true);
};
const setFalse = () => {
$boolean.set(false);
};
const set = (value: boolean) => {
$boolean.set(value);
};
const toggle = () => {
$boolean.set(!$boolean.get());
};
const useBoolean = () => {
const isTrue = useStore($boolean);
return {
isTrue,
set,
setTrue,
setFalse,
set,
toggle,
}),
[isTrue, set, setTrue, setFalse, toggle]
);
};
};
return api;
return [useBoolean, $boolean] as const;
};
export const buildUseBoolean = ($boolean: WritableAtom<boolean>) => {
return () => {
const setTrue = useCallback(() => {
$boolean.set(true);
}, []);
const setFalse = useCallback(() => {
$boolean.set(false);
}, []);
const set = useCallback((value: boolean) => {
$boolean.set(value);
}, []);
const toggle = useCallback(() => {
$boolean.set(!$boolean.get());
}, []);
/**
* Hook to manage a boolean state. Use this for a local boolean state.
* @param initialValue Initial value of the boolean
*/
export const useBoolean = (initialValue: boolean) => {
const [isTrue, set] = useState(initialValue);
const api = useMemo(
() => ({
setTrue,
setFalse,
set,
toggle,
$boolean,
}),
[set, setFalse, setTrue, toggle]
);
const setTrue = useCallback(() => {
set(true);
}, [set]);
const setFalse = useCallback(() => {
set(false);
}, [set]);
const toggle = useCallback(() => {
set((val) => !val);
}, [set]);
return api;
return {
isTrue,
setTrue,
setFalse,
set,
toggle,
};
};

View File

@@ -1,7 +1,7 @@
import { useAppDispatch } from 'app/store/storeHooks';
import { addScope, removeScope, setScopes } from 'common/hooks/interactionScopes';
import { useClearQueue } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
import { useQueueBack } from 'features/queue/hooks/useQueueBack';
import { useQueueFront } from 'features/queue/hooks/useQueueFront';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';

View File

@@ -31,22 +31,22 @@ export const CanvasAddEntityButtons = memo(() => {
}, [dispatch]);
return (
<Flex flexDir="column" w="full" h="full" alignItems="center" justifyContent="center">
<ButtonGroup orientation="vertical" isAttached={false}>
<Flex flexDir="column" w="full" h="full" alignItems="center">
<ButtonGroup position="relative" orientation="vertical" isAttached={false} top="20%">
<Button variant="ghost" justifyContent="flex-start" leftIcon={<PiPlusBold />} onClick={addInpaintMask}>
{t('controlLayers.inpaintMask', { count: 1 })}
{t('controlLayers.inpaintMask')}
</Button>
<Button variant="ghost" justifyContent="flex-start" leftIcon={<PiPlusBold />} onClick={addRegionalGuidance}>
{t('controlLayers.regionalGuidance', { count: 1 })}
{t('controlLayers.regionalGuidance')}
</Button>
<Button variant="ghost" justifyContent="flex-start" leftIcon={<PiPlusBold />} onClick={addRasterLayer}>
{t('controlLayers.rasterLayer', { count: 1 })}
{t('controlLayers.rasterLayer')}
</Button>
<Button variant="ghost" justifyContent="flex-start" leftIcon={<PiPlusBold />} onClick={addControlLayer}>
{t('controlLayers.controlLayer', { count: 1 })}
{t('controlLayers.controlLayer')}
</Button>
<Button variant="ghost" justifyContent="flex-start" leftIcon={<PiPlusBold />} onClick={addIPAdapter}>
{t('controlLayers.ipAdapter', { count: 1 })}
{t('controlLayers.ipAdapter')}
</Button>
</ButtonGroup>
</Flex>

View File

@@ -33,19 +33,19 @@ export const CanvasEntityListMenuItems = memo(() => {
return (
<>
<MenuItem icon={<PiPlusBold />} onClick={addInpaintMask}>
{t('controlLayers.inpaintMask', { count: 1 })}
{t('controlLayers.inpaintMask')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addRegionalGuidance}>
{t('controlLayers.regionalGuidance', { count: 1 })}
{t('controlLayers.regionalGuidance')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addRasterLayer}>
{t('controlLayers.rasterLayer', { count: 1 })}
{t('controlLayers.rasterLayer')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addControlLayer}>
{t('controlLayers.controlLayer', { count: 1 })}
{t('controlLayers.controlLayer')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addIPAdapter}>
{t('controlLayers.ipAdapter', { count: 1 })}
{t('controlLayers.ipAdapter')}
</MenuItem>
</>
);

View File

@@ -157,7 +157,7 @@ export const SelectedEntityOpacity = memo(() => {
clampValueOnBlur={false}
variant="outline"
>
<NumberInputField paddingInlineEnd={7} />
<NumberInputField paddingInlineEnd={7} _focusVisible={{ zIndex: 0 }} />
<PopoverTrigger>
<IconButton
aria-label="open-slider"

View File

@@ -1,29 +0,0 @@
import { Button, ButtonGroup } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSessionSlice, sessionModeChanged } from 'features/controlLayers/store/canvasSessionSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const selectCanvasMode = createSelector(selectCanvasSessionSlice, (canvasSession) => canvasSession.mode);
export const CanvasModeSwitcher = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const mode = useAppSelector(selectCanvasMode);
const onClickGenerate = useCallback(() => dispatch(sessionModeChanged({ mode: 'generate' })), [dispatch]);
const onClickCompose = useCallback(() => dispatch(sessionModeChanged({ mode: 'compose' })), [dispatch]);
return (
<ButtonGroup variant="outline">
<Button onClick={onClickGenerate} colorScheme={mode === 'generate' ? 'invokeBlue' : 'base'}>
{t('controlLayers.generateMode')}
</Button>
<Button onClick={onClickCompose} colorScheme={mode === 'compose' ? 'invokeBlue' : 'base'}>
{t('controlLayers.composeMode')}
</Button>
</ButtonGroup>
);
});
CanvasModeSwitcher.displayName = 'CanvasModeSwitcher';

View File

@@ -9,6 +9,7 @@ import { memo } from 'react';
export const CanvasPanelContent = memo(() => {
const hasEntities = useAppSelector(selectHasEntities);
return (
<CanvasManagerProviderGate>
<Flex flexDir="column" gap={2} w="full" h="full">

View File

@@ -0,0 +1,59 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { IconSwitch } from 'common/components/IconSwitch';
import { selectIsComposing, sessionSendToCanvasChanged } from 'features/controlLayers/store/canvasSessionSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiImageBold, PiPaintBrushBold } from 'react-icons/pi';
const TooltipSendToGallery = memo(() => {
const { t } = useTranslation();
return (
<Flex flexDir="column">
<Text fontWeight="semibold">{t('controlLayers.sendToGallery')}</Text>
<Text fontWeight="normal">{t('controlLayers.sendToGalleryDesc')}</Text>
</Flex>
);
});
TooltipSendToGallery.displayName = 'TooltipSendToGallery';
const TooltipSendToCanvas = memo(() => {
const { t } = useTranslation();
return (
<Flex flexDir="column">
<Text fontWeight="semibold">{t('controlLayers.sendToCanvas')}</Text>
<Text fontWeight="normal">{t('controlLayers.sendToCanvasDesc')}</Text>
</Flex>
);
});
TooltipSendToCanvas.displayName = 'TooltipSendToCanvas';
export const CanvasSendToToggle = memo(() => {
const dispatch = useAppDispatch();
const isComposing = useAppSelector(selectIsComposing);
const onChange = useCallback(
(isChecked: boolean) => {
dispatch(sessionSendToCanvasChanged(isChecked));
},
[dispatch]
);
return (
<IconSwitch
isChecked={isComposing}
onChange={onChange}
iconUnchecked={<PiImageBold />}
tooltipUnchecked={<TooltipSendToGallery />}
iconChecked={<PiPaintBrushBold />}
tooltipChecked={<TooltipSendToCanvas />}
ariaLabel="Toggle canvas mode"
/>
);
});
CanvasSendToToggle.displayName = 'CanvasSendToToggle';

View File

@@ -1,8 +1,7 @@
import { Spacer } from '@invoke-ai/ui-library';
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle';
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper';
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
@@ -29,8 +28,7 @@ export const ControlLayer = memo(({ id }: Props) => {
<CanvasEntityEditableTitle />
<Spacer />
<ControlLayerBadges />
<CanvasEntityIsLockedToggle />
<CanvasEntityEnabledToggle />
<CanvasEntityHeaderCommonActions />
</CanvasEntityHeader>
<CanvasEntitySettingsWrapper>
<ControlLayerControlAdapter />

View File

@@ -1,19 +1,21 @@
/* eslint-disable i18next/no-literal-string */
import { Flex, Spacer } from '@invoke-ai/ui-library';
import { CanvasModeSwitcher } from 'features/controlLayers/components/CanvasModeSwitcher';
import { CanvasResetViewButton } from 'features/controlLayers/components/CanvasResetViewButton';
import { CanvasScale } from 'features/controlLayers/components/CanvasScale';
import { SaveToGalleryButton } from 'features/controlLayers/components/SaveToGalleryButton';
import { CanvasSettingsPopover } from 'features/controlLayers/components/Settings/CanvasSettingsPopover';
import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser';
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 { useCanvasUndoRedo } from 'features/controlLayers/hooks/useCanvasUndoRedo';
import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
import { ViewerToggleMenu } from 'features/gallery/components/ImageViewer/ViewerToggleMenu';
import { ViewerToggle } from 'features/gallery/components/ImageViewer/ViewerToggleMenu';
import { memo } from 'react';
export const ControlLayersToolbar = memo(() => {
useCanvasUndoRedo();
return (
<CanvasManagerProviderGate>
<Flex w="full" gap={2} alignItems="center">
@@ -26,10 +28,9 @@ export const ControlLayersToolbar = memo(() => {
<CanvasResetViewButton />
<Spacer />
<ToolFillColorPicker />
<CanvasModeSwitcher />
<UndoRedoButtonGroup />
<SaveToGalleryButton />
<CanvasSettingsPopover />
<ViewerToggleMenu />
<ViewerToggle />
</Flex>
</CanvasManagerProviderGate>
);

View File

@@ -1,53 +1,27 @@
import { Box, Flex, Text } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { Grid, GridItem, Text } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { round } from 'lodash-es';
import { memo } from 'react';
const selectBbox = createSelector(selectCanvasSlice, (canvas) => canvas.bbox);
export const HeadsUpDisplay = memo(() => {
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(selectBbox);
return (
<Flex flexDir="column" bg="blackAlpha.400" borderBottomEndRadius="base" p={2} minW={64} gap={2}>
<HUDItem label="Zoom" value={`${round(stageAttrs.scale * 100, 2)}%`} />
<HUDItem label="Stage Pos" value={`${round(stageAttrs.x, 3)}, ${round(stageAttrs.y, 3)}`} />
<HUDItem
label="Stage Size"
value={`${round(stageAttrs.width / stageAttrs.scale, 2)}×${round(stageAttrs.height / stageAttrs.scale, 2)} px`}
/>
<HUDItem label="BBox Size" value={`${bbox.rect.width}×${bbox.rect.height} px`} />
<HUDItem label="BBox Position" value={`${bbox.rect.x}, ${bbox.rect.y}`} />
<HUDItem label="BBox Width % 8" value={round(bbox.rect.width % 8, 2)} />
<HUDItem label="BBox Height % 8" value={round(bbox.rect.height % 8, 2)} />
<HUDItem label="BBox X % 8" value={round(bbox.rect.x % 8, 2)} />
<HUDItem label="BBox Y % 8" value={round(bbox.rect.y % 8, 2)} />
<HUDItem
label="Cursor Position"
value={cursorPos ? `${round(cursorPos.x, 2)}, ${round(cursorPos.y, 2)}` : '?, ?'}
/>
<HUDItem label="Is Drawing" value={isDrawing ? 'True' : 'False'} />
<HUDItem label="Is Mouse Down" value={isMouseDown ? 'True' : 'False'} />
<HUDItem
label="Last Mouse Down Pos"
value={lastMouseDownPos ? `${round(lastMouseDownPos.x, 2)}, ${round(lastMouseDownPos.y, 2)}` : '?, ?'}
/>
<HUDItem
label="Last Added Point"
value={lastAddedPoint ? `${round(lastAddedPoint.x, 2)}, ${round(lastAddedPoint.y, 2)}` : '?, ?'}
/>
</Flex>
<Grid
bg="base.900"
borderBottomEndRadius="base"
p={2}
gap={2}
borderRadius="base"
templateColumns="auto auto"
opacity={0.6}
>
<HUDItem label="BBox" value={`${bbox.rect.width}×${bbox.rect.height} px`} />
<HUDItem label="Scaled BBox" value={`${bbox.scaledSize.width}×${bbox.scaledSize.height} px`} />
</Grid>
);
});
@@ -55,12 +29,14 @@ HeadsUpDisplay.displayName = 'HeadsUpDisplay';
const HUDItem = memo(({ label, value }: { label: string; value: string | number }) => {
return (
<Box display="inline-block" lineHeight={1}>
<Text as="span">{label}: </Text>
<Text as="span" fontWeight="semibold">
{value}
</Text>
</Box>
<>
<GridItem>
<Text textAlign="end">{label}: </Text>
</GridItem>
<GridItem fontWeight="semibold">
<Text>{value}</Text>
</GridItem>
</>
);
});

View File

@@ -1,7 +1,7 @@
import { Spacer } from '@invoke-ai/ui-library';
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
import { IPAdapterSettings } from 'features/controlLayers/components/IPAdapter/IPAdapterSettings';
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
@@ -18,10 +18,10 @@ export const IPAdapter = memo(({ id }: Props) => {
return (
<EntityIdentifierContext.Provider value={entityIdentifier}>
<CanvasEntityContainer>
<CanvasEntityHeader ps={4}>
<CanvasEntityHeader ps={4} py={5}>
<CanvasEntityEditableTitle />
<Spacer />
<CanvasEntityEnabledToggle />
<CanvasEntityHeaderCommonActions />
</CanvasEntityHeader>
<IPAdapterSettings />
</CanvasEntityContainer>

View File

@@ -1,8 +1,7 @@
import { Spacer } from '@invoke-ai/ui-library';
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle';
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
import { EntityMaskAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
@@ -25,8 +24,7 @@ export const InpaintMask = memo(({ id }: Props) => {
<CanvasEntityPreviewImage />
<CanvasEntityEditableTitle />
<Spacer />
<CanvasEntityIsLockedToggle />
<CanvasEntityEnabledToggle />
<CanvasEntityHeaderCommonActions />
</CanvasEntityHeader>
</CanvasEntityContainer>
</EntityMaskAdapterGate>

View File

@@ -1,8 +1,7 @@
import { Spacer } from '@invoke-ai/ui-library';
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle';
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
import { EntityLayerAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
@@ -25,8 +24,7 @@ export const RasterLayer = memo(({ id }: Props) => {
<CanvasEntityPreviewImage />
<CanvasEntityEditableTitle />
<Spacer />
<CanvasEntityIsLockedToggle />
<CanvasEntityEnabledToggle />
<CanvasEntityHeaderCommonActions />
</CanvasEntityHeader>
</CanvasEntityContainer>
</EntityLayerAdapterGate>

View File

@@ -1,8 +1,7 @@
import { Spacer } from '@invoke-ai/ui-library';
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle';
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
import { RegionalGuidanceBadges } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges';
@@ -28,8 +27,7 @@ export const RegionalGuidance = memo(({ id }: Props) => {
<CanvasEntityEditableTitle />
<Spacer />
<RegionalGuidanceBadges />
<CanvasEntityIsLockedToggle />
<CanvasEntityEnabledToggle />
<CanvasEntityHeaderCommonActions />
</CanvasEntityHeader>
<RegionalGuidanceSettings />
</CanvasEntityContainer>

View File

@@ -0,0 +1,53 @@
import { IconButton, useShiftModifier } from '@invoke-ai/ui-library';
import { logger } from 'app/logging/logger';
import { buildUseBoolean } from 'common/hooks/useBoolean';
import { isOk, withResultAsync } from 'common/util/result';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { toast } from 'features/toast/toast';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiFloppyDiskBold } from 'react-icons/pi';
import { serializeError } from 'serialize-error';
const log = logger('canvas');
const [useIsSaving] = buildUseBoolean(false);
export const SaveToGalleryButton = memo(() => {
const { t } = useTranslation();
const shift = useShiftModifier();
const canvasManager = useCanvasManager();
const isSaving = useIsSaving();
const onClick = useCallback(async () => {
isSaving.setTrue();
const rect = shift ? canvasManager.stateApi.getBbox().rect : canvasManager.stage.getVisibleRect('raster_layer');
const result = await withResultAsync(() =>
canvasManager.compositor.rasterizeAndUploadCompositeRasterLayer(rect, true)
);
if (isOk(result)) {
toast({ title: t('controlLayers.savedToGalleryOk') });
} else {
log.error({ error: serializeError(result.error) }, 'Failed to save canvas to gallery');
toast({ title: t('controlLayers.savedToGalleryError'), status: 'error' });
}
isSaving.setFalse();
}, [canvasManager.compositor, canvasManager.stage, canvasManager.stateApi, isSaving, shift, t]);
return (
<IconButton
variant="ghost"
onClick={onClick}
icon={<PiFloppyDiskBold />}
isLoading={isSaving.isTrue}
aria-label={shift ? t('controlLayers.saveBboxToGallery') : t('controlLayers.saveCanvasToGallery')}
tooltip={shift ? t('controlLayers.saveBboxToGallery') : t('controlLayers.saveCanvasToGallery')}
/>
);
});
SaveToGalleryButton.displayName = 'SaveToGalleryButton';

View File

@@ -18,6 +18,7 @@ import { CanvasSettingsInvertScrollCheckbox } from 'features/controlLayers/compo
import { CanvasSettingsLogDebugInfoButton } from 'features/controlLayers/components/Settings/CanvasSettingsLogDebugInfo';
import { CanvasSettingsRecalculateRectsButton } from 'features/controlLayers/components/Settings/CanvasSettingsRecalculateRectsButton';
import { CanvasSettingsResetButton } from 'features/controlLayers/components/Settings/CanvasSettingsResetButton';
import { CanvasSettingsShowHUDSwitch } from 'features/controlLayers/components/Settings/CanvasSettingsShowHUDSwitch';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { RiSettings4Fill } from 'react-icons/ri';
@@ -37,6 +38,7 @@ export const CanvasSettingsPopover = memo(() => {
<CanvasSettingsInvertScrollCheckbox />
<CanvasSettingsClipToBboxCheckbox />
<CanvasSettingsDynamicGridSwitch />
<CanvasSettingsShowHUDSwitch />
<CanvasSettingsResetButton />
<DebugSettings />
</Flex>

View File

@@ -0,0 +1,28 @@
import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSettingsSlice, settingsShowHUDToggled } from 'features/controlLayers/store/canvasSettingsSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const selectShowHUD = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.showHUD);
export const CanvasSettingsShowHUDSwitch = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const showHUD = useAppSelector(selectShowHUD);
const onChange = useCallback(() => {
dispatch(settingsShowHUDToggled());
}, [dispatch]);
return (
<FormControl>
<FormLabel m={0} flexGrow={1}>
{t('controlLayers.showHUD')}
</FormLabel>
<Switch size="sm" isChecked={showHUD} onChange={onChange} />
</FormControl>
);
});
CanvasSettingsShowHUDSwitch.displayName = 'CanvasSettingsShowHUDSwitch';

View File

@@ -8,20 +8,18 @@ import { useAppSelector } from 'app/store/storeHooks';
import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay';
import { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
import Konva from 'konva';
import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react';
import { useDevicePixelRatio } from 'use-device-pixel-ratio';
import { v4 as uuidv4 } from 'uuid';
const log = logger('canvas');
const showHud = false;
// This will log warnings when layers > 5 - maybe use `import.meta.env.MODE === 'development'` instead?
Konva.showWarnings = false;
const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, asPreview: boolean) => {
const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null) => {
const store = useAppStore();
const socket = useStore($socket);
const dpr = useDevicePixelRatio({ round: false });
@@ -42,28 +40,25 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
const manager = new CanvasManager(stage, container, store, socket);
manager.initialize();
return manager.destroy;
}, [asPreview, container, socket, stage, store]);
}, [container, socket, stage, store]);
useLayoutEffect(() => {
Konva.pixelRatio = dpr;
}, [dpr]);
};
type Props = {
asPreview?: boolean;
};
const selectDynamicGrid = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.dynamicGrid);
const selectShowHUD = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.showHUD);
export const StageComponent = memo(({ asPreview = false }: Props) => {
export const StageComponent = memo(() => {
const dynamicGrid = useAppSelector(selectDynamicGrid);
const showHUD = useAppSelector(selectShowHUD);
const [stage] = useState(
() =>
new Konva.Stage({
id: uuidv4(),
id: getPrefixedId('konva_stage'),
container: document.createElement('div'),
listening: !asPreview,
})
);
const [container, setContainer] = useState<HTMLDivElement | null>(null);
@@ -72,7 +67,7 @@ export const StageComponent = memo(({ asPreview = false }: Props) => {
setContainer(el);
}, []);
useStageRenderer(stage, container, asPreview);
useStageRenderer(stage, container);
useEffect(
() => () => {
@@ -106,9 +101,9 @@ export const StageComponent = memo(({ asPreview = false }: Props) => {
overflow="hidden"
data-testid="control-layers-canvas"
/>
{!asPreview && (
<Flex position="absolute" top={0} insetInlineStart={0} pointerEvents="none">
{showHud && <HeadsUpDisplay />}
{showHUD && (
<Flex position="absolute" top={1} insetInlineStart={1} pointerEvents="none">
<HeadsUpDisplay />
</Flex>
)}
</Flex>

View File

@@ -98,9 +98,9 @@ export const StagingAreaToolbar = memo(() => {
onPrev,
{
preventDefault: true,
enabled: isCanvasActive,
enabled: isCanvasActive && shouldShowStagedImage && imageCount > 1,
},
[isCanvasActive]
[isCanvasActive, shouldShowStagedImage, imageCount]
);
useHotkeys(
@@ -108,9 +108,9 @@ export const StagingAreaToolbar = memo(() => {
onNext,
{
preventDefault: true,
enabled: isCanvasActive,
enabled: isCanvasActive && shouldShowStagedImage && imageCount > 1,
},
[isCanvasActive]
[isCanvasActive, shouldShowStagedImage, imageCount]
);
useHotkeys(
@@ -118,9 +118,9 @@ export const StagingAreaToolbar = memo(() => {
onAccept,
{
preventDefault: true,
enabled: isCanvasActive,
enabled: isCanvasActive && shouldShowStagedImage && imageCount > 1,
},
[isCanvasActive]
[isCanvasActive, shouldShowStagedImage, imageCount]
);
const counterText = useMemo(() => {

View File

@@ -1,4 +1,4 @@
import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library';
import { Box, Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger, Tooltip } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIColorPicker from 'common/components/IAIColorPicker';
@@ -23,17 +23,13 @@ export const ToolFillColorPicker = memo(() => {
return (
<Popover isLazy>
<PopoverTrigger>
<Flex
as="button"
aria-label={t('controlLayers.brushColor')}
borderRadius="full"
borderWidth={1}
bg={rgbaColorToString(fill)}
w={8}
h={8}
cursor="pointer"
tabIndex={-1}
/>
<Flex role="button" aria-label={t('controlLayers.fill.fillColor')} tabIndex={-1} w={8} h={8}>
<Tooltip label={t('controlLayers.fill.fillColor')}>
<Flex w="full" h="full" alignItems="center" justifyContent="center">
<Box borderRadius="full" w={6} h={6} borderWidth={1} bg={rgbaColorToString(fill)} />
</Flex>
</Tooltip>
</Flex>
</PopoverTrigger>
<PopoverContent>
<PopoverBody minH={64}>

View File

@@ -0,0 +1,70 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import {
controlLayerAdded,
inpaintMaskAdded,
ipaAdded,
rasterLayerAdded,
rgAdded,
} from 'features/controlLayers/store/canvasSlice';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi';
type Props = {
type: CanvasEntityIdentifier['type'];
};
export const CanvasEntityAddOfTypeButton = memo(({ type }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const onClick = useCallback(() => {
switch (type) {
case 'inpaint_mask':
dispatch(inpaintMaskAdded({ isSelected: true }));
break;
case 'regional_guidance':
dispatch(rgAdded({ isSelected: true }));
break;
case 'raster_layer':
dispatch(rasterLayerAdded({ isSelected: true }));
break;
case 'control_layer':
dispatch(controlLayerAdded({ isSelected: true }));
break;
case 'ip_adapter':
dispatch(ipaAdded({ isSelected: true }));
break;
}
}, [dispatch, type]);
const label = useMemo(() => {
switch (type) {
case 'inpaint_mask':
return t('controlLayers.addInpaintMask');
case 'regional_guidance':
return t('controlLayers.addRegionalGuidance');
case 'raster_layer':
return t('controlLayers.addRasterLayer');
case 'control_layer':
return t('controlLayers.addControlLayer');
case 'ip_adapter':
return t('controlLayers.addIPAdapter');
}
}, [type, t]);
return (
<IconButton
size="sm"
aria-label={label}
tooltip={label}
variant="link"
icon={<PiPlusBold />}
onClick={onClick}
alignSelf="stretch"
/>
);
});
CanvasEntityAddOfTypeButton.displayName = 'CanvasEntityAddOfTypeButton';

View File

@@ -0,0 +1,31 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { entityDeleted } from 'features/controlLayers/store/canvasSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiTrashSimpleFill } from 'react-icons/pi';
export const CanvasEntityDeleteButton = memo(() => {
const { t } = useTranslation();
const entityIdentifier = useEntityIdentifierContext();
const dispatch = useAppDispatch();
const onClick = useCallback(() => {
dispatch(entityDeleted({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
return (
<IconButton
size="sm"
aria-label={t('common.delete')}
tooltip={t('common.delete')}
variant="link"
alignSelf="stretch"
icon={<PiTrashSimpleFill />}
onClick={onClick}
colorScheme="error"
/>
);
});
CanvasEntityDeleteButton.displayName = 'CanvasEntityDeleteButton';

View File

@@ -21,7 +21,8 @@ export const CanvasEntityEnabledToggle = memo(() => {
size="sm"
aria-label={t(isEnabled ? 'common.enabled' : 'common.disabled')}
tooltip={t(isEnabled ? 'common.enabled' : 'common.disabled')}
variant="ghost"
variant="link"
alignSelf="stretch"
icon={isEnabled ? <PiCircleFill /> : <PiCircleBold />}
onClick={onClick}
/>

View File

@@ -1,11 +1,13 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Button, Collapse, Flex, Icon, Spacer, Text } from '@invoke-ai/ui-library';
import { useBoolean } from 'common/hooks/useBoolean';
import { CanvasEntityAddOfTypeButton } from 'features/controlLayers/components/common/CanvasEntityAddOfTypeButton';
import { CanvasEntityMergeVisibleButton } from 'features/controlLayers/components/common/CanvasEntityMergeVisibleButton';
import { CanvasEntityTypeIsHiddenToggle } from 'features/controlLayers/components/common/CanvasEntityTypeIsHiddenToggle';
import { useEntityTypeTitle } from 'features/controlLayers/hooks/useEntityTypeTitle';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
import { memo, useMemo } from 'react';
import { PiCaretDownBold } from 'react-icons/pi';
type Props = PropsWithChildren<{
@@ -20,6 +22,9 @@ const _hover: SystemStyleObject = {
export const CanvasEntityGroupList = memo(({ isSelected, type, children }: Props) => {
const title = useEntityTypeTitle(type);
const collapse = useBoolean(true);
const canMergeVisible = useMemo(() => type === 'raster_layer' || type === 'inpaint_mask', [type]);
const canHideAll = useMemo(() => type !== 'ip_adapter', [type]);
return (
<Flex flexDir="column" w="full">
<Flex w="full">
@@ -53,7 +58,9 @@ export const CanvasEntityGroupList = memo(({ isSelected, type, children }: Props
</Text>
<Spacer />
</Flex>
{type !== 'ip_adapter' && <CanvasEntityTypeIsHiddenToggle type={type} />}
{canMergeVisible && <CanvasEntityMergeVisibleButton type={type} />}
<CanvasEntityAddOfTypeButton type={type} />
{canHideAll && <CanvasEntityTypeIsHiddenToggle type={type} />}
</Flex>
<Collapse in={collapse.isTrue}>
<Flex flexDir="column" gap={2} pt={2}>

View File

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

View File

@@ -0,0 +1,20 @@
import { Flex } from '@invoke-ai/ui-library';
import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton';
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { memo } from 'react';
export const CanvasEntityHeaderCommonActions = memo(() => {
const entityIdentifier = useEntityIdentifierContext();
return (
<Flex alignSelf="stretch">
{entityIdentifier.type !== 'ip_adapter' && <CanvasEntityIsLockedToggle />}
<CanvasEntityEnabledToggle />
<CanvasEntityDeleteButton />
</Flex>
);
});
CanvasEntityHeaderCommonActions.displayName = 'CanvasEntityHeaderCommonActions';

View File

@@ -21,7 +21,8 @@ export const CanvasEntityIsLockedToggle = memo(() => {
size="sm"
aria-label={t(isLocked ? 'controlLayers.locked' : 'controlLayers.unlocked')}
tooltip={t(isLocked ? 'controlLayers.locked' : 'controlLayers.unlocked')}
variant="ghost"
variant="link"
alignSelf="stretch"
icon={isLocked ? <PiLockSimpleFill /> : <PiLockSimpleOpenBold />}
onClick={onClick}
/>

View File

@@ -0,0 +1,88 @@
import { IconButton } from '@invoke-ai/ui-library';
import { logger } from 'app/logging/logger';
import { useAppDispatch } from 'app/store/storeHooks';
import { isOk, withResultAsync } from 'common/util/result';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { inpaintMaskAdded, rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
import { toast } from 'features/toast/toast';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiStackBold } from 'react-icons/pi';
import { serializeError } from 'serialize-error';
const log = logger('canvas');
type Props = {
type: CanvasEntityIdentifier['type'];
};
export const CanvasEntityMergeVisibleButton = memo(({ type }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const canvasManager = useCanvasManager();
const onClick = useCallback(async () => {
if (type === 'raster_layer') {
const rect = canvasManager.stage.getVisibleRect('raster_layer');
const result = await withResultAsync(() =>
canvasManager.compositor.rasterizeAndUploadCompositeRasterLayer(rect, false)
);
if (isOk(result)) {
dispatch(
rasterLayerAdded({
isSelected: true,
overrides: {
objects: [imageDTOToImageObject(result.value)],
position: { x: Math.floor(rect.x), y: Math.floor(rect.y) },
},
deleteOthers: true,
})
);
toast({ title: t('controlLayers.mergeVisibleOk') });
} else {
log.error({ error: serializeError(result.error) }, 'Failed to merge visible');
toast({ title: t('controlLayers.mergeVisibleError'), status: 'error' });
}
} else if (type === 'inpaint_mask') {
const rect = canvasManager.stage.getVisibleRect('inpaint_mask');
const result = await withResultAsync(() =>
canvasManager.compositor.rasterizeAndUploadCompositeInpaintMask(rect, false)
);
if (isOk(result)) {
dispatch(
inpaintMaskAdded({
isSelected: true,
overrides: {
objects: [imageDTOToImageObject(result.value)],
position: { x: Math.floor(rect.x), y: Math.floor(rect.y) },
},
deleteOthers: true,
})
);
toast({ title: t('controlLayers.mergeVisibleOk') });
} else {
log.error({ error: serializeError(result.error) }, 'Failed to merge visible');
toast({ title: t('controlLayers.mergeVisibleError'), status: 'error' });
}
} else {
log.error({ type }, 'Unsupported type for merge visible');
}
}, [canvasManager.compositor, canvasManager.stage, dispatch, t, type]);
return (
<IconButton
size="sm"
aria-label={t('controlLayers.mergeVisible')}
tooltip={t('controlLayers.mergeVisible')}
variant="link"
icon={<PiStackBold />}
onClick={onClick}
alignSelf="stretch"
/>
);
});
CanvasEntityMergeVisibleButton.displayName = 'CanvasEntityMergeVisibleButton';

View File

@@ -1,16 +1,14 @@
/* eslint-disable i18next/no-literal-string */
import { ButtonGroup, IconButton } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { canvasRedo, canvasUndo } from 'features/controlLayers/store/canvasSlice';
import { selectCanvasMayRedo, selectCanvasMayUndo } from 'features/controlLayers/store/selectors';
import { memo, useCallback } from 'react';
import { 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';
export const UndoRedoButtonGroup = memo(() => {
const { t } = useTranslation();
export const useCanvasUndoRedo = () => {
useAssertSingleton('useCanvasUndoRedo');
const dispatch = useDispatch();
const mayUndo = useAppSelector(selectCanvasMayUndo);
@@ -27,27 +25,4 @@ export const UndoRedoButtonGroup = memo(() => {
mayRedo,
handleRedo,
]);
return (
<ButtonGroup isAttached={false}>
<IconButton
aria-label={t('unifiedCanvas.undo')}
tooltip={t('unifiedCanvas.undo')}
onClick={handleUndo}
icon={<PiArrowCounterClockwiseBold />}
isDisabled={!mayUndo}
variant="ghost"
/>
<IconButton
aria-label={t('unifiedCanvas.redo')}
tooltip={t('unifiedCanvas.redo')}
onClick={handleRedo}
icon={<PiArrowClockwiseBold />}
isDisabled={!mayRedo}
variant="ghost"
/>
</ButtonGroup>
);
});
UndoRedoButtonGroup.displayName = 'UndoRedoButtonGroup';
};

View File

@@ -29,15 +29,15 @@ export const useEntityTitle = (entityIdentifier: CanvasEntityIdentifier) => {
const parts: string[] = [];
if (entityIdentifier.type === 'inpaint_mask') {
parts.push(t('controlLayers.inpaintMask', { count: 1 }));
parts.push(t('controlLayers.inpaintMask'));
} else if (entityIdentifier.type === 'control_layer') {
parts.push(t('controlLayers.controlLayer', { count: 1 }));
parts.push(t('controlLayers.controlLayer'));
} else if (entityIdentifier.type === 'raster_layer') {
parts.push(t('controlLayers.rasterLayer', { count: 1 }));
parts.push(t('controlLayers.rasterLayer'));
} else if (entityIdentifier.type === 'ip_adapter') {
parts.push(t('common.ipAdapter', { count: 1 }));
parts.push(t('common.ipAdapter'));
} else if (entityIdentifier.type === 'regional_guidance') {
parts.push(t('controlLayers.regionalGuidance', { count: 1 }));
parts.push(t('controlLayers.regionalGuidance'));
} else {
assert(false, 'Unexpected entity type');
}

View File

@@ -8,15 +8,15 @@ export const useEntityTypeString = (type: CanvasEntityIdentifier['type']): strin
const typeString = useMemo(() => {
switch (type) {
case 'control_layer':
return t('controlLayers.controlLayer', { count: 0 });
return t('controlLayers.controlLayer');
case 'raster_layer':
return t('controlLayers.rasterLayer', { count: 0 });
return t('controlLayers.rasterLayer');
case 'inpaint_mask':
return t('controlLayers.inpaintMask', { count: 0 });
return t('controlLayers.inpaintMask');
case 'regional_guidance':
return t('controlLayers.regionalGuidance', { count: 0 });
return t('controlLayers.regionalGuidance');
case 'ip_adapter':
return t('controlLayers.ipAdapter', { count: 0 });
return t('controlLayers.ipAdapter');
default:
return '';
}

View File

@@ -147,6 +147,19 @@ export class CanvasCompositorModule extends CanvasModuleABC {
return stableHash(data);
};
rasterizeAndUploadCompositeRasterLayer = async (rect: Rect, saveToGallery: boolean) => {
this.log.trace({ rect }, 'Rasterizing composite raster layer');
const canvas = this.getCompositeRasterLayerCanvas(rect);
const blob = await canvasToBlob(canvas);
if (this.manager._isDebugging) {
previewBlob(blob, 'Composite raster layer canvas');
}
return uploadImage(blob, 'composite-raster-layer.png', 'general', !saveToGallery);
};
getCompositeRasterLayerImageDTO = async (rect: Rect): Promise<ImageDTO> => {
let imageDTO: ImageDTO | null = null;
@@ -161,19 +174,23 @@ export class CanvasCompositorModule extends CanvasModuleABC {
}
}
this.log.trace({ rect }, 'Rasterizing composite raster layer');
const canvas = this.getCompositeRasterLayerCanvas(rect);
const blob = await canvasToBlob(canvas);
if (this.manager._isDebugging) {
previewBlob(blob, 'Composite raster layer canvas');
}
imageDTO = await uploadImage(blob, 'composite-raster-layer.png', 'general', true);
imageDTO = await this.rasterizeAndUploadCompositeRasterLayer(rect, false);
this.manager.cache.imageNameCache.set(hash, imageDTO.image_name);
return imageDTO;
};
rasterizeAndUploadCompositeInpaintMask = async (rect: Rect, saveToGallery: boolean) => {
this.log.trace({ rect }, 'Rasterizing composite inpaint mask');
const canvas = this.getCompositeInpaintMaskCanvas(rect);
const blob = await canvasToBlob(canvas);
if (this.manager._isDebugging) {
previewBlob(blob, 'Composite inpaint mask canvas');
}
return uploadImage(blob, 'composite-inpaint-mask.png', 'general', !saveToGallery);
};
getCompositeInpaintMaskImageDTO = async (rect: Rect): Promise<ImageDTO> => {
let imageDTO: ImageDTO | null = null;
@@ -188,15 +205,7 @@ export class CanvasCompositorModule extends CanvasModuleABC {
}
}
this.log.trace({ rect }, 'Rasterizing composite inpaint mask');
const canvas = this.getCompositeInpaintMaskCanvas(rect);
const blob = await canvasToBlob(canvas);
if (this.manager._isDebugging) {
previewBlob(blob, 'Composite inpaint mask canvas');
}
imageDTO = await uploadImage(blob, 'composite-inpaint-mask.png', 'general', true);
imageDTO = await this.rasterizeAndUploadCompositeInpaintMask(rect, false);
this.manager.cache.imageNameCache.set(hash, imageDTO.image_name);
return imageDTO;
};

View File

@@ -2,7 +2,7 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasModuleABC } from 'features/controlLayers/konva/CanvasModuleABC';
import { CANVAS_SCALE_BY } from 'features/controlLayers/konva/constants';
import { getPrefixedId, getRectUnion } from 'features/controlLayers/konva/util';
import type { Coordinate, Dimensions, Rect } from 'features/controlLayers/store/types';
import type { CanvasEntityIdentifier, Coordinate, Dimensions, Rect } from 'features/controlLayers/store/types';
import type Konva from 'konva';
import type { KonvaEventObject } from 'konva/lib/Node';
import { clamp } from 'lodash-es';
@@ -76,35 +76,36 @@ export class CanvasStageModule extends CanvasModuleABC {
});
};
getVisibleRect = (): Rect => {
getVisibleRect = (type?: Exclude<CanvasEntityIdentifier['type'], 'ip_adapter'>): Rect => {
const rects = [];
for (const adapter of this.manager.adapters.getAll()) {
if (adapter.state.isEnabled) {
rects.push(adapter.transformer.getRelativeRect());
if (!adapter.state.isEnabled) {
continue;
}
if (type && adapter.state.type !== type) {
continue;
}
rects.push(adapter.transformer.getRelativeRect());
}
const rectUnion = getRectUnion(...rects);
if (rectUnion.width === 0 || rectUnion.height === 0) {
// fall back to the bbox if there is no content
return this.manager.stateApi.getBbox().rect;
} else {
return rectUnion;
}
return getRectUnion(...rects);
};
fitBboxToStage = () => {
this.log.trace('Fitting bbox to stage');
const bbox = this.manager.stateApi.getBbox();
this.fitRect(bbox.rect);
const { rect } = this.manager.stateApi.getBbox();
this.log.trace({ rect }, 'Fitting bbox to stage');
this.fitRect(rect);
};
fitLayersToStage() {
this.log.trace('Fitting layers to stage');
const rect = this.getVisibleRect();
this.fitRect(rect);
if (rect.width === 0 || rect.height === 0) {
this.fitBboxToStage();
} else {
this.log.trace({ rect }, 'Fitting layers to stage');
this.fitRect(rect);
}
}
fitRect = (rect: Rect) => {

View File

@@ -250,12 +250,10 @@ export class CanvasToolModule extends CanvasModuleABC {
this.konva.colorPicker.group.visible(tool === 'colorPicker');
};
render = () => {
syncCursorStyle = () => {
const stage = this.manager.stage;
const renderedEntityCount = this.manager.stateApi.getRenderedEntityCount();
const toolState = this.manager.stateApi.getToolState();
const selectedEntity = this.manager.stateApi.getSelectedEntity();
const cursorPos = this.manager.stateApi.$lastCursorPos.get();
const isMouseDown = this.manager.stateApi.$isMouseDown.get();
const tool = this.manager.stateApi.$tool.get();
@@ -294,6 +292,158 @@ export class CanvasToolModule extends CanvasModuleABC {
// Non-drawable layers don't have tools
stage.container.style.cursor = 'not-allowed';
}
};
renderBrushTool = (cursorPos: Coordinate) => {
const toolState = this.manager.stateApi.getToolState();
const brushPreviewFill = this.manager.stateApi.getBrushPreviewFill();
const alignedCursorPos = alignCoordForTool(cursorPos, toolState.brush.width);
const onePixel = this.manager.stage.getScaledPixels(1);
const twoPixels = this.manager.stage.getScaledPixels(2);
const radius = toolState.brush.width / 2;
// The circle is scaled
this.konva.brush.fillCircle.setAttrs({
x: alignedCursorPos.x,
y: alignedCursorPos.y,
radius,
fill: rgbaColorToString(brushPreviewFill),
});
// But the borders are in screen-pixels
this.konva.brush.innerBorder.setAttrs({
x: cursorPos.x,
y: cursorPos.y,
innerRadius: radius,
outerRadius: radius + onePixel,
});
this.konva.brush.outerBorder.setAttrs({
x: cursorPos.x,
y: cursorPos.y,
innerRadius: radius + onePixel,
outerRadius: radius + twoPixels,
});
};
renderEraserTool = (cursorPos: Coordinate) => {
const toolState = this.manager.stateApi.getToolState();
const alignedCursorPos = alignCoordForTool(cursorPos, toolState.eraser.width);
const onePixel = this.manager.stage.getScaledPixels(1);
const twoPixels = this.manager.stage.getScaledPixels(2);
const radius = toolState.eraser.width / 2;
// The circle is scaled
this.konva.eraser.fillCircle.setAttrs({
x: alignedCursorPos.x,
y: alignedCursorPos.y,
radius,
fill: 'white',
});
// But the borders are in screen-pixels
this.konva.eraser.innerBorder.setAttrs({
x: cursorPos.x,
y: cursorPos.y,
innerRadius: radius,
outerRadius: radius + onePixel,
});
this.konva.eraser.outerBorder.setAttrs({
x: cursorPos.x,
y: cursorPos.y,
innerRadius: radius + onePixel,
outerRadius: radius + twoPixels,
});
};
renderColorPicker = (cursorPos: Coordinate) => {
const toolState = this.manager.stateApi.getToolState();
const colorUnderCursor = this.manager.stateApi.$colorUnderCursor.get();
const colorPickerInnerRadius = this.manager.stage.getScaledPixels(CanvasToolModule.COLOR_PICKER_RADIUS);
const colorPickerOuterRadius = this.manager.stage.getScaledPixels(
CanvasToolModule.COLOR_PICKER_RADIUS + CanvasToolModule.COLOR_PICKER_THICKNESS
);
const onePixel = this.manager.stage.getScaledPixels(1);
const twoPixels = this.manager.stage.getScaledPixels(2);
this.konva.colorPicker.newColor.setAttrs({
x: cursorPos.x,
y: cursorPos.y,
fill: rgbColorToString(colorUnderCursor),
innerRadius: colorPickerInnerRadius,
outerRadius: colorPickerOuterRadius,
});
this.konva.colorPicker.oldColor.setAttrs({
x: cursorPos.x,
y: cursorPos.y,
fill: rgbColorToString(toolState.fill),
innerRadius: colorPickerInnerRadius,
outerRadius: colorPickerOuterRadius,
});
this.konva.colorPicker.innerBorder.setAttrs({
x: cursorPos.x,
y: cursorPos.y,
innerRadius: colorPickerOuterRadius,
outerRadius: colorPickerOuterRadius + onePixel,
});
this.konva.colorPicker.outerBorder.setAttrs({
x: cursorPos.x,
y: cursorPos.y,
innerRadius: colorPickerOuterRadius + onePixel,
outerRadius: colorPickerOuterRadius + twoPixels,
});
const size = this.manager.stage.getScaledPixels(CanvasToolModule.COLOR_PICKER_CROSSHAIR_SIZE);
const space = this.manager.stage.getScaledPixels(CanvasToolModule.COLOR_PICKER_CROSSHAIR_SPACE);
const innerThickness = this.manager.stage.getScaledPixels(CanvasToolModule.COLOR_PICKER_CROSSHAIR_INNER_THICKNESS);
const outerThickness = this.manager.stage.getScaledPixels(CanvasToolModule.COLOR_PICKER_CROSSHAIR_OUTER_THICKNESS);
this.konva.colorPicker.crosshairNorthOuter.setAttrs({
strokeWidth: outerThickness,
points: [cursorPos.x, cursorPos.y - size, cursorPos.x, cursorPos.y - space],
});
this.konva.colorPicker.crosshairNorthInner.setAttrs({
strokeWidth: innerThickness,
points: [cursorPos.x, cursorPos.y - size, cursorPos.x, cursorPos.y - space],
});
this.konva.colorPicker.crosshairEastOuter.setAttrs({
strokeWidth: outerThickness,
points: [cursorPos.x + space, cursorPos.y, cursorPos.x + size, cursorPos.y],
});
this.konva.colorPicker.crosshairEastInner.setAttrs({
strokeWidth: innerThickness,
points: [cursorPos.x + space, cursorPos.y, cursorPos.x + size, cursorPos.y],
});
this.konva.colorPicker.crosshairSouthOuter.setAttrs({
strokeWidth: outerThickness,
points: [cursorPos.x, cursorPos.y + space, cursorPos.x, cursorPos.y + size],
});
this.konva.colorPicker.crosshairSouthInner.setAttrs({
strokeWidth: innerThickness,
points: [cursorPos.x, cursorPos.y + space, cursorPos.x, cursorPos.y + size],
});
this.konva.colorPicker.crosshairWestOuter.setAttrs({
strokeWidth: outerThickness,
points: [cursorPos.x - space, cursorPos.y, cursorPos.x - size, cursorPos.y],
});
this.konva.colorPicker.crosshairWestInner.setAttrs({
strokeWidth: innerThickness,
points: [cursorPos.x - space, cursorPos.y, cursorPos.x - size, cursorPos.y],
});
};
render = () => {
const stage = this.manager.stage;
const renderedEntityCount = this.manager.stateApi.getRenderedEntityCount();
const selectedEntity = this.manager.stateApi.getSelectedEntity();
const cursorPos = this.manager.stateApi.$lastCursorPos.get();
const tool = this.manager.stateApi.$tool.get();
const isDrawable =
!!selectedEntity &&
selectedEntity.state.isEnabled &&
!selectedEntity.state.isLocked &&
isDrawableEntity(selectedEntity.state);
this.syncCursorStyle();
stage.setIsDraggable(tool === 'view');
@@ -305,136 +455,11 @@ export class CanvasToolModule extends CanvasModuleABC {
// No need to render the brush preview if the cursor position or color is missing
if (cursorPos && tool === 'brush') {
const brushPreviewFill = this.manager.stateApi.getBrushPreviewFill();
const alignedCursorPos = alignCoordForTool(cursorPos, toolState.brush.width);
const onePixel = this.manager.stage.getScaledPixels(1);
const twoPixels = this.manager.stage.getScaledPixels(2);
const radius = toolState.brush.width / 2;
// The circle is scaled
this.konva.brush.fillCircle.setAttrs({
x: alignedCursorPos.x,
y: alignedCursorPos.y,
radius,
fill: rgbaColorToString(brushPreviewFill),
});
// But the borders are in screen-pixels
this.konva.brush.innerBorder.setAttrs({
x: cursorPos.x,
y: cursorPos.y,
innerRadius: radius,
outerRadius: radius + onePixel,
});
this.konva.brush.outerBorder.setAttrs({
x: cursorPos.x,
y: cursorPos.y,
innerRadius: radius + onePixel,
outerRadius: radius + twoPixels,
});
this.renderBrushTool(cursorPos);
} else if (cursorPos && tool === 'eraser') {
const alignedCursorPos = alignCoordForTool(cursorPos, toolState.eraser.width);
const onePixel = this.manager.stage.getScaledPixels(1);
const twoPixels = this.manager.stage.getScaledPixels(2);
const radius = toolState.eraser.width / 2;
// The circle is scaled
this.konva.eraser.fillCircle.setAttrs({
x: alignedCursorPos.x,
y: alignedCursorPos.y,
radius,
fill: 'white',
});
// But the borders are in screen-pixels
this.konva.eraser.innerBorder.setAttrs({
x: cursorPos.x,
y: cursorPos.y,
innerRadius: radius,
outerRadius: radius + onePixel,
});
this.konva.eraser.outerBorder.setAttrs({
x: cursorPos.x,
y: cursorPos.y,
innerRadius: radius + onePixel,
outerRadius: radius + twoPixels,
});
this.renderEraserTool(cursorPos);
} else if (cursorPos && tool === 'colorPicker') {
const colorUnderCursor = this.manager.stateApi.$colorUnderCursor.get();
const colorPickerInnerRadius = this.manager.stage.getScaledPixels(CanvasToolModule.COLOR_PICKER_RADIUS);
const colorPickerOuterRadius = this.manager.stage.getScaledPixels(
CanvasToolModule.COLOR_PICKER_RADIUS + CanvasToolModule.COLOR_PICKER_THICKNESS
);
const onePixel = this.manager.stage.getScaledPixels(1);
const twoPixels = this.manager.stage.getScaledPixels(2);
this.konva.colorPicker.newColor.setAttrs({
x: cursorPos.x,
y: cursorPos.y,
fill: rgbColorToString(colorUnderCursor),
innerRadius: colorPickerInnerRadius,
outerRadius: colorPickerOuterRadius,
});
this.konva.colorPicker.oldColor.setAttrs({
x: cursorPos.x,
y: cursorPos.y,
fill: rgbColorToString(toolState.fill),
innerRadius: colorPickerInnerRadius,
outerRadius: colorPickerOuterRadius,
});
this.konva.colorPicker.innerBorder.setAttrs({
x: cursorPos.x,
y: cursorPos.y,
innerRadius: colorPickerOuterRadius,
outerRadius: colorPickerOuterRadius + onePixel,
});
this.konva.colorPicker.outerBorder.setAttrs({
x: cursorPos.x,
y: cursorPos.y,
innerRadius: colorPickerOuterRadius + onePixel,
outerRadius: colorPickerOuterRadius + twoPixels,
});
const size = this.manager.stage.getScaledPixels(CanvasToolModule.COLOR_PICKER_CROSSHAIR_SIZE);
const space = this.manager.stage.getScaledPixels(CanvasToolModule.COLOR_PICKER_CROSSHAIR_SPACE);
const innerThickness = this.manager.stage.getScaledPixels(
CanvasToolModule.COLOR_PICKER_CROSSHAIR_INNER_THICKNESS
);
const outerThickness = this.manager.stage.getScaledPixels(
CanvasToolModule.COLOR_PICKER_CROSSHAIR_OUTER_THICKNESS
);
this.konva.colorPicker.crosshairNorthOuter.setAttrs({
strokeWidth: outerThickness,
points: [cursorPos.x, cursorPos.y - size, cursorPos.x, cursorPos.y - space],
});
this.konva.colorPicker.crosshairNorthInner.setAttrs({
strokeWidth: innerThickness,
points: [cursorPos.x, cursorPos.y - size, cursorPos.x, cursorPos.y - space],
});
this.konva.colorPicker.crosshairEastOuter.setAttrs({
strokeWidth: outerThickness,
points: [cursorPos.x + space, cursorPos.y, cursorPos.x + size, cursorPos.y],
});
this.konva.colorPicker.crosshairEastInner.setAttrs({
strokeWidth: innerThickness,
points: [cursorPos.x + space, cursorPos.y, cursorPos.x + size, cursorPos.y],
});
this.konva.colorPicker.crosshairSouthOuter.setAttrs({
strokeWidth: outerThickness,
points: [cursorPos.x, cursorPos.y + space, cursorPos.x, cursorPos.y + size],
});
this.konva.colorPicker.crosshairSouthInner.setAttrs({
strokeWidth: innerThickness,
points: [cursorPos.x, cursorPos.y + space, cursorPos.x, cursorPos.y + size],
});
this.konva.colorPicker.crosshairWestOuter.setAttrs({
strokeWidth: outerThickness,
points: [cursorPos.x - space, cursorPos.y, cursorPos.x - size, cursorPos.y],
});
this.konva.colorPicker.crosshairWestInner.setAttrs({
strokeWidth: innerThickness,
points: [cursorPos.x - space, cursorPos.y, cursorPos.x - size, cursorPos.y],
});
this.renderColorPicker(cursorPos);
}
this.setToolVisibility(tool, isDrawable);
@@ -864,6 +889,10 @@ export class CanvasToolModule extends CanvasModuleABC {
this.manager.stateApi.$spaceKey.set(true);
this.manager.stateApi.$lastCursorPos.set(null);
this.manager.stateApi.$lastMouseDownPos.set(null);
} else if (e.key === 'Alt') {
// Select the color picker on alt key down
this.manager.stateApi.$toolBuffer.set(this.manager.stateApi.$tool.get());
this.manager.stateApi.$tool.set('colorPicker');
}
};
@@ -880,6 +909,11 @@ export class CanvasToolModule extends CanvasModuleABC {
this.manager.stateApi.$tool.set(toolBuffer ?? 'move');
this.manager.stateApi.$toolBuffer.set(null);
this.manager.stateApi.$spaceKey.set(false);
} else if (e.key === 'Alt') {
// Revert the tool to the previous tool on alt key up
const toolBuffer = this.manager.stateApi.$toolBuffer.get();
this.manager.stateApi.$tool.set(toolBuffer ?? 'move');
this.manager.stateApi.$toolBuffer.set(null);
}
};

View File

@@ -0,0 +1,47 @@
import { getPrefixedId, getRectUnion } from 'features/controlLayers/konva/util';
import { describe, expect, it } from 'vitest';
describe('util', () => {
describe('getPrefixedId', () => {
it('should return a prefixed id', () => {
expect(getPrefixedId('foo').split(':')[0]).toBe('foo');
});
});
describe('getRectUnion', () => {
it('should return the union of rects (2 rects)', () => {
const rect1 = { x: 0, y: 0, width: 10, height: 10 };
const rect2 = { x: 5, y: 5, width: 10, height: 10 };
const union = getRectUnion(rect1, rect2);
expect(union).toEqual({ x: 0, y: 0, width: 15, height: 15 });
});
it('should return the union of rects (3 rects)', () => {
const rect1 = { x: 0, y: 0, width: 10, height: 10 };
const rect2 = { x: 5, y: 5, width: 10, height: 10 };
const rect3 = { x: 10, y: 10, width: 10, height: 10 };
const union = getRectUnion(rect1, rect2, rect3);
expect(union).toEqual({ x: 0, y: 0, width: 20, height: 20 });
});
it('should return the union of rects (2 rects none from zero)', () => {
const rect1 = { x: 5, y: 5, width: 10, height: 10 };
const rect2 = { x: 10, y: 10, width: 10, height: 10 };
const union = getRectUnion(rect1, rect2);
expect(union).toEqual({ x: 5, y: 5, width: 15, height: 15 });
});
it('should return the union of rects (2 rects with negative x/y)', () => {
const rect1 = { x: -5, y: -5, width: 10, height: 10 };
const rect2 = { x: 0, y: 0, width: 10, height: 10 };
const union = getRectUnion(rect1, rect2);
expect(union).toEqual({ x: -5, y: -5, width: 15, height: 15 });
});
it('should return the union of the first rect if only one rect is provided', () => {
const rect = { x: 0, y: 0, width: 10, height: 10 };
const union = getRectUnion(rect);
expect(union).toEqual(rect);
});
it('should fall back on an empty rect if no rects are provided', () => {
const union = getRectUnion();
expect(union).toEqual({ x: 0, y: 0, width: 0, height: 0 });
});
});
});

View File

@@ -302,10 +302,13 @@ export const konvaNodeToCanvas = (node: Konva.Node, bbox?: Rect): HTMLCanvasElem
* @returns A Promise that resolves with Blob of the node cropped to the bounding box
*/
export const canvasToBlob = (canvas: HTMLCanvasElement): Promise<Blob> => {
return new Promise((resolve) => {
return new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
assert(blob, 'blob is null');
resolve(blob);
if (!blob) {
reject('Failed to convert canvas to blob');
} else {
resolve(blob);
}
});
});
};
@@ -418,19 +421,25 @@ export function snapToNearest(value: number, candidateValues: number[], threshol
}
/**
* Gets the union of two rects
* @param rect1 The first rect
* @param rect2 The second rect
* Gets the union of any number of rects.
* @params rects The rects to union
* @returns The union of the two rects
*/
export const getRectUnion = (...rects: Rect[]): Rect => {
const firstRect = rects.shift();
if (!firstRect) {
return getEmptyRect();
}
const rect = rects.reduce<Rect>((acc, r) => {
const x = Math.min(acc.x, r.x);
const y = Math.min(acc.y, r.y);
const width = Math.max(acc.x + acc.width, r.x + r.width) - x;
const height = Math.max(acc.y + acc.height, r.y + r.height) - y;
return { x, y, width, height };
}, getEmptyRect());
}, firstRect);
return rect;
};

View File

@@ -1,17 +1,17 @@
import { createAction, createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { canvasSlice } from 'features/controlLayers/store/canvasSlice';
import type { SessionMode, StagingAreaImage } from 'features/controlLayers/store/types';
import type { StagingAreaImage } from 'features/controlLayers/store/types';
export type CanvasSessionState = {
mode: SessionMode;
sendToCanvas: boolean;
isStaging: boolean;
stagedImages: StagingAreaImage[];
selectedStagedImageIndex: number;
};
const initialState: CanvasSessionState = {
mode: 'generate',
sendToCanvas: false,
isStaging: false,
stagedImages: [],
selectedStagedImageIndex: 0,
@@ -27,6 +27,7 @@ export const canvasSessionSlice = createSlice({
},
sessionImageStaged: (state, action: PayloadAction<{ stagingAreaImage: StagingAreaImage }>) => {
const { stagingAreaImage } = action.payload;
state.isStaging = true;
state.stagedImages.push(stagingAreaImage);
state.selectedStagedImageIndex = state.stagedImages.length - 1;
},
@@ -50,9 +51,8 @@ export const canvasSessionSlice = createSlice({
state.stagedImages = [];
state.selectedStagedImageIndex = 0;
},
sessionModeChanged: (state, action: PayloadAction<{ mode: SessionMode }>) => {
const { mode } = action.payload;
state.mode = mode;
sessionSendToCanvasChanged: (state, action: PayloadAction<boolean>) => {
state.sendToCanvas = action.payload;
},
},
});
@@ -64,7 +64,7 @@ export const {
sessionStagingAreaReset,
sessionNextStagedImageSelected,
sessionPrevStagedImageSelected,
sessionModeChanged,
sessionSendToCanvasChanged,
} = canvasSessionSlice.actions;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
@@ -85,3 +85,7 @@ export const sessionStagingAreaImageAccepted = createAction<{ index: number }>(
export const selectCanvasSessionSlice = (s: RootState) => s.canvasSession;
export const selectIsStaging = createSelector(selectCanvasSessionSlice, (canvasSession) => canvasSession.isStaging);
export const selectIsComposing = createSelector(
selectCanvasSessionSlice,
(canvasSession) => canvasSession.sendToCanvas
);

View File

@@ -35,10 +35,14 @@ export const canvasSettingsSlice = createSlice({
settingsAutoSaveToggled: (state) => {
state.autoSave = !state.autoSave;
},
settingsShowHUDToggled: (state) => {
state.showHUD = !state.showHUD;
},
},
});
export const { clipToBboxChanged, settingsAutoSaveToggled, settingsDynamicGridToggled } = canvasSettingsSlice.actions;
export const { clipToBboxChanged, settingsAutoSaveToggled, settingsDynamicGridToggled, settingsShowHUDToggled } =
canvasSettingsSlice.actions;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrate = (state: any): any => {

View File

@@ -123,9 +123,14 @@ export const canvasSlice = createSlice({
rasterLayerAdded: {
reducer: (
state,
action: PayloadAction<{ id: string; overrides?: Partial<CanvasRasterLayerState>; isSelected?: boolean }>
action: PayloadAction<{
id: string;
overrides?: Partial<CanvasRasterLayerState>;
isSelected?: boolean;
deleteOthers?: boolean;
}>
) => {
const { id, overrides, isSelected } = action.payload;
const { id, overrides, isSelected, deleteOthers } = action.payload;
const entity: CanvasRasterLayerState = {
id,
name: null,
@@ -137,12 +142,25 @@ export const canvasSlice = createSlice({
position: { x: 0, y: 0 },
};
merge(entity, overrides);
state.rasterLayers.entities.push(entity);
if (deleteOthers) {
state.rasterLayers.entities = [entity];
} else {
state.rasterLayers.entities.push(entity);
}
if (isSelected) {
state.selectedEntityIdentifier = getEntityIdentifier(entity);
}
},
prepare: (payload: { overrides?: Partial<CanvasRasterLayerState>; isSelected?: boolean }) => ({
prepare: (payload: {
overrides?: Partial<CanvasRasterLayerState>;
isSelected?: boolean;
/**
* asdf
*/
deleteOthers?: boolean;
}) => ({
payload: { ...payload, id: getPrefixedId('raster_layer') },
}),
},
@@ -603,9 +621,14 @@ export const canvasSlice = createSlice({
inpaintMaskAdded: {
reducer: (
state,
action: PayloadAction<{ id: string; overrides?: Partial<CanvasInpaintMaskState>; isSelected?: boolean }>
action: PayloadAction<{
id: string;
overrides?: Partial<CanvasInpaintMaskState>;
isSelected?: boolean;
deleteOthers?: boolean;
}>
) => {
const { id, overrides, isSelected } = action.payload;
const { id, overrides, isSelected, deleteOthers } = action.payload;
const entity: CanvasInpaintMaskState = {
id,
name: null,
@@ -621,12 +644,22 @@ export const canvasSlice = createSlice({
},
};
merge(entity, overrides);
state.inpaintMasks.entities.push(entity);
if (deleteOthers) {
state.inpaintMasks.entities = [entity];
} else {
state.inpaintMasks.entities.push(entity);
}
if (isSelected) {
state.selectedEntityIdentifier = getEntityIdentifier(entity);
}
},
prepare: (payload?: { overrides?: Partial<CanvasInpaintMaskState>; isSelected?: boolean }) => ({
prepare: (payload?: {
overrides?: Partial<CanvasInpaintMaskState>;
isSelected?: boolean;
deleteOthers?: boolean;
}) => ({
payload: { ...payload, id: getPrefixedId('inpaint_mask') },
}),
},

View File

@@ -685,8 +685,6 @@ export type StagingAreaImage = {
offsetY: number;
};
export type SessionMode = 'generate' | 'compose';
export type CanvasState = {
_version: 3;
selectedEntityIdentifier: CanvasEntityIdentifier | null;

View File

@@ -1,55 +1,61 @@
import { ButtonGroup, Flex, IconButton, Text, Tooltip } from '@invoke-ai/ui-library';
import { Flex, Text } from '@invoke-ai/ui-library';
import { IconSwitch } from 'common/components/IconSwitch';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { PiEyeBold, PiPencilBold } from 'react-icons/pi';
export const ViewerToggleMenu = () => {
const TooltipEdit = memo(() => {
const { t } = useTranslation();
return (
<Flex flexDir="column">
<Text fontWeight="semibold">{t('common.edit')}</Text>
<Text fontWeight="normal">{t('common.editDesc')}</Text>
</Flex>
);
});
TooltipEdit.displayName = 'TooltipEdit';
const TooltipView = memo(() => {
const { t } = useTranslation();
return (
<Flex flexDir="column">
<Text fontWeight="semibold">{t('common.view')}</Text>
<Text fontWeight="normal">{t('common.viewDesc')}</Text>
</Flex>
);
});
TooltipView.displayName = 'TooltipView';
export const ViewerToggle = memo(() => {
const imageViewer = useImageViewer();
useHotkeys('z', imageViewer.onToggle, [imageViewer]);
useHotkeys('esc', imageViewer.onClose, [imageViewer]);
const onChange = useCallback(
(isChecked: boolean) => {
if (isChecked) {
imageViewer.onClose();
} else {
imageViewer.onOpen();
}
},
[imageViewer]
);
return (
<Flex gap={4} alignItems="center" justifyContent="center">
<ButtonGroup size="md">
<Tooltip
hasArrow
label={
<Flex flexDir="column">
<Text fontWeight="semibold">{t('common.viewing')}</Text>
<Text fontWeight="normal">{t('common.viewingDesc')}</Text>
</Flex>
}
>
<IconButton
icon={<PiEyeBold />}
onClick={imageViewer.onOpen}
variant={imageViewer.isOpen ? 'solid' : 'outline'}
colorScheme={imageViewer.isOpen ? 'invokeBlue' : 'base'}
aria-label={t('common.viewing')}
w={12}
/>
</Tooltip>
<Tooltip
hasArrow
label={
<Flex flexDir="column">
<Text fontWeight="semibold">{t('common.editing')}</Text>
<Text fontWeight="normal">{t('common.editingDesc')}</Text>
</Flex>
}
>
<IconButton
icon={<PiPencilBold />}
onClick={imageViewer.onClose}
variant={!imageViewer.isOpen ? 'solid' : 'outline'}
colorScheme={!imageViewer.isOpen ? 'invokeBlue' : 'base'}
aria-label={t('common.editing')}
w={12}
/>
</Tooltip>
</ButtonGroup>
</Flex>
<IconSwitch
isChecked={!imageViewer.isOpen}
onChange={onChange}
iconUnchecked={<PiEyeBold />}
tooltipUnchecked={<TooltipView />}
iconChecked={<PiPencilBold />}
tooltipChecked={<TooltipEdit />}
ariaLabel="Toggle viewer"
/>
);
};
});
ViewerToggle.displayName = 'ViewerToggle';

View File

@@ -7,7 +7,7 @@ import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo } from 'react';
import CurrentImageButtons from './CurrentImageButtons';
import { ViewerToggleMenu } from './ViewerToggleMenu';
import { ViewerToggle } from './ViewerToggleMenu';
const selectShowToggle = createSelector(selectActiveTab, (tab) => {
if (tab === 'upscaling' || tab === 'workflows') {
@@ -31,7 +31,7 @@ export const ViewerToolbar = memo(() => {
</Flex>
<Flex flex={1} justifyContent="center">
<Flex gap={2} marginInlineStart="auto">
{showToggle && <ViewerToggleMenu />}
{showToggle && <ViewerToggle />}
</Flex>
</Flex>
</Flex>

View File

@@ -179,12 +179,12 @@ const ModelList = () => {
{/* T5 Encoders List */}
{isLoadingT5EncoderModels && <FetchingModelsLoader loadingMessage="Loading T5 Encoder Models..." />}
{!isLoadingT5EncoderModels && filteredT5EncoderModels.length > 0 && (
<ModelListWrapper title="T5 Encoder" modelList={filteredT5EncoderModels} key="t5-encoder" />
<ModelListWrapper title={t('modelManager.t5Encoder')} modelList={filteredT5EncoderModels} key="t5-encoder" />
)}
{/* Clip Embed List */}
{isLoadingClipEmbedModels && <FetchingModelsLoader loadingMessage="Loading Clip Embed Models..." />}
{!isLoadingClipEmbedModels && filteredClipEmbedModels.length > 0 && (
<ModelListWrapper title="Clip Embed" modelList={filteredClipEmbedModels} key="clip-embed" />
<ModelListWrapper title={t('modelManager.clipEmbed')} modelList={filteredClipEmbedModels} key="clip-embed" />
)}
{/* Spandrel Image to Image List */}
{isLoadingSpandrelImageToImageModels && (
@@ -192,7 +192,7 @@ const ModelList = () => {
)}
{!isLoadingSpandrelImageToImageModels && filteredSpandrelImageToImageModels.length > 0 && (
<ModelListWrapper
title="Image-to-Image"
title={t('modelManager.spandrelImageToImage')}
modelList={filteredSpandrelImageToImageModels}
key="spandrel-image-to-image"
/>

View File

@@ -19,11 +19,10 @@ export const ModelTypeFilter = memo(() => {
controlnet: 'ControlNet',
vae: 'VAE',
t2i_adapter: t('common.t2iAdapter'),
t5_encoder: 'T5Encoder',
clip_embed: 'Clip Embed',
t5_encoder: t('modelManager.t5Encoder'),
clip_embed: t('modelManager.clipEmbed'),
ip_adapter: t('common.ipAdapter'),
clip_vision: 'Clip Vision',
spandrel_image_to_image: 'Image-to-Image',
spandrel_image_to_image: t('modelManager.spandrelImageToImage'),
}),
[t]
);

View File

@@ -4,6 +4,7 @@ import { Flex } from '@invoke-ai/ui-library';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { AddNodeCmdk } from 'features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk';
import TopPanel from 'features/nodes/components/flow/panels/TopPanel/TopPanel';
import WorkflowEditorSettings from 'features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings';
import { LoadWorkflowFromGraphModal } from 'features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal';
import { SaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog/SaveWorkflowAsDialog';
import { memo } from 'react';
@@ -39,6 +40,7 @@ const NodeEditor = () => {
<LoadWorkflowFromGraphModal />
</>
)}
<WorkflowEditorSettings />
{isLoading && <IAINoContentFallback label={t('nodes.loadingNodes')} icon={MdDeviceHub} />}
</Flex>
);

View File

@@ -12,7 +12,7 @@ import {
Text,
} from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppStore } from 'app/store/storeHooks';
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
import { CommandEmpty, CommandItem, CommandList, CommandRoot } from 'cmdk';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
@@ -33,6 +33,7 @@ import { connectionToEdge } from 'features/nodes/store/util/reactFlowUtil';
import { validateConnectionTypes } from 'features/nodes/store/util/validateConnectionTypes';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { toast } from 'features/toast/toast';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memoize } from 'lodash-es';
import { computed } from 'nanostores';
import type { ChangeEvent } from 'react';
@@ -162,13 +163,13 @@ const cmdkRootSx: SystemStyleObject = {
export const AddNodeCmdk = memo(() => {
const { t } = useTranslation();
const addNodeCmdk = useAddNodeCmdk();
const addNodeCmdkIsOpen = useStore(addNodeCmdk.$boolean);
const inputRef = useRef<HTMLInputElement>(null);
const [searchTerm, setSearchTerm] = useState('');
const addNode = useAddNode();
const tab = useAppSelector(selectActiveTab);
const throttledSearchTerm = useThrottle(searchTerm, 100);
useHotkeys(['shift+a', 'space'], addNodeCmdk.setTrue, { preventDefault: true });
useHotkeys(['shift+a', 'space'], addNodeCmdk.setTrue, { enabled: tab === 'workflows', preventDefault: true }, [tab]);
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setSearchTerm(e.target.value);
@@ -190,7 +191,7 @@ export const AddNodeCmdk = memo(() => {
return (
<Modal
isOpen={addNodeCmdkIsOpen}
isOpen={addNodeCmdk.isTrue}
onClose={onClose}
useInert={false}
initialFocusRef={inputRef}

View File

@@ -6,6 +6,8 @@ import {
isBoardFieldInputTemplate,
isBooleanFieldInputInstance,
isBooleanFieldInputTemplate,
isCLIPEmbedModelFieldInputInstance,
isCLIPEmbedModelFieldInputTemplate,
isColorFieldInputInstance,
isColorFieldInputTemplate,
isControlNetModelFieldInputInstance,
@@ -16,6 +18,8 @@ import {
isFloatFieldInputTemplate,
isFluxMainModelFieldInputInstance,
isFluxMainModelFieldInputTemplate,
isFluxVAEModelFieldInputInstance,
isFluxVAEModelFieldInputTemplate,
isImageFieldInputInstance,
isImageFieldInputTemplate,
isIntegerFieldInputInstance,
@@ -49,10 +53,12 @@ import { memo } from 'react';
import BoardFieldInputComponent from './inputs/BoardFieldInputComponent';
import BooleanFieldInputComponent from './inputs/BooleanFieldInputComponent';
import CLIPEmbedModelFieldInputComponent from './inputs/CLIPEmbedModelFieldInputComponent';
import ColorFieldInputComponent from './inputs/ColorFieldInputComponent';
import ControlNetModelFieldInputComponent from './inputs/ControlNetModelFieldInputComponent';
import EnumFieldInputComponent from './inputs/EnumFieldInputComponent';
import FluxMainModelFieldInputComponent from './inputs/FluxMainModelFieldInputComponent';
import FluxVAEModelFieldInputComponent from './inputs/FluxVAEModelFieldInputComponent';
import ImageFieldInputComponent from './inputs/ImageFieldInputComponent';
import IPAdapterModelFieldInputComponent from './inputs/IPAdapterModelFieldInputComponent';
import LoRAModelFieldInputComponent from './inputs/LoRAModelFieldInputComponent';
@@ -122,6 +128,13 @@ const InputFieldRenderer = ({ nodeId, fieldName }: InputFieldProps) => {
if (isT5EncoderModelFieldInputInstance(fieldInstance) && isT5EncoderModelFieldInputTemplate(fieldTemplate)) {
return <T5EncoderModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
}
if (isCLIPEmbedModelFieldInputInstance(fieldInstance) && isCLIPEmbedModelFieldInputTemplate(fieldTemplate)) {
return <CLIPEmbedModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
}
if (isFluxVAEModelFieldInputInstance(fieldInstance) && isFluxVAEModelFieldInputTemplate(fieldTemplate)) {
return <FluxVAEModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
}
if (isLoRAModelFieldInputInstance(fieldInstance) && isLoRAModelFieldInputTemplate(fieldTemplate)) {
return <LoRAModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;

View File

@@ -0,0 +1,60 @@
import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
import { fieldCLIPEmbedValueChanged } from 'features/nodes/store/nodesSlice';
import type { CLIPEmbedModelFieldInputInstance, CLIPEmbedModelFieldInputTemplate } from 'features/nodes/types/field';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useClipEmbedModels } from 'services/api/hooks/modelsByType';
import type { ClipEmbedModelConfig } from 'services/api/types';
import type { FieldComponentProps } from './types';
type Props = FieldComponentProps<CLIPEmbedModelFieldInputInstance, CLIPEmbedModelFieldInputTemplate>;
const CLIPEmbedModelFieldInputComponent = (props: Props) => {
const { nodeId, field } = props;
const { t } = useTranslation();
const disabledTabs = useAppSelector((s) => s.config.disabledTabs);
const dispatch = useAppDispatch();
const [modelConfigs, { isLoading }] = useClipEmbedModels();
const _onChange = useCallback(
(value: ClipEmbedModelConfig | null) => {
if (!value) {
return;
}
dispatch(
fieldCLIPEmbedValueChanged({
nodeId,
fieldName: field.name,
value,
})
);
},
[dispatch, field.name, nodeId]
);
const { options, value, onChange, placeholder, noOptionsMessage } = useGroupedModelCombobox({
modelConfigs,
onChange: _onChange,
isLoading,
selectedModel: field.value,
});
return (
<Flex w="full" alignItems="center" gap={2}>
<Tooltip label={!disabledTabs.includes('models') && t('modelManager.starterModelsInModelManager')}>
<FormControl className="nowheel nodrag" isDisabled={!options.length} isInvalid={!value}>
<Combobox
value={value}
placeholder={placeholder}
options={options}
onChange={onChange}
noOptionsMessage={noOptionsMessage}
/>
</FormControl>
</Tooltip>
</Flex>
);
};
export default memo(CLIPEmbedModelFieldInputComponent);

View File

@@ -0,0 +1,60 @@
import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
import { fieldFluxVAEModelValueChanged } from 'features/nodes/store/nodesSlice';
import type { FluxVAEModelFieldInputInstance, FluxVAEModelFieldInputTemplate } from 'features/nodes/types/field';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useFluxVAEModels } from 'services/api/hooks/modelsByType';
import type { VAEModelConfig } from 'services/api/types';
import type { FieldComponentProps } from './types';
type Props = FieldComponentProps<FluxVAEModelFieldInputInstance, FluxVAEModelFieldInputTemplate>;
const FluxVAEModelFieldInputComponent = (props: Props) => {
const { nodeId, field } = props;
const { t } = useTranslation();
const disabledTabs = useAppSelector((s) => s.config.disabledTabs);
const dispatch = useAppDispatch();
const [modelConfigs, { isLoading }] = useFluxVAEModels();
const _onChange = useCallback(
(value: VAEModelConfig | null) => {
if (!value) {
return;
}
dispatch(
fieldFluxVAEModelValueChanged({
nodeId,
fieldName: field.name,
value,
})
);
},
[dispatch, field.name, nodeId]
);
const { options, value, onChange, placeholder, noOptionsMessage } = useGroupedModelCombobox({
modelConfigs,
onChange: _onChange,
isLoading,
selectedModel: field.value,
});
return (
<Flex w="full" alignItems="center" gap={2}>
<Tooltip label={!disabledTabs.includes('models') && t('modelManager.starterModelsInModelManager')}>
<FormControl className="nowheel nodrag" isDisabled={!options.length} isInvalid={!value}>
<Combobox
value={value}
placeholder={placeholder}
options={options}
onChange={onChange}
noOptionsMessage={noOptionsMessage}
/>
</FormControl>
</Tooltip>
</Flex>
);
};
export default memo(FluxVAEModelFieldInputComponent);

View File

@@ -14,9 +14,9 @@ import {
ModalHeader,
ModalOverlay,
Switch,
useDisclosure,
} from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { buildUseBoolean } from 'common/hooks/useBoolean';
import ReloadNodeTemplatesButton from 'features/nodes/components/flow/panels/TopRightPanel/ReloadSchemaButton';
import {
selectionModeChanged,
@@ -32,20 +32,17 @@ import {
shouldSnapToGridChanged,
shouldValidateGraphChanged,
} from 'features/nodes/store/workflowSettingsSlice';
import type { ChangeEvent, ReactNode } from 'react';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { SelectionMode } from 'reactflow';
const formLabelProps: FormLabelProps = { flexGrow: 1 };
export const [useWorkflowEditorSettingsModal] = buildUseBoolean(false);
type Props = {
children: (props: { onOpen: () => void }) => ReactNode;
};
const WorkflowEditorSettings = ({ children }: Props) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const WorkflowEditorSettings = () => {
const dispatch = useAppDispatch();
const modal = useWorkflowEditorSettingsModal();
const shouldSnapToGrid = useAppSelector(selectShouldSnapToGrid);
const selectionMode = useAppSelector(selectSelectionMode);
@@ -99,76 +96,72 @@ const WorkflowEditorSettings = ({ children }: Props) => {
const { t } = useTranslation();
return (
<>
{children({ onOpen })}
<Modal isOpen={isOpen} onClose={onClose} size="2xl" isCentered useInert={false}>
<ModalOverlay />
<ModalContent>
<ModalHeader>{t('nodes.workflowSettings')}</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Flex flexDirection="column" gap={4} py={4}>
<Heading size="sm">{t('parameters.general')}</Heading>
<FormControlGroup orientation="vertical" formLabelProps={formLabelProps}>
<FormControl>
<Flex w="full">
<FormLabel>{t('nodes.animatedEdges')}</FormLabel>
<Switch onChange={handleChangeShouldAnimate} isChecked={shouldAnimateEdges} />
</Flex>
<FormHelperText>{t('nodes.animatedEdgesHelp')}</FormHelperText>
</FormControl>
<Divider />
<FormControl>
<Flex w="full">
<FormLabel>{t('nodes.snapToGrid')}</FormLabel>
<Switch isChecked={shouldSnapToGrid} onChange={handleChangeShouldSnap} />
</Flex>
<FormHelperText>{t('nodes.snapToGridHelp')}</FormHelperText>
</FormControl>
<Divider />
<FormControl>
<Flex w="full">
<FormLabel>{t('nodes.colorCodeEdges')}</FormLabel>
<Switch isChecked={shouldColorEdges} onChange={handleChangeShouldColor} />
</Flex>
<FormHelperText>{t('nodes.colorCodeEdgesHelp')}</FormHelperText>
</FormControl>
<Divider />
<FormControl>
<Flex w="full">
<FormLabel>{t('nodes.fullyContainNodes')}</FormLabel>
<Switch isChecked={selectionMode === SelectionMode.Full} onChange={handleChangeSelectionMode} />
</Flex>
<FormHelperText>{t('nodes.fullyContainNodesHelp')}</FormHelperText>
</FormControl>
<Divider />
<FormControl>
<Flex w="full">
<FormLabel>{t('nodes.showEdgeLabels')}</FormLabel>
<Switch isChecked={shouldShowEdgeLabels} onChange={handleChangeShouldShowEdgeLabels} />
</Flex>
<FormHelperText>{t('nodes.showEdgeLabelsHelp')}</FormHelperText>
</FormControl>
<Divider />
<Heading size="sm" pt={4}>
{t('common.advanced')}
</Heading>
<FormControl>
<Flex w="full">
<FormLabel>{t('nodes.validateConnections')}</FormLabel>
<Switch isChecked={shouldValidateGraph} onChange={handleChangeShouldValidate} />
</Flex>
<FormHelperText>{t('nodes.validateConnectionsHelp')}</FormHelperText>
</FormControl>
<Divider />
</FormControlGroup>
<ReloadNodeTemplatesButton />
</Flex>
</ModalBody>
</ModalContent>
</Modal>
</>
<Modal isOpen={modal.isTrue} onClose={modal.setFalse} size="2xl" isCentered useInert={false}>
<ModalOverlay />
<ModalContent>
<ModalHeader>{t('nodes.workflowSettings')}</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Flex flexDirection="column" gap={4} py={4}>
<Heading size="sm">{t('parameters.general')}</Heading>
<FormControlGroup orientation="vertical" formLabelProps={formLabelProps}>
<FormControl>
<Flex w="full">
<FormLabel>{t('nodes.animatedEdges')}</FormLabel>
<Switch onChange={handleChangeShouldAnimate} isChecked={shouldAnimateEdges} />
</Flex>
<FormHelperText>{t('nodes.animatedEdgesHelp')}</FormHelperText>
</FormControl>
<Divider />
<FormControl>
<Flex w="full">
<FormLabel>{t('nodes.snapToGrid')}</FormLabel>
<Switch isChecked={shouldSnapToGrid} onChange={handleChangeShouldSnap} />
</Flex>
<FormHelperText>{t('nodes.snapToGridHelp')}</FormHelperText>
</FormControl>
<Divider />
<FormControl>
<Flex w="full">
<FormLabel>{t('nodes.colorCodeEdges')}</FormLabel>
<Switch isChecked={shouldColorEdges} onChange={handleChangeShouldColor} />
</Flex>
<FormHelperText>{t('nodes.colorCodeEdgesHelp')}</FormHelperText>
</FormControl>
<Divider />
<FormControl>
<Flex w="full">
<FormLabel>{t('nodes.fullyContainNodes')}</FormLabel>
<Switch isChecked={selectionMode === SelectionMode.Full} onChange={handleChangeSelectionMode} />
</Flex>
<FormHelperText>{t('nodes.fullyContainNodesHelp')}</FormHelperText>
</FormControl>
<Divider />
<FormControl>
<Flex w="full">
<FormLabel>{t('nodes.showEdgeLabels')}</FormLabel>
<Switch isChecked={shouldShowEdgeLabels} onChange={handleChangeShouldShowEdgeLabels} />
</Flex>
<FormHelperText>{t('nodes.showEdgeLabelsHelp')}</FormHelperText>
</FormControl>
<Divider />
<Heading size="sm" pt={4}>
{t('common.advanced')}
</Heading>
<FormControl>
<Flex w="full">
<FormLabel>{t('nodes.validateConnections')}</FormLabel>
<Switch isChecked={shouldValidateGraph} onChange={handleChangeShouldValidate} />
</Flex>
<FormHelperText>{t('nodes.validateConnectionsHelp')}</FormHelperText>
</FormControl>
<Divider />
</FormControlGroup>
<ReloadNodeTemplatesButton />
</Flex>
</ModalBody>
</ModalContent>
</Modal>
);
};

View File

@@ -3,7 +3,6 @@ import 'reactflow/dist/style.css';
import { Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { selectWorkflowMode } from 'features/nodes/store/workflowSlice';
import QueueControls from 'features/queue/components/QueueControls';
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton';
@@ -34,7 +33,6 @@ const NodeEditorPanelGroup = () => {
return (
<Flex w="full" h="full" gap={2} flexDir="column">
<QueueControls />
<Flex w="full" justifyContent="space-between" alignItems="center" gap="4" padding={1}>
<Flex justifyContent="space-between" alignItems="center" gap="4">
<WorkflowLibraryButton />

View File

@@ -2,12 +2,12 @@ import { useStore } from '@nanostores/react';
import { useAppStore } from 'app/store/storeHooks';
import { $mouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
import {
$addNodeCmdk,
$didUpdateEdge,
$edgePendingUpdate,
$pendingConnection,
$templates,
edgesChanged,
useAddNodeCmdk,
} from 'features/nodes/store/nodesSlice';
import { selectNodes, selectNodesSlice } from 'features/nodes/store/selectors';
import { getFirstValidConnection } from 'features/nodes/store/util/getFirstValidConnection';
@@ -21,6 +21,7 @@ export const useConnection = () => {
const store = useAppStore();
const templates = useStore($templates);
const updateNodeInternals = useUpdateNodeInternals();
const addNodeCmdk = useAddNodeCmdk();
const onConnectStart = useCallback<OnConnectStart>(
(event, { nodeId, handleId, handleType }) => {
@@ -107,9 +108,9 @@ export const useConnection = () => {
$pendingConnection.set(null);
} else {
// The mouse is not over a node - we should open the add node popover
$addNodeCmdk.set(true);
addNodeCmdk.setTrue();
}
}, [store, templates, updateNodeInternals]);
}, [addNodeCmdk, store, templates, updateNodeInternals]);
const api = useMemo(() => ({ onConnectStart, onConnect, onConnectEnd }), [onConnectStart, onConnect, onConnectEnd]);
return api;

View File

@@ -7,11 +7,13 @@ import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants';
import type {
BoardFieldValue,
BooleanFieldValue,
CLIPEmbedModelFieldValue,
ColorFieldValue,
ControlNetModelFieldValue,
EnumFieldValue,
FieldValue,
FloatFieldValue,
FluxVAEModelFieldValue,
ImageFieldValue,
IntegerFieldValue,
IPAdapterModelFieldValue,
@@ -30,10 +32,12 @@ import type {
import {
zBoardFieldValue,
zBooleanFieldValue,
zCLIPEmbedModelFieldValue,
zColorFieldValue,
zControlNetModelFieldValue,
zEnumFieldValue,
zFloatFieldValue,
zFluxVAEModelFieldValue,
zImageFieldValue,
zIntegerFieldValue,
zIPAdapterModelFieldValue,
@@ -347,6 +351,12 @@ export const nodesSlice = createSlice({
fieldT5EncoderValueChanged: (state, action: FieldValueAction<T5EncoderModelFieldValue>) => {
fieldValueReducer(state, action, zT5EncoderModelFieldValue);
},
fieldCLIPEmbedValueChanged: (state, action: FieldValueAction<CLIPEmbedModelFieldValue>) => {
fieldValueReducer(state, action, zCLIPEmbedModelFieldValue);
},
fieldFluxVAEModelValueChanged: (state, action: FieldValueAction<FluxVAEModelFieldValue>) => {
fieldValueReducer(state, action, zFluxVAEModelFieldValue);
},
fieldEnumModelValueChanged: (state, action: FieldValueAction<EnumFieldValue>) => {
fieldValueReducer(state, action, zEnumFieldValue);
},
@@ -409,6 +419,8 @@ export const {
fieldStringValueChanged,
fieldVaeModelValueChanged,
fieldT5EncoderValueChanged,
fieldCLIPEmbedValueChanged,
fieldFluxVAEModelValueChanged,
nodeEditorReset,
nodeIsIntermediateChanged,
nodeIsOpenChanged,
@@ -432,8 +444,7 @@ export const $didUpdateEdge = atom(false);
export const $lastEdgeUpdateMouseEvent = atom<MouseEvent | null>(null);
export const $viewport = atom<Viewport>({ x: 0, y: 0, zoom: 1 });
export const $addNodeCmdk = atom(false);
export const useAddNodeCmdk = buildUseBoolean($addNodeCmdk);
export const [useAddNodeCmdk, $addNodeCmdk] = buildUseBoolean(false);
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrateNodesState = (state: any): any => {
@@ -514,6 +525,8 @@ export const isAnyNodeOrEdgeMutation = isAnyOf(
fieldStringValueChanged,
fieldVaeModelValueChanged,
fieldT5EncoderValueChanged,
fieldCLIPEmbedValueChanged,
fieldFluxVAEModelValueChanged,
nodesChanged,
nodeIsIntermediateChanged,
nodeIsOpenChanged,

View File

@@ -151,6 +151,14 @@ const zT5EncoderModelFieldType = zFieldTypeBase.extend({
name: z.literal('T5EncoderModelField'),
originalType: zStatelessFieldType.optional(),
});
const zCLIPEmbedModelFieldType = zFieldTypeBase.extend({
name: z.literal('CLIPEmbedModelField'),
originalType: zStatelessFieldType.optional(),
});
const zFluxVAEModelFieldType = zFieldTypeBase.extend({
name: z.literal('FluxVAEModelField'),
originalType: zStatelessFieldType.optional(),
});
const zSchedulerFieldType = zFieldTypeBase.extend({
name: z.literal('SchedulerField'),
originalType: zStatelessFieldType.optional(),
@@ -175,6 +183,8 @@ const zStatefulFieldType = z.union([
zT2IAdapterModelFieldType,
zSpandrelImageToImageModelFieldType,
zT5EncoderModelFieldType,
zCLIPEmbedModelFieldType,
zFluxVAEModelFieldType,
zColorFieldType,
zSchedulerFieldType,
]);
@@ -667,7 +677,53 @@ export const isT5EncoderModelFieldInputInstance = (val: unknown): val is T5Encod
export const isT5EncoderModelFieldInputTemplate = (val: unknown): val is T5EncoderModelFieldInputTemplate =>
zT5EncoderModelFieldInputTemplate.safeParse(val).success;
// #endregio
// #endregion
// #region FluxVAEModelField
export const zFluxVAEModelFieldValue = zModelIdentifierField.optional();
const zFluxVAEModelFieldInputInstance = zFieldInputInstanceBase.extend({
value: zFluxVAEModelFieldValue,
});
const zFluxVAEModelFieldInputTemplate = zFieldInputTemplateBase.extend({
type: zFluxVAEModelFieldType,
originalType: zFieldType.optional(),
default: zFluxVAEModelFieldValue,
});
export type FluxVAEModelFieldValue = z.infer<typeof zFluxVAEModelFieldValue>;
export type FluxVAEModelFieldInputInstance = z.infer<typeof zFluxVAEModelFieldInputInstance>;
export type FluxVAEModelFieldInputTemplate = z.infer<typeof zFluxVAEModelFieldInputTemplate>;
export const isFluxVAEModelFieldInputInstance = (val: unknown): val is FluxVAEModelFieldInputInstance =>
zFluxVAEModelFieldInputInstance.safeParse(val).success;
export const isFluxVAEModelFieldInputTemplate = (val: unknown): val is FluxVAEModelFieldInputTemplate =>
zFluxVAEModelFieldInputTemplate.safeParse(val).success;
// #endregion
// #region CLIPEmbedModelField
export const zCLIPEmbedModelFieldValue = zModelIdentifierField.optional();
const zCLIPEmbedModelFieldInputInstance = zFieldInputInstanceBase.extend({
value: zCLIPEmbedModelFieldValue,
});
const zCLIPEmbedModelFieldInputTemplate = zFieldInputTemplateBase.extend({
type: zCLIPEmbedModelFieldType,
originalType: zFieldType.optional(),
default: zCLIPEmbedModelFieldValue,
});
export type CLIPEmbedModelFieldValue = z.infer<typeof zCLIPEmbedModelFieldValue>;
export type CLIPEmbedModelFieldInputInstance = z.infer<typeof zCLIPEmbedModelFieldInputInstance>;
export type CLIPEmbedModelFieldInputTemplate = z.infer<typeof zCLIPEmbedModelFieldInputTemplate>;
export const isCLIPEmbedModelFieldInputInstance = (val: unknown): val is CLIPEmbedModelFieldInputInstance =>
zCLIPEmbedModelFieldInputInstance.safeParse(val).success;
export const isCLIPEmbedModelFieldInputTemplate = (val: unknown): val is CLIPEmbedModelFieldInputTemplate =>
zCLIPEmbedModelFieldInputTemplate.safeParse(val).success;
// #endregion
// #region SchedulerField
@@ -758,6 +814,8 @@ export const zStatefulFieldValue = z.union([
zT2IAdapterModelFieldValue,
zSpandrelImageToImageModelFieldValue,
zT5EncoderModelFieldValue,
zFluxVAEModelFieldValue,
zCLIPEmbedModelFieldValue,
zColorFieldValue,
zSchedulerFieldValue,
]);
@@ -788,6 +846,8 @@ const zStatefulFieldInputInstance = z.union([
zT2IAdapterModelFieldInputInstance,
zSpandrelImageToImageModelFieldInputInstance,
zT5EncoderModelFieldInputInstance,
zFluxVAEModelFieldInputInstance,
zCLIPEmbedModelFieldInputInstance,
zColorFieldInputInstance,
zSchedulerFieldInputInstance,
]);
@@ -819,6 +879,8 @@ const zStatefulFieldInputTemplate = z.union([
zT2IAdapterModelFieldInputTemplate,
zSpandrelImageToImageModelFieldInputTemplate,
zT5EncoderModelFieldInputTemplate,
zFluxVAEModelFieldInputTemplate,
zCLIPEmbedModelFieldInputTemplate,
zColorFieldInputTemplate,
zSchedulerFieldInputTemplate,
zStatelessFieldInputTemplate,

View File

@@ -10,7 +10,9 @@ export const prepareLinearUIBatch = (
g: Graph,
prepend: boolean,
noise: Invocation<'noise'>,
posCond: Invocation<'compel' | 'sdxl_compel_prompt'>
posCond: Invocation<'compel' | 'sdxl_compel_prompt'>,
origin: 'generation' | 'workflows' | 'upscaling',
destination: 'canvas' | 'gallery'
): BatchConfig => {
const { iterations, model, shouldRandomizeSeed, seed, shouldConcatPrompts } = state.params;
const { prompts, seedBehaviour } = state.dynamicPrompts;
@@ -103,7 +105,8 @@ export const prepareLinearUIBatch = (
graph: g.getGraph(),
runs: 1,
data,
origin: 'canvas',
origin,
destination,
},
};

View File

@@ -29,7 +29,7 @@ export const addInpaint = async (
const canvas = selectCanvasSlice(state);
const { bbox } = canvas;
const { mode } = canvasSession;
const { sendToCanvas: isComposing } = canvasSession;
const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect);
const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect);
@@ -99,7 +99,7 @@ export const addInpaint = async (
g.addEdge(resizeImageToOriginalSize, 'image', canvasPasteBack, 'generated_image');
g.addEdge(resizeMaskToOriginalSize, 'image', canvasPasteBack, 'mask');
if (mode === 'generate') {
if (!isComposing) {
canvasPasteBack.source_image = { image_name: initialImage.image_name };
}
@@ -143,7 +143,7 @@ export const addInpaint = async (
g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image');
if (mode === 'generate') {
if (!isComposing) {
canvasPasteBack.source_image = { image_name: initialImage.image_name };
}

View File

@@ -30,7 +30,7 @@ export const addOutpaint = async (
const canvas = selectCanvasSlice(state);
const { bbox } = canvas;
const { mode } = canvasSession;
const { sendToCanvas: isComposing } = canvasSession;
const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect);
const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect);
@@ -123,7 +123,7 @@ export const addOutpaint = async (
g.addEdge(resizeOutputImageToOriginalSize, 'image', canvasPasteBack, 'generated_image');
g.addEdge(resizeOutputMaskToOriginalSize, 'image', canvasPasteBack, 'mask');
if (mode === 'generate') {
if (!isComposing) {
canvasPasteBack.source_image = { image_name: initialImage.image_name };
}
@@ -173,7 +173,7 @@ export const addOutpaint = async (
g.addEdge(createGradientMask, 'expanded_mask_area', canvasPasteBack, 'mask');
g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image');
if (mode === 'generate') {
if (!isComposing) {
canvasPasteBack.source_image = { image_name: initialImage.image_name };
}

View File

@@ -282,7 +282,7 @@ export const buildSD1Graph = async (
canvasOutput = addWatermarker(g, canvasOutput);
}
const shouldSaveToGallery = canvasSession.mode === 'generate' || canvasSettings.autoSave;
const shouldSaveToGallery = !canvasSession.sendToCanvas || canvasSettings.autoSave;
g.updateNode(canvasOutput, {
id: getPrefixedId('canvas_output'),

View File

@@ -285,7 +285,7 @@ export const buildSDXLGraph = async (
canvasOutput = addWatermarker(g, canvasOutput);
}
const shouldSaveToGallery = canvasSession.mode === 'generate' || canvasSettings.autoSave;
const shouldSaveToGallery = !canvasSession.sendToCanvas || canvasSettings.autoSave;
g.updateNode(canvasOutput, {
id: getPrefixedId('canvas_output'),

View File

@@ -23,6 +23,8 @@ const FIELD_VALUE_FALLBACK_MAP: Record<StatefulFieldType['name'], FieldValue> =
VAEModelField: undefined,
ControlNetModelField: undefined,
T5EncoderModelField: undefined,
FluxVAEModelField: undefined,
CLIPEmbedModelField: undefined,
};
export const buildFieldInputInstance = (id: string, template: FieldInputTemplate): FieldInputInstance => {

View File

@@ -2,6 +2,7 @@ import { FieldParseError } from 'features/nodes/types/error';
import type {
BoardFieldInputTemplate,
BooleanFieldInputTemplate,
CLIPEmbedModelFieldInputTemplate,
ColorFieldInputTemplate,
ControlNetModelFieldInputTemplate,
EnumFieldInputTemplate,
@@ -9,6 +10,7 @@ import type {
FieldType,
FloatFieldInputTemplate,
FluxMainModelFieldInputTemplate,
FluxVAEModelFieldInputTemplate,
ImageFieldInputTemplate,
IntegerFieldInputTemplate,
IPAdapterModelFieldInputTemplate,
@@ -238,6 +240,34 @@ const buildT5EncoderModelFieldInputTemplate: FieldInputTemplateBuilder<T5Encoder
return template;
};
const buildCLIPEmbedModelFieldInputTemplate: FieldInputTemplateBuilder<CLIPEmbedModelFieldInputTemplate> = ({
schemaObject,
baseField,
fieldType,
}) => {
const template: CLIPEmbedModelFieldInputTemplate = {
...baseField,
type: fieldType,
default: schemaObject.default ?? undefined,
};
return template;
};
const buildFluxVAEModelFieldInputTemplate: FieldInputTemplateBuilder<FluxVAEModelFieldInputTemplate> = ({
schemaObject,
baseField,
fieldType,
}) => {
const template: FluxVAEModelFieldInputTemplate = {
...baseField,
type: fieldType,
default: schemaObject.default ?? undefined,
};
return template;
};
const buildLoRAModelFieldInputTemplate: FieldInputTemplateBuilder<LoRAModelFieldInputTemplate> = ({
schemaObject,
baseField,
@@ -423,6 +453,8 @@ export const TEMPLATE_BUILDER_MAP: Record<StatefulFieldType['name'], FieldInputT
SpandrelImageToImageModelField: buildSpandrelImageToImageModelFieldInputTemplate,
VAEModelField: buildVAEModelFieldInputTemplate,
T5EncoderModelField: buildT5EncoderModelFieldInputTemplate,
CLIPEmbedModelField: buildCLIPEmbedModelFieldInputTemplate,
FluxVAEModelField: buildFluxVAEModelFieldInputTemplate,
} as const;
export const buildFieldInputTemplate = (

View File

@@ -1,27 +1,26 @@
import type { ButtonProps } from '@invoke-ai/ui-library';
import { Button } from '@invoke-ai/ui-library';
import { useClearQueueConfirmationAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiTrashSimpleFill } from 'react-icons/pi';
import { useClearQueue } from './ClearQueueConfirmationAlertDialog';
type Props = ButtonProps;
const ClearQueueButton = (props: Props) => {
const { t } = useTranslation();
const dialogState = useClearQueueConfirmationAlertDialog();
const { isLoading, isDisabled } = useClearQueue();
const clearQueue = useClearQueue();
return (
<>
<Button
isDisabled={isDisabled}
isLoading={isLoading}
isDisabled={clearQueue.isDisabled}
isLoading={clearQueue.isLoading}
tooltip={t('queue.clearTooltip')}
leftIcon={<PiTrashSimpleFill />}
colorScheme="error"
onClick={dialogState.setTrue}
onClick={clearQueue.openDialog}
data-testid={t('queue.clear')}
{...props}
>

View File

@@ -1,26 +1,72 @@
import { ConfirmationAlertDialog, Text } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { $isConnected } from 'app/hooks/useSocketIO';
import { useAppDispatch } from 'app/store/storeHooks';
import { buildUseBoolean } from 'common/hooks/useBoolean';
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
import { atom } from 'nanostores';
import { memo } from 'react';
import { listCursorChanged, listPriorityChanged } from 'features/queue/store/queueSlice';
import { toast } from 'features/toast/toast';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useClearQueueMutation, useGetQueueStatusQuery } from 'services/api/endpoints/queue';
const $boolean = atom(false);
export const useClearQueueConfirmationAlertDialog = buildUseBoolean($boolean);
const [useClearQueueConfirmationAlertDialog] = buildUseBoolean(false);
export const useClearQueue = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const dialog = useClearQueueConfirmationAlertDialog();
const { data: queueStatus } = useGetQueueStatusQuery();
const isConnected = useStore($isConnected);
const [trigger, { isLoading }] = useClearQueueMutation({
fixedCacheKey: 'clearQueue',
});
const clearQueue = useCallback(async () => {
if (!queueStatus?.queue.total) {
return;
}
try {
await trigger().unwrap();
toast({
id: 'QUEUE_CLEAR_SUCCEEDED',
title: t('queue.clearSucceeded'),
status: 'success',
});
dispatch(listCursorChanged(undefined));
dispatch(listPriorityChanged(undefined));
} catch {
toast({
id: 'QUEUE_CLEAR_FAILED',
title: t('queue.clearFailed'),
status: 'error',
});
}
}, [queueStatus?.queue.total, trigger, dispatch, t]);
const isDisabled = useMemo(() => !isConnected || !queueStatus?.queue.total, [isConnected, queueStatus?.queue.total]);
return {
clearQueue,
isOpen: dialog.isTrue,
openDialog: dialog.setTrue,
closeDialog: dialog.setFalse,
isLoading,
queueStatus,
isDisabled,
};
};
export const ClearQueueConfirmationsAlertDialog = memo(() => {
const { t } = useTranslation();
const dialogState = useClearQueueConfirmationAlertDialog();
const isOpen = useStore(dialogState.$boolean);
const { clearQueue } = useClearQueue();
const clearQueue = useClearQueue();
return (
<ConfirmationAlertDialog
isOpen={isOpen}
onClose={dialogState.setFalse}
isOpen={clearQueue.isOpen}
onClose={clearQueue.closeDialog}
title={t('queue.clearTooltip')}
acceptCallback={clearQueue}
acceptCallback={clearQueue.clearQueue}
acceptButtonText={t('queue.clear')}
useInert={false}
>

View File

@@ -1,67 +1,40 @@
import type { IconButtonProps } from '@invoke-ai/ui-library';
import { IconButton, useShiftModifier } from '@invoke-ai/ui-library';
import { useClearQueueConfirmationAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
import { QueueCountBadge } from 'features/queue/components/QueueCountBadge';
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
import { memo } from 'react';
import { memo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { PiTrashSimpleBold, PiXBold } from 'react-icons/pi';
type ClearQueueButtonProps = Omit<IconButtonProps, 'aria-label'>;
import { useClearQueue } from './ClearQueueConfirmationAlertDialog';
export const ClearAllQueueIconButton = memo((props: ClearQueueButtonProps) => {
export const ClearQueueIconButton = memo((_) => {
const ref = useRef<HTMLDivElement>(null);
const { t } = useTranslation();
const dialogState = useClearQueueConfirmationAlertDialog();
const { isLoading, isDisabled } = useClearQueue();
const clearQueue = useClearQueue();
const cancelCurrentQueueItem = useCancelCurrentQueueItem();
return (
<IconButton
isDisabled={isDisabled}
isLoading={isLoading}
aria-label={t('queue.clear')}
tooltip={t('queue.clearTooltip')}
icon={<PiTrashSimpleBold size="16px" />}
colorScheme="error"
onClick={dialogState.setTrue}
data-testid={t('queue.clear')}
{...props}
/>
);
});
ClearAllQueueIconButton.displayName = 'ClearAllQueueIconButton';
const ClearSingleQueueItemIconButton = memo((props: ClearQueueButtonProps) => {
const { t } = useTranslation();
const { cancelQueueItem, isLoading, isDisabled } = useCancelCurrentQueueItem();
return (
<IconButton
isDisabled={isDisabled}
isLoading={isLoading}
aria-label={t('queue.cancel')}
tooltip={t('queue.cancelTooltip')}
icon={<PiXBold size="16px" />}
colorScheme="error"
onClick={cancelQueueItem}
data-testid={t('queue.cancel')}
{...props}
/>
);
});
ClearSingleQueueItemIconButton.displayName = 'ClearSingleQueueItemIconButton';
export const ClearQueueIconButton = memo((props: ClearQueueButtonProps) => {
// Show the single item clear button when shift is pressed
// Otherwise show the clear queue button
const shift = useShiftModifier();
if (shift) {
return <ClearAllQueueIconButton {...props} />;
}
return <ClearSingleQueueItemIconButton {...props} />;
return (
<>
<IconButton
ref={ref}
size="lg"
isDisabled={shift ? clearQueue.isDisabled : cancelCurrentQueueItem.isDisabled}
isLoading={shift ? clearQueue.isLoading : cancelCurrentQueueItem.isLoading}
aria-label={shift ? t('queue.clear') : t('queue.cancel')}
tooltip={shift ? t('queue.clearTooltip') : t('queue.cancelTooltip')}
icon={shift ? <PiTrashSimpleBold /> : <PiXBold />}
colorScheme="error"
onClick={shift ? clearQueue.openDialog : cancelCurrentQueueItem.cancelQueueItem}
data-testid={shift ? t('queue.clear') : t('queue.cancel')}
/>
{/* The badge is dynamically positioned, needs a ref to the target element */}
<QueueCountBadge targetRef={ref} />
</>
);
});
ClearQueueIconButton.displayName = 'ClearQueueIconButton';

View File

@@ -15,7 +15,7 @@ export const InvokeQueueBackButton = memo(() => {
const isLoadingDynamicPrompts = useAppSelector(selectDynamicPromptsIsLoading);
return (
<Flex pos="relative" flexGrow={1} minW="240px">
<Flex pos="relative" w="192px">
<QueueIterationsNumberInput />
<QueueButtonTooltip>
<Button

View File

@@ -1,120 +0,0 @@
import {
Badge,
Box,
IconButton,
Menu,
MenuButton,
MenuDivider,
MenuItem,
MenuList,
Portal,
useDisclosure,
} from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import type { Coordinate } from 'features/controlLayers/store/types';
import { useClearQueueConfirmationAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
import { usePauseProcessor } from 'features/queue/hooks/usePauseProcessor';
import { useResumeProcessor } from 'features/queue/hooks/useResumeProcessor';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPauseFill, PiPlayFill, PiTrashSimpleBold } from 'react-icons/pi';
import { RiListCheck, RiPlayList2Fill } from 'react-icons/ri';
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
export const QueueActionsMenuButton = memo(() => {
const { isOpen, onOpen, onClose } = useDisclosure();
const dispatch = useAppDispatch();
const { t } = useTranslation();
const [badgePos, setBadgePos] = useState<Coordinate | null>(null);
const menuButtonRef = useRef<HTMLButtonElement>(null);
const dialogState = useClearQueueConfirmationAlertDialog();
const isPauseEnabled = useFeatureStatus('pauseQueue');
const isResumeEnabled = useFeatureStatus('resumeQueue');
const { queueSize } = useGetQueueStatusQuery(undefined, {
selectFromResult: (res) => ({
queueSize: res.data ? res.data.queue.pending + res.data.queue.in_progress : 0,
}),
});
const { isLoading: isLoadingClearQueue, isDisabled: isDisabledClearQueue } = useClearQueue();
const {
resumeProcessor,
isLoading: isLoadingResumeProcessor,
isDisabled: isDisabledResumeProcessor,
} = useResumeProcessor();
const {
pauseProcessor,
isLoading: isLoadingPauseProcessor,
isDisabled: isDisabledPauseProcessor,
} = usePauseProcessor();
const openQueue = useCallback(() => {
dispatch(setActiveTab('queue'));
}, [dispatch]);
useEffect(() => {
if (menuButtonRef.current) {
const { x, y } = menuButtonRef.current.getBoundingClientRect();
setBadgePos({ x: x - 10, y: y - 10 });
}
}, []);
return (
<Box pos="relative">
<Menu isOpen={isOpen} onOpen={onOpen} onClose={onClose} placement="bottom-end">
<MenuButton ref={menuButtonRef} as={IconButton} aria-label="Queue Actions Menu" icon={<RiListCheck />} />
<MenuList>
<MenuItem
isDestructive
icon={<PiTrashSimpleBold size="16px" />}
onClick={dialogState.setTrue}
isLoading={isLoadingClearQueue}
isDisabled={isDisabledClearQueue}
>
{t('queue.clearTooltip')}
</MenuItem>
{isResumeEnabled && (
<MenuItem
icon={<PiPlayFill size="14px" />}
onClick={resumeProcessor}
isLoading={isLoadingResumeProcessor}
isDisabled={isDisabledResumeProcessor}
>
{t('queue.resumeTooltip')}
</MenuItem>
)}
{isPauseEnabled && (
<MenuItem
icon={<PiPauseFill size="14px" />}
onClick={pauseProcessor}
isLoading={isLoadingPauseProcessor}
isDisabled={isDisabledPauseProcessor}
>
{t('queue.pauseTooltip')}
</MenuItem>
)}
<MenuDivider />
<MenuItem icon={<RiPlayList2Fill />} onClick={openQueue}>
{t('queue.openQueue')}
</MenuItem>
</MenuList>
</Menu>
{queueSize > 0 && badgePos !== null && (
<Portal>
<Badge
pos="absolute"
insetInlineStart={badgePos.x}
insetBlockStart={badgePos.y}
colorScheme="invokeYellow"
zIndex="docked"
>
{queueSize}
</Badge>
</Portal>
)}
</Box>
);
});
QueueActionsMenuButton.displayName = 'QueueActionsMenuButton';

View File

@@ -1,24 +1,27 @@
import { ButtonGroup, Flex, Spacer } from '@invoke-ai/ui-library';
import { Flex, Spacer } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { CanvasSendToToggle } from 'features/controlLayers/components/CanvasSendToToggle';
import { ClearQueueIconButton } from 'features/queue/components/ClearQueueIconButton';
import QueueFrontButton from 'features/queue/components/QueueFrontButton';
import ProgressBar from 'features/system/components/ProgressBar';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo } from 'react';
import { InvokeQueueBackButton } from './InvokeQueueBackButton';
import { QueueActionsMenuButton } from './QueueActionsMenuButton';
const QueueControls = () => {
const isPrependEnabled = useFeatureStatus('prependQueue');
const tab = useAppSelector(selectActiveTab);
return (
<Flex w="full" position="relative" borderRadius="base" gap={2} flexDir="column">
<ButtonGroup size="lg" isAttached={false}>
<Flex gap={2}>
{isPrependEnabled && <QueueFrontButton />}
<InvokeQueueBackButton />
<Spacer />
<QueueActionsMenuButton />
{tab === 'generation' && <CanvasSendToToggle />}
<ClearQueueIconButton />
</ButtonGroup>
</Flex>
<ProgressBar />
</Flex>
);

View File

@@ -0,0 +1,77 @@
import { Badge, Portal } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { $isParametersPanelOpen } from 'features/ui/store/uiSlice';
import type { RefObject } from 'react';
import { memo, useEffect, useState } from 'react';
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
type Props = {
targetRef: RefObject<HTMLDivElement>;
};
export const QueueCountBadge = memo(({ targetRef }: Props) => {
const [badgePos, setBadgePos] = useState<{ x: string; y: string } | null>(null);
const isParametersPanelOpen = useStore($isParametersPanelOpen);
const { queueSize } = useGetQueueStatusQuery(undefined, {
selectFromResult: (res) => ({
queueSize: res.data ? res.data.queue.pending + res.data.queue.in_progress : 0,
}),
});
useEffect(() => {
if (!targetRef.current) {
return;
}
const target = targetRef.current;
const parent = target.parentElement;
if (!parent) {
return;
}
const cb = () => {
if (!$isParametersPanelOpen.get()) {
return;
}
const { x, y } = target.getBoundingClientRect();
setBadgePos({ x: `${x - 7}px`, y: `${y - 5}px` });
};
const resizeObserver = new ResizeObserver(cb);
resizeObserver.observe(parent);
cb();
return () => {
resizeObserver.disconnect();
};
}, [targetRef]);
if (queueSize === 0) {
return null;
}
if (!badgePos) {
return null;
}
if (!isParametersPanelOpen) {
return null;
}
return (
<Portal>
<Badge
pos="absolute"
insetInlineStart={badgePos.x}
insetBlockStart={badgePos.y}
colorScheme="invokeYellow"
zIndex="docked"
shadow="dark-lg"
userSelect="none"
>
{queueSize}
</Badge>
</Portal>
);
});
QueueCountBadge.displayName = 'QueueCountBadge';

View File

@@ -1,6 +1,7 @@
import type { ChakraProps, CollapseProps } from '@invoke-ai/ui-library';
import { ButtonGroup, Collapse, Flex, IconButton, Text } from '@invoke-ai/ui-library';
import QueueStatusBadge from 'features/queue/components/common/QueueStatusBadge';
import { useDestinationText } from 'features/queue/components/QueueList/useDestinationText';
import { useOriginText } from 'features/queue/components/QueueList/useOriginText';
import { useCancelQueueItem } from 'features/queue/hooks/useCancelQueueItem';
import { getSecondsFromTimestamps } from 'features/queue/util/getSecondsFromTimestamps';
@@ -52,6 +53,7 @@ const QueueItemComponent = ({ index, item, context }: InnerItemProps) => {
const isCanceled = useMemo(() => ['canceled', 'completed', 'failed'].includes(item.status), [item.status]);
const originText = useOriginText(item.origin);
const destinationText = useDestinationText(item.destination);
const icon = useMemo(() => <PiXBold />, []);
return (
@@ -76,6 +78,11 @@ const QueueItemComponent = ({ index, item, context }: InnerItemProps) => {
{originText}
</Text>
</Flex>
<Flex w={COLUMN_WIDTHS.destination} flexShrink={0}>
<Text overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap" alignItems="center">
{destinationText}
</Text>
</Flex>
<Flex w={COLUMN_WIDTHS.time} alignItems="center" flexShrink={0}>
{executionTime || '-'}
</Flex>

View File

@@ -1,5 +1,6 @@
import { Button, ButtonGroup, Flex, Heading, Spinner, Text } from '@invoke-ai/ui-library';
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
import { useDestinationText } from 'features/queue/components/QueueList/useDestinationText';
import { useOriginText } from 'features/queue/components/QueueList/useOriginText';
import { useCancelBatch } from 'features/queue/hooks/useCancelBatch';
import { useCancelQueueItem } from 'features/queue/hooks/useCancelQueueItem';
@@ -17,7 +18,7 @@ type Props = {
};
const QueueItemComponent = ({ queueItemDTO }: Props) => {
const { session_id, batch_id, item_id, origin } = queueItemDTO;
const { session_id, batch_id, item_id, origin, destination } = queueItemDTO;
const { t } = useTranslation();
const { cancelBatch, isLoading: isLoadingCancelBatch, isCanceled } = useCancelBatch(batch_id);
@@ -26,6 +27,7 @@ const QueueItemComponent = ({ queueItemDTO }: Props) => {
const { data: queueItem } = useGetQueueItemQuery(item_id);
const originText = useOriginText(origin);
const destinationText = useDestinationText(destination);
const statusAndTiming = useMemo(() => {
if (!queueItem) {
@@ -54,6 +56,7 @@ const QueueItemComponent = ({ queueItemDTO }: Props) => {
>
<QueueItemData label={t('queue.status')} data={statusAndTiming} />
<QueueItemData label={t('queue.origin')} data={originText} />
<QueueItemData label={t('queue.destination')} data={destinationText} />
<QueueItemData label={t('queue.item')} data={item_id} />
<QueueItemData label={t('queue.batch')} data={batch_id} />
<QueueItemData label={t('queue.session')} data={session_id} />

View File

@@ -25,6 +25,9 @@ const QueueListHeader = () => {
<Flex ps={0.5} w={COLUMN_WIDTHS.origin} alignItems="center">
<Text variant="subtext">{t('queue.origin')}</Text>
</Flex>
<Flex ps={0.5} w={COLUMN_WIDTHS.destination} alignItems="center">
<Text variant="subtext">{t('queue.destination')}</Text>
</Flex>
<Flex ps={0.5} w={COLUMN_WIDTHS.time} alignItems="center">
<Text variant="subtext">{t('queue.time')}</Text>
</Flex>

View File

@@ -4,7 +4,8 @@ export const COLUMN_WIDTHS = {
statusDot: 2,
time: '4rem',
origin: '5rem',
destination: '6rem',
batchId: '5rem',
fieldValues: 'auto',
actions: 'auto',
};
} as const;

View File

@@ -0,0 +1,16 @@
import { useTranslation } from 'react-i18next';
import type { SessionQueueItemDTO } from 'services/api/types';
export const useDestinationText = (destination: SessionQueueItemDTO['destination']) => {
const { t } = useTranslation();
if (destination === 'canvas') {
return t('queue.canvas');
}
if (destination === 'gallery') {
return t('queue.gallery');
}
return t('queue.other');
};

View File

@@ -4,13 +4,17 @@ import type { SessionQueueItemDTO } from 'services/api/types';
export const useOriginText = (origin: SessionQueueItemDTO['origin']) => {
const { t } = useTranslation();
if (origin === 'canvas') {
return t('queue.originCanvas');
if (origin === 'generation') {
return t('queue.generation');
}
if (origin === 'workflows') {
return t('queue.originWorkflows');
return t('queue.workflows');
}
return t('queue.originOther');
if (origin === 'upscaling') {
return t('queue.upscaling');
}
return t('queue.other');
};

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