Compare commits

..

544 Commits

Author SHA1 Message Date
psychedelicious
37ed567aad chore: release v4.2.9.dev7 2024-08-28 22:05:15 +10:00
psychedelicious
9692a5f996 fix(ui): pending node connection stuck 2024-08-28 22:05:03 +10:00
psychedelicious
5db7b48cd8 chore(ui): lint 2024-08-28 21:38:57 +10:00
psychedelicious
ea014c66ac chore: release v4.2.9.dev6 2024-08-28 21:33:12 +10:00
psychedelicious
25918c28aa 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-28 21:32:10 +10:00
psychedelicious
0c60469401 fix(ui): schema parsing now that node_pack is guaranteed to be present 2024-08-28 21:31:15 +10:00
psychedelicious
f1aa50f447 chore(ui): typegen 2024-08-28 21:06:45 +10:00
psychedelicious
a413b261f0 fix(app): node_pack not added to openapi schema correctly 2024-08-28 21:06:33 +10:00
psychedelicious
4a1a6639f6 fix(ui): unnecessary z-index on invoke button 2024-08-28 16:51:44 +10:00
psychedelicious
201c370ca1 feat(ui): split settings modal 2024-08-28 16:51:38 +10:00
psychedelicious
d070c7c726 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-28 16:49:19 +10:00
psychedelicious
e38e20a992 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-28 16:37:50 +10:00
psychedelicious
39a94ec70e fix(ui): transparency effect not updating 2024-08-28 16:08:12 +10:00
psychedelicious
c7bfae2d1e feat(ui): tidy canvas toolbar buttons 2024-08-28 16:04:37 +10:00
psychedelicious
e7944c427d feat(ui): revised viewer toggle @joshistoast 2024-08-28 16:04:25 +10:00
psychedelicious
48ed4e120d fix(ui): opacity reset value incorrect 2024-08-28 16:03:48 +10:00
psychedelicious
a5b038a1b1 revert(ui): roll back flip, doesn't work with rotate yet 2024-08-28 12:03:38 +10:00
psychedelicious
dc752c98b0 fix(ui): disable opacity slider fully when no valid entity selected 2024-08-28 09:00:55 +10:00
psychedelicious
85a47cc6fe 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-28 08:59:38 +10:00
psychedelicious
6450f42cfa feat(ui): tweak regional prompt box styles 2024-08-28 08:44:04 +10:00
psychedelicious
3876f71ff4 feat(ui): tweak enabled/locked toggle styles 2024-08-28 08:25:37 +10:00
psychedelicious
cf819e8eab feat(ui): tweak filter styling 2024-08-28 08:07:36 +10:00
psychedelicious
2217fb8485 feat(ui): add flip & reset to transform 2024-08-28 07:57:15 +10:00
psychedelicious
43652e830a tidy(ui): use helper to sync scaled bbox size on model change 2024-08-28 07:11:05 +10:00
psychedelicious
a3417bf81d fix(ui): randomize seed toggle linked to prompt concat 2024-08-28 07:05:14 +10:00
psychedelicious
06637161e3 chore: release v4.2.9.dev5 2024-08-27 19:57:34 +10:00
psychedelicious
c4f4b16a36 chore(ui): lint 2024-08-27 19:57:02 +10:00
psychedelicious
3001718f9f feat(ui): generalize mask fill, add to action bar 2024-08-27 19:56:57 +10:00
psychedelicious
ac16fa65a3 feat(ui): implement interaction locking on layers 2024-08-27 19:27:38 +10:00
psychedelicious
bc0b5335ff 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-27 19:08:57 +10:00
psychedelicious
e91c7c5a30 feat(ui): collapsible entity groups 2024-08-27 17:59:21 +10:00
psychedelicious
74791cc490 tidy(ui): rename some classes to be consistent 2024-08-27 17:27:38 +10:00
psychedelicious
68409c6a0f 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-27 17:10:01 +10:00
psychedelicious
85613b220c tidy(ui): move all undoable reducers back to canvas slice 2024-08-27 16:25:42 +10:00
psychedelicious
80085ad854 fix(ui): dnd image count 2024-08-27 16:16:11 +10:00
psychedelicious
b6bfa65104 fix(ui): canvas entity opacity scale 2024-08-27 16:16:02 +10:00
psychedelicious
0bfa033089 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-27 16:04:05 +10:00
psychedelicious
6f0974b5bc 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-27 13:19:14 +10:00
psychedelicious
c3b53fc4f6 feat(ui): rough out undo/redo on canvas 2024-08-27 12:05:45 +10:00
psychedelicious
8f59a32d81 chore: release v4.2.9.dev4
Canvas dev build.
2024-08-27 11:12:28 +10:00
psychedelicious
3b4f20f433 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-27 11:02:18 +10:00
psychedelicious
73da6e9628 feat(ui): split out loras state from canvas rendering state 2024-08-27 11:02:18 +10:00
psychedelicious
2fc482141d feat(ui): split out session state from canvas rendering state 2024-08-27 11:02:18 +10:00
psychedelicious
a41ec5f3fc feat(ui): split out settings state from canvas rendering state 2024-08-27 11:02:18 +10:00
psychedelicious
c906225d03 feat(ui): split out tool state from canvas rendering state 2024-08-27 11:02:18 +10:00
psychedelicious
2985ea3716 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-27 11:02:18 +10:00
psychedelicious
4f151c6c6f 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-27 11:02:04 +10:00
psychedelicious
c4f5252c1a feat(ui): move selected tool and tool buffer out of redux
This ephemeral state can live in the canvas classes.
2024-08-27 11:02:04 +10:00
psychedelicious
77c13f2cf3 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-27 11:02:04 +10:00
psychedelicious
3270d36fca 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-27 11:02:04 +10:00
psychedelicious
b6b30ff01f feat(ui): move events into modules who care about them 2024-08-27 11:02:04 +10:00
psychedelicious
aa9bfdff35 fix(ui): color picker resets brush opacity 2024-08-27 11:02:04 +10:00
psychedelicious
80308cc3b8 fix(ui): scaled bbox loses sync 2024-08-27 11:02:04 +10:00
psychedelicious
f6db73bf1f feat(ui): add context menu to entity list 2024-08-27 11:02:04 +10:00
psychedelicious
ef9f61a39f chore(ui): bump @invoke-ai/ui-library 2024-08-27 11:02:04 +10:00
psychedelicious
a1a0881133 fix(ui): missing vae precision in graph builders 2024-08-27 11:02:04 +10:00
psychedelicious
9956919ab6 chore: release v4.2.9.dev3
Instead of using dates, just going to increment.
2024-08-27 11:02:04 +10:00
psychedelicious
abc07f57d6 feat(ui): use new Result utils for enqueueing 2024-08-27 11:02:04 +10:00
psychedelicious
1a1cae79f1 fix(ui): graph building issue w/ controlnet 2024-08-27 11:02:04 +10:00
psychedelicious
bcfafe7b06 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-27 11:02:04 +10:00
psychedelicious
34e8ced592 chore: release v4.2.9.dev20240824 2024-08-27 11:02:04 +10:00
psychedelicious
1fdada65b6 fix(ui): lint & fix issues with adding regional ip adapters 2024-08-27 11:02:04 +10:00
psychedelicious
433f3e1971 feat(ui): add knipignore tag
I'm not ready to delete some things but still want to build the app.
2024-08-27 11:02:04 +10:00
psychedelicious
a60e23f825 feat(ui): duplicate entity 2024-08-27 11:02:04 +10:00
psychedelicious
f69de3148e feat(ui): autocomplete on getPrefixeId 2024-08-27 11:02:04 +10:00
psychedelicious
cbcd36ef54 feat(ui): paste canvas gens back on source in generate mode 2024-08-27 11:02:04 +10:00
psychedelicious
aa76134340 chore(ui): typegen 2024-08-27 11:02:04 +10:00
psychedelicious
55758acae8 feat(nodes): CanvasV2MaskAndCropInvocation can paste generated image back on source
This is needed for `Generate` mode.
2024-08-27 11:02:04 +10:00
psychedelicious
196e43b5e5 fix(ui): extraneous entity preview updates 2024-08-27 11:02:04 +10:00
psychedelicious
38b9828441 fix(ui): newly-added entities are selected 2024-08-27 11:02:04 +10:00
psychedelicious
0048a7077e feat(ui): add crosshair to color picker 2024-08-27 11:02:04 +10:00
psychedelicious
527a39a3ad fix(ui): color picker ignores alpha 2024-08-27 11:02:04 +10:00
psychedelicious
30ce4c55c7 fix(ui): calculate renderable entities correctly in tool module 2024-08-27 11:02:04 +10:00
psychedelicious
ca082d4288 feat(ui): better color picker 2024-08-27 11:02:04 +10:00
psychedelicious
5e59a4f43a feat(ui): colored mask preview image 2024-08-27 11:02:04 +10:00
psychedelicious
9f86605049 fix(ui): new rectangles don't trigger rerender 2024-08-27 11:02:04 +10:00
psychedelicious
79058a7894 chore: bump version v4.2.9.dev20240823 2024-08-27 11:02:04 +10:00
psychedelicious
bb3ad8c2f1 feat(ui): disable most interaction while filtering 2024-08-27 11:02:04 +10:00
psychedelicious
799688514b fix(ui): filter preview offset 2024-08-27 11:02:04 +10:00
psychedelicious
b7344b0df2 feat(ui): tweak layout of staging area toolbar 2024-08-27 11:02:04 +10:00
psychedelicious
7e382c5f3f chore(ui): typegen 2024-08-27 11:02:04 +10:00
psychedelicious
9cf357e184 tidy(app): clean up app changes for canvas v2 2024-08-27 11:01:53 +10:00
psychedelicious
95b6c773d4 feat(ui): use singleton for clear q confirm dialog 2024-08-27 11:01:53 +10:00
psychedelicious
89d8c5ba00 fix(ui): rip out broken recall logic, NO TS ERRORS 2024-08-27 11:01:53 +10:00
psychedelicious
59580cf6ed chore(ui): lint 2024-08-27 11:01:53 +10:00
psychedelicious
2b0c084f5b fix(ui): staging area interaction scopes 2024-08-27 11:01:53 +10:00
psychedelicious
4d896073ff fix(ui): staging area actions 2024-08-27 11:01:53 +10:00
psychedelicious
9f69503a80 tidy(ui): more cleanup 2024-08-27 11:01:53 +10:00
psychedelicious
0311e852a0 fix(ui): upscale tab graph 2024-08-27 11:01:53 +10:00
psychedelicious
7003a3d546 fix(ui): sdxl graph builder 2024-08-27 11:01:53 +10:00
psychedelicious
dc73072e27 fix(ui): select next entity in the list when deleting 2024-08-27 11:01:53 +10:00
psychedelicious
e549c44ad7 feat(ui): fix delete layer hotkey 2024-08-27 11:01:52 +10:00
psychedelicious
45a4231cbe tidy(ui): "eye dropper" -> "color picker" 2024-08-27 11:01:52 +10:00
psychedelicious
81f046ebac tidy(ui): regional guidance buttons 2024-08-27 11:01:52 +10:00
psychedelicious
6ef6c593c4 feat(ui): update entity list menu 2024-08-27 11:01:52 +10:00
psychedelicious
5b53eefef7 feat(ui): add log debug button 2024-08-27 11:01:52 +10:00
psychedelicious
9a9919c0af chore(ui): lint 2024-08-27 11:01:52 +10:00
psychedelicious
10661b33d4 chore(ui): prettier 2024-08-27 11:01:52 +10:00
psychedelicious
52193d604d chore(ui): eslint 2024-08-27 11:01:52 +10:00
psychedelicious
2568441e6a tidy(ui): remove unused stuff 4 2024-08-27 11:01:52 +10:00
psychedelicious
1a14860b3b tidy(ui): remove unused stuff 3 2024-08-27 11:01:52 +10:00
psychedelicious
9ff7647ec5 tidy(ui): remove unused pkg @chakra-ui/react-use-size 2024-08-27 11:01:52 +10:00
psychedelicious
b49106e8fe feat(ui): revise graph building for control layers, fix issues w/ invocation complete events 2024-08-27 11:01:52 +10:00
psychedelicious
906d0902a3 feat(ui): use unique id for metadata in Graph class 2024-08-27 11:01:52 +10:00
psychedelicious
fbde6f5a7f tidy(ui): remove unused stuff 2 2024-08-27 11:01:52 +10:00
psychedelicious
b388268987 tidy(ui): remove unused stuff 2024-08-27 11:01:52 +10:00
psychedelicious
3b4164bd62 tidy(ui): reduce use of parseify util 2024-08-27 11:01:52 +10:00
psychedelicious
b7fc6fe573 feat(ui): refine canvas entity list items & menus 2024-08-27 11:01:52 +10:00
psychedelicious
2954a19d27 feat(ui): canvas layer preview, revised reactivity for adapters 2024-08-27 11:01:52 +10:00
psychedelicious
aa45ce7fbd feat(ui): add SyncableMap
Can be used with useSyncExternal store to make a `Map` reactive.
2024-08-27 11:01:52 +10:00
psychedelicious
77e5078e4a tidy(ui): removed unused transform methods from canvasmanager 2024-08-27 11:01:52 +10:00
psychedelicious
603cc7bf2e feat(ui): transform tool ux 2024-08-27 11:01:52 +10:00
psychedelicious
cd517a102d feat(ui): rough out canvas mode 2024-08-27 11:01:52 +10:00
psychedelicious
9a442918b5 feat(ui): add canvas autosave checkbox 2024-08-27 11:01:52 +10:00
psychedelicious
f9c03d85a5 fix(ui): memory leak when getting image DTO
must unsubscribe!
2024-08-27 11:01:52 +10:00
psychedelicious
10d07c71c4 feat(ui): rework settings menu 2024-08-27 11:01:52 +10:00
psychedelicious
cd05a78219 feat(ui): no entities fallback buttons 2024-08-27 11:01:52 +10:00
psychedelicious
f8ee572abc perf(ui): optimize gallery image delete button rendering 2024-08-27 11:01:52 +10:00
psychedelicious
d918654509 feat(ui): remove "solid" background option 2024-08-27 11:01:52 +10:00
psychedelicious
582e30c542 tidy(ui): organise files and classes 2024-08-27 11:01:52 +10:00
psychedelicious
34a6555301 tidy(ui): abstract compositing logic to module 2024-08-27 11:01:52 +10:00
psychedelicious
fff860090b fix(ui): fix canvas cache property access 2024-08-27 11:01:52 +10:00
psychedelicious
f4971197c1 tidy(ui): clean up CanvasFilter class 2024-08-27 11:01:52 +10:00
psychedelicious
621d5e0462 tidy(ui): clean up a few bits and bobs 2024-08-27 11:01:52 +10:00
psychedelicious
0b68a69a6c tidy(ui): abstract canvas rendering logic to module 2024-08-27 11:01:52 +10:00
psychedelicious
9a599ce595 tidy(ui): abstract caching logic to module 2024-08-27 11:01:52 +10:00
psychedelicious
1467ba276f tidy(ui): abstract worker logic to module 2024-08-27 11:01:52 +10:00
psychedelicious
708facf707 tidy(ui): abstract stage logic into module 2024-08-27 11:01:52 +10:00
psychedelicious
9c6c6adb1f feat(ui): add entity group hiding 2024-08-27 11:01:52 +10:00
psychedelicious
c335b8581c 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-27 11:01:52 +10:00
psychedelicious
f1348e45bd 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-27 11:01:52 +10:00
psychedelicious
ce6cf9b079 feat(ui): revise filter implementation 2024-08-27 11:01:52 +10:00
psychedelicious
13ec80736a fix(ui): add button to delete inpaint mask 2024-08-27 11:01:52 +10:00
psychedelicious
c9690a4b21 feat(ui): add contexts/hooks to access entity adapters directly 2024-08-27 11:01:52 +10:00
psychedelicious
489e875a6e 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-27 11:01:52 +10:00
psychedelicious
8651396048 feat(ui) do not set $canvasManager until ready 2024-08-27 11:01:52 +10:00
psychedelicious
2bab5a6179 fix(ui): inpaint mask naming 2024-08-27 11:01:52 +10:00
psychedelicious
006f06b615 feat(ui): efficient canvas compositing
Also solves issue of exporting layers at different opacities than what is visible
2024-08-27 11:01:52 +10:00
psychedelicious
d603923d1b feat(ui): allow multiple inpaint masks
This is easier than making it a nullable singleton
2024-08-27 11:01:52 +10:00
psychedelicious
86878e855b fix(ui): missing rasterization cache invalidations 2024-08-27 11:01:52 +10:00
psychedelicious
35de60a8fa feat(ui): iterate on filter UI, flow 2024-08-27 11:01:52 +10:00
psychedelicious
2c444a1941 fix(ui): rehydration data loss 2024-08-27 11:01:52 +10:00
psychedelicious
3dfef01889 feat(ui): sort log namespaces 2024-08-27 11:01:52 +10:00
psychedelicious
a845a2daa5 fix(ui): do not merge arrays by index during rehydration 2024-08-27 11:01:52 +10:00
psychedelicious
df41f4fbce 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-27 11:01:52 +10:00
psychedelicious
76482da6f5 fix(ui): fix logger filter
was accidetnally replacing the filter instead of appending to it.
2024-08-27 11:01:52 +10:00
psychedelicious
8205abbbbf 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-27 11:01:52 +10:00
psychedelicious
926873de26 fix(ui): handle opacity for masks 2024-08-27 11:01:52 +10:00
psychedelicious
00cb1903ba feat(ui): default background to checkerboard 2024-08-27 11:01:52 +10:00
psychedelicious
58ba38b9c7 feat(ui): clean up logging namespaces, allow skipping namespaces 2024-08-27 11:01:52 +10:00
psychedelicious
2f6a5617f9 chore(ui): bump ui library 2024-08-27 11:01:52 +10:00
psychedelicious
e0d84743be fix(ui): do not allow drawing if layer disabled 2024-08-27 11:01:52 +10:00
psychedelicious
ee7c62acc4 fix(ui): stale state causing race conditions & extraneous renders 2024-08-27 11:01:52 +10:00
psychedelicious
daf3e58bd9 fix(ui): do not clear buffer when rendering "real" objects 2024-08-27 11:01:52 +10:00
psychedelicious
c5b9209057 tidy(ui): remove "filter" from CanvasImageState 2024-08-27 11:01:52 +10:00
psychedelicious
2a4d6d98e2 feat(ui): better editable title 2024-08-27 11:01:52 +10:00
psychedelicious
cfdf59d906 fix(ui): stroke eraserline 2024-08-27 11:01:52 +10:00
psychedelicious
f91ce1a47c feat(ui): restore transparency effect for control layers 2024-08-27 11:01:52 +10:00
psychedelicious
4af2888168 feat(ui): use text cursor for entity title 2024-08-27 11:01:52 +10:00
psychedelicious
8471c6fe86 tidy(ui): remove extraneous logging in CanvasStateApi 2024-08-27 11:01:52 +10:00
psychedelicious
fe65a5a2db feat(ui): better buffer commit logic 2024-08-27 11:01:52 +10:00
psychedelicious
0df26e967c feat(ui): render buffer separately from "real" objects 2024-08-27 11:01:52 +10:00
psychedelicious
d4822b305e fix(ui): pixelRect should always be integer 2024-08-27 11:01:52 +10:00
psychedelicious
8df5447563 fix(ui): only update stage attrs when stage itself is dragged 2024-08-27 11:01:52 +10:00
psychedelicious
7b5a43df9b feat(ui): add line simplification
This fixes some awkward issues where line segments stack up.
2024-08-27 11:01:52 +10:00
psychedelicious
61ef630175 fix(ui): various things listening when they need not listen 2024-08-27 11:01:52 +10:00
psychedelicious
4eda2ef555 feat(ui): layer opacity via caching 2024-08-27 11:01:52 +10:00
psychedelicious
57f4489520 feat(ui): reset view fits all visible objects 2024-08-27 11:01:52 +10:00
psychedelicious
fb6cf9e3da fix(ui): rerenders when changing canvas scale 2024-08-27 11:01:52 +10:00
psychedelicious
f776326cff fix(ui): do not render rasterized layer unless renderObjects=true 2024-08-27 11:01:52 +10:00
psychedelicious
5be32d5733 feat(ui): revise app layout strategy, add interaction scopes for hotkeys 2024-08-27 11:01:52 +10:00
psychedelicious
1f73435241 feat(ui): tweak mask patterns 2024-08-27 11:01:52 +10:00
psychedelicious
3251a00631 fix(ui): dynamic prompts recalcs when presets are loaded 2024-08-27 11:01:52 +10:00
psychedelicious
49c4ad1dd7 fix(ui): use style preset prompts correctly 2024-08-27 11:01:52 +10:00
psychedelicious
5857e95c4a fix(ui): discard selected staging image not all other images 2024-08-27 11:01:52 +10:00
psychedelicious
85be2532c6 fix(ui): respect image size in staging preview 2024-08-27 11:01:52 +10:00
psychedelicious
8b81a00def tidy(ui): cleanup after events change 2024-08-27 11:01:52 +10:00
psychedelicious
8544595c27 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-27 11:01:52 +10:00
psychedelicious
a6a5d1470c fix(ui): rebase conflicts 2024-08-27 11:01:39 +10:00
psychedelicious
febcc12ec9 fix(ui): update compositing rect when fill changes 2024-08-27 11:01:39 +10:00
psychedelicious
ab64078b76 feat(ui): add canvas background style 2024-08-27 11:01:39 +10:00
psychedelicious
0ff3459b07 feat(ui): mask layers choose own opacity 2024-08-27 11:01:39 +10:00
psychedelicious
2abd7c9bfe feat(ui): mask fill patterns 2024-08-27 11:01:39 +10:00
psychedelicious
8e5330bdc9 build(ui): add vite types to tsconfig 2024-08-27 11:01:39 +10:00
psychedelicious
1ecec4ea3a fix(ui): do not smooth pixel data when using eyeDropper 2024-08-27 11:01:39 +10:00
psychedelicious
700dbe69f3 tidy(ui): tool components & translations 2024-08-27 11:01:39 +10:00
psychedelicious
af7ba3b7e4 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-27 11:01:39 +10:00
psychedelicious
1f7144d62e fix(ui): ip adapters work 2024-08-27 11:01:39 +10:00
psychedelicious
4e389e415b feat(ui): rename layers 2024-08-27 11:01:39 +10:00
psychedelicious
2db29fb6ab feat(ui): revise entity menus 2024-08-27 11:01:39 +10:00
psychedelicious
79653fcff5 feat(ui): split control layers from raster layers for UI and internal state, same rendering as raster layers 2024-08-27 11:01:39 +10:00
psychedelicious
9e39180fbc feat(ui): implement cache for image rasterization, rip out some old controladapters code 2024-08-27 11:01:39 +10:00
psychedelicious
dc0f832d8f feat(ui, app): use layer as control (wip) 2024-08-27 11:01:39 +10:00
psychedelicious
0dcd6aa5d9 feat(ui): add contextmenu for canvas entities 2024-08-27 11:01:39 +10:00
psychedelicious
9f4a8f11f8 feat(ui): more better logging & naming 2024-08-27 11:01:39 +10:00
psychedelicious
6f9579d6ec feat(ui): better logging w/ path 2024-08-27 11:01:39 +10:00
psychedelicious
2b2aabb234 feat(ui): always show marks on canvas scale slider 2024-08-27 11:01:39 +10:00
psychedelicious
a79b9633ab fix(ui): do not import button from chakra 2024-08-27 11:01:39 +10:00
psychedelicious
7b628c908b fix(ui): scaled bbox preview 2024-08-27 11:01:39 +10:00
psychedelicious
181703a709 feat(ui): tidy up atoms 2024-08-27 11:01:39 +10:00
psychedelicious
c439e3c204 feat(ui): convert all my pubsubs to atoms
its the same but better
2024-08-27 11:01:39 +10:00
psychedelicious
11e81eb456 feat(ui): add trnalsation 2024-08-27 11:01:39 +10:00
psychedelicious
3e24bf640e fix(ui): give up on thumbnail loading, causes flash during transformer 2024-08-27 11:01:39 +10:00
psychedelicious
a17664fb75 fix(ui): depth anything v2 2024-08-27 11:01:39 +10:00
psychedelicious
5aed23dc91 tidy(ui): remove unused code, comments 2024-08-27 11:01:39 +10:00
psychedelicious
7f389716d0 fix(ui): staging area works 2024-08-27 11:01:39 +10:00
psychedelicious
eca13b674a feat(nodes): temp disable canvas output crop 2024-08-27 11:01:39 +10:00
psychedelicious
7c982a1bdf fix(ui): max scale 1 when reset view 2024-08-27 11:01:39 +10:00
psychedelicious
bdfe6870fd feat(ui): better scale changer component, reset view functionality 2024-08-27 11:01:39 +10:00
psychedelicious
7eebbc0dd9 fix(ui): img2img 2024-08-27 11:01:39 +10:00
psychedelicious
6ae46d7c8b feat(ui): add manual scale controls 2024-08-27 11:01:39 +10:00
psychedelicious
8aae30566e fix(ui): do not await clearBuffer 2024-08-27 11:01:39 +10:00
psychedelicious
4b7c3e221c feat(ui): dnd image into layer 2024-08-27 11:01:39 +10:00
psychedelicious
4015795b7f fix(ui): do not await commitBuffer 2024-08-27 11:01:39 +10:00
psychedelicious
132dd61d8d fix(ui): properly destroy entities in manager cleanup 2024-08-27 11:01:39 +10:00
psychedelicious
6f2b548dd1 tidy(ui): clearer component names for regional guidance 2024-08-27 11:01:39 +10:00
psychedelicious
49dd316f17 tidy(ui): clearer component names for ip adapter 2024-08-27 11:01:39 +10:00
psychedelicious
e0a8bb149d tidy(ui): clearer component names for inpaint mask 2024-08-27 11:01:39 +10:00
psychedelicious
020b6db34b tidy(ui): clearer component names for control adapters 2024-08-27 11:01:39 +10:00
psychedelicious
f6d2f0bf8c feat(ui): simplify canvas list item headers 2024-08-27 11:01:39 +10:00
psychedelicious
1280cce803 fix(ui): ip adapter list item 2024-08-27 11:01:39 +10:00
psychedelicious
82463d12e2 tidy(ui): clean up unused logic 2024-08-27 11:01:39 +10:00
psychedelicious
ba6c1b84e4 feat(ui): clean up state, add mutex for image loading, add thumbnail loading 2024-08-27 11:01:39 +10:00
psychedelicious
92b6d3198a chore(ui): add async-mutex dep 2024-08-27 11:01:39 +10:00
psychedelicious
693ae1af50 feat(ui): txt2img, img2img, inpaint & outpaint working 2024-08-27 11:01:39 +10:00
psychedelicious
3873a3096c feat(ui): no padding on transformer outlines 2024-08-27 11:01:39 +10:00
psychedelicious
4a9f6ab5ef feat(ui): restore object count to layer titles 2024-08-27 11:01:39 +10:00
psychedelicious
2fc29c5125 tidy(ui): "useIsEntitySelected" -> "useEntityIsSelected" 2024-08-27 11:01:39 +10:00
psychedelicious
5fbc876cfd tidy(ui): move transformer statics into class 2024-08-27 11:01:39 +10:00
psychedelicious
89b0673ac9 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-27 11:01:39 +10:00
psychedelicious
3179a16189 perf(ui): do not add duplicate points to lines 2024-08-27 11:01:39 +10:00
psychedelicious
3ce216b391 feat(ui): up line tension to 0.3 2024-08-27 11:01:39 +10:00
psychedelicious
8a3a94e21a perf(ui): disable stroke, perfect draw on compositing rect 2024-08-27 11:01:39 +10:00
psychedelicious
f61af188f9 tidy(ui): remove unused code, initial image 2024-08-27 11:01:39 +10:00
psychedelicious
73fc52bfed tidy(ui): remove unused state & actions 2024-08-27 11:01:39 +10:00
psychedelicious
4eeff4eef8 feat(ui): region mask rendering 2024-08-27 11:01:39 +10:00
psychedelicious
cff2c43030 feat(ui): esc cancels drawing buffer
maybe this is not wanted? we'll see
2024-08-27 11:01:39 +10:00
psychedelicious
504b1f2425 fix(ui): render transformer over objects, fix issue w/ inpaint rect color 2024-08-27 11:01:38 +10:00
psychedelicious
eff5b56990 fix(ui): brush preview fill for inpaint/region 2024-08-27 11:01:38 +10:00
psychedelicious
0e673f1a18 fix(ui): no objects rendered until vis toggled 2024-08-27 11:01:38 +10:00
psychedelicious
4fea22aea4 feat(ui): inpaint mask transform 2024-08-27 11:01:38 +10:00
psychedelicious
c2478c9ac3 fix(ui): layer accidental early set isFirstRender=false 2024-08-27 11:01:38 +10:00
psychedelicious
ed8243825e fix(ui): inpaint mask rendering 2024-08-27 11:01:38 +10:00
psychedelicious
c7ac2b5278 feat(ui): wip inpaint mask uses new API 2024-08-27 11:01:38 +10:00
psychedelicious
a783003556 feat(ui): move updatePosition to transformer 2024-08-27 11:01:38 +10:00
psychedelicious
44ba1c6113 feat(ui): move resetScale to transformer 2024-08-27 11:01:38 +10:00
psychedelicious
a02d67fcc6 tidy(ui): more imperative naming 2024-08-27 11:01:38 +10:00
psychedelicious
14c8f7c4f5 tidy(ui): use imperative names for setters in stateapi 2024-08-27 11:01:38 +10:00
psychedelicious
a487ecb50f fix(ui): commit drawing buffer on tool change, fixing bbox not calculating 2024-08-27 11:01:38 +10:00
psychedelicious
fc55862823 fix(ui): sync transformer when requesting bbox calc 2024-08-27 11:01:38 +10:00
psychedelicious
e5400601d6 tidy(ui): rename union CanvasEntity -> CanvasEntityState 2024-08-27 11:01:38 +10:00
psychedelicious
735f9f1483 fix(ui): request rect calc immediately on transform, hiding rect 2024-08-27 11:01:38 +10:00
psychedelicious
d139db0a0f feat(ui): move bbox calculation to transformer 2024-08-27 11:01:38 +10:00
psychedelicious
a35bb450b1 feat(ui): use set for transformer subscriptions 2024-08-27 11:01:38 +10:00
psychedelicious
26c01dfa48 tidy(ui): clean up worker tasks when complete 2024-08-27 11:01:38 +10:00
psychedelicious
2b1839374a tidy(ui): remove unused code in CanvasTool 2024-08-27 11:01:38 +10:00
psychedelicious
59f5f18e1d feat(ui): use pubsub for isTransforming on manager 2024-08-27 11:01:38 +10:00
psychedelicious
e41fcb081c docs(ui): update transformer docstrings 2024-08-27 11:01:38 +10:00
psychedelicious
3032042b35 feat(ui): revised event pubsub, transformer logic split out 2024-08-27 11:01:38 +10:00
psychedelicious
544db61044 feat(ui): add simple pubsub 2024-08-27 11:01:38 +10:00
psychedelicious
67a3aa6dff feat(ui): document & clean up object renderer 2024-08-27 11:01:38 +10:00
psychedelicious
34f4468b20 feat(ui): split out object renderer 2024-08-27 11:01:38 +10:00
psychedelicious
d99ae58001 fix(ui): unable to hold shit while transforming to retain ratio 2024-08-27 11:01:38 +10:00
psychedelicious
d60ec53762 tidy(ui): rename canvas stuff 2024-08-27 11:01:38 +10:00
psychedelicious
0ece9361d5 tidy(ui): consolidate getLoggingContext builders 2024-08-27 11:01:38 +10:00
psychedelicious
69987a2f00 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-27 11:01:38 +10:00
psychedelicious
7346bfccb9 feat(ui): disable image smoothing on layers 2024-08-27 11:01:38 +10:00
psychedelicious
b705083ce2 fix(ui): round position when rasterizing layer 2024-08-27 11:01:38 +10:00
psychedelicious
4b3c82df6f feat(ui): continue modularizing transform 2024-08-27 11:01:38 +10:00
psychedelicious
46290205d5 feat(ui): fix a few things that didn't unsubscribe correctly, add helper to manage subscriptions 2024-08-27 11:01:38 +10:00
psychedelicious
7b15585b80 feat(ui): merge bbox outline into transformer 2024-08-27 11:01:38 +10:00
psychedelicious
cbfeeb9079 fix(ui): update parent's pos not transformers 2024-08-27 11:01:38 +10:00
psychedelicious
fdac20b43e feat(ui): merge interaction rect into transformer class 2024-08-27 11:01:38 +10:00
psychedelicious
ae2312104e feat(ui): prepare staging area 2024-08-27 11:01:38 +10:00
psychedelicious
74b6674af6 feat(ui): typing for logging context 2024-08-27 11:01:38 +10:00
psychedelicious
81adce3238 feat(ui): remove inheritance of CanvasObject
JS is terrible
2024-08-27 11:01:38 +10:00
psychedelicious
48b7f460a8 feat(ui): split & document transformer logic, iterate on class structures 2024-08-27 11:01:38 +10:00
psychedelicious
38a8232341 feat(ui): rotation snap to nearest 45deg when holding shift 2024-08-27 11:01:38 +10:00
psychedelicious
39a51c4f4c feat(ui): expose subscribe method for nanostores 2024-08-27 11:01:38 +10:00
psychedelicious
220bfeb37d tidy(ui): remove layer scaling reducers 2024-08-27 11:01:38 +10:00
psychedelicious
e766279950 fix(ui): pixel-perfect transforms 2024-08-27 11:01:38 +10:00
psychedelicious
db82406525 fix(ui): layer visibility toggle 2024-08-27 11:01:38 +10:00
psychedelicious
2d44f332d9 fix(nodes): fix canvas mask erode
it wasn't eroding enough and caused incorrect transparency in result images
2024-08-27 11:01:38 +10:00
psychedelicious
ea1526689a fix(ui): do not reset layer on first render 2024-08-27 11:01:38 +10:00
psychedelicious
200338ed72 feat(ui): revised logging and naming setup, fix staging area 2024-08-27 11:01:38 +10:00
psychedelicious
88003a61bd feat(ui): add repr methods to layer and object classes 2024-08-27 11:01:38 +10:00
psychedelicious
b82089b30b feat(ui): use nanoid(10) instead of uuidv4 for canvas
Shorter ids makes it much more readable
2024-08-27 11:01:38 +10:00
psychedelicious
efd780d395 build(ui): add nanoid as explicit dep 2024-08-27 11:01:38 +10:00
psychedelicious
3f90f783de fix(ui): move CanvasImage's konva image to correct object 2024-08-27 11:01:38 +10:00
psychedelicious
4f5b755117 fix(ui): prevent flash when applying transform 2024-08-27 11:01:38 +10:00
psychedelicious
a455f12581 build(ui): add eslint rules for async stuff 2024-08-27 11:01:38 +10:00
psychedelicious
de516db383 feat(ui): trying to fix flicker after transform 2024-08-27 11:01:38 +10:00
psychedelicious
6781575293 feat(ui): transform cleanup 2024-08-27 11:01:38 +10:00
psychedelicious
65b0e40fc8 feat(ui): fix transform when rotated 2024-08-27 11:01:38 +10:00
psychedelicious
19e78a07b7 fix(ui): use pixel bbox when image is in layer 2024-08-27 11:01:38 +10:00
psychedelicious
e3b60dda07 fix(ui): transforming when axes flipped 2024-08-27 11:01:38 +10:00
psychedelicious
a9696d3193 feat(ui): hallelujah (???) 2024-08-27 11:01:38 +10:00
psychedelicious
336d72873f feat(ui): add debug button 2024-08-27 11:01:38 +10:00
psychedelicious
111a380bce fix(ui): transformer padding 2024-08-27 11:01:38 +10:00
psychedelicious
012a8351af feat(ui): wip transform mode 2 2024-08-27 11:01:38 +10:00
psychedelicious
deeb80ea9b feat(ui): wip transform mode 2024-08-27 11:01:38 +10:00
psychedelicious
1e97a917d6 feat(ui): wip transform mode 2024-08-27 11:01:38 +10:00
psychedelicious
a15ba925db fix(ui): dnd to canvas broke 2024-08-27 11:01:38 +10:00
psychedelicious
6e5a968aad fix(ui): conflicts after rebasing 2024-08-27 11:01:38 +10:00
psychedelicious
915edaa02f fix(ui): imageDropped listener 2024-08-27 11:01:38 +10:00
psychedelicious
f6b0fa7c18 wip 2024-08-27 11:01:38 +10:00
psychedelicious
3f1fba0f35 fix(ui): transform tool seems to be working 2024-08-27 11:01:38 +10:00
psychedelicious
d9fa85a4c6 fix(ui): move tool fixes, add transform tool 2024-08-27 11:01:38 +10:00
psychedelicious
022bb8649c feat(ui): move tool now only moves 2024-08-27 11:01:38 +10:00
psychedelicious
673bc33a01 feat(ui): layer bbox calc in worker 2024-08-27 11:01:38 +10:00
psychedelicious
579a64928d feat(ui): tweaked entity & group selection styles 2024-08-27 11:01:38 +10:00
psychedelicious
5316df7d7d feat(ui): canvas entity list headers 2024-08-27 11:01:38 +10:00
psychedelicious
62fe61dc30 tidy(ui): CanvasRegion 2024-08-27 11:01:38 +10:00
psychedelicious
ea819a4a2f tidy(ui): CanvasRect 2024-08-27 11:01:38 +10:00
psychedelicious
868a25dae2 tidy(ui): CanvasLayer 2024-08-27 11:01:38 +10:00
psychedelicious
3a25d00cb6 tidy(ui): CanvasInpaintMask 2024-08-27 11:01:38 +10:00
psychedelicious
6301b74d87 tidy(ui): CanvasInitialImage 2024-08-27 11:01:38 +10:00
psychedelicious
b43b90c299 tidy(ui): CanvasImage 2024-08-27 11:01:38 +10:00
psychedelicious
0a43444ab3 tidy(ui): CanvasEraserLine 2024-08-27 11:01:38 +10:00
psychedelicious
75ea4d2155 tidy(ui): CanvasControlAdapter 2024-08-27 11:01:38 +10:00
psychedelicious
9d281388e0 tidy(ui): CanvasBrushLine 2024-08-27 11:01:38 +10:00
psychedelicious
178e1cc50b tidy(ui): CanvasBbox 2024-08-27 11:01:38 +10:00
psychedelicious
6eafe53b2d tidy(ui): CanvasBackground 2024-08-27 11:01:38 +10:00
psychedelicious
7514b2b7d4 tidy(ui): update canvas classes, organise location of konva nodes 2024-08-27 11:01:38 +10:00
psychedelicious
51dee2dba2 feat(ui): add names to all konva objects
Makes troubleshooting much simpler
2024-08-27 11:01:38 +10:00
psychedelicious
a81c3d841c 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-27 11:01:38 +10:00
psychedelicious
154487f966 feat(ui): use position and dimensions instead of separate x,y,width,height attrs 2024-08-27 11:01:38 +10:00
psychedelicious
588daafcf5 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-27 11:01:38 +10:00
psychedelicious
ab00097aed feat(ui): rename types size and position to dimensions and coordinate 2024-08-27 11:01:38 +10:00
psychedelicious
5aefae71a2 tidy(ui): hide layer settings by default 2024-08-27 11:01:38 +10:00
psychedelicious
0edd598970 fix(ui): layer rendering when starting as disabled 2024-08-27 11:01:38 +10:00
psychedelicious
0bb485031f feat(invocation): reduce canvas v2 mask & crop mask dilation 2024-08-27 11:01:38 +10:00
psychedelicious
01ffd86367 feat(ui): de-jank staging area and progress images 2024-08-27 11:01:38 +10:00
psychedelicious
e2e02f31b6 feat(ui): update staging handling to work w/ cropped mask 2024-08-27 11:01:38 +10:00
psychedelicious
0008617348 chore(ui): typegen 2024-08-27 11:01:38 +10:00
psychedelicious
f8f21c0edd feat(app): update CanvasV2MaskAndCropInvocation 2024-08-27 11:01:38 +10:00
psychedelicious
010916158b feat(ui): use new canvas output node 2024-08-27 11:01:38 +10:00
psychedelicious
df0ba004ca chore(ui): typegen 2024-08-27 11:01:38 +10:00
psychedelicious
038b29e15b feat(app): add CanvasV2MaskAndCropInvocation & CanvasV2MaskAndCropOutput
This handles some masking and cropping that the canvas needs.
2024-08-27 11:01:21 +10:00
psychedelicious
763ab73923 fix(ui): restore nodes output tracking 2024-08-27 11:01:21 +10:00
psychedelicious
9a060b4437 feat(ui): rip out document size
barely knew ye
2024-08-27 11:01:21 +10:00
psychedelicious
2307a30892 feat(ui): convert initial image to layer when starting canvas session 2024-08-27 11:01:21 +10:00
psychedelicious
2006f84f6e fix(ui): fix layer transparency calculation 2024-08-27 11:01:21 +10:00
psychedelicious
23b15fef6a fix(ui): reset initial image when resetting canvas 2024-08-27 11:01:21 +10:00
psychedelicious
41e72f929d fix(ui): reset node executions states when loading workflow 2024-08-27 11:01:21 +10:00
psychedelicious
29fc49bb3b fix(ui): entity display list 2024-08-27 11:01:21 +10:00
psychedelicious
6f65b6a40f feat(ui): img2img working 2024-08-27 11:01:21 +10:00
psychedelicious
1e9b22e3a4 feat(ui): rough out img2img on canvas 2024-08-27 11:01:21 +10:00
psychedelicious
3dc2c723c3 UNDO ME WIP 2024-08-27 11:01:21 +10:00
psychedelicious
6f007cbd48 feat(ui): log invocation source id on socket event 2024-08-27 11:01:21 +10:00
psychedelicious
9a402dd10e feat(ui): restore document size overlay renderer 2024-08-27 11:01:21 +10:00
psychedelicious
4249d0e13b feat(ui): make documnet size a rect 2024-08-27 11:01:21 +10:00
psychedelicious
f6050bad67 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-27 11:01:21 +10:00
psychedelicious
d3a4b7b51b feat(ui): initialState is for generation mode 2024-08-27 11:01:21 +10:00
psychedelicious
3f5f9ac764 feat(ui): split out canvas entity list component 2024-08-27 11:01:21 +10:00
psychedelicious
e7c1299a7f feat(ui): hide bbox button when no canvas session active 2024-08-27 11:01:21 +10:00
psychedelicious
56a3918a1e 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-27 11:01:21 +10:00
psychedelicious
dabf7718cf feat(ui): split up tool chooser buttons
Prep for distinct toolbars for generation vs canvas modes
2024-08-27 11:01:21 +10:00
psychedelicious
ef22c29288 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-27 11:01:21 +10:00
psychedelicious
74f06074f7 feat(ui): "stagingArea" -> "session" 2024-08-27 11:01:21 +10:00
psychedelicious
b97bf52faa feat(ui): add reset button to canvas 2024-08-27 11:01:21 +10:00
psychedelicious
0a77f5cec8 feat(ui): add snapToRect util 2024-08-27 11:01:21 +10:00
psychedelicious
f8e92f7b73 fix(ui): fiddle with control adapter filters
some jank still
2024-08-27 11:01:21 +10:00
psychedelicious
530c6e3a59 feat(ui): temp disable doc size overlay 2024-08-27 11:01:21 +10:00
psychedelicious
11b95cfaf4 feat(ui): no animation on layer selection
Felt sluggish
2024-08-27 11:01:21 +10:00
psychedelicious
707c005a26 feat(ui): use canvas as source for control images (wip) 2024-08-27 11:01:21 +10:00
psychedelicious
19fa8e7e33 fix(ui): control adapter translate & scale 2024-08-27 11:01:21 +10:00
psychedelicious
b252ded366 tidy(ui): removed unused state related to non-buffered drawing 2024-08-27 11:01:21 +10:00
psychedelicious
84305d4e73 feat(ui): control adapter image rendering 2024-08-27 11:01:21 +10:00
psychedelicious
1150b41e14 fix(ui): do not floor bbox calc, it cuts off the last pixels 2024-08-27 11:01:21 +10:00
psychedelicious
88d8ccb34b feat(ui): fix issue where creating line needs 2 points 2024-08-27 11:01:21 +10:00
psychedelicious
4111b3f1aa fix(ui): edge cases when holding shift and drawing lines 2024-08-27 11:01:21 +10:00
psychedelicious
36862be2aa fix(ui): set buffered rect color to full alpha 2024-08-27 11:01:21 +10:00
psychedelicious
425665e0d9 fix(ui): handle mouseup correctly 2024-08-27 11:01:21 +10:00
psychedelicious
9abd604f69 feat(ui): buffered rect drawing 2024-08-27 11:01:21 +10:00
psychedelicious
59bdc288b5 fix(ui): buffered drawing edge cases 2024-08-27 11:01:21 +10:00
psychedelicious
eb37d2958e perf(ui): do not use stage.find 2024-08-27 11:01:21 +10:00
psychedelicious
2a9738a341 perf(ui): object groups do not listen 2024-08-27 11:01:21 +10:00
psychedelicious
6aac1cf33a perf(ui): buffered drawing (wip) 2024-08-27 11:01:21 +10:00
psychedelicious
9ca4d072ab tidy(ui): organise files 2024-08-27 11:01:21 +10:00
psychedelicious
7aaf14c26b tidy(ui): organise files 2024-08-27 11:01:21 +10:00
psychedelicious
cf598ca175 tidy(ui): organise files 2024-08-27 11:01:21 +10:00
psychedelicious
a722790afc fix(ui): background rendering 2024-08-27 11:01:21 +10:00
psychedelicious
320151a040 pkg(ui): remove unused deps react-konva & use-image 2024-08-27 11:01:21 +10:00
psychedelicious
c090f511c3 feat(ui): organize konva state and files 2024-08-27 11:01:21 +10:00
psychedelicious
86dd1475b3 fix(ui): merge conflicts in image deletion listener 2024-08-27 11:01:21 +10:00
psychedelicious
0b71ac258c fix(ui): region rendering 2024-08-27 11:01:21 +10:00
psychedelicious
54e1eae509 fix(ui): inpaint mask rendering 2024-08-27 11:01:21 +10:00
psychedelicious
bf57b2dc77 fix(ui): staging area rendering 2024-08-27 11:01:21 +10:00
psychedelicious
de3c27b44f fix(ui): stale selected entity 2024-08-27 11:01:21 +10:00
psychedelicious
05717fea93 fix(ui): staging area image offset 2024-08-27 11:01:21 +10:00
psychedelicious
191584d229 feat(ui): tweak layer ui component 2024-08-27 11:01:21 +10:00
psychedelicious
6069169e6b fix(ui): resetting layer resets position 2024-08-27 11:01:21 +10:00
psychedelicious
07438587f3 feat(ui): updated layer list component styling 2024-08-27 11:01:21 +10:00
psychedelicious
913e36d6fd feat(ui): transformable layers 2024-08-27 11:01:21 +10:00
psychedelicious
139004b976 feat(ui): move tool icon is pointer like in other apps 2024-08-27 11:01:21 +10:00
psychedelicious
ef4269d585 feat(ui): do not floor cursor position 2024-08-27 11:01:21 +10:00
psychedelicious
954cb129a4 feat(ui): disable gallery hotkeys while staging 2024-08-27 11:01:21 +10:00
psychedelicious
02c4b28de5 feat(ui): revised canvas progress & staging image handling 2024-08-27 11:01:21 +10:00
psychedelicious
febea88b58 feat(ui): show queue item origin in queue list 2024-08-27 11:01:21 +10:00
psychedelicious
40ccfac514 chore(ui): typegen 2024-08-27 11:01:21 +10:00
psychedelicious
831fb814cc 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-27 11:01:21 +10:00
psychedelicious
bf166fdd61 fix(ui): denoise start on outpainting 2024-08-27 11:01:21 +10:00
psychedelicious
384bde3539 feat(ui): add redux events for queue cleared & batch enqueued socket events 2024-08-27 11:01:21 +10:00
psychedelicious
6f1d238d0a feat(ui): canvas staging area works 2024-08-27 11:01:21 +10:00
psychedelicious
ac524153a7 feat(ui): switch to view tool when staging 2024-08-27 11:01:21 +10:00
psychedelicious
2cad2b15cf tidy(ui): disable preview images on every enqueue 2024-08-27 11:01:21 +10:00
psychedelicious
fd63e202fe feat(ui): rough out save staging image 2024-08-27 11:01:21 +10:00
psychedelicious
a0250e47e3 feat(ui): staging area image visibility toggle 2024-08-27 11:01:21 +10:00
psychedelicious
7d8ece45bb fix(ui): batch building after removing canvas files 2024-08-27 11:01:21 +10:00
psychedelicious
ffb8f053da feat(ui): make Graph class's getMetadataNode public 2024-08-27 11:01:21 +10:00
psychedelicious
fb46f457f9 tidy(ui): remove old canvas graphs 2024-08-27 11:01:21 +10:00
psychedelicious
6d4f4152a7 fix(ui): do not select already-selected entity 2024-08-27 11:01:20 +10:00
psychedelicious
d3d0ac7327 tidy(ui): naming things 2024-08-27 11:01:20 +10:00
psychedelicious
f57df46995 tidy(ui): file organisation 2024-08-27 11:01:20 +10:00
psychedelicious
a747171745 fix(ui): reset cursor pos when fitting document 2024-08-27 11:01:20 +10:00
psychedelicious
e9ae9e80d4 feat(ui): staging area works more better 2024-08-27 11:01:20 +10:00
psychedelicious
3b9a59b98d feat(ui): staging area barely works 2024-08-27 11:01:20 +10:00
psychedelicious
8a381e7f74 feat(ui): consolidate konva API 2024-08-27 11:01:20 +10:00
psychedelicious
61513fc800 feat(ui): consolidate konva API 2024-08-27 11:01:20 +10:00
psychedelicious
1d26c49e92 feat(ui): staging area (rendering wip) 2024-08-27 11:01:20 +10:00
psychedelicious
77be9836d2 tidy(ui): type "Dimensions" -> "Size" 2024-08-27 11:01:20 +10:00
psychedelicious
4427960acb feat(ui): add updateNode to Graph 2024-08-27 11:01:20 +10:00
psychedelicious
84aa4fb7bc feat(ui): sdxl graphs 2024-08-27 11:01:20 +10:00
psychedelicious
01df96cbe0 feat(ui): sd1 outpaint graph 2024-08-27 11:01:20 +10:00
psychedelicious
1ac0634f57 tests(ui): add missing tests for Graph class 2024-08-27 11:01:20 +10:00
psychedelicious
8e7d3634b1 feat(ui): add Graph.getid() util 2024-08-27 11:01:20 +10:00
psychedelicious
fadafe5c77 feat(ui): outpaint graph, organize builder a bit 2024-08-27 11:01:20 +10:00
psychedelicious
b2ea1f6690 feat(ui): inpaint sd1 graph 2024-08-27 11:01:20 +10:00
psychedelicious
0a03c1f882 feat(ui): temp disable image caching while testing 2024-08-27 11:01:20 +10:00
psychedelicious
ce8b490ed8 feat(ui): txt2img & img2img graphs 2024-08-27 11:01:20 +10:00
psychedelicious
86eccba80d feat(ui): minor change to canvas bbox state type 2024-08-27 11:01:20 +10:00
psychedelicious
97453e7c6c feat(ui): simplified konva node to blob/imagedata utils 2024-08-27 11:01:20 +10:00
psychedelicious
9e1084b701 feat(ui): node manager getter/setter 2024-08-27 11:01:20 +10:00
psychedelicious
41aec81f3f feat(ui): generation mode calculation, fudged graphs 2024-08-27 11:01:20 +10:00
psychedelicious
a5741a0551 feat(ui): add utils for getting images from canvas 2024-08-27 11:01:20 +10:00
psychedelicious
72f73c231a feat(ui): even more simplified API - lean on the konva node manager to abstract imperative state API & rendering 2024-08-27 11:01:20 +10:00
psychedelicious
b8fcaa274e feat(ui): revised docstrings for renderers & simplified api 2024-08-27 11:01:20 +10:00
psychedelicious
a33bbf48bb feat(ui): inpaint mask UI components 2024-08-27 11:01:20 +10:00
psychedelicious
98d9490fb9 feat(ui): inpaint mask rendering (wip) 2024-08-27 11:01:20 +10:00
psychedelicious
51c643c4f8 fix(ui): models loaded handler 2024-08-27 11:01:20 +10:00
psychedelicious
2d7370ca6c feat(ui): internal state for inpaint mask 2024-08-27 11:01:20 +10:00
psychedelicious
0d68141387 refactor(ui): divvy up canvas state a bit 2024-08-27 11:01:20 +10:00
psychedelicious
e88a8c6639 feat(ui): get region and base layer canvas to blob logic working 2024-08-27 11:01:20 +10:00
psychedelicious
2d04bb286e refactor(ui): node manager handles more tedious annoying stuff 2024-08-27 11:01:20 +10:00
psychedelicious
6d9ba24c32 feat(ui): use node manager for addRegions 2024-08-27 11:01:20 +10:00
psychedelicious
7645a1c86e feat(ui): persist bbox 2024-08-27 11:01:20 +10:00
psychedelicious
dc284b9a48 fix(ui): fix generation graphs 2024-08-27 11:01:20 +10:00
psychedelicious
8a38332d44 feat(ui): add toggle for clipToBbox 2024-08-27 11:01:20 +10:00
psychedelicious
2407d7d148 feat(ui): rename konva node manager 2024-08-27 11:01:20 +10:00
psychedelicious
23275cf607 refactor(ui): create classes to abstract mgmt of konva nodes 2024-08-27 11:01:20 +10:00
psychedelicious
8a0e02d335 tidy(ui): organise renderers 2024-08-27 11:01:20 +10:00
psychedelicious
913873623c 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-27 11:01:20 +10:00
psychedelicious
5d81e7dd4d perf(ui): fix lag w/ region rendering
Needed to memoize these selectors
2024-08-27 11:01:20 +10:00
psychedelicious
418650fdf3 feat(ui): move canvas fill color picker to right 2024-08-27 11:01:20 +10:00
psychedelicious
7c24e56d9f refactor(ui): remove unused ellipse & polygon objects 2024-08-27 11:01:20 +10:00
psychedelicious
f6faed46c3 fix(ui): incorrect rect/brush/eraser positions 2024-08-27 11:01:20 +10:00
psychedelicious
1d393eecf1 refactor(ui): enable global debugging flag 2024-08-27 11:01:20 +10:00
psychedelicious
03a72240c0 refactor(ui): disable the preview renderer for now 2024-08-27 11:01:20 +10:00
psychedelicious
acf62450fb tweak(ui): canvas editor layout 2024-08-27 11:01:20 +10:00
psychedelicious
d0269310cf perf(ui): memoize layeractionsmenu valid actions 2024-08-27 11:01:20 +10:00
psychedelicious
43618a74e7 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-27 11:01:20 +10:00
psychedelicious
56fed637ec feat(ui): clip lines to bbox 2024-08-27 11:01:20 +10:00
psychedelicious
944ae4a604 fix(ui): document fit positioning 2024-08-27 11:01:20 +10:00
psychedelicious
f3da609102 feat(ui): document bounds overlay 2024-08-27 11:01:20 +10:00
psychedelicious
11c8a8cf72 tidy(ui): background layer 2024-08-27 11:01:20 +10:00
psychedelicious
656978676f refactor(ui): use "entity" instead of "data" for canvas 2024-08-27 11:01:20 +10:00
psychedelicious
d216e6519f feat(ui): brush size border radius = 1 2024-08-27 11:01:20 +10:00
psychedelicious
0a2bcae0e3 fix(ui): canvas HUD doesn't interrupt tool 2024-08-27 11:01:20 +10:00
psychedelicious
31cf244420 refactor(ui): split up canvas entity renderers, temp disable preview 2024-08-27 11:01:20 +10:00
psychedelicious
d761effac1 fix(ui): delete all layers button 2024-08-27 11:01:20 +10:00
psychedelicious
5efaf2c661 fix(ui): ignore keyboard shortcuts in input/textarea elements 2024-08-27 11:01:20 +10:00
psychedelicious
b79161fbec fix(ui): canvas entity ids getting clobbered 2024-08-27 11:01:20 +10:00
psychedelicious
89a2f2134b fix(ui): move lora followup fixes 2024-08-27 11:01:20 +10:00
psychedelicious
343c3b19b1 chore(ui): lint 2024-08-27 11:01:20 +10:00
psychedelicious
e6ec646b2c refactor(ui): move loras to canvas slice 2024-08-27 11:01:20 +10:00
psychedelicious
b557fe4e40 fix(ui): layer is selected when added 2024-08-27 11:01:20 +10:00
psychedelicious
45908dfbd2 feat(ui): r to center & fit stage on document 2024-08-27 11:01:20 +10:00
psychedelicious
1cf0673a22 feat(ui): better HUD 2024-08-27 11:01:20 +10:00
psychedelicious
189847f8a5 fix(ui): always use current brush width when making straight lines 2024-08-27 11:01:20 +10:00
psychedelicious
b27929f702 feat(ui): hold shift w/ brush to draw straight line 2024-08-27 11:01:20 +10:00
psychedelicious
b1a6a9835d fix(ui): update bg on canvas resize 2024-08-27 11:01:20 +10:00
psychedelicious
5564a16d4b refactor(ui): better hud 2024-08-27 11:01:20 +10:00
psychedelicious
b2aa447d50 refactor(ui): scaled tool preview border 2024-08-27 11:01:20 +10:00
psychedelicious
8666f9a848 refactor(ui): port remaining canvasV1 rendering logic to V2, remove old code 2024-08-27 11:01:20 +10:00
psychedelicious
e7dc7c4917 refactor(ui): fix more types 2024-08-27 11:01:20 +10:00
psychedelicious
513d0f1e5c refactor(ui): metadata recall (wip)
just enough let the app run
2024-08-27 11:01:20 +10:00
psychedelicious
c6ce7618cf refactor(ui): undo/redo button temp fix 2024-08-27 11:01:20 +10:00
psychedelicious
9b78b8dc91 refactor(ui): fix renderer stuff 2024-08-27 11:01:20 +10:00
psychedelicious
3bae233f40 refactor(ui): fix misc types 2024-08-27 11:01:20 +10:00
psychedelicious
0a305c4291 refactor(ui): fix gallery stuff 2024-08-27 11:01:20 +10:00
psychedelicious
54fe4ddf3e refactor(ui): fix delete image stuff 2024-08-27 11:01:20 +10:00
psychedelicious
aa877b981c refactor(ui): fix useIsReadyToEnqueue for new adapterType field 2024-08-27 11:01:20 +10:00
psychedelicious
b8ec4348a5 refactor(ui): update generation tab graphs 2024-08-27 11:01:20 +10:00
psychedelicious
0e3e27668c refactor(ui): add adapterType to ControlAdapterData 2024-08-27 11:01:20 +10:00
psychedelicious
e8c8025119 refactor(ui): update components & logic to use new unified slice (again) 2024-08-27 11:01:20 +10:00
psychedelicious
6a72eda5d2 refactor(ui): update components & logic to use new unified slice 2024-08-27 11:01:20 +10:00
psychedelicious
35c8c54466 refactor(ui): merge compositing, params into canvasV2 slice 2024-08-27 10:59:24 +10:00
psychedelicious
2eda45ca5b refactor(ui): add scaled bbox state 2024-08-27 10:59:24 +10:00
psychedelicious
ecd6e7960c refactor(ui): update dnd/image upload 2024-08-27 10:59:24 +10:00
psychedelicious
0d3a61cbdb refactor(ui): update size/prompts state 2024-08-27 10:59:24 +10:00
psychedelicious
6737c275d7 refactor(ui): rip out old control adapter implementation 2024-08-27 10:59:24 +10:00
psychedelicious
201a3e5838 refactor(ui): canvas v2 (wip)
fix entity count select
2024-08-27 10:59:24 +10:00
psychedelicious
85cb239219 refactor(ui): canvas v2 (wip)
delete unused file
2024-08-27 10:59:24 +10:00
psychedelicious
002f45e383 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-27 10:59:24 +10:00
psychedelicious
470e5ba290 refactor(ui): canvas v2 (wip)
Fix a few more components
2024-08-27 10:59:24 +10:00
psychedelicious
b6722b3a10 refactor(ui): canvas v2 (wip)
missed a spot
2024-08-27 10:59:24 +10:00
psychedelicious
6a624916ca refactor(ui): canvas v2 (wip)
Redo all UI components for different canvas entity types
2024-08-27 10:59:24 +10:00
psychedelicious
65653e0932 refactor(ui): canvas v2 (wip) 2024-08-27 10:59:24 +10:00
psychedelicious
a7e59d6697 refactor(ui): canvas v2 (wip) 2024-08-27 10:59:24 +10:00
psychedelicious
f1356105c1 refactor(ui): canvas v2 (wip) 2024-08-27 10:59:24 +10:00
psychedelicious
8acc6379fb refactor(ui): canvas v2 (wip) 2024-08-27 10:59:24 +10:00
psychedelicious
d13014c5d9 feat(ui): bbox tool 2024-08-27 10:59:24 +10:00
psychedelicious
db11d8ba90 fix(ui): rect tool preview 2024-08-27 10:59:24 +10:00
psychedelicious
46a4ee2360 fix(ui): multiple stages 2024-08-27 10:59:24 +10:00
psychedelicious
a19c053b88 feat(ui): decouple konva logic from nanostores 2024-08-27 10:59:24 +10:00
psychedelicious
5b47a32d31 feat(ui): store all stage attrs in nanostores 2024-08-27 10:59:24 +10:00
psychedelicious
32ae9efb6a feat(ui): round stage scale 2024-08-27 10:59:24 +10:00
psychedelicious
b81bee551a chore(ui): bump konva 2024-08-27 10:59:24 +10:00
psychedelicious
8c9dffd082 feat(ui): generation bbox transformation working
whew
2024-08-27 10:59:24 +10:00
psychedelicious
cdfae643e4 feat(ui): wip generation bbox 2024-08-27 10:59:24 +10:00
psychedelicious
cb93108206 feat(ui): wip generation bbox 2024-08-27 10:59:24 +10:00
psychedelicious
c11343dc1c feat(ui): CL zoom and pan, some rendering optimizations 2024-08-27 10:59:24 +10:00
psychedelicious
3733c6f89d Revert "feat(ui): add x,y,scaleX,scaleY,rotation to objects"
This reverts commit 53318b396c967c72326a7e4dea09667b2ab20bdd.
2024-08-27 10:59:24 +10:00
psychedelicious
3c23a0eac0 feat(ui): layers manage their own bbox 2024-08-27 10:59:24 +10:00
psychedelicious
7fd69ab1f1 docs(ui): konva image object docstrings 2024-08-27 10:59:24 +10:00
psychedelicious
3f511774de feat(ui): add x,y,scaleX,scaleY,rotation to objects 2024-08-27 10:59:24 +10:00
psychedelicious
3600100879 fix(ui): show color picker when using rect tool 2024-08-27 10:59:24 +10:00
psychedelicious
8fae372103 feat(ui): image loading fallback for raster layers 2024-08-27 10:59:24 +10:00
psychedelicious
2fce1fe04c feat(ui): bbox calc for raster layers 2024-08-27 10:59:24 +10:00
psychedelicious
22f3e975c7 feat(ui): do not fill brush preview when drawing 2024-08-27 10:59:24 +10:00
psychedelicious
c6ced5a210 fix(ui): brush spacing handling 2024-08-27 10:59:24 +10:00
psychedelicious
5d66e85205 fix(ui): jank when starting a shape when not already focused on stage 2024-08-27 10:59:24 +10:00
psychedelicious
649f163bf7 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-27 10:59:24 +10:00
psychedelicious
4c821bd930 feat(ui): support image objects on raster layers
Just the UI and internal state, not rendering yet.
2024-08-27 10:59:24 +10:00
psychedelicious
6359de4e25 tidy(ui): clean up event handlers
Separate logic for each tool in preparation for ellipse and polygon tools.
2024-08-27 10:59:24 +10:00
psychedelicious
2c610c8cd4 feat(ui): raster layer reset, object group util 2024-08-27 10:59:24 +10:00
psychedelicious
7efe8a249b feat(ui): rect shape preview now has fill 2024-08-27 10:59:24 +10:00
psychedelicious
bd421d184e feat(ui): cancel shape drawing on esc 2024-08-27 10:59:24 +10:00
psychedelicious
78f5844ba0 feat(ui): temp disable history on CL 2024-08-27 10:59:23 +10:00
psychedelicious
f6bb4d5051 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-27 10:59:23 +10:00
psychedelicious
56642c3e87 feat(ui): add raster layer rendering and interaction (WIP) 2024-08-27 10:59:23 +10:00
psychedelicious
9fd8678d3d 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-27 10:59:23 +10:00
psychedelicious
8b89518fd6 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-27 10:59:23 +10:00
117 changed files with 1190 additions and 2046 deletions

