Compare commits

..

609 Commits

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

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

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

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

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

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

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

Not good! Better is to create a selector outside the function and use it. Doing that for all selectors now, most of the way through now. Feels snappier.
2024-08-30 22:20:12 +10:00
f126a61f66 feat(ui): rough out undo/redo on canvas 2024-08-30 22:20:12 +10:00
89c79276f3 chore: release v4.2.9.dev4
Canvas dev build.
2024-08-30 22:20:12 +10:00
423e463b95 fix(ui): handle error from internal konva method
We are dipping into konva's private API for preview images and it appears to be unsafe (got an error once). Wrapped in a try/catch.
2024-08-30 22:20:12 +10:00
52202e45de feat(ui): split out loras state from canvas rendering state 2024-08-30 22:20:12 +10:00
100832c66d feat(ui): split out session state from canvas rendering state 2024-08-30 22:20:12 +10:00
a58b91b221 feat(ui): split out settings state from canvas rendering state 2024-08-30 22:20:12 +10:00
3af6d79852 feat(ui): split out tool state from canvas rendering state 2024-08-30 22:20:12 +10:00
1303e18e93 feat(ui): split out params/compositing state from canvas rendering state
First step to restoring undo/redo - the undoable state must be in its own slice. So params and settings must be isolated.
2024-08-30 22:20:12 +10:00
301da97670 feat(ui): add CanvasModuleBase class to standardize canvas APIs
I did this ages ago but undid it for some reason, not sure why. Caught a few issues related to subscriptions.
2024-08-30 22:20:12 +10:00
17e76981bb feat(ui): move selected tool and tool buffer out of redux
This ephemeral state can live in the canvas classes.
2024-08-30 22:20:12 +10:00
9c1732e2bb feat(ui): move ephemeral state into canvas classes
Things like `$lastCursorPos` are now created within the canvas drawing classes. Consumers in react access them via `useCanvasManager`.

For example:
```tsx
const canvasManager = useCanvasManager();
const lastCursorPos = useStore(canvasManager.stateApi.$lastCursorPos);
```
2024-08-30 22:20:12 +10:00
a3179e7a3f feat(ui): normalize all actions to accept an entityIdentifier
Previously, canvas actions specific to an entity type only needed the id of that entity type. This allowed you to pass in the id of an entity of the wrong type.

All actions for a specific entity now take a full entity identifier, and the entity identifier type can be narrowed.

`selectEntity` and `selectEntityOrThrow` now need a full entity identifier, and narrow their return values to a specific entity type _if_ the entity identifier is narrowed.

The types for canvas entities are updated with optional type parameters for this purpose.

All reducers, actions and components have been updated.
2024-08-30 22:20:12 +10:00
f86b50d18a feat(ui): move events into modules who care about them 2024-08-30 22:20:12 +10:00
307885f505 fix(ui): color picker resets brush opacity 2024-08-30 22:20:12 +10:00
4b49c1dd6b fix(ui): scaled bbox loses sync 2024-08-30 22:20:12 +10:00
f917cefa84 feat(ui): add context menu to entity list 2024-08-30 22:20:12 +10:00
bea98438fc chore(ui): bump @invoke-ai/ui-library 2024-08-30 22:20:12 +10:00
17d3275086 fix(ui): missing vae precision in graph builders 2024-08-30 22:20:12 +10:00
059b7a0fcf chore: release v4.2.9.dev3
Instead of using dates, just going to increment.
2024-08-30 22:20:12 +10:00
05d3a989f6 feat(ui): use new Result utils for enqueueing 2024-08-30 22:20:12 +10:00
590ae70c12 fix(ui): graph building issue w/ controlnet 2024-08-30 22:20:12 +10:00
5240ec6e6f feat(ui): add Result type & helpers
Wrappers to capture errors and turn into results:
- `withResult` wraps a sync function
- `withResultAsync` wraps an async function

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

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

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

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

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

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

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

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

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

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

## QA Instructions

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

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

## Checklist

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

## Related Issues / Discussions

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

## QA Instructions

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

## Merge Plan

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

## Checklist

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

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

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

View File