View File

@@ -45,13 +45,11 @@ 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
@@ -130,7 +128,6 @@ 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,10 +40,7 @@ class FluxTextEncoderInvocation(BaseInvocation):
@torch.no_grad()
def invoke(self, context: InvocationContext) -> FluxConditioningOutput:
# 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)
t5_embeddings, clip_embeddings = self._encode_prompt(context)
conditioning_data = ConditioningFieldData(
conditionings=[FLUXConditioningInfo(clip_embeds=clip_embeddings, t5_embeds=t5_embeddings)]
)
@@ -51,7 +48,12 @@ class FluxTextEncoderInvocation(BaseInvocation):
conditioning_name = context.conditioning.save(conditioning_data)
return FluxConditioningOutput.build(conditioning_name)
def _t5_encode(self, context: InvocationContext) -> torch.Tensor:
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.
t5_tokenizer_info = context.models.load(self.t5_encoder.tokenizer)
t5_text_encoder_info = context.models.load(self.t5_encoder.text_encoder)
@@ -68,15 +70,6 @@ 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,
@@ -88,5 +81,6 @@ class FluxTextEncoderInvocation(BaseInvocation):
pooled_prompt_embeds = clip_encoder(prompt)
assert isinstance(prompt_embeds, torch.Tensor)
assert isinstance(pooled_prompt_embeds, torch.Tensor)
return pooled_prompt_embeds
return prompt_embeds, pooled_prompt_embeds

View File

@@ -58,7 +58,13 @@ class FluxTextToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
@torch.no_grad()
def invoke(self, context: InvocationContext) -> ImageOutput:
latents = self._run_diffusion(context)
# 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)
image = self._run_vae_decoding(context, latents)
image_dto = context.images.save(image=image)
return ImageOutput.build(image_dto)
@@ -66,19 +72,11 @@ class FluxTextToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
def _run_diffusion(
self,
context: InvocationContext,
clip_embeddings: torch.Tensor,
t5_embeddings: torch.Tensor,
):
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)
inference_dtype = torch.bfloat16
# Prepare input noise.
x = get_noise(
@@ -90,19 +88,24 @@ class FluxTextToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
seed=self.seed,
)
x, img_ids = prepare_latent_img_patches(x)
img, 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=x.shape[1],
image_seq_len=img.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)
@@ -137,7 +140,7 @@ class FluxTextToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
x = denoise(
model=transformer,
img=x,
img=img,
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.4",
version="1.0.3",
classification=Classification.Prototype,
)
class FluxModelLoaderInvocation(BaseInvocation):
@@ -169,35 +169,23 @@ class FluxModelLoaderInvocation(BaseInvocation):
input=Input.Direct,
)
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,
t5_encoder: ModelIdentifierField = InputField(
description=FieldDescriptions.t5_encoder,
ui_type=UIType.T5EncoderModel,
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:
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})
model_key = self.model.key
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)
@@ -209,6 +197,52 @@ 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,8 +88,7 @@ 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 queue item")
destination: str | None = Field(default=None, description="The destination of the queue item")
origin: str | None = Field(default=None, description="The origin of the batch")
class InvocationEventBase(QueueItemEventBase):
@@ -115,7 +114,6 @@ 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],
@@ -150,7 +148,6 @@ 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],
@@ -189,7 +186,6 @@ 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],
@@ -223,7 +219,6 @@ 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],
@@ -262,7 +257,6 @@ 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,14 +77,7 @@ 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 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",
)
origin: str | None = Field(default=None, description="The origin of this batch.")
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(
@@ -203,14 +196,7 @@ 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. 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",
)
origin: str | None = Field(default=None, description="The origin of this queue item. ")
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."
)
@@ -311,7 +297,6 @@ 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'")
@@ -458,7 +443,6 @@ class SessionQueueValueToInsert(NamedTuple):
priority: int # priority
workflow: Optional[str] # workflow json
origin: str | None
destination: str | None
ValuesToInsert: TypeAlias = list[SessionQueueValueToInsert]
@@ -480,7 +464,6 @@ 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, destination)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
INSERT INTO session_queue (queue_id, session, session_id, batch_id, field_values, priority, workflow, origin)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
values_to_insert,
)
@@ -579,8 +579,7 @@ class SqliteSessionQueue(SessionQueueBase):
session_id,
batch_id,
queue_id,
origin,
destination
origin
FROM session_queue
WHERE queue_id = ?
"""
@@ -660,7 +659,7 @@ class SqliteSessionQueue(SessionQueueBase):
self.__lock.acquire()
self.__cursor.execute(
"""--sql
SELECT status, count(*), origin, destination
SELECT status, count(*), origin
FROM session_queue
WHERE
queue_id = ?
@@ -673,7 +672,6 @@ 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
@@ -683,7 +681,6 @@ 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,11 +10,9 @@ 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:
@@ -23,7 +21,6 @@ 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.4",
"version": "1.0.0",
"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": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
"nodeId": "4f0207c2-ff40-41fd-b047-ad33fbb1c33a",
"fieldName": "model"
},
{
@@ -20,8 +20,8 @@
"fieldName": "num_steps"
},
{
"nodeId": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
"fieldName": "t5_encoder_model"
"nodeId": "4f0207c2-ff40-41fd-b047-ad33fbb1c33a",
"fieldName": "t5_encoder"
}
],
"meta": {
@@ -30,12 +30,12 @@
},
"nodes": [
{
"id": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
"id": "4f0207c2-ff40-41fd-b047-ad33fbb1c33a",
"type": "invocation",
"data": {
"id": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
"id": "4f0207c2-ff40-41fd-b047-ad33fbb1c33a",
"type": "flux_model_loader",
"version": "1.0.4",
"version": "1.0.3",
"label": "",
"notes": "",
"isOpen": true,
@@ -44,25 +44,31 @@
"inputs": {
"model": {
"name": "model",
"label": ""
"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"
}
},
"t5_encoder_model": {
"name": "t5_encoder_model",
"label": ""
},
"clip_embed_model": {
"name": "clip_embed_model",
"label": ""
},
"vae_model": {
"name": "vae_model",
"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"
}
}
}
},
"position": {
"x": 381.1882713063478,
"y": -95.89663532854017
"x": 337.09365228062825,
"y": 40.63469521079861
}
},
{
@@ -201,45 +207,45 @@
],
"edges": [
{
"id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90max_seq_len-01f674f8-b3d1-4df1-acac-6cb8e0bfb63ct5_max_seq_len",
"id": "reactflow__edge-4f0207c2-ff40-41fd-b047-ad33fbb1c33amax_seq_len-01f674f8-b3d1-4df1-acac-6cb8e0bfb63ct5_max_seq_len",
"type": "default",
"source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
"source": "4f0207c2-ff40-41fd-b047-ad33fbb1c33a",
"target": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
"sourceHandle": "max_seq_len",
"targetHandle": "t5_max_seq_len"
},
{
"id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90vae-159bdf1b-79e7-4174-b86e-d40e646964c8vae",
"id": "reactflow__edge-4f0207c2-ff40-41fd-b047-ad33fbb1c33avae-159bdf1b-79e7-4174-b86e-d40e646964c8vae",
"type": "default",
"source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
"source": "4f0207c2-ff40-41fd-b047-ad33fbb1c33a",
"target": "159bdf1b-79e7-4174-b86e-d40e646964c8",
"sourceHandle": "vae",
"targetHandle": "vae"
},
{
"id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90t5_encoder-01f674f8-b3d1-4df1-acac-6cb8e0bfb63ct5_encoder",
"id": "reactflow__edge-4f0207c2-ff40-41fd-b047-ad33fbb1c33atransformer-159bdf1b-79e7-4174-b86e-d40e646964c8transformer",
"type": "default",
"source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
"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",
"target": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
"sourceHandle": "t5_encoder",
"targetHandle": "t5_encoder"
},
{
"id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90clip-01f674f8-b3d1-4df1-acac-6cb8e0bfb63cclip",
"id": "reactflow__edge-4f0207c2-ff40-41fd-b047-ad33fbb1c33aclip-01f674f8-b3d1-4df1-acac-6cb8e0bfb63cclip",
"type": "default",
"source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
"source": "4f0207c2-ff40-41fd-b047-ad33fbb1c33a",
"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,7 +111,16 @@ def denoise(
step_callback: Callable[[], None],
guidance: float = 4.0,
):
# guidance_vec is ignored for schnell.
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 = 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)
@@ -159,9 +168,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, 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 = 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 = repeat(img_ids, "h w c -> b (h w) c", b=bs)
return img, img_ids