@ -20,7 +20,6 @@ from typing import (
Type,
TypeVar,
Union,
cast,
)
import semver
@ -80,7 +79,7 @@ class UIConfigBase(BaseModel):
version: str = Field(
description='The node\'s version. Should be a valid semver string e.g. "1.0.0" or "3.8.13".',
)
node_pack: Optional[str] = Field(default=None, description="Whether or not this is a custom node")
node_pack: str = Field(description="The node pack that this node belongs to, will be 'invokeai' for built-in nodes")
classification: Classification = Field(default=Classification.Stable, description="The node's classification")
model_config = ConfigDict(
@ -230,18 +229,16 @@ class BaseInvocation(ABC, BaseModel):
@staticmethod
def json_schema_extra(schema: dict[str, Any], model_class: Type[BaseInvocation]) -> None:
"""Adds various UI-facing attributes to the invocation's OpenAPI schema."""
uiconfig = cast(UIConfigBase | None, getattr(model_class, "UIConfig", None))
if uiconfig is not None:
if uiconfig.title is not None:
schema["title"] = uiconfig.title
if uiconfig.tags is not None:
schema["tags"] = uiconfig.tags
if uiconfig.category is not None:
schema["category"] = uiconfig.category
if uiconfig.node_pack is not None:
schema["node_pack"] = uiconfig.node_pack
schema["classification"] = uiconfig.classification
schema["version"] = uiconfig.version
if title := model_class.UIConfig.title:
schema["title"] = title
if tags := model_class.UIConfig.tags:
schema["tags"] = tags
if category := model_class.UIConfig.category:
schema["category"] = category
if node_pack := model_class.UIConfig.node_pack:
schema["node_pack"] = node_pack
schema["classification"] = model_class.UIConfig.classification
schema["version"] = model_class.UIConfig.version
if "required" not in schema or not isinstance(schema["required"], list):
schema["required"] = []
schema["class"] = "invocation"
@ -312,7 +309,7 @@ class BaseInvocation(ABC, BaseModel):
json_schema_extra={"field_kind": FieldKind.NodeAttribute},
)
UIConfig: ClassVar[Type[UIConfigBase]]
UIConfig: ClassVar[UIConfigBase]
model_config = ConfigDict(
protected_namespaces=(),
@ -441,30 +438,25 @@ def invocation(
validate_fields(cls.model_fields, invocation_type)
# Add OpenAPI schema extras
uiconfig_name = cls.__qualname__ + ".UIConfig"
if not hasattr(cls, "UIConfig") or cls.UIConfig.__qualname__ != uiconfig_name:
cls.UIConfig = type(uiconfig_name, (UIConfigBase,), {})
cls.UIConfig.title = title
cls.UIConfig.tags = tags
cls.UIConfig.category = category
cls.UIConfig.classification = classification
# Grab the node pack's name from the module name, if it's a custom node
is_custom_node = cls.__module__.rsplit(".", 1)[0] == "invokeai.app.invocations"
if is_custom_node:
cls.UIConfig.node_pack = cls.__module__.split(".")[0]
else:
cls.UIConfig.node_pack = None
uiconfig: dict[str, Any] = {}
uiconfig["title"] = title
uiconfig["tags"] = tags
uiconfig["category"] = category
uiconfig["classification"] = classification
# The node pack is the module name - will be "invokeai" for built-in nodes
uiconfig["node_pack"] = cls.__module__.split(".")[0]
if version is not None:
try:
semver.Version.parse(version)
except ValueError as e:
raise InvalidVersionError(f'Invalid version string for node "{invocation_type}": "{version}"') from e
cls.UIConfig.version = version
uiconfig["version"] = version
else:
logger.warn(f'No version specified for node "{invocation_type}", using "1.0.0"')
cls.UIConfig.version = "1.0.0"
uiconfig["version"] = "1.0.0"
cls.UIConfig = UIConfigBase(**uiconfig)
if use_cache is not None:
cls.model_fields["use_cache"].default = use_cache

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -64,6 +64,7 @@
"@roarr/browser-log-writer": "^1.3.0",
"async-mutex": "^0.5.0",
"chakra-react-select": "^4.9.1",
"cmdk": "^1.0.0",
"compare-versions": "^6.1.1",
"dateformat": "^5.0.3",
"fracturedjsonjs": "^4.0.2",
@ -92,7 +93,6 @@
"react-icons": "^5.2.1",
"react-redux": "9.1.2",
"react-resizable-panels": "^2.0.23",
"react-select": "5.8.0",
"react-use": "^17.5.1",
"react-virtuoso": "^4.9.0",
"reactflow": "^11.11.4",

View File

@ -41,6 +41,9 @@ dependencies:
chakra-react-select:
specifier: ^4.9.1
version: 4.9.1(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.13.3)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
cmdk:
specifier: ^1.0.0
version: 1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
compare-versions:
specifier: ^6.1.1
version: 6.1.1
@ -125,9 +128,6 @@ dependencies:
react-resizable-panels:
specifier: ^2.0.23
version: 2.0.23(react-dom@18.3.1)(react@18.3.1)
react-select:
specifier: 5.8.0
version: 5.8.0(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
react-use:
specifier: ^17.5.1
version: 17.5.1(react-dom@18.3.1)(react@18.3.1)
@ -2053,7 +2053,7 @@ packages:
dependencies:
'@chakra-ui/dom-utils': 2.1.0
react: 18.3.1
react-focus-lock: 2.12.1(@types/react@18.3.3)(react@18.3.1)
react-focus-lock: 2.13.0(@types/react@18.3.3)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
dev: false
@ -3784,6 +3784,288 @@ packages:
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
dev: false
/@radix-ui/primitive@1.0.1:
resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==}
dependencies:
'@babel/runtime': 7.25.4
dev: false
/@radix-ui/react-compose-refs@1.0.1(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.25.4
'@types/react': 18.3.3
react: 18.3.1
dev: false
/@radix-ui/react-context@1.0.1(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.25.4
'@types/react': 18.3.3
react: 18.3.1
dev: false
/@radix-ui/react-dialog@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.25.4
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-focus-guards': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-id': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-portal': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@types/react': 18.3.3
'@types/react-dom': 18.3.0
aria-hidden: 1.2.4
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-remove-scroll: 2.5.5(@types/react@18.3.3)(react@18.3.1)
dev: false
/@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.25.4
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.3.3)(react@18.3.1)
'@types/react': 18.3.3
'@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-focus-guards@1.0.1(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.25.4
'@types/react': 18.3.3
react: 18.3.1
dev: false
/@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.25.4
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@types/react': 18.3.3
'@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-id@1.0.1(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.25.4
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@types/react': 18.3.3
react: 18.3.1
dev: false
/@radix-ui/react-portal@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.25.4
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@types/react': 18.3.3
'@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-presence@1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.25.4
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@types/react': 18.3.3
'@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-primitive@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.25.4
'@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@18.3.1)
'@types/react': 18.3.3
'@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-slot@1.0.2(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.25.4
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@types/react': 18.3.3
react: 18.3.1
dev: false
/@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.25.4
'@types/react': 18.3.3
react: 18.3.1
dev: false
/@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.25.4
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@types/react': 18.3.3
react: 18.3.1
dev: false
/@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.25.4
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@types/react': 18.3.3
react: 18.3.1
dev: false
/@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.25.4
'@types/react': 18.3.3
react: 18.3.1
dev: false
/@reactflow/background@11.3.14(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==}
peerDependencies:
@ -5210,7 +5492,6 @@ packages:
resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==}
dependencies:
'@types/react': 18.3.3
dev: true
/@types/react-transition-group@4.4.10:
resolution: {integrity: sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==}
@ -6268,6 +6549,21 @@ packages:
requiresBuild: true
dev: true
/cmdk@1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==}
peerDependencies:
react: ^18.0.0
react-dom: ^18.0.0
dependencies:
'@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- '@types/react-dom'
dev: false
/color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
dependencies:
@ -9712,8 +10008,8 @@ packages:
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
dev: false
/react-focus-lock@2.12.1(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-lfp8Dve4yJagkHiFrC1bGtib3mF2ktqwPJw4/WGcgPW+pJ/AVQA5X2vI7xgp13FcxFEpYBBHpXai/N2DBNC0Jw==}
/react-focus-lock@2.13.0(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-w7aIcTwZwNzUp2fYQDMICy+khFwVmKmOrLF8kNsPS+dz4Oi/oxoVJ2wCMVvX6rWGriM/+mYaTyp1MRmkcs2amw==}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
@ -9867,6 +10163,25 @@ packages:
use-sidecar: 1.1.2(@types/react@18.3.3)(react@18.3.1)
dev: false
/react-remove-scroll@2.5.5(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.3.3
react: 18.3.1
react-remove-scroll-bar: 2.3.6(@types/react@18.3.3)(react@18.3.1)
react-style-singleton: 2.2.1(@types/react@18.3.3)(react@18.3.1)
tslib: 2.7.0
use-callback-ref: 1.3.2(@types/react@18.3.3)(react@18.3.1)
use-sidecar: 1.1.2(@types/react@18.3.3)(react@18.3.1)
dev: false
/react-resizable-panels@2.0.23(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-8ZKTwTU11t/FYwiwhMdtZYYyFxic5U5ysRu2YwfkAgDbUJXFvnWSJqhnzkSlW+mnDoNAzDCrJhdOSXBPA76wug==}
peerDependencies:

View File

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

View File

@ -16,6 +16,9 @@ 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';
import { selectLanguage } from 'features/system/store/systemSelectors';
import { AppContent } from 'features/ui/components/AppContent';
@ -41,10 +44,17 @@ interface Props {
action: 'sendToImg2Img' | 'sendToCanvas' | 'useAllParameters';
};
selectedWorkflowId?: string;
destination?: TabName | undefined;
selectedStylePresetId?: string;
destination?: TabName;
}
const App = ({ config = DEFAULT_CONFIG, selectedImage, selectedWorkflowId, destination }: Props) => {
const App = ({
config = DEFAULT_CONFIG,
selectedImage,
selectedWorkflowId,
selectedStylePresetId,
destination,
}: Props) => {
const language = useAppSelector(selectLanguage);
const logger = useLogger('system');
const dispatch = useAppDispatch();
@ -83,6 +93,12 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage, selectedWorkflowId, desti
}
}, [selectedWorkflowId, getAndLoadWorkflow]);
useEffect(() => {
if (selectedStylePresetId) {
dispatch(activeStylePresetIdChanged(selectedStylePresetId));
}
}, [dispatch, selectedStylePresetId]);
useEffect(() => {
if (destination) {
dispatch(setActiveTab(destination));
@ -121,6 +137,8 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage, selectedWorkflowId, desti
<StylePresetModal />
<ClearQueueConfirmationsAlertDialog />
<PreselectedImage selectedImage={selectedImage} />
<SettingsModal />
<RefreshAfterResetModal />
</ErrorBoundary>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -83,6 +83,7 @@ const ChangeBoardModal = () => {
acceptCallback={handleChangeBoard}
acceptButtonText={t('boards.move')}
cancelButtonText={t('boards.cancel')}
useInert={false}
>
<Flex flexDir="column" gap={4}>
<Text>

View File

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

View File

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

View File

@ -57,7 +57,7 @@ const marks = [
mapOpacityToSliderValue(1),
];
const sliderDefaultValue = mapOpacityToSliderValue(100);
const sliderDefaultValue = mapOpacityToSliderValue(1);
const snapCandidates = marks.slice(1, marks.length - 1);
@ -134,7 +134,11 @@ export const SelectedEntityOpacity = memo(() => {
return (
<Popover>
<FormControl w="min-content" gap={2}>
<FormControl
w="min-content"
gap={2}
isDisabled={selectedEntityIdentifier === null || selectedEntityIdentifier.type === 'ip_adapter'}
>
<FormLabel m={0}>{t('controlLayers.opacity')}</FormLabel>
<PopoverAnchor>
<NumberInput
@ -152,9 +156,8 @@ export const SelectedEntityOpacity = memo(() => {
onKeyDown={onKeyDown}
clampValueOnBlur={false}
variant="outline"
isDisabled={selectedEntityIdentifier === null || selectedEntityIdentifier.type === 'ip_adapter'}
>
<NumberInputField paddingInlineEnd={7} />
<NumberInputField paddingInlineEnd={7} _focusVisible={{ zIndex: 0 }} />
<PopoverTrigger>
<IconButton
aria-label="open-slider"
@ -164,6 +167,7 @@ export const SelectedEntityOpacity = memo(() => {
position="absolute"
insetInlineEnd={0}
h="full"
isDisabled={selectedEntityIdentifier === null || selectedEntityIdentifier.type === 'ip_adapter'}
/>
</PopoverTrigger>
</NumberInput>
@ -181,6 +185,7 @@ export const SelectedEntityOpacity = memo(() => {
marks={marks}
formatValue={formatSliderValue}
alwaysShowMarks
isDisabled={selectedEntityIdentifier === null || selectedEntityIdentifier.type === 'ip_adapter'}
/>
</PopoverBody>
</PopoverContent>

View File

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

View File

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

View File

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

View File

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

View File

@ -19,8 +19,6 @@ export const CanvasEditor = memo(() => {
<Flex
tabIndex={-1}
ref={ref}
layerStyle="first"
p={2}
borderRadius="base"
position="relative"
flexDirection="column"
@ -32,14 +30,14 @@ export const CanvasEditor = memo(() => {
>
<ControlLayersToolbar />
<StageComponent />
<Flex position="absolute" bottom={16} gap={2} align="center" justify="center">
<Flex position="absolute" bottom={8} gap={2} align="center" justify="center">
<CanvasManagerProviderGate>
<StagingAreaIsStagingGate>
<StagingAreaToolbar />
</StagingAreaIsStagingGate>
</CanvasManagerProviderGate>
</Flex>
<Flex position="absolute" bottom={16}>
<Flex position="absolute" bottom={8}>
<CanvasManagerProviderGate>
<Filter />
<Transform />

View File

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

View File

@ -15,18 +15,6 @@ export const Filter = memo(() => {
const isFiltering = useStore(canvasManager.filter.$isFiltering);
const isProcessing = useStore(canvasManager.filter.$isProcessing);
const previewFilter = useCallback(() => {
canvasManager.filter.previewFilter();
}, [canvasManager.filter]);
const applyFilter = useCallback(() => {
canvasManager.filter.applyFilter();
}, [canvasManager.filter]);
const cancelFilter = useCallback(() => {
canvasManager.filter.cancelFilter();
}, [canvasManager.filter]);
const onChangeFilterConfig = useCallback(
(filterConfig: FilterConfig) => {
canvasManager.filter.$config.set(filterConfig);
@ -65,24 +53,27 @@ export const Filter = memo(() => {
<FilterSettings filterConfig={config} onChange={onChangeFilterConfig} />
<ButtonGroup isAttached={false} size="sm" alignSelf="self-end">
<Button
variant="ghost"
leftIcon={<PiShootingStarBold />}
onClick={previewFilter}
onClick={canvasManager.filter.previewFilter}
isLoading={isProcessing}
loadingText={t('controlLayers.filter.preview')}
>
{t('controlLayers.filter.preview')}
</Button>
<Button
variant="ghost"
leftIcon={<PiCheckBold />}
onClick={applyFilter}
onClick={canvasManager.filter.applyFilter}
isLoading={isProcessing}
loadingText={t('controlLayers.filter.apply')}
>
{t('controlLayers.filter.apply')}
</Button>
<Button
variant="ghost"
leftIcon={<PiXBold />}
onClick={cancelFilter}
onClick={canvasManager.filter.cancelFilter}
isLoading={isProcessing}
loadingText={t('controlLayers.filter.cancel')}
>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,10 +12,13 @@ export const RegionalGuidanceDeletePromptButton = memo(({ onDelete }: Props) =>
return (
<Tooltip label={t('controlLayers.deletePrompt')}>
<IconButton
variant="promptOverlay"
variant="link"
aria-label={t('controlLayers.deletePrompt')}
icon={<PiTrashSimpleBold />}
onClick={onDelete}
flexGrow={0}
size="sm"
p={0}
/>
</Tooltip>
);

View File

@ -1,17 +1,21 @@
import { Box, Textarea } from '@invoke-ai/ui-library';
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, Flex, Textarea } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { RegionalGuidanceDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceDeletePromptButton';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { rgNegativePromptChanged } from 'features/controlLayers/store/canvasSlice';
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
import { PromptPopover } from 'features/prompt/PromptPopover';
import { usePrompt } from 'features/prompt/usePrompt';
import { memo, useCallback, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
const _focusVisible: SystemStyleObject = {
outline: 'none',
};
export const RegionalGuidanceNegativePrompt = memo(() => {
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const selectPrompt = useMemo(
@ -49,14 +53,25 @@ export const RegionalGuidanceNegativePrompt = memo(() => {
placeholder={t('parameters.negativePromptPlaceholder')}
onChange={onChange}
onKeyDown={onKeyDown}
variant="darkFilled"
paddingRight={30}
variant="outline"
paddingInlineStart={2}
paddingInlineEnd={8}
fontSize="sm"
zIndex="0 !important"
_focusVisible={_focusVisible}
/>
<PromptOverlayButtonWrapper>
<Flex
flexDir="column"
gap={2}
position="absolute"
insetBlockStart={2}
insetInlineEnd={0}
alignItems="center"
justifyContent="center"
>
<RegionalGuidanceDeletePromptButton onDelete={onDeletePrompt} />
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
</PromptOverlayButtonWrapper>
</Flex>
</Box>
</PromptPopover>
);

View File

@ -1,17 +1,21 @@
import { Box, Textarea } from '@invoke-ai/ui-library';
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, Flex, Textarea } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { RegionalGuidanceDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceDeletePromptButton';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { rgPositivePromptChanged } from 'features/controlLayers/store/canvasSlice';
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
import { PromptPopover } from 'features/prompt/PromptPopover';
import { usePrompt } from 'features/prompt/usePrompt';
import { memo, useCallback, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
const _focusVisible: SystemStyleObject = {
outline: 'none',
};
export const RegionalGuidancePositivePrompt = memo(() => {
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const selectPrompt = useMemo(
@ -49,14 +53,25 @@ export const RegionalGuidancePositivePrompt = memo(() => {
placeholder={t('parameters.positivePromptPlaceholder')}
onChange={onChange}
onKeyDown={onKeyDown}
variant="darkFilled"
paddingRight={30}
variant="outline"
paddingInlineStart={2}
paddingInlineEnd={8}
minH={28}
zIndex="0 !important"
_focusVisible={_focusVisible}
/>
<PromptOverlayButtonWrapper>
<Flex
flexDir="column"
gap={2}
position="absolute"
insetBlockStart={2}
insetInlineEnd={0}
alignItems="center"
justifyContent="center"
>
<RegionalGuidanceDeletePromptButton onDelete={onDeletePrompt} />
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
</PromptOverlayButtonWrapper>
</Flex>
</Box>
</PromptPopover>
);

View File

@ -35,7 +35,7 @@ export const RegionalGuidanceSettings = memo(() => {
{flags.hasPositivePrompt && (
<>
<RegionalGuidancePositivePrompt />
{(flags.hasNegativePrompt || flags.hasIPAdapters) && <Divider />}
{!flags.hasNegativePrompt && flags.hasIPAdapters && <Divider />}
</>
)}
{flags.hasNegativePrompt && (

View File

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

View File

@ -18,6 +18,7 @@ import { CanvasSettingsInvertScrollCheckbox } from 'features/controlLayers/compo
import { CanvasSettingsLogDebugInfoButton } from 'features/controlLayers/components/Settings/CanvasSettingsLogDebugInfo';
import { CanvasSettingsRecalculateRectsButton } from 'features/controlLayers/components/Settings/CanvasSettingsRecalculateRectsButton';
import { CanvasSettingsResetButton } from 'features/controlLayers/components/Settings/CanvasSettingsResetButton';
import { CanvasSettingsShowHUDSwitch } from 'features/controlLayers/components/Settings/CanvasSettingsShowHUDSwitch';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { RiSettings4Fill } from 'react-icons/ri';
@ -27,7 +28,7 @@ export const CanvasSettingsPopover = memo(() => {
return (
<Popover isLazy>
<PopoverTrigger>
<IconButton aria-label={t('common.settingsLabel')} icon={<RiSettings4Fill />} />
<IconButton aria-label={t('common.settingsLabel')} icon={<RiSettings4Fill />} variant="ghost" />
</PopoverTrigger>
<PopoverContent>
<PopoverArrow />
@ -37,6 +38,7 @@ export const CanvasSettingsPopover = memo(() => {
<CanvasSettingsInvertScrollCheckbox />
<CanvasSettingsClipToBboxCheckbox />
<CanvasSettingsDynamicGridSwitch />
<CanvasSettingsShowHUDSwitch />
<CanvasSettingsResetButton />
<DebugSettings />
</Flex>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { Button, ButtonGroup, Flex, Heading } from '@invoke-ai/ui-library';
import { Button, ButtonGroup, Flex, Heading, Spacer } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import {
@ -6,9 +6,9 @@ import {
useEntityIdentifierContext,
} from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useEntityAdapter } from 'features/controlLayers/hooks/useEntityAdapter';
import { memo, useCallback } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCheckBold, PiXBold } from 'react-icons/pi';
import { PiArrowsCounterClockwiseBold, PiCheckBold, PiXBold } from 'react-icons/pi';
const TransformBox = memo(() => {
const { t } = useTranslation();
@ -16,14 +16,6 @@ const TransformBox = memo(() => {
const adapter = useEntityAdapter(entityIdentifier);
const isProcessing = useStore(adapter.transformer.$isProcessing);
const applyTransform = useCallback(() => {
adapter.transformer.applyTransform();
}, [adapter.transformer]);
const cancelFilter = useCallback(() => {
adapter.transformer.stopTransform();
}, [adapter.transformer]);
return (
<Flex
bg="base.800"
@ -40,16 +32,33 @@ const TransformBox = memo(() => {
<Heading size="md" color="base.300" userSelect="none">
{t('controlLayers.tool.transform')}
</Heading>
<ButtonGroup isAttached={false} size="sm" alignSelf="self-end">
<ButtonGroup isAttached={false} size="sm" w="full">
<Button
leftIcon={<PiArrowsCounterClockwiseBold />}
onClick={adapter.transformer.resetTransform}
isLoading={isProcessing}
loadingText={t('controlLayers.reset')}
variant="ghost"
>
{t('accessibility.reset')}
</Button>
<Spacer />
<Button
leftIcon={<PiCheckBold />}
onClick={applyTransform}
onClick={adapter.transformer.applyTransform}
isLoading={isProcessing}
loadingText={t('common.apply')}
variant="ghost"
>
{t('common.apply')}
</Button>
<Button leftIcon={<PiXBold />} onClick={cancelFilter} isLoading={isProcessing} loadingText={t('common.cancel')}>
<Button
leftIcon={<PiXBold />}
onClick={adapter.transformer.stopTransform}
isLoading={isProcessing}
loadingText={t('common.cancel')}
variant="ghost"
>
{t('common.cancel')}
</Button>
</ButtonGroup>

View File

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

View File

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

View File

@ -1,34 +1,29 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useBoolean } from 'common/hooks/useBoolean';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useEntityIsEnabled } from 'features/controlLayers/hooks/useEntityIsEnabled';
import { entityIsEnabledToggled } from 'features/controlLayers/store/canvasSlice';
import { memo, useCallback, useRef } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCheckBold } from 'react-icons/pi';
import { PiCircleBold, PiCircleFill } from 'react-icons/pi';
export const CanvasEntityEnabledToggle = memo(() => {
const { t } = useTranslation();
const entityIdentifier = useEntityIdentifierContext();
const ref = useRef<HTMLButtonElement>(null);
const isEnabled = useEntityIsEnabled(entityIdentifier);
const dispatch = useAppDispatch();
const onClick = useCallback(() => {
dispatch(entityIsEnabledToggled({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
const isHovered = useBoolean(false);
return (
<IconButton
ref={ref}
size="sm"
onMouseOver={isHovered.setTrue}
onMouseOut={isHovered.setFalse}
aria-label={t(isEnabled ? 'common.enabled' : 'common.disabled')}
tooltip={t(isEnabled ? 'common.enabled' : 'common.disabled')}
variant="ghost"
icon={isEnabled || isHovered.isTrue ? <PiCheckBold /> : undefined}
variant="link"
alignSelf="stretch"
icon={isEnabled ? <PiCircleFill /> : <PiCircleBold />}
onClick={onClick}
/>
);

View File

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

View File

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

View File

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

View File

@ -1,34 +1,29 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useBoolean } from 'common/hooks/useBoolean';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import { entityIsLockedToggled } from 'features/controlLayers/store/canvasSlice';
import { memo, useCallback, useRef } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiLockSimpleFill } from 'react-icons/pi';
import { PiLockSimpleFill, PiLockSimpleOpenBold } from 'react-icons/pi';
export const CanvasEntityIsLockedToggle = memo(() => {
const { t } = useTranslation();
const entityIdentifier = useEntityIdentifierContext();
const ref = useRef<HTMLButtonElement>(null);
const isLocked = useEntityIsLocked(entityIdentifier);
const dispatch = useAppDispatch();
const onClick = useCallback(() => {
dispatch(entityIsLockedToggled({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
const isHovered = useBoolean(false);
return (
<IconButton
ref={ref}
size="sm"
onMouseOver={isHovered.setTrue}
onMouseOut={isHovered.setFalse}
aria-label={t(isLocked ? 'controlLayers.locked' : 'controlLayers.unlocked')}
tooltip={t(isLocked ? 'controlLayers.locked' : 'controlLayers.unlocked')}
variant="ghost"
icon={isLocked || isHovered.isTrue ? <PiLockSimpleFill /> : undefined}
variant="link"
alignSelf="stretch"
icon={isLocked ? <PiLockSimpleFill /> : <PiLockSimpleOpenBold />}
onClick={onClick}
/>
);

View File

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

View File

@ -11,7 +11,10 @@ import { useSelector } from 'react-redux';
const ChakraCanvas = chakra.canvas;
const PADDING = 4;
const PADDING = 2;
const CONTAINER_WIDTH = 36; // this is size 12 in our theme - need it in px for the canvas
const CONTAINER_WIDTH_PX = `${CONTAINER_WIDTH}px`;
export const CanvasEntityPreviewImage = memo(() => {
const entityIdentifier = useEntityIdentifierContext();
@ -31,11 +34,10 @@ export const CanvasEntityPreviewImage = memo(() => {
[entityIdentifier]
);
const maskColor = useSelector(selectMaskColor);
const containerRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const cache = useStore(adapter.renderer.$canvasCache);
useEffect(() => {
if (!cache || !canvasRef.current || !containerRef.current) {
if (!cache || !canvasRef.current) {
return;
}
const ctx = canvasRef.current.getContext('2d');
@ -49,7 +51,7 @@ export const CanvasEntityPreviewImage = memo(() => {
canvasRef.current.width = rect.width;
canvasRef.current.height = rect.height;
const scale = containerRef.current.offsetWidth / rect.width;
const scale = CONTAINER_WIDTH / rect.width;
const sx = rect.x;
const sy = rect.y;
@ -72,11 +74,10 @@ export const CanvasEntityPreviewImage = memo(() => {
return (
<Flex
position="relative"
ref={containerRef}
alignItems="center"
justifyContent="center"
w={12}
h={12}
w={CONTAINER_WIDTH_PX}
h={CONTAINER_WIDTH_PX}
borderRadius="sm"
borderWidth={1}
bg="base.900"

View File

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

View File

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

View File

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

View File

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

View File

@ -112,12 +112,11 @@ export class CanvasEntityLayerAdapter extends CanvasModuleABC {
this.renderer.updateOpacity(opacity);
}
if (state.type === 'control_layer' && this.state.type === 'control_layer') {
if (this.isFirstRender || state.withTransparencyEffect !== this.state.withTransparencyEffect) {
if (state.type === 'control_layer' && prevState.type === 'control_layer') {
if (this.isFirstRender || state.withTransparencyEffect !== prevState.withTransparencyEffect) {
this.renderer.updateTransparencyEffect(state.withTransparencyEffect);
}
}
// this.transformer.syncInteractionState();
if (this.isFirstRender) {
this.transformer.updateBbox();

View File

@ -259,13 +259,7 @@ export class CanvasEntityTransformer extends CanvasModuleABC {
// This is called when a transform anchor is dragged. By this time, the transform constraints in the above
// callbacks have been enforced, and the transformer has updated its nodes' attributes. We need to pass the
// updated attributes to the object group, propagating the transformation on down.
this.parent.renderer.konva.objectGroup.setAttrs({
x: this.konva.proxyRect.x(),
y: this.konva.proxyRect.y(),
scaleX: this.konva.proxyRect.scaleX(),
scaleY: this.konva.proxyRect.scaleY(),
rotation: this.konva.proxyRect.rotation(),
});
this.syncObjectGroupWithProxyRect();
});
this.konva.transformer.on('transformend', () => {
@ -395,6 +389,54 @@ export class CanvasEntityTransformer extends CanvasModuleABC {
this.parent.konva.layer.add(this.konva.transformer);
}
// TODO(psyche): These don't work when the entity is rotated, need to do some math to offset the flip after rotation
// flipHorizontal = () => {
// if (!this.isTransforming || this.$isProcessing.get()) {
// return;
// }
// // Flipping horizontally = flipping across the vertical axis:
// // - Flip by negating the x scale
// // - Restore position by translating the rect rightwards by the width of the rect
// const x = this.konva.proxyRect.x();
// const width = this.konva.proxyRect.width();
// const scaleX = this.konva.proxyRect.scaleX();
// this.konva.proxyRect.setAttrs({
// scaleX: -scaleX,
// x: x + width * scaleX,
// });
// this.syncObjectGroupWithProxyRect();
// };
// flipVertical = () => {
// if (!this.isTransforming || this.$isProcessing.get()) {
// return;
// }
// // Flipping vertically = flipping across the horizontal axis:
// // - Flip by negating the y scale
// // - Restore position by translating the rect downwards by the height of the rect
// const y = this.konva.proxyRect.y();
// const height = this.konva.proxyRect.height();
// const scaleY = this.konva.proxyRect.scaleY();
// this.konva.proxyRect.setAttrs({
// scaleY: -scaleY,
// y: y + height * scaleY,
// });
// this.syncObjectGroupWithProxyRect();
// };
syncObjectGroupWithProxyRect = () => {
this.parent.renderer.konva.objectGroup.setAttrs({
x: this.konva.proxyRect.x(),
y: this.konva.proxyRect.y(),
scaleX: this.konva.proxyRect.scaleX(),
scaleY: this.konva.proxyRect.scaleY(),
rotation: this.konva.proxyRect.rotation(),
});
};
/**
* Updates the transformer's visual components to match the parent entity's position and bounding box.
* @param position The position of the parent entity
@ -510,6 +552,12 @@ export class CanvasEntityTransformer extends CanvasModuleABC {
this.stopTransform();
};
resetTransform = () => {
this.resetScale();
this.updatePosition();
this.updateBbox();
};
/**
* Stops the transformation of the entity. If the transformation is in progress, the entity will be reset to its
* original state.
@ -520,12 +568,9 @@ export class CanvasEntityTransformer extends CanvasModuleABC {
this.isTransforming = false;
this.setInteractionMode('off');
// Reset the scale of the the entity. We've either replaced the transformed objects with a rasterized image, or
// Reset the transform of the the entity. We've either replaced the transformed objects with a rasterized image, or
// canceled a transformation. In either case, the scale should be reset.
this.resetScale();
this.updatePosition();
this.updateBbox();
this.resetTransform();
this.syncInteractionState();
this.manager.stateApi.$transformingEntity.set(null);
this.$isProcessing.set(false);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -317,7 +317,7 @@ export const selectScheduler = createParamsSelector((params) => params.scheduler
export const selectSeamlessXAxis = createParamsSelector((params) => params.seamlessXAxis);
export const selectSeamlessYAxis = createParamsSelector((params) => params.seamlessYAxis);
export const selectSeed = createParamsSelector((params) => params.seed);
export const selectShouldRandomizeSeed = createParamsSelector((params) => params.shouldConcatPrompts);
export const selectShouldRandomizeSeed = createParamsSelector((params) => params.shouldRandomizeSeed);
export const selectVAEPrecision = createParamsSelector((params) => params.vaePrecision);
export const selectIterations = createParamsSelector((params) => params.iterations);
export const selectShouldUseCPUNoise = createParamsSelector((params) => params.shouldUseCpuNoise);

View File

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

View File

@ -81,6 +81,7 @@ const DeleteImageModal = () => {
cancelButtonText={t('boards.cancel')}
acceptButtonText={t('controlnet.delete')}
acceptCallback={handleDelete}
useInert={false}
>
<Flex direction="column" gap={3}>
<ImageUsageMessage imageUsage={imageUsageSummary} />

View File

@ -20,7 +20,7 @@ export const DynamicPromptsModal = memo(() => {
const { isOpen, onClose } = useDynamicPromptsModal();
return (
<Modal isOpen={isOpen} onClose={onClose} isCentered>
<Modal isOpen={isOpen} onClose={onClose} isCentered useInert={false}>
<ModalOverlay />
<ModalContent w="80vw" h="80vh" maxW="unset" maxH="unset">
<ModalHeader>{t('dynamicPrompts.dynamicPrompts')}</ModalHeader>

View File

@ -59,7 +59,7 @@ const GalleryPanelContent = () => {
}, [boardSearchText.length, boardSearchDisclosure, boardsListPanel, dispatch]);
return (
<Flex ref={ref} position="relative" flexDirection="column" h="full" w="full" pt={2} tabIndex={-1}>
<Flex ref={ref} position="relative" flexDirection="column" h="full" w="full" tabIndex={-1}>
<Flex alignItems="center" gap={0}>
<GalleryHeader />
<Flex alignItems="center" justifyContent="space-between" w="full">

View File

@ -21,7 +21,7 @@ export const ImageViewer = memo(() => {
<Flex
ref={ref}
tabIndex={-1}
layerStyle="first"
layerStyle="body"
borderRadius="base"
position="absolute"
flexDirection="column"
@ -29,7 +29,6 @@ export const ImageViewer = memo(() => {
right={0}
bottom={0}
left={0}
p={2}
rowGap={4}
alignItems="center"
justifyContent="center"

View File

@ -1,69 +1,61 @@
import {
Button,
Flex,
Icon,
Popover,
PopoverArrow,
PopoverBody,
PopoverContent,
PopoverTrigger,
Text,
} from '@invoke-ai/ui-library';
import { Flex, Text } from '@invoke-ai/ui-library';
import { IconSwitch } from 'common/components/IconSwitch';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { PiCaretDownBold, PiCheckBold, PiEyeBold, PiPencilBold } from 'react-icons/pi';
import { PiEyeBold, PiPencilBold } from 'react-icons/pi';
export const ViewerToggleMenu = () => {
const TooltipEdit = memo(() => {
const { t } = useTranslation();
return (
<Flex flexDir="column">
<Text fontWeight="semibold">{t('common.edit')}</Text>
<Text fontWeight="normal">{t('common.editDesc')}</Text>
</Flex>
);
});
TooltipEdit.displayName = 'TooltipEdit';
const TooltipView = memo(() => {
const { t } = useTranslation();
return (
<Flex flexDir="column">
<Text fontWeight="semibold">{t('common.view')}</Text>
<Text fontWeight="normal">{t('common.viewDesc')}</Text>
</Flex>
);
});
TooltipView.displayName = 'TooltipView';
export const ViewerToggle = memo(() => {
const imageViewer = useImageViewer();
useHotkeys('z', imageViewer.onToggle, [imageViewer]);
useHotkeys('esc', imageViewer.onClose, [imageViewer]);
const onChange = useCallback(
(isChecked: boolean) => {
if (isChecked) {
imageViewer.onClose();
} else {
imageViewer.onOpen();
}
},
[imageViewer]
);
return (
<Popover isLazy>
<PopoverTrigger>
<Button variant="outline" data-testid="toggle-viewer-menu-button" pointerEvents="auto">
<Flex gap={3} w="full" alignItems="center">
{imageViewer.isOpen ? <Icon as={PiEyeBold} /> : <Icon as={PiPencilBold} />}
<Text fontSize="md">{imageViewer.isOpen ? t('common.viewing') : t('common.editing')}</Text>
<Icon as={PiCaretDownBold} />
</Flex>
</Button>
</PopoverTrigger>
<PopoverContent p={2} pointerEvents="auto">
<PopoverArrow />
<PopoverBody>
<Flex flexDir="column">
<Button onClick={imageViewer.onOpen} variant="ghost" h="auto" w="auto" p={2}>
<Flex gap={2} w="full">
<Icon as={PiCheckBold} visibility={imageViewer.isOpen ? 'visible' : 'hidden'} />
<Flex flexDir="column" gap={2} alignItems="flex-start">
<Text fontWeight="semibold" color="base.100">
{t('common.viewing')}
</Text>
<Text fontWeight="normal" color="base.300">
{t('common.viewingDesc')}
</Text>
</Flex>
</Flex>
</Button>
<Button onClick={imageViewer.onClose} variant="ghost" h="auto" w="auto" p={2}>
<Flex gap={2} w="full">
<Icon as={PiCheckBold} visibility={imageViewer.isOpen ? 'hidden' : 'visible'} />
<Flex flexDir="column" gap={2} alignItems="flex-start">
<Text fontWeight="semibold" color="base.100">
{t('common.editing')}
</Text>
<Text fontWeight="normal" color="base.300">
{t('common.editingDesc')}
</Text>
</Flex>
</Flex>
</Button>
</Flex>
</PopoverBody>
</PopoverContent>
</Popover>
<IconSwitch
isChecked={!imageViewer.isOpen}
onChange={onChange}
iconUnchecked={<PiEyeBold />}
tooltipUnchecked={<TooltipView />}
iconChecked={<PiPencilBold />}
tooltipChecked={<TooltipEdit />}
ariaLabel="Toggle viewer"
/>
);
};
});
ViewerToggle.displayName = 'ViewerToggle';

View File

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

View File

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

View File

@ -123,6 +123,7 @@ const ModelListItem = ({ model }: ModelListItemProps) => {
title={t('modelManager.deleteModel')}
acceptCallback={handleModelDelete}
acceptButtonText={t('modelManager.delete')}
useInert={false}
>
<Flex rowGap={4} flexDirection="column">
<Text fontWeight="bold">{t('modelManager.deleteMsg1')}</Text>

View File

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

View File

@ -67,6 +67,7 @@ export const ModelConvertButton = memo(({ modelConfig }: ModelConvertProps) => {
acceptButtonText={`${t('modelManager.convert')}`}
isOpen={isOpen}
onClose={onClose}
useInert={false}
>
<Flex flexDirection="column" rowGap={4}>
<Text fontSize="md">{t('modelManager.convertToDiffusersHelpText1')}</Text>

View File

@ -2,7 +2,9 @@ import 'reactflow/dist/style.css';
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';
@ -10,7 +12,6 @@ import { useTranslation } from 'react-i18next';
import { MdDeviceHub } from 'react-icons/md';
import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
import AddNodePopover from './flow/AddNodePopover/AddNodePopover';
import { Flow } from './flow/Flow';
import BottomLeftPanel from './flow/panels/BottomLeftPanel/BottomLeftPanel';
import MinimapPanel from './flow/panels/MinimapPanel/MinimapPanel';
@ -31,7 +32,7 @@ const NodeEditor = () => {
{data && (
<>
<Flow />
<AddNodePopover />
<AddNodeCmdk />
<TopPanel />
<BottomLeftPanel />
<MinimapPanel />
@ -39,6 +40,7 @@ const NodeEditor = () => {
<LoadWorkflowFromGraphModal />
</>
)}
<WorkflowEditorSettings />
{isLoading && <IAINoContentFallback label={t('nodes.loadingNodes')} icon={MdDeviceHub} />}
</Flex>
);

View File

@ -0,0 +1,419 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import {
Box,
Flex,
Icon,
Input,
Modal,
ModalBody,
ModalContent,
ModalOverlay,
Spacer,
Text,
} from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
import { CommandEmpty, CommandItem, CommandList, CommandRoot } from 'cmdk';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { useBuildNode } from 'features/nodes/hooks/useBuildNode';
import {
$cursorPos,
$edgePendingUpdate,
$pendingConnection,
$templates,
edgesChanged,
nodesChanged,
useAddNodeCmdk,
} from 'features/nodes/store/nodesSlice';
import { selectNodesSlice } from 'features/nodes/store/selectors';
import { findUnoccupiedPosition } from 'features/nodes/store/util/findUnoccupiedPosition';
import { getFirstValidConnection } from 'features/nodes/store/util/getFirstValidConnection';
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';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { PiFlaskBold, PiHammerBold } from 'react-icons/pi';
import type { EdgeChange, NodeChange } from 'reactflow';
import type { S } from 'services/api/types';
const useThrottle = <T,>(value: T, limit: number) => {
const [throttledValue, setThrottledValue] = useState(value);
const lastRan = useRef(Date.now());
useEffect(() => {
const handler = setTimeout(
function () {
if (Date.now() - lastRan.current >= limit) {
setThrottledValue(value);
lastRan.current = Date.now();
}
},
limit - (Date.now() - lastRan.current)
);
return () => {
clearTimeout(handler);
};
}, [value, limit]);
return throttledValue;
};
const useAddNode = () => {
const { t } = useTranslation();
const store = useAppStore();
const buildInvocation = useBuildNode();
const templates = useStore($templates);
const pendingConnection = useStore($pendingConnection);
const addNode = useCallback(
(nodeType: string): void => {
const node = buildInvocation(nodeType);
if (!node) {
const errorMessage = t('nodes.unknownNode', {
nodeType: nodeType,
});
toast({
status: 'error',
title: errorMessage,
});
return;
}
// Find a cozy spot for the node
const cursorPos = $cursorPos.get();
const { nodes, edges } = selectNodesSlice(store.getState());
node.position = findUnoccupiedPosition(nodes, cursorPos?.x ?? node.position.x, cursorPos?.y ?? node.position.y);
node.selected = true;
// Deselect all other nodes and edges
const nodeChanges: NodeChange[] = [{ type: 'add', item: node }];
const edgeChanges: EdgeChange[] = [];
nodes.forEach(({ id, selected }) => {
if (selected) {
nodeChanges.push({ type: 'select', id, selected: false });
}
});
edges.forEach(({ id, selected }) => {
if (selected) {
edgeChanges.push({ type: 'select', id, selected: false });
}
});
// Onwards!
if (nodeChanges.length > 0) {
store.dispatch(nodesChanged(nodeChanges));
}
if (edgeChanges.length > 0) {
store.dispatch(edgesChanged(edgeChanges));
}
// Auto-connect an edge if we just added a node and have a pending connection
if (pendingConnection && isInvocationNode(node)) {
const edgePendingUpdate = $edgePendingUpdate.get();
const { handleType } = pendingConnection;
const source = handleType === 'source' ? pendingConnection.nodeId : node.id;
const sourceHandle = handleType === 'source' ? pendingConnection.handleId : null;
const target = handleType === 'target' ? pendingConnection.nodeId : node.id;
const targetHandle = handleType === 'target' ? pendingConnection.handleId : null;
const { nodes, edges } = selectNodesSlice(store.getState());
const connection = getFirstValidConnection(
source,
sourceHandle,
target,
targetHandle,
nodes,
edges,
templates,
edgePendingUpdate
);
if (connection) {
const newEdge = connectionToEdge(connection);
store.dispatch(edgesChanged([{ type: 'add', item: newEdge }]));
}
}
},
[buildInvocation, pendingConnection, store, t, templates]
);
return addNode;
};
const cmdkRootSx: SystemStyleObject = {
'[cmdk-root]': {
w: 'full',
h: 'full',
},
'[cmdk-list]': {
w: 'full',
h: 'full',
},
};
export const AddNodeCmdk = memo(() => {
const { t } = useTranslation();
const addNodeCmdk = useAddNodeCmdk();
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]);
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setSearchTerm(e.target.value);
}, []);
const onClose = useCallback(() => {
addNodeCmdk.setFalse();
setSearchTerm('');
$pendingConnection.set(null);
}, [addNodeCmdk]);
const onSelect = useCallback(
(value: string) => {
addNode(value);
onClose();
},
[addNode, onClose]
);
return (
<Modal
isOpen={addNodeCmdk.isTrue}
onClose={onClose}
useInert={false}
initialFocusRef={inputRef}
size="xl"
isCentered
>
<ModalOverlay />
<ModalContent h="512" maxH="70%">
<ModalBody p={2} h="full" sx={cmdkRootSx}>
<CommandRoot loop shouldFilter={false}>
<Flex flexDir="column" h="full" gap={2}>
<Input ref={inputRef} value={searchTerm} onChange={onChange} placeholder={t('nodes.nodeSearch')} />
<Box w="full" h="full">
<ScrollableContent>
<CommandEmpty>
<IAINoContentFallback
position="absolute"
top={0}
right={0}
bottom={0}
left={0}
icon={null}
label="No matching items"
/>
</CommandEmpty>
<CommandList>
<NodeCommandList searchTerm={throttledSearchTerm} onSelect={onSelect} />
</CommandList>
</ScrollableContent>
</Box>
</Flex>
</CommandRoot>
</ModalBody>
</ModalContent>
</Modal>
);
});
AddNodeCmdk.displayName = 'AddNodeCmdk';
const cmdkItemSx: SystemStyleObject = {
'&[data-selected="true"]': {
bg: 'base.700',
},
};
type NodeCommandItemData = {
value: string;
label: string;
description: string;
classification: S['Classification'];
nodePack: string;
};
const $templatesArray = computed($templates, (templates) => Object.values(templates));
const createRegex = memoize(
(inputValue: string) =>
new RegExp(
inputValue
.trim()
.replace(/[-[\]{}()*+!<=:?./\\^$|#,]/g, '')
.split(' ')
.join('.*'),
'gi'
)
);
// Filterable items are a subset of Invocation template - we also want to filter for notes or current image node,
// so we are using a less specific type instead of `InvocationTemplate`
type FilterableItem = {
type: string;
title: string;
description: string;
tags: string[];
classification: S['Classification'];
nodePack: string;
};
const filter = memoize(
(item: FilterableItem, searchTerm: string) => {
const regex = createRegex(searchTerm);
if (!searchTerm) {
return true;
}
if (item.title.includes(searchTerm) || regex.test(item.title)) {
return true;
}
if (item.type.includes(searchTerm) || regex.test(item.type)) {
return true;
}
if (item.description.includes(searchTerm) || regex.test(item.description)) {
return true;
}
if (item.nodePack.includes(searchTerm) || regex.test(item.nodePack)) {
return true;
}
if (item.classification.includes(searchTerm) || regex.test(item.classification)) {
return true;
}
for (const tag of item.tags) {
if (tag.includes(searchTerm) || regex.test(tag)) {
return true;
}
}
return false;
},
(item: FilterableItem, searchTerm: string) => `${item.type}-${searchTerm}`
);
const NodeCommandList = memo(({ searchTerm, onSelect }: { searchTerm: string; onSelect: (value: string) => void }) => {
const { t } = useTranslation();
const templatesArray = useStore($templatesArray);
const pendingConnection = useStore($pendingConnection);
const currentImageFilterItem = useMemo<FilterableItem>(
() => ({
type: 'current_image',
title: t('nodes.currentImage'),
description: t('nodes.currentImageDescription'),
tags: ['progress', 'image', 'current'],
classification: 'stable',
nodePack: 'invokeai',
}),
[t]
);
const notesFilterItem = useMemo<FilterableItem>(
() => ({
type: 'notes',
title: t('nodes.notes'),
description: t('nodes.notesDescription'),
tags: ['notes'],
classification: 'stable',
nodePack: 'invokeai',
}),
[t]
);
const items = useMemo<NodeCommandItemData[]>(() => {
// If we have a connection in progress, we need to filter the node choices
const _items: NodeCommandItemData[] = [];
if (!pendingConnection) {
for (const template of templatesArray) {
if (filter(template, searchTerm)) {
_items.push({
label: template.title,
value: template.type,
description: template.description,
classification: template.classification,
nodePack: template.nodePack,
});
}
}
for (const item of [currentImageFilterItem, notesFilterItem]) {
if (filter(item, searchTerm)) {
_items.push({
label: item.title,
value: item.type,
description: item.description,
classification: item.classification,
nodePack: item.nodePack,
});
}
}
} else {
for (const template of templatesArray) {
if (filter(template, searchTerm)) {
const candidateFields = pendingConnection.handleType === 'source' ? template.inputs : template.outputs;
for (const field of Object.values(candidateFields)) {
const sourceType =
pendingConnection.handleType === 'source' ? field.type : pendingConnection.fieldTemplate.type;
const targetType =
pendingConnection.handleType === 'target' ? field.type : pendingConnection.fieldTemplate.type;
if (validateConnectionTypes(sourceType, targetType)) {
_items.push({
label: template.title,
value: template.type,
description: template.description,
classification: template.classification,
nodePack: template.nodePack,
});
break;
}
}
}
}
}
return _items;
}, [pendingConnection, currentImageFilterItem, searchTerm, notesFilterItem, templatesArray]);
return (
<>
{items.map((item) => (
<CommandItem key={item.value} value={item.value} onSelect={onSelect} asChild>
<Flex role="button" flexDir="column" sx={cmdkItemSx} py={1} px={2} borderRadius="base">
<Flex alignItems="center" gap={2}>
{item.classification === 'beta' && <Icon boxSize={4} color="invokeYellow.300" as={PiHammerBold} />}
{item.classification === 'prototype' && <Icon boxSize={4} color="invokeRed.300" as={PiFlaskBold} />}
<Text fontWeight="semibold">{item.label}</Text>
<Spacer />
<Text variant="subtext" fontWeight="semibold">
{item.nodePack}
</Text>
</Flex>
{item.description && <Text color="base.200">{item.description}</Text>}
</Flex>
</CommandItem>
))}
</>
);
});
NodeCommandList.displayName = 'CommandListItems';

View File

@ -1,267 +0,0 @@
import 'reactflow/dist/style.css';
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
import { Combobox, Flex, Popover, PopoverAnchor, PopoverBody, PopoverContent } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppStore } from 'app/store/storeHooks';
import type { SelectInstance } from 'chakra-react-select';
import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes';
import { useBuildNode } from 'features/nodes/hooks/useBuildNode';
import {
$cursorPos,
$edgePendingUpdate,
$isAddNodePopoverOpen,
$pendingConnection,
$templates,
closeAddNodePopover,
edgesChanged,
nodesChanged,
openAddNodePopover,
} from 'features/nodes/store/nodesSlice';
import { selectNodesSlice } from 'features/nodes/store/selectors';
import { findUnoccupiedPosition } from 'features/nodes/store/util/findUnoccupiedPosition';
import { getFirstValidConnection } from 'features/nodes/store/util/getFirstValidConnection';
import { connectionToEdge } from 'features/nodes/store/util/reactFlowUtil';
import { validateConnectionTypes } from 'features/nodes/store/util/validateConnectionTypes';
import type { AnyNode } from 'features/nodes/types/invocation';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { toast } from 'features/toast/toast';
import { filter, map, memoize, some } from 'lodash-es';
import { memo, useCallback, useMemo, useRef } from 'react';
import { flushSync } from 'react-dom';
import { useHotkeys } from 'react-hotkeys-hook';
import type { HotkeyCallback } from 'react-hotkeys-hook/dist/types';
import { useTranslation } from 'react-i18next';
import type { FilterOptionOption } from 'react-select/dist/declarations/src/filters';
import type { EdgeChange, NodeChange } from 'reactflow';
const createRegex = memoize(
(inputValue: string) =>
new RegExp(
inputValue
.trim()
.replace(/[-[\]{}()*+!<=:?./\\^$|#,]/g, '')
.split(' ')
.join('.*'),
'gi'
)
);
const filterOption = memoize((option: FilterOptionOption<ComboboxOption>, inputValue: string) => {
if (!inputValue) {
return true;
}
const regex = createRegex(inputValue);
return (
regex.test(option.label) ||
regex.test(option.data.description ?? '') ||
(option.data.tags ?? []).some((tag) => regex.test(tag))
);
});
const AddNodePopover = () => {
const dispatch = useAppDispatch();
const buildInvocation = useBuildNode();
const { t } = useTranslation();
const selectRef = useRef<SelectInstance<ComboboxOption> | null>(null);
const inputRef = useRef<HTMLInputElement>(null);
const templates = useStore($templates);
const pendingConnection = useStore($pendingConnection);
const isOpen = useStore($isAddNodePopoverOpen);
const store = useAppStore();
const isWorkflowsActive = useStore(INTERACTION_SCOPES.workflows.$isActive);
const filteredTemplates = useMemo(() => {
// If we have a connection in progress, we need to filter the node choices
const templatesArray = map(templates);
if (!pendingConnection) {
return templatesArray;
}
return filter(templates, (template) => {
const candidateFields = pendingConnection.handleType === 'source' ? template.inputs : template.outputs;
return some(candidateFields, (field) => {
const sourceType =
pendingConnection.handleType === 'source' ? field.type : pendingConnection.fieldTemplate.type;
const targetType =
pendingConnection.handleType === 'target' ? field.type : pendingConnection.fieldTemplate.type;
return validateConnectionTypes(sourceType, targetType);
});
});
}, [templates, pendingConnection]);
const options = useMemo(() => {
const _options: ComboboxOption[] = map(filteredTemplates, (template) => {
return {
label: template.title,
value: template.type,
description: template.description,
tags: template.tags,
};
});
//We only want these nodes if we're not filtered
if (!pendingConnection) {
_options.push({
label: t('nodes.currentImage'),
value: 'current_image',
description: t('nodes.currentImageDescription'),
tags: ['progress'],
});
_options.push({
label: t('nodes.notes'),
value: 'notes',
description: t('nodes.notesDescription'),
tags: ['notes'],
});
}
_options.sort((a, b) => a.label.localeCompare(b.label));
return _options;
}, [filteredTemplates, pendingConnection, t]);
const addNode = useCallback(
(nodeType: string): AnyNode | null => {
const node = buildInvocation(nodeType);
if (!node) {
const errorMessage = t('nodes.unknownNode', {
nodeType: nodeType,
});
toast({
status: 'error',
title: errorMessage,
});
return null;
}
// Find a cozy spot for the node
const cursorPos = $cursorPos.get();
const { nodes, edges } = selectNodesSlice(store.getState());
node.position = findUnoccupiedPosition(nodes, cursorPos?.x ?? node.position.x, cursorPos?.y ?? node.position.y);
node.selected = true;
// Deselect all other nodes and edges
const nodeChanges: NodeChange[] = [{ type: 'add', item: node }];
const edgeChanges: EdgeChange[] = [];
nodes.forEach(({ id, selected }) => {
if (selected) {
nodeChanges.push({ type: 'select', id, selected: false });
}
});
edges.forEach(({ id, selected }) => {
if (selected) {
edgeChanges.push({ type: 'select', id, selected: false });
}
});
// Onwards!
if (nodeChanges.length > 0) {
dispatch(nodesChanged(nodeChanges));
}
if (edgeChanges.length > 0) {
dispatch(edgesChanged(edgeChanges));
}
return node;
},
[buildInvocation, store, dispatch, t]
);
const onChange = useCallback<ComboboxOnChange>(
(v) => {
if (!v) {
return;
}
const node = addNode(v.value);
// Auto-connect an edge if we just added a node and have a pending connection
if (pendingConnection && isInvocationNode(node)) {
const edgePendingUpdate = $edgePendingUpdate.get();
const { handleType } = pendingConnection;
const source = handleType === 'source' ? pendingConnection.nodeId : node.id;
const sourceHandle = handleType === 'source' ? pendingConnection.handleId : null;
const target = handleType === 'target' ? pendingConnection.nodeId : node.id;
const targetHandle = handleType === 'target' ? pendingConnection.handleId : null;
const { nodes, edges } = selectNodesSlice(store.getState());
const connection = getFirstValidConnection(
source,
sourceHandle,
target,
targetHandle,
nodes,
edges,
templates,
edgePendingUpdate
);
if (connection) {
const newEdge = connectionToEdge(connection);
dispatch(edgesChanged([{ type: 'add', item: newEdge }]));
}
}
closeAddNodePopover();
},
[addNode, dispatch, pendingConnection, store, templates]
);
const handleHotkeyOpen: HotkeyCallback = useCallback((e) => {
if (!$isAddNodePopoverOpen.get()) {
e.preventDefault();
openAddNodePopover();
flushSync(() => {
selectRef.current?.inputRef?.focus();
});
}
}, []);
useHotkeys(['shift+a', 'space'], handleHotkeyOpen, { enabled: isWorkflowsActive }, [isWorkflowsActive]);
const noOptionsMessage = useCallback(() => t('nodes.noMatchingNodes'), [t]);
return (
<Popover
isOpen={isOpen}
onClose={closeAddNodePopover}
placement="bottom"
openDelay={0}
closeDelay={0}
closeOnBlur={true}
returnFocusOnClose={true}
initialFocusRef={inputRef}
isLazy
>
<PopoverAnchor>
<Flex position="absolute" top="15%" insetInlineStart="50%" pointerEvents="none" />
</PopoverAnchor>
<PopoverContent
p={0}
top={-1}
shadow="dark-lg"
borderColor="invokeBlue.400"
borderWidth="2px"
borderStyle="solid"
>
<PopoverBody w="32rem" p={0}>
<Combobox
menuIsOpen={isOpen}
selectRef={selectRef}
value={null}
placeholder={t('nodes.nodeSearch')}
options={options}
noOptionsMessage={noOptionsMessage}
filterOption={filterOption}
onChange={onChange}
onMenuClose={closeAddNodePopover}
inputRef={inputRef}
closeMenuOnSelect={false}
/>
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default memo(AddNodePopover);

View File

@ -8,10 +8,10 @@ import { useSyncExecutionState } from 'features/nodes/hooks/useExecutionState';
import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection';
import { useWorkflowWatcher } from 'features/nodes/hooks/useWorkflowWatcher';
import {
$addNodeCmdk,
$cursorPos,
$didUpdateEdge,
$edgePendingUpdate,
$isAddNodePopoverOpen,
$lastEdgeUpdateMouseEvent,
$pendingConnection,
$viewport,
@ -281,7 +281,7 @@ export const Flow = memo(() => {
const onEscapeHotkey = useCallback(() => {
if (!$edgePendingUpdate.get()) {
$pendingConnection.set(null);
$isAddNodePopoverOpen.set(false);
$addNodeCmdk.set(false);
cancelConnection();
}
}, [cancelConnection]);

View File

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

View File

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

View File

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

View File

@ -1,10 +1,11 @@
import { IconButton } from '@invoke-ai/ui-library';
import { openAddNodePopover } from 'features/nodes/store/nodesSlice';
import { useAddNodeCmdk } from 'features/nodes/store/nodesSlice';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi';
const AddNodeButton = () => {
const addNodeCmdk = useAddNodeCmdk();
const { t } = useTranslation();
return (
@ -12,7 +13,7 @@ const AddNodeButton = () => {
tooltip={t('nodes.addNodeToolTip')}
aria-label={t('nodes.addNode')}
icon={<PiPlusBold />}
onClick={openAddNodePopover}
onClick={addNodeCmdk.setTrue}
pointerEvents="auto"
/>
);

View File

@ -47,6 +47,7 @@ const ClearFlowButton = () => {
onClose={onClose}
title={t('nodes.clearWorkflow')}
acceptCallback={handleNewWorkflow}
useInert={false}
>
<Flex flexDir="column" gap={2}>
<Text>{t('nodes.clearWorkflowDesc')}</Text>

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