View File

@@ -72,7 +72,6 @@ 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,6 +193,15 @@ 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,6 +1,22 @@
# 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
@@ -24,64 +40,45 @@ 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
# Size of a GB in bytes.
GB = 2**30
# 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 MB in bytes.
MB = 2**20
class ModelCache(ModelCacheBase[AnyModel]):
"""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)
```
"""
"""Implementation of ModelCacheBase."""
def __init__(
self,
max_cache_size: float,
max_vram_cache_size: float,
max_cache_size: float = DEFAULT_MAX_CACHE_SIZE,
max_vram_cache_size: float = DEFAULT_MAX_VRAM_CACHE_SIZE,
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 storage_device cache in GBs.
:param max_vram_cache_size: Maximum size of the execution_device cache in GBs.
:param max_cache_size: Maximum size of the RAM cache [6.0 GB]
: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 lazy_offloading: Keep model in VRAM until another model needs to be loaded.
: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 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
@@ -89,6 +86,7 @@ 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
@@ -147,6 +145,15 @@ 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,
@@ -196,7 +203,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 * GB)
self.stats.cache_size = int(self._max_cache_size * GIG)
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(
@@ -224,13 +231,10 @@ class ModelCache(ModelCacheBase[AnyModel]):
return model_key
def offload_unlocked_models(self, size_required: int) -> None:
"""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
"""Move any unused models from VRAM."""
reserved = self._max_vram_cache_size * GIG
vram_in_use = torch.cuda.memory_allocated() + size_required
self.logger.debug(f"{(vram_in_use/GB):.2f}GB VRAM needed for models; max allowed={(reserved/GB):.2f}GB")
self.logger.debug(f"{(vram_in_use/GIG):.2f}GB VRAM needed for models; max allowed={(reserved/GIG):.2f}GB")
for _, cache_entry in sorted(self._cached_models.items(), key=lambda x: x[1].size):
if vram_in_use <= reserved:
break
@@ -241,7 +245,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/GB):.2f}GB; vram free = {(torch.cuda.memory_allocated()/GB):.2f}GB"
f"Removing {cache_entry.key} from VRAM to free {(cache_entry.size/GIG):.2f}GB; vram free = {(torch.cuda.memory_allocated()/GIG):.2f}GB"
)
TorchDevice.empty_cache()
@@ -299,7 +303,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/GB):.3f} GB."
f"Estimated model size: {(cache_entry.size/GIG):.3f} GB."
f"{get_pretty_snapshot_diff(snapshot_before, snapshot_after)}"
)
@@ -322,14 +326,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/GB):.3f} GB.\n"
f" {(cache_entry.size/GIG):.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() / GB)
ram = "%4.2fG" % (self.cache_size() / GB)
vram = "%4.2fG" % (torch.cuda.memory_allocated() / GIG)
ram = "%4.2fG" % (self.cache_size() / GIG)
in_ram_models = 0
in_vram_models = 0
@@ -349,20 +353,17 @@ 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.
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.
"""
"""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
bytes_needed = size
maximum_size = self.max_cache_size * GB # stored in GB, convert to bytes
maximum_size = self.max_cache_size * GIG # 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/GB):.2f}/{self.max_cache_size:.2f} GB, need an additional"
f" {(bytes_needed/GB):.2f} GB"
f"Max cache size exceeded: {(current_size/GIG):.2f}/{self.max_cache_size:.2f} GB, need an additional"
f" {(bytes_needed/GIG):.2f} GB"
)
self.logger.debug(f"Before making_room: cached_models={len(self._cached_models)}")
@@ -379,7 +380,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/GB):.2f} GB (-{(cache_entry.size/GB):.2f} GB)"
f"Removing {model_key} from RAM cache to free at least {(size/GIG):.2f} GB (-{(cache_entry.size/GIG):.2f} GB)"
)
current_size -= cache_entry.size
models_cleared += 1

View File

@@ -54,10 +54,8 @@ 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)
# Currently, we only support weight_format=0.
weight_format = state_dict.pop(prefix + "weight_format", None)
assert weight_format == 0
# 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)
# 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.
@@ -91,14 +89,6 @@ 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,11 +43,6 @@ 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,9 +3,10 @@ Initialization file for invokeai.backend.util
"""
from invokeai.backend.util.logging import InvokeAILogger
from invokeai.backend.util.util import Chdir, directory_size
from invokeai.backend.util.util import GIG, Chdir, directory_size
__all__ = [
"GIG",
"directory_size",
"Chdir",
"InvokeAILogger",

View File

@@ -7,6 +7,9 @@ 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",
"view": "View",
"viewDesc": "Review images in a large gallery view",
"edit": "Edit",
"editDesc": "Edit on the Canvas",
"viewing": "Viewing",
"viewingDesc": "Review images in a large gallery view",
"editing": "Editing",
"editingDesc": "Edit on the Control Layers canvas",
"comparing": "Comparing",
"comparingDesc": "Comparing two images",
"enabled": "Enabled",
@@ -328,13 +328,9 @@
"completedIn": "Completed in",
"batch": "Batch",
"origin": "Origin",
"destination": "Destination",
"upscaling": "Upscaling",
"canvas": "Canvas",
"generation": "Generation",
"workflows": "Workflows",
"other": "Other",
"gallery": "Gallery",
"originCanvas": "Canvas",
"originWorkflows": "Workflows",
"originOther": "Other",
"batchFieldValues": "Batch Field Values",
"item": "Item",
"session": "Session",
@@ -706,8 +702,6 @@
"availableModels": "Available Models",
"baseModel": "Base Model",
"cancel": "Cancel",
"clipEmbed": "CLIP Embed",
"clipVision": "CLIP Vision",
"config": "Config",
"convert": "Convert",
"convertingModelBegin": "Converting Model. Please wait.",
@@ -795,7 +789,6 @@
"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",
@@ -804,7 +797,6 @@
"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",
@@ -1654,13 +1646,6 @@
"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.",
@@ -1690,44 +1675,32 @@
"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": "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",
"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",
"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}})",
@@ -1764,7 +1737,6 @@
"flipHorizontal": "Flip Horizontal",
"flipVertical": "Flip Vertical",
"fill": {
"fillColor": "Fill Color",
"fillStyle": "Fill Style",
"solid": "Solid",
"grid": "Grid",

View File

@@ -16,7 +16,6 @@ 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';
@@ -44,17 +43,10 @@ interface Props {
action: 'sendToImg2Img' | 'sendToCanvas' | 'useAllParameters';
};
selectedWorkflowId?: string;
selectedStylePresetId?: string;
destination?: TabName;
destination?: TabName | undefined;
}
const App = ({
config = DEFAULT_CONFIG,
selectedImage,
selectedWorkflowId,
selectedStylePresetId,
destination,
}: Props) => {
const App = ({ config = DEFAULT_CONFIG, selectedImage, selectedWorkflowId, destination }: Props) => {
const language = useAppSelector(selectLanguage);
const logger = useLogger('system');
const dispatch = useAppDispatch();
@@ -93,12 +85,6 @@ const App = ({
}
}, [selectedWorkflowId, getAndLoadWorkflow]);
useEffect(() => {
if (selectedStylePresetId) {
dispatch(activeStylePresetIdChanged(selectedStylePresetId));
}
}, [dispatch, selectedStylePresetId]);
useEffect(() => {
if (destination) {
dispatch(setActiveTab(destination));

View File

@@ -45,7 +45,6 @@ interface Props extends PropsWithChildren {
action: 'sendToImg2Img' | 'sendToCanvas' | 'useAllParameters';
};
selectedWorkflowId?: string;
selectedStylePresetId?: string;
destination?: TabName;
customStarUi?: CustomStarUi;
socketOptions?: Partial<ManagerOptions & SocketOptions>;
@@ -67,7 +66,6 @@ const InvokeAIUI = ({
queueId,
selectedImage,
selectedWorkflowId,
selectedStylePresetId,
destination,
customStarUi,
socketOptions,
@@ -229,7 +227,6 @@ 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: false }));
api.dispatch(rasterLayerAdded({ overrides, isSelected: true }));
api.dispatch(sessionStagingAreaReset());
},
});

View File

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

View File

@@ -32,7 +32,6 @@ 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, 'upscaling', 'gallery');
const batchConfig = prepareLinearUIBatch(state, g, prepend, noise, posCond);
const req = dispatch(
queueApi.endpoints.enqueueBatch.initiate(batchConfig, {

View File

@@ -1,104 +0,0 @@
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,53 +1,52 @@
import { useStore } from '@nanostores/react';
import type { WritableAtom } from 'nanostores';
import { atom } from 'nanostores';
import { useCallback, useMemo, useState } from 'react';
type UseBoolean = {
isTrue: boolean;
setTrue: () => void;
setFalse: () => void;
set: (value: boolean) => void;
toggle: () => void;
};
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), []);
/**
* 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 {
const api = useMemo(
() => ({
isTrue,
set,
setTrue,
setFalse,
set,
toggle,
};
};
}),
[isTrue, set, setTrue, setFalse, toggle]
);
return [useBoolean, $boolean] as const;
return api;
};
/**
* 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) => buildUseBoolean(initialValue)[0]();
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());
}, []);
const api = useMemo(
() => ({
setTrue,
setFalse,
set,
toggle,
$boolean,
}),
[set, setFalse, setTrue, toggle]
);
return api;
};
};

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">
<ButtonGroup position="relative" orientation="vertical" isAttached={false} top="20%">
<Flex flexDir="column" w="full" h="full" alignItems="center" justifyContent="center">
<ButtonGroup orientation="vertical" isAttached={false}>
<Button variant="ghost" justifyContent="flex-start" leftIcon={<PiPlusBold />} onClick={addInpaintMask}>
{t('controlLayers.inpaintMask')}
{t('controlLayers.inpaintMask', { count: 1 })}
</Button>
<Button variant="ghost" justifyContent="flex-start" leftIcon={<PiPlusBold />} onClick={addRegionalGuidance}>
{t('controlLayers.regionalGuidance')}
{t('controlLayers.regionalGuidance', { count: 1 })}
</Button>
<Button variant="ghost" justifyContent="flex-start" leftIcon={<PiPlusBold />} onClick={addRasterLayer}>
{t('controlLayers.rasterLayer')}
{t('controlLayers.rasterLayer', { count: 1 })}
</Button>
<Button variant="ghost" justifyContent="flex-start" leftIcon={<PiPlusBold />} onClick={addControlLayer}>
{t('controlLayers.controlLayer')}
{t('controlLayers.controlLayer', { count: 1 })}
</Button>
<Button variant="ghost" justifyContent="flex-start" leftIcon={<PiPlusBold />} onClick={addIPAdapter}>
{t('controlLayers.ipAdapter')}
{t('controlLayers.ipAdapter', { count: 1 })}
</Button>
</ButtonGroup>
</Flex>

View File

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

View File

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

View File

@@ -0,0 +1,29 @@
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

@@ -1,37 +1,21 @@
import { Box, ContextMenu, Divider, Flex, MenuList } from '@invoke-ai/ui-library';
import { Divider, Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { CanvasAddEntityButtons } from 'features/controlLayers/components/CanvasAddEntityButtons';
import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityList';
import { EntityListActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBar';
import { CanvasEntityListMenuItems } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuItems';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectHasEntities } from 'features/controlLayers/store/selectors';
import { memo, useCallback } from 'react';
import { memo } from 'react';
export const CanvasPanelContent = memo(() => {
const hasEntities = useAppSelector(selectHasEntities);
const renderMenu = useCallback(
() => (
<MenuList>
<CanvasEntityListMenuItems />
</MenuList>
),
[]
);
return (
<CanvasManagerProviderGate>
<Flex flexDir="column" gap={2} w="full" h="full">
<EntityListActionBar />
<Divider py={0} />
<ContextMenu<HTMLDivElement> renderMenu={renderMenu}>
{(ref) => (
<Box ref={ref} w="full" h="full">
{!hasEntities && <CanvasAddEntityButtons />}
{hasEntities && <CanvasEntityList />}
</Box>
)}
</ContextMenu>
{!hasEntities && <CanvasAddEntityButtons />}
{hasEntities && <CanvasEntityList />}
</Flex>
</CanvasManagerProviderGate>
);

View File

@@ -1,59 +0,0 @@
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,7 +1,8 @@
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 { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle';
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper';
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
@@ -28,7 +29,8 @@ export const ControlLayer = memo(({ id }: Props) => {
<CanvasEntityEditableTitle />
<Spacer />
<ControlLayerBadges />
<CanvasEntityHeaderCommonActions />
<CanvasEntityIsLockedToggle />
<CanvasEntityEnabledToggle />
</CanvasEntityHeader>
<CanvasEntitySettingsWrapper>
<ControlLayerControlAdapter />

View File

@@ -1,21 +1,19 @@
/* 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 { ViewerToggle } from 'features/gallery/components/ImageViewer/ViewerToggleMenu';
import { ViewerToggleMenu } from 'features/gallery/components/ImageViewer/ViewerToggleMenu';
import { memo } from 'react';
export const ControlLayersToolbar = memo(() => {
useCanvasUndoRedo();
return (
<CanvasManagerProviderGate>
<Flex w="full" gap={2} alignItems="center">
@@ -28,9 +26,10 @@ export const ControlLayersToolbar = memo(() => {
<CanvasResetViewButton />
<Spacer />
<ToolFillColorPicker />
<SaveToGalleryButton />
<CanvasModeSwitcher />
<UndoRedoButtonGroup />
<CanvasSettingsPopover />
<ViewerToggle />
<ViewerToggleMenu />
</Flex>
</CanvasManagerProviderGate>
);

View File

@@ -1,27 +1,53 @@
import { Grid, GridItem, Text } from '@invoke-ai/ui-library';
import { Box, Flex, Text } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
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 (
<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>
<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>
);
});
@@ -29,14 +55,12 @@ HeadsUpDisplay.displayName = 'HeadsUpDisplay';
const HUDItem = memo(({ label, value }: { label: string; value: string | number }) => {
return (
<>
<GridItem>
<Text textAlign="end">{label}: </Text>
</GridItem>
<GridItem fontWeight="semibold">
<Text>{value}</Text>
</GridItem>
</>
<Box display="inline-block" lineHeight={1}>
<Text as="span">{label}: </Text>
<Text as="span" fontWeight="semibold">
{value}
</Text>
</Box>
);
});

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} py={5}>
<CanvasEntityHeader ps={4}>
<CanvasEntityEditableTitle />
<Spacer />
<CanvasEntityHeaderCommonActions />
<CanvasEntityEnabledToggle />
</CanvasEntityHeader>
<IPAdapterSettings />
</CanvasEntityContainer>

View File

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

View File

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

View File

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

View File

@@ -1,53 +0,0 @@
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,7 +18,6 @@ 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';
@@ -38,7 +37,6 @@ export const CanvasSettingsPopover = memo(() => {
<CanvasSettingsInvertScrollCheckbox />
<CanvasSettingsClipToBboxCheckbox />
<CanvasSettingsDynamicGridSwitch />
<CanvasSettingsShowHUDSwitch />
<CanvasSettingsResetButton />
<DebugSettings />
</Flex>

View File

@@ -1,28 +0,0 @@
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,18 +8,20 @@ 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) => {
const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, asPreview: boolean) => {
const store = useAppStore();
const socket = useStore($socket);
const dpr = useDevicePixelRatio({ round: false });
@@ -40,25 +42,28 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null)
const manager = new CanvasManager(stage, container, store, socket);
manager.initialize();
return manager.destroy;
}, [container, socket, stage, store]);
}, [asPreview, container, socket, stage, store]);
useLayoutEffect(() => {
Konva.pixelRatio = dpr;
}, [dpr]);
};
const selectDynamicGrid = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.dynamicGrid);
const selectShowHUD = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.showHUD);
type Props = {
asPreview?: boolean;
};
export const StageComponent = memo(() => {
const selectDynamicGrid = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.dynamicGrid);
export const StageComponent = memo(({ asPreview = false }: Props) => {
const dynamicGrid = useAppSelector(selectDynamicGrid);
const showHUD = useAppSelector(selectShowHUD);
const [stage] = useState(
() =>
new Konva.Stage({
id: getPrefixedId('konva_stage'),
id: uuidv4(),
container: document.createElement('div'),
listening: !asPreview,
})
);
const [container, setContainer] = useState<HTMLDivElement | null>(null);
@@ -67,7 +72,7 @@ export const StageComponent = memo(() => {
setContainer(el);
}, []);
useStageRenderer(stage, container);
useStageRenderer(stage, container, asPreview);
useEffect(
() => () => {
@@ -101,9 +106,9 @@ export const StageComponent = memo(() => {
overflow="hidden"
data-testid="control-layers-canvas"
/>
{showHUD && (
<Flex position="absolute" top={1} insetInlineStart={1} pointerEvents="none">
<HeadsUpDisplay />
{!asPreview && (
<Flex position="absolute" top={0} insetInlineStart={0} pointerEvents="none">
{showHud && <HeadsUpDisplay />}
</Flex>
)}
</Flex>

View File

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

View File

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

View File

@@ -1,14 +1,16 @@
/* 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 { useCallback } from 'react';
import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { PiArrowClockwiseBold, PiArrowCounterClockwiseBold } from 'react-icons/pi';
import { useDispatch } from 'react-redux';
export const useCanvasUndoRedo = () => {
useAssertSingleton('useCanvasUndoRedo');
export const UndoRedoButtonGroup = memo(() => {
const { t } = useTranslation();
const dispatch = useDispatch();
const mayUndo = useAppSelector(selectCanvasMayUndo);
@@ -25,4 +27,27 @@ export const useCanvasUndoRedo = () => {
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

@@ -1,70 +0,0 @@
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

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

View File

@@ -1,13 +1,11 @@
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, useMemo } from 'react';
import { memo } from 'react';
import { PiCaretDownBold } from 'react-icons/pi';
type Props = PropsWithChildren<{
@@ -22,9 +20,6 @@ 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">
@@ -58,9 +53,7 @@ export const CanvasEntityGroupList = memo(({ isSelected, type, children }: Props
</Text>
<Spacer />
</Flex>
{canMergeVisible && <CanvasEntityMergeVisibleButton type={type} />}
<CanvasEntityAddOfTypeButton type={type} />
{canHideAll && <CanvasEntityTypeIsHiddenToggle type={type} />}
{type !== 'ip_adapter' && <CanvasEntityTypeIsHiddenToggle type={type} />}
</Flex>
<Collapse in={collapse.isTrue}>
<Flex flexDir="column" gap={2} pt={2}>

View File

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

View File

@@ -1,88 +0,0 @@
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

@@ -29,15 +29,15 @@ export const useEntityTitle = (entityIdentifier: CanvasEntityIdentifier) => {
const parts: string[] = [];
if (entityIdentifier.type === 'inpaint_mask') {
parts.push(t('controlLayers.inpaintMask'));
parts.push(t('controlLayers.inpaintMask', { count: 1 }));
} else if (entityIdentifier.type === 'control_layer') {
parts.push(t('controlLayers.controlLayer'));
parts.push(t('controlLayers.controlLayer', { count: 1 }));
} else if (entityIdentifier.type === 'raster_layer') {
parts.push(t('controlLayers.rasterLayer'));
parts.push(t('controlLayers.rasterLayer', { count: 1 }));
} else if (entityIdentifier.type === 'ip_adapter') {
parts.push(t('common.ipAdapter'));
parts.push(t('common.ipAdapter', { count: 1 }));
} else if (entityIdentifier.type === 'regional_guidance') {
parts.push(t('controlLayers.regionalGuidance'));
parts.push(t('controlLayers.regionalGuidance', { count: 1 }));
} 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');
return t('controlLayers.controlLayer', { count: 0 });
case 'raster_layer':
return t('controlLayers.rasterLayer');
return t('controlLayers.rasterLayer', { count: 0 });
case 'inpaint_mask':
return t('controlLayers.inpaintMask');
return t('controlLayers.inpaintMask', { count: 0 });
case 'regional_guidance':
return t('controlLayers.regionalGuidance');
return t('controlLayers.regionalGuidance', { count: 0 });
case 'ip_adapter':
return t('controlLayers.ipAdapter');
return t('controlLayers.ipAdapter', { count: 0 });
default:
return '';
}

View File

@@ -147,19 +147,6 @@ 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;
@@ -174,21 +161,17 @@ export class CanvasCompositorModule extends CanvasModuleABC {
}
}
imageDTO = await this.rasterizeAndUploadCompositeRasterLayer(rect, false);
this.manager.cache.imageNameCache.set(hash, imageDTO.image_name);
return imageDTO;
};
this.log.trace({ rect }, 'Rasterizing composite raster layer');
rasterizeAndUploadCompositeInpaintMask = async (rect: Rect, saveToGallery: boolean) => {
this.log.trace({ rect }, 'Rasterizing composite inpaint mask');
const canvas = this.getCompositeInpaintMaskCanvas(rect);
const canvas = this.getCompositeRasterLayerCanvas(rect);
const blob = await canvasToBlob(canvas);
if (this.manager._isDebugging) {
previewBlob(blob, 'Composite inpaint mask canvas');
previewBlob(blob, 'Composite raster layer canvas');
}
return uploadImage(blob, 'composite-inpaint-mask.png', 'general', !saveToGallery);
imageDTO = await uploadImage(blob, 'composite-raster-layer.png', 'general', true);
this.manager.cache.imageNameCache.set(hash, imageDTO.image_name);
return imageDTO;
};
getCompositeInpaintMaskImageDTO = async (rect: Rect): Promise<ImageDTO> => {
@@ -205,7 +188,15 @@ export class CanvasCompositorModule extends CanvasModuleABC {
}
}
imageDTO = await this.rasterizeAndUploadCompositeInpaintMask(rect, false);
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);
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 { CanvasEntityIdentifier, Coordinate, Dimensions, Rect } from 'features/controlLayers/store/types';
import type { 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,36 +76,35 @@ export class CanvasStageModule extends CanvasModuleABC {
});
};
getVisibleRect = (type?: Exclude<CanvasEntityIdentifier['type'], 'ip_adapter'>): Rect => {
getVisibleRect = (): Rect => {
const rects = [];
for (const adapter of this.manager.adapters.getAll()) {
if (!adapter.state.isEnabled) {
continue;
if (adapter.state.isEnabled) {
rects.push(adapter.transformer.getRelativeRect());
}
if (type && adapter.state.type !== type) {
continue;
}
rects.push(adapter.transformer.getRelativeRect());
}
return getRectUnion(...rects);
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;
}
};
fitBboxToStage = () => {
const { rect } = this.manager.stateApi.getBbox();
this.log.trace({ rect }, 'Fitting bbox to stage');
this.fitRect(rect);
this.log.trace('Fitting bbox to stage');
const bbox = this.manager.stateApi.getBbox();
this.fitRect(bbox.rect);
};
fitLayersToStage() {
this.log.trace('Fitting layers to stage');
const rect = this.getVisibleRect();
if (rect.width === 0 || rect.height === 0) {
this.fitBboxToStage();
} else {
this.log.trace({ rect }, 'Fitting layers to stage');
this.fitRect(rect);
}
this.fitRect(rect);
}
fitRect = (rect: Rect) => {

View File

@@ -250,10 +250,12 @@ export class CanvasToolModule extends CanvasModuleABC {
this.konva.colorPicker.group.visible(tool === 'colorPicker');
};
syncCursorStyle = () => {
render = () => {
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();
@@ -292,158 +294,6 @@ 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');
@@ -455,11 +305,136 @@ export class CanvasToolModule extends CanvasModuleABC {
// No need to render the brush preview if the cursor position or color is missing
if (cursorPos && tool === 'brush') {
this.renderBrushTool(cursorPos);
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,
});
} else if (cursorPos && tool === 'eraser') {
this.renderEraserTool(cursorPos);
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,
});
} else if (cursorPos && tool === 'colorPicker') {
this.renderColorPicker(cursorPos);
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.setToolVisibility(tool, isDrawable);
@@ -889,10 +864,6 @@ 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');
}
};
@@ -909,11 +880,6 @@ 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

@@ -1,47 +0,0 @@
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,13 +302,10 @@ 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, reject) => {
return new Promise((resolve) => {
canvas.toBlob((blob) => {
if (!blob) {
reject('Failed to convert canvas to blob');
} else {
resolve(blob);
}
assert(blob, 'blob is null');
resolve(blob);
});
});
};
@@ -421,25 +418,19 @@ export function snapToNearest(value: number, candidateValues: number[], threshol
}
/**
* Gets the union of any number of rects.
* @params rects The rects to union
* Gets the union of two rects
* @param rect1 The first rect
* @param rect2 The second rect
* @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 };
}, firstRect);
}, getEmptyRect());
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 { StagingAreaImage } from 'features/controlLayers/store/types';
import type { SessionMode, StagingAreaImage } from 'features/controlLayers/store/types';
export type CanvasSessionState = {
sendToCanvas: boolean;
mode: SessionMode;
isStaging: boolean;
stagedImages: StagingAreaImage[];
selectedStagedImageIndex: number;
};
const initialState: CanvasSessionState = {
sendToCanvas: false,
mode: 'generate',
isStaging: false,
stagedImages: [],
selectedStagedImageIndex: 0,
@@ -27,7 +27,6 @@ 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;
},
@@ -51,8 +50,9 @@ export const canvasSessionSlice = createSlice({
state.stagedImages = [];
state.selectedStagedImageIndex = 0;
},
sessionSendToCanvasChanged: (state, action: PayloadAction<boolean>) => {
state.sendToCanvas = action.payload;
sessionModeChanged: (state, action: PayloadAction<{ mode: SessionMode }>) => {
const { mode } = action.payload;
state.mode = mode;
},
},
});
@@ -64,7 +64,7 @@ export const {
sessionStagingAreaReset,
sessionNextStagedImageSelected,
sessionPrevStagedImageSelected,
sessionSendToCanvasChanged,
sessionModeChanged,
} = canvasSessionSlice.actions;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
@@ -85,7 +85,3 @@ 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,14 +35,10 @@ export const canvasSettingsSlice = createSlice({
settingsAutoSaveToggled: (state) => {
state.autoSave = !state.autoSave;
},
settingsShowHUDToggled: (state) => {
state.showHUD = !state.showHUD;
},
},
});
export const { clipToBboxChanged, settingsAutoSaveToggled, settingsDynamicGridToggled, settingsShowHUDToggled } =
canvasSettingsSlice.actions;
export const { clipToBboxChanged, settingsAutoSaveToggled, settingsDynamicGridToggled } = canvasSettingsSlice.actions;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrate = (state: any): any => {

View File

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

View File

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

View File

@@ -1,61 +1,55 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import { IconSwitch } from 'common/components/IconSwitch';
import { ButtonGroup, Flex, IconButton, Text, Tooltip } from '@invoke-ai/ui-library';
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';
const TooltipEdit = memo(() => {
export const ViewerToggleMenu = () => {
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 (
<IconSwitch
isChecked={!imageViewer.isOpen}
onChange={onChange}
iconUnchecked={<PiEyeBold />}
tooltipUnchecked={<TooltipView />}
iconChecked={<PiPencilBold />}
tooltipChecked={<TooltipEdit />}
ariaLabel="Toggle viewer"
/>
<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>
);
});
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 { ViewerToggle } from './ViewerToggleMenu';
import { ViewerToggleMenu } 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 && <ViewerToggle />}
{showToggle && <ViewerToggleMenu />}
</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={t('modelManager.t5Encoder')} modelList={filteredT5EncoderModels} key="t5-encoder" />
<ModelListWrapper title="T5 Encoder" modelList={filteredT5EncoderModels} key="t5-encoder" />
)}
{/* Clip Embed List */}
{isLoadingClipEmbedModels && <FetchingModelsLoader loadingMessage="Loading Clip Embed Models..." />}
{!isLoadingClipEmbedModels && filteredClipEmbedModels.length > 0 && (
<ModelListWrapper title={t('modelManager.clipEmbed')} modelList={filteredClipEmbedModels} key="clip-embed" />
<ModelListWrapper title="Clip Embed" modelList={filteredClipEmbedModels} key="clip-embed" />
)}
{/* Spandrel Image to Image List */}
{isLoadingSpandrelImageToImageModels && (
@@ -192,7 +192,7 @@ const ModelList = () => {
)}
{!isLoadingSpandrelImageToImageModels && filteredSpandrelImageToImageModels.length > 0 && (
<ModelListWrapper
title={t('modelManager.spandrelImageToImage')}
title="Image-to-Image"
modelList={filteredSpandrelImageToImageModels}
key="spandrel-image-to-image"
/>

View File

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

View File

@@ -4,7 +4,6 @@ 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';
@@ -40,7 +39,6 @@ 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 { useAppSelector, useAppStore } from 'app/store/storeHooks';
import { 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,7 +33,6 @@ 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';
@@ -163,13 +162,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, { enabled: tab === 'workflows', preventDefault: true }, [tab]);
useHotkeys(['shift+a', 'space'], addNodeCmdk.setTrue, { preventDefault: true });
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setSearchTerm(e.target.value);
@@ -191,7 +190,7 @@ export const AddNodeCmdk = memo(() => {
return (
<Modal
isOpen={addNodeCmdk.isTrue}
isOpen={addNodeCmdkIsOpen}
onClose={onClose}
useInert={false}
initialFocusRef={inputRef}

View File

@@ -6,8 +6,6 @@ import {
isBoardFieldInputTemplate,
isBooleanFieldInputInstance,
isBooleanFieldInputTemplate,
isCLIPEmbedModelFieldInputInstance,
isCLIPEmbedModelFieldInputTemplate,
isColorFieldInputInstance,
isColorFieldInputTemplate,
isControlNetModelFieldInputInstance,
@@ -18,8 +16,6 @@ import {
isFloatFieldInputTemplate,
isFluxMainModelFieldInputInstance,
isFluxMainModelFieldInputTemplate,
isFluxVAEModelFieldInputInstance,
isFluxVAEModelFieldInputTemplate,
isImageFieldInputInstance,
isImageFieldInputTemplate,
isIntegerFieldInputInstance,
@@ -53,12 +49,10 @@ 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';
@@ -128,13 +122,6 @@ 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

@@ -1,60 +0,0 @@
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

@@ -1,60 +0,0 @@
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,17 +32,20 @@ import {
shouldSnapToGridChanged,
shouldValidateGraphChanged,
} from 'features/nodes/store/workflowSettingsSlice';
import type { ChangeEvent } from 'react';
import type { ChangeEvent, ReactNode } 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);
const WorkflowEditorSettings = () => {
type Props = {
children: (props: { onOpen: () => void }) => ReactNode;
};
const WorkflowEditorSettings = ({ children }: Props) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const dispatch = useAppDispatch();
const modal = useWorkflowEditorSettingsModal();
const shouldSnapToGrid = useAppSelector(selectShouldSnapToGrid);
const selectionMode = useAppSelector(selectSelectionMode);
@@ -96,72 +99,76 @@ const WorkflowEditorSettings = () => {
const { t } = useTranslation();
return (
<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>
<>
{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>
</>
);
};

View File

@@ -3,6 +3,7 @@ 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';
@@ -33,6 +34,7 @@ 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,7 +21,6 @@ export const useConnection = () => {
const store = useAppStore();
const templates = useStore($templates);
const updateNodeInternals = useUpdateNodeInternals();
const addNodeCmdk = useAddNodeCmdk();
const onConnectStart = useCallback<OnConnectStart>(
(event, { nodeId, handleId, handleType }) => {
@@ -108,9 +107,9 @@ export const useConnection = () => {
$pendingConnection.set(null);
} else {
// The mouse is not over a node - we should open the add node popover
addNodeCmdk.setTrue();
$addNodeCmdk.set(true);
}
}, [addNodeCmdk, store, templates, updateNodeInternals]);
}, [store, templates, updateNodeInternals]);
const api = useMemo(() => ({ onConnectStart, onConnect, onConnectEnd }), [onConnectStart, onConnect, onConnectEnd]);
return api;

View File

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

View File

@@ -151,14 +151,6 @@ 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(),
@@ -183,8 +175,6 @@ const zStatefulFieldType = z.union([
zT2IAdapterModelFieldType,
zSpandrelImageToImageModelFieldType,
zT5EncoderModelFieldType,
zCLIPEmbedModelFieldType,
zFluxVAEModelFieldType,
zColorFieldType,
zSchedulerFieldType,
]);
@@ -677,53 +667,7 @@ export const isT5EncoderModelFieldInputInstance = (val: unknown): val is T5Encod
export const isT5EncoderModelFieldInputTemplate = (val: unknown): val is T5EncoderModelFieldInputTemplate =>
zT5EncoderModelFieldInputTemplate.safeParse(val).success;
// #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
// #endregio
// #region SchedulerField
@@ -814,8 +758,6 @@ export const zStatefulFieldValue = z.union([
zT2IAdapterModelFieldValue,
zSpandrelImageToImageModelFieldValue,
zT5EncoderModelFieldValue,
zFluxVAEModelFieldValue,
zCLIPEmbedModelFieldValue,
zColorFieldValue,
zSchedulerFieldValue,
]);
@@ -846,8 +788,6 @@ const zStatefulFieldInputInstance = z.union([
zT2IAdapterModelFieldInputInstance,
zSpandrelImageToImageModelFieldInputInstance,
zT5EncoderModelFieldInputInstance,
zFluxVAEModelFieldInputInstance,
zCLIPEmbedModelFieldInputInstance,
zColorFieldInputInstance,
zSchedulerFieldInputInstance,
]);
@@ -879,8 +819,6 @@ const zStatefulFieldInputTemplate = z.union([
zT2IAdapterModelFieldInputTemplate,
zSpandrelImageToImageModelFieldInputTemplate,
zT5EncoderModelFieldInputTemplate,
zFluxVAEModelFieldInputTemplate,
zCLIPEmbedModelFieldInputTemplate,
zColorFieldInputTemplate,
zSchedulerFieldInputTemplate,
zStatelessFieldInputTemplate,

View File

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

View File

@@ -29,7 +29,7 @@ export const addInpaint = async (
const canvas = selectCanvasSlice(state);
const { bbox } = canvas;
const { sendToCanvas: isComposing } = canvasSession;
const { mode } = 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 (!isComposing) {
if (mode === 'generate') {
canvasPasteBack.source_image = { image_name: initialImage.image_name };
}
@@ -143,7 +143,7 @@ export const addInpaint = async (
g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image');
if (!isComposing) {
if (mode === 'generate') {
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 { sendToCanvas: isComposing } = canvasSession;
const { mode } = 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 (!isComposing) {
if (mode === 'generate') {
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 (!isComposing) {
if (mode === 'generate') {
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.sendToCanvas || canvasSettings.autoSave;
const shouldSaveToGallery = canvasSession.mode === 'generate' || 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.sendToCanvas || canvasSettings.autoSave;
const shouldSaveToGallery = canvasSession.mode === 'generate' || canvasSettings.autoSave;
g.updateNode(canvasOutput, {
id: getPrefixedId('canvas_output'),

View File

@@ -23,8 +23,6 @@ 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,7 +2,6 @@ import { FieldParseError } from 'features/nodes/types/error';
import type {
BoardFieldInputTemplate,
BooleanFieldInputTemplate,
CLIPEmbedModelFieldInputTemplate,
ColorFieldInputTemplate,
ControlNetModelFieldInputTemplate,
EnumFieldInputTemplate,
@@ -10,7 +9,6 @@ import type {
FieldType,
FloatFieldInputTemplate,
FluxMainModelFieldInputTemplate,
FluxVAEModelFieldInputTemplate,
ImageFieldInputTemplate,
IntegerFieldInputTemplate,
IPAdapterModelFieldInputTemplate,
@@ -240,34 +238,6 @@ 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,
@@ -453,8 +423,6 @@ 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,26 +1,27 @@
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 clearQueue = useClearQueue();
const dialogState = useClearQueueConfirmationAlertDialog();
const { isLoading, isDisabled } = useClearQueue();
return (
<>
<Button
isDisabled={clearQueue.isDisabled}
isLoading={clearQueue.isLoading}
isDisabled={isDisabled}
isLoading={isLoading}
tooltip={t('queue.clearTooltip')}
leftIcon={<PiTrashSimpleFill />}
colorScheme="error"
onClick={clearQueue.openDialog}
onClick={dialogState.setTrue}
data-testid={t('queue.clear')}
{...props}
>

View File

@@ -1,72 +1,26 @@
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 { listCursorChanged, listPriorityChanged } from 'features/queue/store/queueSlice';
import { toast } from 'features/toast/toast';
import { memo, useCallback, useMemo } from 'react';
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
import { atom } from 'nanostores';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { useClearQueueMutation, useGetQueueStatusQuery } from 'services/api/endpoints/queue';
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,
};
};
const $boolean = atom(false);
export const useClearQueueConfirmationAlertDialog = buildUseBoolean($boolean);
export const ClearQueueConfirmationsAlertDialog = memo(() => {
const { t } = useTranslation();
const clearQueue = useClearQueue();
const dialogState = useClearQueueConfirmationAlertDialog();
const isOpen = useStore(dialogState.$boolean);
const { clearQueue } = useClearQueue();
return (
<ConfirmationAlertDialog
isOpen={clearQueue.isOpen}
onClose={clearQueue.closeDialog}
isOpen={isOpen}
onClose={dialogState.setFalse}
title={t('queue.clearTooltip')}
acceptCallback={clearQueue.clearQueue}
acceptCallback={clearQueue}
acceptButtonText={t('queue.clear')}
useInert={false}
>

View File

@@ -1,40 +1,67 @@
import type { IconButtonProps } from '@invoke-ai/ui-library';
import { IconButton, useShiftModifier } from '@invoke-ai/ui-library';
import { QueueCountBadge } from 'features/queue/components/QueueCountBadge';
import { useClearQueueConfirmationAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
import { memo, useRef } from 'react';
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiTrashSimpleBold, PiXBold } from 'react-icons/pi';
import { useClearQueue } from './ClearQueueConfirmationAlertDialog';
type ClearQueueButtonProps = Omit<IconButtonProps, 'aria-label'>;
export const ClearQueueIconButton = memo((_) => {
const ref = useRef<HTMLDivElement>(null);
export const ClearAllQueueIconButton = memo((props: ClearQueueButtonProps) => {
const { t } = useTranslation();
const clearQueue = useClearQueue();
const cancelCurrentQueueItem = useCancelCurrentQueueItem();
const dialogState = useClearQueueConfirmationAlertDialog();
const { isLoading, isDisabled } = useClearQueue();
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();
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} />
</>
);
if (shift) {
return <ClearAllQueueIconButton {...props} />;
}
return <ClearSingleQueueItemIconButton {...props} />;
});
ClearQueueIconButton.displayName = 'ClearQueueIconButton';

View File

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

View File

@@ -0,0 +1,120 @@
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,27 +1,24 @@
import { Flex, Spacer } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { CanvasSendToToggle } from 'features/controlLayers/components/CanvasSendToToggle';
import { ButtonGroup, Flex, Spacer } from '@invoke-ai/ui-library';
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">
<Flex gap={2}>
<ButtonGroup size="lg" isAttached={false}>
{isPrependEnabled && <QueueFrontButton />}
<InvokeQueueBackButton />
<Spacer />
{tab === 'generation' && <CanvasSendToToggle />}
<QueueActionsMenuButton />
<ClearQueueIconButton />
</Flex>
</ButtonGroup>
<ProgressBar />
</Flex>
);

View File

@@ -1,77 +0,0 @@
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,7 +1,6 @@
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';
@@ -53,7 +52,6 @@ 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 (
@@ -78,11 +76,6 @@ 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,6 +1,5 @@
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';
@@ -18,7 +17,7 @@ type Props = {
};
const QueueItemComponent = ({ queueItemDTO }: Props) => {
const { session_id, batch_id, item_id, origin, destination } = queueItemDTO;
const { session_id, batch_id, item_id, origin } = queueItemDTO;
const { t } = useTranslation();
const { cancelBatch, isLoading: isLoadingCancelBatch, isCanceled } = useCancelBatch(batch_id);
@@ -27,7 +26,6 @@ const QueueItemComponent = ({ queueItemDTO }: Props) => {
const { data: queueItem } = useGetQueueItemQuery(item_id);
const originText = useOriginText(origin);
const destinationText = useDestinationText(destination);
const statusAndTiming = useMemo(() => {
if (!queueItem) {
@@ -56,7 +54,6 @@ 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,9 +25,6 @@ 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,8 +4,7 @@ export const COLUMN_WIDTHS = {
statusDot: 2,
time: '4rem',
origin: '5rem',
destination: '6rem',
batchId: '5rem',
fieldValues: 'auto',
actions: 'auto',
} as const;
};

View File

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

View File

@@ -0,0 +1,45 @@
import { useStore } from '@nanostores/react';
import { $isConnected } from 'app/hooks/useSocketIO';
import { useAppDispatch } from 'app/store/storeHooks';
import { listCursorChanged, listPriorityChanged } from 'features/queue/store/queueSlice';
import { toast } from 'features/toast/toast';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useClearQueueMutation, useGetQueueStatusQuery } from 'services/api/endpoints/queue';
export const useClearQueue = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
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, isLoading, queueStatus, isDisabled };
};

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